@@ -27,6 +27,10 @@ struct TypeWatcher {
2727 jit::UnorderedMap<BorrowedRef<PyTypeObject>, jit::UnorderedSet<T*>> caches;
2828
2929 void watch (BorrowedRef<PyTypeObject> type, T* cache) {
30+ if (PyType_HasFeature (type, Py_TPFLAGS_IMMUTABLETYPE)) {
31+ // We don't watch immutable types - they can't be modified.
32+ return ;
33+ }
3034 JIT_CHECK (
3135 cinderx::getModuleState ()->watcher_state .watchType (type) == 0 ,
3236 " Failed to watch type {} for attribute cache" ,
@@ -43,20 +47,30 @@ struct TypeWatcher {
4347 // don't unwatch type; other watchers may still be watching it
4448 }
4549
46- void typeChanged (BorrowedRef<PyTypeObject> type) {
50+ // Notify caches watching a type. Calls Callback(cache, type) for each
51+ // registered cache. The default callback calls cache->typeChanged(type).
52+ template <typename Callback>
53+ void typeChanged (BorrowedRef<PyTypeObject> type, Callback cb) {
4754 auto it = caches.find (type);
4855 if (it == caches.end ()) {
4956 return ;
5057 }
5158 jit::UnorderedSet<T*> to_notify = std::move (it->second );
5259 caches.erase (it);
5360 for (T* cache : to_notify) {
54- cache-> typeChanged ( type);
61+ cb (cache, type);
5562 }
5663 }
64+
65+ void typeChanged (BorrowedRef<PyTypeObject> type) {
66+ typeChanged (type, [](T* cache, BorrowedRef<PyTypeObject> tp) {
67+ cache->typeChanged (tp);
68+ });
69+ }
5770};
5871
5972TypeWatcher<AttributeCache> ac_watcher;
73+ TypeWatcher<AttributeCache> ac_descr_watcher;
6074TypeWatcher<LoadTypeAttrCache> ltac_watcher;
6175TypeWatcher<LoadMethodCache> lm_watcher;
6276TypeWatcher<LoadTypeMethodCache> ltm_watcher;
@@ -563,6 +577,7 @@ void AttributeMutator::set_combined(PyTypeObject* type) {
563577void AttributeMutator::set_data_descr (PyTypeObject* type, PyObject* descr) {
564578 set_type (type, Kind::kDataDescr );
565579 data_descr_.descr = descr;
580+ data_descr_.descr_type = Py_TYPE (descr);
566581}
567582
568583void AttributeMutator::set_member_descr (PyTypeObject* type, PyObject* descr) {
@@ -589,6 +604,13 @@ void AttributeMutator::set_split(
589604 split_.keys = keys;
590605}
591606
607+ BorrowedRef<PyTypeObject> AttributeMutator::watchedDescrType () const {
608+ if (get_kind () == Kind::kDataDescr ) {
609+ return data_descr_.descr_type ;
610+ }
611+ return nullptr ;
612+ }
613+
592614inline int
593615AttributeMutator::setAttr (PyObject* obj, PyObject* name, PyObject* value) {
594616 JIT_CHECK (
@@ -675,14 +697,61 @@ AttributeCache::~AttributeCache() {
675697 for (auto & entry : entries ()) {
676698 if (entry.type () != nullptr ) {
677699 ac_watcher.unwatch (entry.type (), this );
700+ BorrowedRef<PyTypeObject> descr_tp = entry.watchedDescrType ();
701+ if (descr_tp != nullptr ) {
702+ ac_descr_watcher.unwatch (descr_tp, this );
703+ }
678704 entry.reset ();
679705 }
680706 }
681707}
682708
683- void AttributeCache::typeChanged (PyTypeObject*) {
709+ void AttributeCache::typeChanged (PyTypeObject* tp ) {
684710 for (auto & entry : entries ()) {
685- entry.reset ();
711+ if (entry.type () == tp) {
712+ BorrowedRef<PyTypeObject> descr_tp = entry.watchedDescrType ();
713+ // Reset the entry, this also resets the kind so after the call
714+ // watchedDescrType returns nullptr.
715+ entry.reset ();
716+ if (descr_tp != nullptr ) {
717+ // For entry.type() the cache is 1 to 1 between types and the cache.
718+ // For descriptors the same descriptor can be shared multiple times
719+ // per a type. So check if we have any descriptors remaining and only
720+ // unwatch when they're all clear.
721+ bool found = false ;
722+ for (auto & other : entries ()) {
723+ if (other.watchedDescrType () == descr_tp) {
724+ found = true ;
725+ break ;
726+ }
727+ }
728+ if (!found) {
729+ // there are no other entries watching this descriptor.
730+ ac_descr_watcher.unwatch (descr_tp, this );
731+ }
732+ }
733+ }
734+ }
735+ }
736+
737+ void AttributeCache::descrTypeChanged (PyTypeObject* tp) {
738+ // We only need to unwatch this AttributeCache in the ac_descr_watcher
739+ // once as it applies to all entries so we use a flag to track if we've
740+ // already unwatched it for multiple entries (it wouldn't hurt to try
741+ // and unwatch multiple times but it's a couple of hash lookups)
742+ bool found = false ;
743+ for (auto & entry : entries ()) {
744+ if (entry.watchedDescrType () == tp) {
745+ // If we were watching the descriptor the whole cache entry is
746+ // invalidated and we no longer need to watch the type.
747+ ac_watcher.unwatch (entry.type (), this );
748+ entry.reset ();
749+ if (!found) {
750+ // unwatch the descriptor watcher once for the whole cache
751+ ac_descr_watcher.unwatch (tp, this );
752+ found = true ;
753+ }
754+ }
686755 }
687756}
688757
@@ -766,9 +835,10 @@ void AttributeCache::fill(
766835 if (descr_type == &PyMemberDescr_Type) {
767836 mut->set_member_descr (type, descr);
768837 } else {
769- // If someone deletes descr_types's __set__ method, it will no longer
770- // be a data descriptor, and the cache kind has to change.
771- ac_watcher.watch (descr_type, this );
838+ // If someone modifies descr_type (e.g., deletes __set__), it may no
839+ // longer be a data descriptor. Watch it via the descriptor watcher
840+ // so the cache is invalidated.
841+ ac_descr_watcher.watch (descr_type, this );
772842 mut->set_data_descr (type, descr);
773843 }
774844 } else {
@@ -1602,6 +1672,10 @@ LoadModuleMethodCache::lookupSlowPath(BorrowedRef<> obj, BorrowedRef<> name) {
16021672
16031673void notifyICsTypeChanged (BorrowedRef<PyTypeObject> type) {
16041674 ac_watcher.typeChanged (type);
1675+ ac_descr_watcher.typeChanged (
1676+ type, [](AttributeCache* cache, BorrowedRef<PyTypeObject> tp) {
1677+ cache->descrTypeChanged (tp);
1678+ });
16051679 ltac_watcher.typeChanged (type);
16061680 lm_watcher.typeChanged (type);
16071681 ltm_watcher.typeChanged (type);
0 commit comments