diff --git a/NEWS.md b/NEWS.md index f9a56d3075647b..c1a0fc688b6529 100644 --- a/NEWS.md +++ b/NEWS.md @@ -69,7 +69,7 @@ releases. * openssl 4.0.1 * 4.0.0 to [v4.0.1][openssl-v4.0.1] * prism 1.9.0 - * 1.8.0 to [v1.9.0][prism-v1.9.0] + * 1.8.0 to [v1.8.1][prism-v1.8.1], [v1.9.0][prism-v1.9.0] * resolv 0.7.1 * 0.7.0 to [v0.7.1][resolv-v0.7.1] * stringio 3.2.1.dev @@ -85,16 +85,11 @@ releases. * minitest 6.0.2 * test-unit 3.7.7 - * 3.7.3 to [3.7.4][test-unit-3.7.4], [3.7.5][test-unit-3.7.5], [3.7.6][test-unit-3.7.6], [3.7.7][test-unit-3.7.7] -* rss 0.3.2 - * 0.3.1 to [0.3.2][rss-0.3.2] + * 3.7.5 to [3.7.6][test-unit-3.7.6], [3.7.7][test-unit-3.7.7] * net-imap 0.6.3 - * 0.6.1 to [v0.6.2][net-imap-v0.6.2], [v0.6.3][net-imap-v0.6.3] -* rbs 3.10.3 - * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3] -* typeprof 0.31.1 -* debug 1.11.1 - * 1.11.0 to [v1.11.1][debug-v1.11.1] + * 0.6.2 to [v0.6.3][net-imap-v0.6.3] +* rbs 4.0.0 + * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3], [v4.0.0.dev.5][rbs-v4.0.0.dev.5], [v4.0.0][rbs-v4.0.0] * mutex_m 0.3.0 * resolv-replace 0.2.0 * 0.1.1 to [v0.2.0][resolv-replace-v0.2.0] @@ -105,7 +100,7 @@ releases. * pstore 0.2.1 * 0.2.0 to [v0.2.1][pstore-v0.2.1] * rdoc 7.2.0 - * 6.17.0 to [v7.0.0][rdoc-v7.0.0], [v7.0.1][rdoc-v7.0.1], [v7.0.2][rdoc-v7.0.2], [v7.0.3][rdoc-v7.0.3], [v7.1.0][rdoc-v7.1.0], [v7.2.0][rdoc-v7.2.0] + * 7.0.3 to [v7.1.0][rdoc-v7.1.0], [v7.2.0][rdoc-v7.2.0] * win32ole 1.9.3 * 1.9.2 to [v1.9.3][win32ole-v1.9.3] * irb 1.17.0 @@ -135,34 +130,37 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21785]: https://bugs.ruby-lang.org/issues/21785 +[test-unit-3.7.4]: https://github.com/test-unit/test-unit/releases/tag/3.7.4 +[test-unit-3.7.5]: https://github.com/test-unit/test-unit/releases/tag/3.7.5 +[rss-0.3.2]: https://github.com/ruby/rss/releases/tag/0.3.2 +[net-imap-v0.6.2]: https://github.com/ruby/net-imap/releases/tag/v0.6.2 +[debug-v1.11.1]: https://github.com/ruby/debug/releases/tag/v1.11.1 +[rdoc-v7.0.0]: https://github.com/ruby/rdoc/releases/tag/v7.0.0 +[rdoc-v7.0.1]: https://github.com/ruby/rdoc/releases/tag/v7.0.1 +[rdoc-v7.0.2]: https://github.com/ruby/rdoc/releases/tag/v7.0.2 +[rdoc-v7.0.3]: https://github.com/ruby/rdoc/releases/tag/v7.0.3 [json-v2.18.1]: https://github.com/ruby/json/releases/tag/v2.18.1 [json-v2.19.0]: https://github.com/ruby/json/releases/tag/v2.19.0 [json-v2.19.1]: https://github.com/ruby/json/releases/tag/v2.19.1 [openssl-v4.0.1]: https://github.com/ruby/openssl/releases/tag/v4.0.1 +[prism-v1.8.1]: https://github.com/ruby/prism/releases/tag/v1.8.1 [prism-v1.9.0]: https://github.com/ruby/prism/releases/tag/v1.9.0 [resolv-v0.7.1]: https://github.com/ruby/resolv/releases/tag/v0.7.1 [strscan-v3.1.7]: https://github.com/ruby/strscan/releases/tag/v3.1.7 [timeout-v0.6.1]: https://github.com/ruby/timeout/releases/tag/v0.6.1 [zlib-v3.2.3]: https://github.com/ruby/zlib/releases/tag/v3.2.3 -[test-unit-3.7.4]: https://github.com/test-unit/test-unit/releases/tag/3.7.4 -[test-unit-3.7.5]: https://github.com/test-unit/test-unit/releases/tag/3.7.5 [test-unit-3.7.6]: https://github.com/test-unit/test-unit/releases/tag/3.7.6 [test-unit-3.7.7]: https://github.com/test-unit/test-unit/releases/tag/3.7.7 -[rss-0.3.2]: https://github.com/ruby/rss/releases/tag/0.3.2 -[net-imap-v0.6.2]: https://github.com/ruby/net-imap/releases/tag/v0.6.2 [net-imap-v0.6.3]: https://github.com/ruby/net-imap/releases/tag/v0.6.3 [rbs-v3.10.1]: https://github.com/ruby/rbs/releases/tag/v3.10.1 [rbs-v3.10.2]: https://github.com/ruby/rbs/releases/tag/v3.10.2 [rbs-v3.10.3]: https://github.com/ruby/rbs/releases/tag/v3.10.3 -[debug-v1.11.1]: https://github.com/ruby/debug/releases/tag/v1.11.1 +[rbs-v4.0.0.dev.5]: https://github.com/ruby/rbs/releases/tag/v4.0.0.dev.5 +[rbs-v4.0.0]: https://github.com/ruby/rbs/releases/tag/v4.0.0 [resolv-replace-v0.2.0]: https://github.com/ruby/resolv-replace/releases/tag/v0.2.0 [syslog-v0.4.0]: https://github.com/ruby/syslog/releases/tag/v0.4.0 [repl_type_completor-v0.1.13]: https://github.com/ruby/repl_type_completor/releases/tag/v0.1.13 [pstore-v0.2.1]: https://github.com/ruby/pstore/releases/tag/v0.2.1 -[rdoc-v7.0.0]: https://github.com/ruby/rdoc/releases/tag/v7.0.0 -[rdoc-v7.0.1]: https://github.com/ruby/rdoc/releases/tag/v7.0.1 -[rdoc-v7.0.2]: https://github.com/ruby/rdoc/releases/tag/v7.0.2 -[rdoc-v7.0.3]: https://github.com/ruby/rdoc/releases/tag/v7.0.3 [rdoc-v7.1.0]: https://github.com/ruby/rdoc/releases/tag/v7.1.0 [rdoc-v7.2.0]: https://github.com/ruby/rdoc/releases/tag/v7.2.0 [win32ole-v1.9.3]: https://github.com/ruby/win32ole/releases/tag/v1.9.3 diff --git a/class.c b/class.c index cc4c6845b49977..dbc8d760ef3615 100644 --- a/class.c +++ b/class.c @@ -80,6 +80,10 @@ #define METACLASS_OF(k) RBASIC(k)->klass #define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls) +static void rb_class_remove_from_super_subclasses(VALUE klass); +static void rb_class_remove_from_module_subclasses(VALUE klass); +static void rb_class_classext_free_subclasses(rb_classext_t *ext); + static enum rb_id_table_iterator_result cvar_table_free_i(VALUE value, void *ctx) { @@ -115,7 +119,10 @@ rb_class_classext_free(VALUE klass, rb_classext_t *ext, bool is_prime) rb_id_table_free(tbl); } - rb_class_classext_free_subclasses(ext, klass, false); + if (is_prime) { + rb_class_remove_from_super_subclasses(klass); + rb_class_classext_free_subclasses(ext); + } if (RCLASSEXT_SUPERCLASSES_WITH_SELF(ext)) { RUBY_ASSERT(is_prime); // superclasses should only be used on prime @@ -143,7 +150,10 @@ rb_iclass_classext_free(VALUE klass, rb_classext_t *ext, bool is_prime) rb_id_table_free(RCLASSEXT_CALLABLE_M_TBL(ext)); } - rb_class_classext_free_subclasses(ext, klass, false); + if (is_prime) { + rb_class_remove_from_super_subclasses(klass); + rb_class_remove_from_module_subclasses(klass); + } if (!is_prime) { // the prime classext will be freed with RClass SIZED_FREE(ext); @@ -162,8 +172,6 @@ iclass_free_orphan_classext(VALUE klass, rb_classext_t *ext) rb_id_table_free(RCLASSEXT_CALLABLE_M_TBL(ext)); } - rb_class_classext_free_subclasses(ext, klass, true); // replacing this classext with a newer one - SIZED_FREE(ext); } @@ -314,66 +322,6 @@ duplicate_classext_const_tbl(struct rb_id_table *src, VALUE klass) return dst; } -static VALUE -box_subclasses_tbl_key(const rb_box_t *box) -{ - if (!box){ - return 0; - } - return (VALUE)box->box_id; -} - -static void -duplicate_classext_subclasses(rb_classext_t *orig, rb_classext_t *copy) -{ - rb_subclass_anchor_t *anchor, *orig_anchor; - rb_subclass_entry_t *head, *cur, *cdr, *entry, *first = NULL; - rb_box_subclasses_t *box_subclasses; - struct st_table *tbl; - - if (RCLASSEXT_SUBCLASSES(orig)) { - orig_anchor = RCLASSEXT_SUBCLASSES(orig); - box_subclasses = orig_anchor->box_subclasses; - tbl = ((rb_box_subclasses_t *)box_subclasses)->tbl; - - anchor = ZALLOC(rb_subclass_anchor_t); - anchor->box_subclasses = rb_box_subclasses_ref_inc(box_subclasses); - - head = ZALLOC(rb_subclass_entry_t); - anchor->head = head; - - RCLASSEXT_SUBCLASSES(copy) = anchor; - - cur = head; - entry = orig_anchor->head; - RUBY_ASSERT(!entry->klass); - // The head entry has NULL klass always. See rb_class_foreach_subclass(). - entry = entry->next; - while (entry) { - if (rb_objspace_garbage_object_p(entry->klass)) { - entry = entry->next; - continue; - } - cdr = ZALLOC(rb_subclass_entry_t); - cdr->klass = entry->klass; - cdr->prev = cur; - cur->next = cdr; - if (!first) { - VALUE box_id = box_subclasses_tbl_key(RCLASSEXT_BOX(copy)); - first = cdr; - st_insert(tbl, box_id, (st_data_t)first); - } - cur = cdr; - entry = entry->next; - } - } - - if (RCLASSEXT_BOX_SUPER_SUBCLASSES(orig)) - RCLASSEXT_BOX_SUPER_SUBCLASSES(copy) = rb_box_subclasses_ref_inc(RCLASSEXT_BOX_SUPER_SUBCLASSES(orig)); - if (RCLASSEXT_BOX_MODULE_SUBCLASSES(orig)) - RCLASSEXT_BOX_MODULE_SUBCLASSES(copy) = rb_box_subclasses_ref_inc(RCLASSEXT_BOX_MODULE_SUBCLASSES(orig)); -} - static void class_duplicate_iclass_classext(VALUE iclass, rb_classext_t *mod_ext, const rb_box_t *box) { @@ -409,8 +357,7 @@ class_duplicate_iclass_classext(VALUE iclass, rb_classext_t *mod_ext, const rb_b // RCLASSEXT_CALLABLE_M_TBL(ext) = NULL; // RCLASSEXT_CC_TBL(ext) = NULL; - // subclasses, box_super_subclasses_tbl, box_module_subclasses_tbl - duplicate_classext_subclasses(src, ext); + // Subclasses/back-pointers are only in the prime classext. RCLASSEXT_SET_ORIGIN(ext, iclass, RCLASSEXT_ORIGIN(src)); RCLASSEXT_ICLASS_IS_ORIGIN(ext) = RCLASSEXT_ICLASS_IS_ORIGIN(src); @@ -466,8 +413,7 @@ rb_class_duplicate_classext(rb_classext_t *orig, VALUE klass, const rb_box_t *bo RCLASSEXT_CVC_TBL(ext) = duplicate_classext_id_table(RCLASSEXT_CVC_TBL(orig), dup_iclass); - // subclasses, subclasses_index - duplicate_classext_subclasses(orig, ext); + // Subclasses/back-pointers are only in the prime classext. RCLASSEXT_SET_ORIGIN(ext, klass, RCLASSEXT_ORIGIN(orig)); /* @@ -485,24 +431,28 @@ rb_class_duplicate_classext(rb_classext_t *orig, VALUE klass, const rb_box_t *bo /* For the usual T_CLASS/T_MODULE, iclass flags are always false */ if (dup_iclass) { - VALUE iclass; /* * ICLASS has the same m_tbl/const_tbl/cvc_tbl with the included module. * So the module's classext is copied, its tables should be also referred * by the ICLASS's classext for the box. + * + * Subclasses are only in the prime classext, so read from orig. */ - rb_subclass_anchor_t *anchor = RCLASSEXT_SUBCLASSES(ext); - rb_subclass_entry_t *subclass_entry = anchor->head; + rb_subclass_entry_t *subclass_entry = RCLASSEXT_SUBCLASSES(orig); + if (subclass_entry) subclass_entry = subclass_entry->next; // skip dummy head while (subclass_entry) { - if (subclass_entry->klass && RB_TYPE_P(subclass_entry->klass, T_ICLASS)) { - iclass = subclass_entry->klass; - VM_ASSERT(RB_TYPE_P(iclass, T_ICLASS)); - if (RBASIC_CLASS(iclass) == klass) { - // Is the subclass an ICLASS including this module into another class - // If so we need to re-associate it under our box with the new ext - VM_ASSERT(FL_TEST_RAW(iclass, RCLASS_BOXABLE)); - class_duplicate_iclass_classext(iclass, ext, box); - } + VALUE iclass = subclass_entry->klass; + + /* every node in the subclass list should be an ICLASS built from this module */ + VM_ASSERT(iclass); + VM_ASSERT(RB_TYPE_P(iclass, T_ICLASS)); + VM_ASSERT(RBASIC_CLASS(iclass) == klass); + + if (FL_TEST_RAW(iclass, RCLASS_BOXABLE)) { + // Non-boxable ICLASSes (included by classes in main/user boxes) can't + // hold per-box classexts, and their includer classes also can't, so + // method lookup through them always uses the prime classext. + class_duplicate_iclass_classext(iclass, ext, box); } subclass_entry = subclass_entry->next; } @@ -563,49 +513,46 @@ rb_class_variation_count(VALUE klass) return RCLASS_VARIATION_COUNT(klass); } -static void -push_subclass_entry_to_list(VALUE super, VALUE klass, bool is_module) +static rb_subclass_entry_t * +push_subclass_entry_to_list(VALUE super, VALUE klass) { rb_subclass_entry_t *entry, *head; - rb_subclass_anchor_t *anchor; - rb_box_subclasses_t *box_subclasses; - struct st_table *tbl; - const rb_box_t *box = rb_current_box(); + + RUBY_ASSERT( + (RB_TYPE_P(super, T_MODULE) && RB_TYPE_P(klass, T_ICLASS)) || + (RB_TYPE_P(super, T_CLASS) && RB_TYPE_P(klass, T_CLASS)) || + (RB_TYPE_P(klass, T_ICLASS) && !NIL_P(RCLASS_REFINED_CLASS(klass))) + ); entry = ZALLOC(rb_subclass_entry_t); entry->klass = klass; RB_VM_LOCKING() { - anchor = RCLASS_WRITABLE_SUBCLASSES(super); - VM_ASSERT(anchor); - box_subclasses = (rb_box_subclasses_t *)anchor->box_subclasses; - VM_ASSERT(box_subclasses); - tbl = box_subclasses->tbl; - VM_ASSERT(tbl); - - head = anchor->head; + head = RCLASS_WRITABLE_SUBCLASSES(super); + if (!head) { + head = ZALLOC(rb_subclass_entry_t); + RCLASS_SET_SUBCLASSES(super, head); + } + entry->next = head->next; + entry->prev = head; + if (head->next) { head->next->prev = entry; - entry->next = head->next; } head->next = entry; - entry->prev = head; - st_insert(tbl, box_subclasses_tbl_key(box), (st_data_t)entry); } - if (is_module) { - RCLASS_WRITE_BOX_MODULE_SUBCLASSES(klass, anchor->box_subclasses); - } - else { - RCLASS_WRITE_BOX_SUPER_SUBCLASSES(klass, anchor->box_subclasses); - } + return entry; } void rb_class_subclass_add(VALUE super, VALUE klass) { if (super && !UNDEF_P(super)) { - push_subclass_entry_to_list(super, klass, false); + RUBY_ASSERT(RB_TYPE_P(super, T_CLASS) || RB_TYPE_P(super, T_MODULE)); + RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_ICLASS)); + rb_subclass_entry_t *entry = push_subclass_entry_to_list(super, klass); + RCLASS_EXT_PRIME(klass)->subclass_entry = entry; } } @@ -613,105 +560,81 @@ static void rb_module_add_to_subclasses_list(VALUE module, VALUE iclass) { if (module && !UNDEF_P(module)) { - push_subclass_entry_to_list(module, iclass, true); + RUBY_ASSERT(RB_TYPE_P(module, T_MODULE)); + RUBY_ASSERT(RB_TYPE_P(iclass, T_ICLASS)); + rb_subclass_entry_t *entry = push_subclass_entry_to_list(module, iclass); + RCLASS_EXT_PRIME(iclass)->module_subclass_entry = entry; } } -static struct rb_subclass_entry * -class_get_subclasses_for_ns(struct st_table *tbl, VALUE box_id) -{ - st_data_t value; - if (st_lookup(tbl, (st_data_t)box_id, &value)) { - return (struct rb_subclass_entry *)value; - } - return NULL; -} - -static int -remove_class_from_subclasses_replace_first_entry(st_data_t *key, st_data_t *value, st_data_t arg, int existing) -{ - *value = arg; - return ST_CONTINUE; -} - static void -remove_class_from_subclasses(struct st_table *tbl, VALUE box_id, VALUE klass) +rb_subclass_entry_remove(rb_subclass_entry_t *entry) { - rb_subclass_entry_t *entry = class_get_subclasses_for_ns(tbl, box_id); - bool first_entry = true; - while (entry) { - if (entry->klass == klass) { - rb_subclass_entry_t *prev = entry->prev, *next = entry->next; - - if (prev) { - prev->next = next; - } - if (next) { - next->prev = prev; - } - - if (first_entry) { - if (next) { - st_update(tbl, box_id, remove_class_from_subclasses_replace_first_entry, (st_data_t)next); - } - else { - // no subclass entries in this ns after the deletion - st_delete(tbl, &box_id, NULL); - } - } - - SIZED_FREE(entry); + if (entry) { + rb_subclass_entry_t *prev = entry->prev, *next = entry->next; - break; + if (prev) { + prev->next = next; } - else if (first_entry) { - first_entry = false; + if (next) { + next->prev = prev; } - entry = entry->next; + + xfree(entry); } } -void +static void rb_class_remove_from_super_subclasses(VALUE klass) { - rb_classext_t *ext = RCLASS_EXT_WRITABLE(klass); - rb_box_subclasses_t *box_subclasses = RCLASSEXT_BOX_SUPER_SUBCLASSES(ext); + rb_classext_t *ext = RCLASS_EXT_PRIME(klass); + rb_subclass_entry_t *entry = RCLASSEXT_SUBCLASS_ENTRY(ext); - if (!box_subclasses) return; - remove_class_from_subclasses(box_subclasses->tbl, box_subclasses_tbl_key(RCLASSEXT_BOX(ext)), klass); - rb_box_subclasses_ref_dec(box_subclasses); - RCLASSEXT_BOX_SUPER_SUBCLASSES(ext) = 0; + if (!entry) return; + rb_subclass_entry_remove(entry); + RCLASSEXT_SUBCLASS_ENTRY(ext) = NULL; } -void -rb_class_classext_free_subclasses(rb_classext_t *ext, VALUE klass, bool replacing) +static void +rb_class_remove_from_module_subclasses(VALUE klass) { - rb_subclass_anchor_t *anchor = RCLASSEXT_SUBCLASSES(ext); - struct st_table *tbl = anchor->box_subclasses->tbl; - VALUE box_id = box_subclasses_tbl_key(RCLASSEXT_BOX(ext)); - rb_subclass_entry_t *next, *entry = anchor->head; + rb_classext_t *ext = RCLASS_EXT_PRIME(klass); + rb_subclass_entry_t *entry = RCLASSEXT_MODULE_SUBCLASS_ENTRY(ext); - while (entry) { - next = entry->next; - SIZED_FREE(entry); - entry = next; - } - VM_ASSERT( - rb_box_subclasses_ref_count(anchor->box_subclasses) > 0, - "box_subclasses refcount (%p) %ld", anchor->box_subclasses, rb_box_subclasses_ref_count(anchor->box_subclasses)); - st_delete(tbl, &box_id, NULL); - rb_box_subclasses_ref_dec(anchor->box_subclasses); - SIZED_FREE(anchor); + if (!entry) return; + rb_subclass_entry_remove(entry); + RCLASSEXT_MODULE_SUBCLASS_ENTRY(ext) = NULL; +} - if (RCLASSEXT_BOX_SUPER_SUBCLASSES(ext)) { - rb_box_subclasses_t *box_sub = RCLASSEXT_BOX_SUPER_SUBCLASSES(ext); - if (!replacing) remove_class_from_subclasses(box_sub->tbl, box_id, klass); - rb_box_subclasses_ref_dec(box_sub); - } - if (RCLASSEXT_BOX_MODULE_SUBCLASSES(ext)) { - rb_box_subclasses_t *box_sub = RCLASSEXT_BOX_MODULE_SUBCLASSES(ext); - if (!replacing) remove_class_from_subclasses(box_sub->tbl, box_id, klass); - rb_box_subclasses_ref_dec(box_sub); +static void +rb_class_classext_free_subclasses(rb_classext_t *ext) +{ + rb_subclass_entry_t *head = RCLASSEXT_SUBCLASSES(ext); + + if (head) { + // Detach all children's back-pointers before freeing the list, + // so they don't try to unlink from a freed entry later. + rb_subclass_entry_t *entry = head->next; // skip dummy head + while (entry) { + if (entry->klass) { + rb_classext_t *child_ext = RCLASS_EXT_PRIME(entry->klass); + if (RCLASSEXT_SUBCLASS_ENTRY(child_ext) == entry) { + RCLASSEXT_SUBCLASS_ENTRY(child_ext) = NULL; + } + if (RCLASSEXT_MODULE_SUBCLASS_ENTRY(child_ext) == entry) { + RCLASSEXT_MODULE_SUBCLASS_ENTRY(child_ext) = NULL; + } + } + entry = entry->next; + } + + entry = head; + while (entry) { + rb_subclass_entry_t *next = entry->next; + xfree(entry); + entry = next; + } + RCLASSEXT_SUBCLASSES(ext) = NULL; } } @@ -760,8 +683,6 @@ class_switch_superclass(VALUE super, VALUE klass) static VALUE class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) { - rb_box_subclasses_t *box_subclasses; - rb_subclass_anchor_t *anchor; const rb_box_t *box = rb_current_box(); if (!ruby_box_init_done) { @@ -773,19 +694,6 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) alloc_size = sizeof(struct RClass_boxable); } - // class_alloc is supposed to return a new object that is not promoted yet. - // So, we need to avoid GC after NEWOBJ_OF. - // To achieve that, we allocate subclass lists before NEWOBJ_OF. - // - // TODO: Note that this could cause memory leak. - // If NEWOBJ_OF fails with out of memory, these buffers will leak. - box_subclasses = ZALLOC(rb_box_subclasses_t); - box_subclasses->refcount = 1; - box_subclasses->tbl = st_init_numtable(); - anchor = ZALLOC(rb_subclass_anchor_t); - anchor->box_subclasses = box_subclasses; - anchor->head = ZALLOC(rb_subclass_entry_t); - RUBY_ASSERT(type == T_CLASS || type == T_ICLASS || type == T_MODULE); VALUE flags = type | FL_SHAREABLE; @@ -817,8 +725,6 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) RCLASS_SET_ORIGIN((VALUE)obj, (VALUE)obj); RCLASS_SET_REFINED_CLASS((VALUE)obj, Qnil); - RCLASS_SET_SUBCLASSES((VALUE)obj, anchor); - return (VALUE)obj; } @@ -833,7 +739,12 @@ static VALUE class_associate_super(VALUE klass, VALUE super, bool init) { if (super && !UNDEF_P(super)) { - class_switch_superclass(super, klass); + // Only maintain subclass lists for T_CLASS→T_CLASS relationships. + // Include/prepend inserts ICLASSes into the super chain, but T_CLASS + // subclass lists should track only the immutable T_CLASS→T_CLASS link. + if (RB_TYPE_P(klass, T_CLASS) && RB_TYPE_P(super, T_CLASS)) { + class_switch_superclass(super, klass); + } } if (init) { RCLASS_SET_SUPER(klass, super); @@ -2220,19 +2131,17 @@ class_descendants_recursive(VALUE klass, VALUE v) { struct subclass_traverse_data *data = (struct subclass_traverse_data *) v; - if (BUILTIN_TYPE(klass) == T_CLASS && !RCLASS_SINGLETON_P(klass)) { + if (RB_TYPE_P(klass, T_ICLASS)) return; // skip refinement ICLASSes + + if (!RCLASS_SINGLETON_P(klass)) { if (data->buffer && data->count < data->maxcount && !rb_objspace_garbage_object_p(klass)) { // assumes that this does not cause GC as long as the length does not exceed the capacity rb_ary_push(data->buffer, klass); } data->count++; - if (!data->immediate_only) { - rb_class_foreach_subclass(klass, class_descendants_recursive, v); - } - } - else { - rb_class_foreach_subclass(klass, class_descendants_recursive, v); + if (data->immediate_only) return; } + rb_class_foreach_subclass(klass, class_descendants_recursive, v); } static VALUE diff --git a/doc/jit/yjit.md b/doc/jit/yjit.md index c739f8ab4d8b41..c55b42cbdadc2a 100644 --- a/doc/jit/yjit.md +++ b/doc/jit/yjit.md @@ -321,8 +321,8 @@ Some of the counters include: * `:code_gc_count` - number of garbage collections of compiled code since process start * `:vm_insns_count` - number of instructions executed by the Ruby interpreter * `:compiled_iseq_count` - number of bytecode sequences compiled -* `:inline_code_size` - size in bytes of compiled YJIT blocks -* `:outline_code_size` - size in bytes of YJIT error-handling compiled code +* `:inline_code_size` - size in bytes of main-line machine code +* `:outlined_code_size` - size in bytes of relatively uncommonly executed machine code * `:side_exit_count` - number of side exits taken at runtime * `:total_exit_count` - number of exits, including side exits, taken at runtime * `:avg_len_in_yjit` - avg. number of instructions in compiled blocks before exiting to interpreter diff --git a/eval.c b/eval.c index fd370a43ccd2f2..7d5ae75e3e6ee9 100644 --- a/eval.c +++ b/eval.c @@ -1436,6 +1436,8 @@ rb_using_refinement(rb_cref_t *cref, VALUE klass, VALUE module) RCLASS_WRITE_M_TBL(c, RCLASS_M_TBL(module)); + rb_class_subclass_add(klass, iclass); + rb_hash_aset(CREF_REFINEMENTS(cref), klass, iclass); } @@ -1536,10 +1538,12 @@ add_activated_refinement(VALUE activated_refinements, superclass = refinement_superclass(superclass); c = iclass = rb_include_class_new(refinement, superclass); RCLASS_SET_REFINED_CLASS(c, klass); + rb_class_subclass_add(klass, iclass); refinement = RCLASS_SUPER(refinement); while (refinement && refinement != klass) { c = rb_class_set_super(c, rb_include_class_new(refinement, RCLASS_SUPER(c))); RCLASS_SET_REFINED_CLASS(c, klass); + rb_class_subclass_add(klass, c); refinement = RCLASS_SUPER(refinement); } rb_hash_aset(activated_refinements, klass, iclass); diff --git a/gc.c b/gc.c index f22c8ff5346802..9a724709fc4db3 100644 --- a/gc.c +++ b/gc.c @@ -3941,10 +3941,8 @@ update_const_tbl(void *objspace, struct rb_id_table *tbl) static void update_subclasses(void *objspace, rb_classext_t *ext) { - rb_subclass_entry_t *entry; - rb_subclass_anchor_t *anchor = RCLASSEXT_SUBCLASSES(ext); - if (!anchor) return; - entry = anchor->head; + rb_subclass_entry_t *entry = RCLASSEXT_SUBCLASSES(ext); + if (!entry) return; while (entry) { if (entry->klass) UPDATE_IF_MOVED(objspace, entry->klass); diff --git a/internal/class.h b/internal/class.h index ea68b07fc20968..164081b5696fbd 100644 --- a/internal/class.h +++ b/internal/class.h @@ -27,41 +27,6 @@ # undef RCLASS_SUPER #endif -struct rb_box_subclasses { - long refcount; - struct st_table *tbl; -}; -typedef struct rb_box_subclasses rb_box_subclasses_t; - -static inline long -rb_box_subclasses_ref_count(rb_box_subclasses_t *box_sub) -{ - return box_sub->refcount; -} - -static inline rb_box_subclasses_t * -rb_box_subclasses_ref_inc(rb_box_subclasses_t *box_sub) -{ - box_sub->refcount++; - return box_sub; -} - -static inline void -rb_box_subclasses_ref_dec(rb_box_subclasses_t *box_sub) -{ - box_sub->refcount--; - if (box_sub->refcount == 0) { - st_free_table(box_sub->tbl); - xfree(box_sub); - } -} - -struct rb_subclass_anchor { - rb_box_subclasses_t *box_subclasses; - struct rb_subclass_entry *head; -}; -typedef struct rb_subclass_anchor rb_subclass_anchor_t; - struct rb_subclass_entry { VALUE klass; struct rb_subclass_entry *next; @@ -87,24 +52,21 @@ struct rb_classext_struct { struct rb_id_table *cvc_tbl; VALUE *superclasses; /** - * The head of subclasses is a blank (w/o klass) entry to be referred from anchor (and be never deleted). - * (anchor -> head -> 1st-entry) + * The head of the subclasses linked list. This is a dummy entry (klass == 0) + * whose `next` points to the first real entry. Only used in prime classext. */ - struct rb_subclass_anchor *subclasses; + struct rb_subclass_entry *subclasses; /** - * The `box_super_subclasses` points the `box_subclasses` struct to retreive the subclasses - * of the super class in a specific box. - * In compaction GCs, collecting a classext should trigger the deletion of a rb_subclass_entry - * from the super's subclasses. But it may be prevented by the read barrier. - * Fetching the super's subclasses for a ns is to avoid the read barrier in that process. + * Back-pointer to this class's entry in its superclass's subclasses list. + * Used for O(1) removal when the class is freed. */ - rb_box_subclasses_t *box_super_subclasses; + struct rb_subclass_entry *subclass_entry; /** - * In the case that this is an `ICLASS`, `box_module_subclasses` points to the link - * in the module's `subclasses` list that indicates that the klass has been - * included. Hopefully that makes sense. + * In the case that this is an `ICLASS`, `module_subclass_entry` points to the + * entry in the module's `subclasses` list that indicates that the klass has been + * included. Used for O(1) removal. */ - rb_box_subclasses_t *box_module_subclasses; + struct rb_subclass_entry *module_subclass_entry; const VALUE origin_; const VALUE refined_class; @@ -188,8 +150,8 @@ static inline rb_classext_t * RCLASS_EXT_WRITABLE(VALUE obj); #define RCLASSEXT_SUPERCLASS_DEPTH(ext) (ext->superclass_depth) #define RCLASSEXT_SUPERCLASSES(ext) (ext->superclasses) #define RCLASSEXT_SUBCLASSES(ext) (ext->subclasses) -#define RCLASSEXT_BOX_SUPER_SUBCLASSES(ext) (ext->box_super_subclasses) -#define RCLASSEXT_BOX_MODULE_SUBCLASSES(ext) (ext->box_module_subclasses) +#define RCLASSEXT_SUBCLASS_ENTRY(ext) (ext->subclass_entry) +#define RCLASSEXT_MODULE_SUBCLASS_ENTRY(ext) (ext->module_subclass_entry) #define RCLASSEXT_ORIGIN(ext) (ext->origin_) #define RCLASSEXT_REFINED_CLASS(ext) (ext->refined_class) // class.allocator/singleton_class.attached_object are not accessed directly via RCLASSEXT_* @@ -227,8 +189,8 @@ static inline void RCLASSEXT_SET_INCLUDER(rb_classext_t *ext, VALUE klass, VALUE * so always those should be writable. */ #define RCLASS_CVC_TBL(c) (RCLASS_EXT_READABLE(c)->cvc_tbl) -#define RCLASS_SUBCLASSES_X(c) (RCLASS_EXT_READABLE(c)->subclasses) -#define RCLASS_SUBCLASSES_FIRST(c) (RCLASS_EXT_READABLE(c)->subclasses->head->next) +#define RCLASS_SUBCLASSES(c) (RCLASS_EXT_PRIME(c)->subclasses) +#define RCLASS_SUBCLASSES_FIRST(c) (RCLASS_EXT_PRIME(c)->subclasses ? RCLASS_EXT_PRIME(c)->subclasses->next : NULL) #define RCLASS_ORIGIN(c) (RCLASS_EXT_READABLE(c)->origin_) #define RICLASS_IS_ORIGIN_P(c) (RCLASS_EXT_READABLE(c)->iclass_is_origin) #define RCLASS_PERMANENT_CLASSPATH_P(c) (RCLASS_EXT_READABLE(c)->permanent_classpath) @@ -255,7 +217,8 @@ static inline void RCLASSEXT_SET_INCLUDER(rb_classext_t *ext, VALUE klass, VALUE #define RCLASS_WRITABLE_CALLABLE_M_TBL(c) (RCLASS_EXT_WRITABLE(c)->callable_m_tbl) #define RCLASS_WRITABLE_CC_TBL(c) (RCLASS_EXT_WRITABLE(c)->cc_tbl) #define RCLASS_WRITABLE_CVC_TBL(c) (RCLASS_EXT_WRITABLE(c)->cvc_tbl) -#define RCLASS_WRITABLE_SUBCLASSES(c) (RCLASS_EXT_WRITABLE(c)->subclasses) +// Subclasses are only in the prime classext (box-invariant) +#define RCLASS_WRITABLE_SUBCLASSES(c) (RCLASS_EXT_PRIME(c)->subclasses) static inline void RCLASS_SET_SUPER(VALUE klass, VALUE super); static inline void RCLASS_WRITE_SUPER(VALUE klass, VALUE super); @@ -267,9 +230,7 @@ static inline void RCLASS_SET_CVC_TBL(VALUE klass, struct rb_id_table *table); static inline void RCLASS_WRITE_CVC_TBL(VALUE klass, struct rb_id_table *table); static inline void RCLASS_WRITE_SUPERCLASSES(VALUE klass, size_t depth, VALUE *superclasses, bool with_self); -static inline void RCLASS_SET_SUBCLASSES(VALUE klass, rb_subclass_anchor_t *anchor); -static inline void RCLASS_WRITE_BOX_SUPER_SUBCLASSES(VALUE klass, rb_box_subclasses_t *box_subclasses); -static inline void RCLASS_WRITE_BOX_MODULE_SUBCLASSES(VALUE klass, rb_box_subclasses_t *box_subclasses); +static inline void RCLASS_SET_SUBCLASSES(VALUE klass, rb_subclass_entry_t *head); static inline void RCLASS_SET_ORIGIN(VALUE klass, VALUE origin); static inline void RCLASS_WRITE_ORIGIN(VALUE klass, VALUE origin); @@ -488,7 +449,6 @@ RCLASSEXT_SET_INCLUDER(rb_classext_t *ext, VALUE klass, VALUE includer) typedef void rb_class_classext_foreach_callback_func(rb_classext_t *classext, bool is_prime, VALUE box_value, void *arg); void rb_class_classext_foreach(VALUE klass, rb_class_classext_foreach_callback_func *func, void *arg); void rb_class_subclass_add(VALUE super, VALUE klass); -void rb_class_classext_free_subclasses(rb_classext_t *, VALUE, bool); void rb_class_foreach_subclass(VALUE klass, void (*f)(VALUE, VALUE), VALUE); void rb_class_update_superclasses(VALUE); int rb_singleton_class_internal_p(VALUE sklass); @@ -727,28 +687,10 @@ RCLASS_WRITE_SUPERCLASSES(VALUE klass, size_t depth, VALUE *superclasses, bool w } static inline void -RCLASS_SET_SUBCLASSES(VALUE klass, struct rb_subclass_anchor *anchor) +RCLASS_SET_SUBCLASSES(VALUE klass, rb_subclass_entry_t *head) { rb_classext_t *ext = RCLASS_EXT_PRIME(klass); - RCLASSEXT_SUBCLASSES(ext) = anchor; -} - -static inline void -RCLASS_WRITE_BOX_SUPER_SUBCLASSES(VALUE klass, rb_box_subclasses_t *box_subclasses) -{ - rb_classext_t *ext = RCLASS_EXT_WRITABLE(klass); - if (RCLASSEXT_BOX_SUPER_SUBCLASSES(ext)) - rb_box_subclasses_ref_dec(RCLASSEXT_BOX_SUPER_SUBCLASSES(ext)); - RCLASSEXT_BOX_SUPER_SUBCLASSES(ext) = rb_box_subclasses_ref_inc(box_subclasses); -} - -static inline void -RCLASS_WRITE_BOX_MODULE_SUBCLASSES(VALUE klass, rb_box_subclasses_t *box_subclasses) -{ - rb_classext_t *ext = RCLASS_EXT_WRITABLE(klass); - if (RCLASSEXT_BOX_MODULE_SUBCLASSES(ext)) - rb_box_subclasses_ref_dec(RCLASSEXT_BOX_MODULE_SUBCLASSES(ext)); - RCLASSEXT_BOX_MODULE_SUBCLASSES(ext) = rb_box_subclasses_ref_inc(box_subclasses); + RCLASSEXT_SUBCLASSES(ext) = head; } static inline void diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb index 090cb7e2cae1c1..49b0f638387d85 100644 --- a/lib/bundler/retry.rb +++ b/lib/bundler/retry.rb @@ -6,6 +6,8 @@ class Retry attr_accessor :name, :total_runs, :current_run class << self + attr_accessor :default_base_delay + def default_attempts default_retries + 1 end @@ -16,11 +18,17 @@ def default_retries end end - def initialize(name, exceptions = nil, retries = self.class.default_retries) + # Set default base delay for exponential backoff + self.default_base_delay = 1.0 + + def initialize(name, exceptions = nil, retries = self.class.default_retries, opts = {}) @name = name @retries = retries @exceptions = Array(exceptions) || [] @total_runs = @retries + 1 # will run once, then upto attempts.times + @base_delay = opts[:base_delay] || self.class.default_base_delay + @max_delay = opts[:max_delay] || 60.0 + @jitter = opts[:jitter] || 0.5 end def attempt(&block) @@ -48,9 +56,27 @@ def fail_attempt(e) Bundler.ui.info "" unless Bundler.ui.debug? raise e end - return true unless name - Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this - Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true + if name + Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this + Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true + end + backoff_sleep if @base_delay > 0 + true + end + + def backoff_sleep + # Exponential backoff: delay = base_delay * 2^(attempt - 1) + # Add jitter to prevent thundering herd: random value between 0 and jitter seconds + delay = @base_delay * (2**(@current_run - 1)) + delay = [@max_delay, delay].min + jitter_amount = rand * @jitter + total_delay = delay + jitter_amount + Bundler.ui.debug "Sleeping for #{total_delay.round(2)} seconds before retry" + sleep(total_delay) + end + + def sleep(duration) + Kernel.sleep(duration) end def keep_trying? diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb index 7481622a967d9d..5c84d0bea5c864 100644 --- a/spec/bundler/bundler/retry_spec.rb +++ b/spec/bundler/bundler/retry_spec.rb @@ -78,4 +78,113 @@ end end end + + context "exponential backoff" do + it "can be disabled by setting base_delay to 0" do + attempts = 0 + expect do + Bundler::Retry.new("test", [], 2, base_delay: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + # Verify no sleep was called (implicitly - if sleep was called, timing would be different) + expect(attempts).to eq(3) + end + + it "is enabled by default with 1 second base delay" do + original_base_delay = Bundler::Retry.default_base_delay + Bundler::Retry.default_base_delay = 1.0 + + attempts = 0 + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, jitter: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + expect(attempts).to eq(3) + expect(sleep_times.length).to eq(2) + # First retry: 1.0 * 2^0 = 1.0 + expect(sleep_times[0]).to eq(1.0) + # Second retry: 1.0 * 2^1 = 2.0 + expect(sleep_times[1]).to eq(2.0) + ensure + Bundler::Retry.default_base_delay = original_base_delay + end + + it "sleeps with exponential backoff when base_delay is set" do + attempts = 0 + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0).attempt do + attempts += 1 + raise "error" + end + end.to raise_error(StandardError) + + expect(attempts).to eq(3) + expect(sleep_times.length).to eq(2) + # First retry: 1.0 * 2^0 = 1.0 + expect(sleep_times[0]).to eq(1.0) + # Second retry: 1.0 * 2^1 = 2.0 + expect(sleep_times[1]).to eq(2.0) + end + + it "respects max_delay" do + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 3, base_delay: 10.0, max_delay: 15.0, jitter: 0).attempt do + raise "error" + end + end.to raise_error(StandardError) + + # First retry: 10.0 * 2^0 = 10.0 + expect(sleep_times[0]).to eq(10.0) + # Second retry: 10.0 * 2^1 = 20.0, capped at 15.0 + expect(sleep_times[1]).to eq(15.0) + # Third retry: 10.0 * 2^2 = 40.0, capped at 15.0 + expect(sleep_times[2]).to eq(15.0) + end + + it "adds jitter to delay" do + sleep_times = [] + + allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay| + sleep_times << delay + end + + expect do + Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0.5).attempt do + raise "error" + end + end.to raise_error(StandardError) + + expect(sleep_times.length).to eq(2) + # First retry should be between 1.0 and 1.5 (base + jitter) + expect(sleep_times[0]).to be >= 1.0 + expect(sleep_times[0]).to be <= 1.5 + # Second retry should be between 2.0 and 2.5 + expect(sleep_times[1]).to be >= 2.0 + expect(sleep_times[1]).to be <= 2.5 + end + end end diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index f3ff8f8fd17d6d..0ea191751dd042 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -116,6 +116,9 @@ def self.ruby=(ruby) require_relative "support/rubygems_ext" Spec::Rubygems.test_setup + # Disable retry delays in tests to speed them up + Bundler::Retry.default_base_delay = 0 + # Simulate bundler has not yet been loaded ENV.replace(ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) }) diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index bb98a2efbe57ca..5da29b497136e7 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -871,4 +871,22 @@ class IMath2; include Math; end end end; end + + def test_user_box_iclass_with_module_modified_in_another_box + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + # A user box creates a class that includes a core module. + # The ICLASS is allocated in the user box context (non-boxable). + box1 = Ruby::Box.new + box1.eval("class IMath; include Math; end") + + # A second user box adds an instance method on that module, + # triggering classext duplication which iterates the module's + # subclass list and encounters box1's non-boxable ICLASS. + box2 = Ruby::Box.new + box2.eval("module Math; def box2_test = :box2; end") + + assert_equal :box2, box2.eval("Class.new { include Math }.new.box2_test") + end; + end end diff --git a/variable.c b/variable.c index a3cc7b6e37a4fd..3576ff00041b47 100644 --- a/variable.c +++ b/variable.c @@ -4240,8 +4240,9 @@ find_cvar(VALUE klass, VALUE * front, VALUE * target, ID id) static void check_for_cvar_table(VALUE subclass, VALUE key) { - // Must not check ivar on ICLASS - if (!RB_TYPE_P(subclass, T_ICLASS) && RTEST(rb_ivar_defined(subclass, key))) { + if (RB_TYPE_P(subclass, T_ICLASS)) return; // skip refinement ICLASSes + + if (RTEST(rb_ivar_defined(subclass, key))) { RB_DEBUG_COUNTER_INC(cvar_class_invalidate); ruby_vm_global_cvar_state++; return; diff --git a/vm_method.c b/vm_method.c index bc9c4c1dd3e29f..30ba186e9e19a4 100644 --- a/vm_method.c +++ b/vm_method.c @@ -439,7 +439,11 @@ clear_method_cache_by_id_in_class(VALUE klass, ID mid) RB_VM_LOCKING() { rb_vm_barrier(); - if (LIKELY(RCLASS_SUBCLASSES_FIRST(klass) == NULL)) { + if (LIKELY(RCLASS_SUBCLASSES_FIRST(klass) == NULL) && + // Non-refinement ICLASSes (from module inclusion) previously had + // subclasses reparented onto them, so they need the tree path for + // broader cme-based invalidation even though they now have no subclasses. + !(RB_TYPE_P(klass, T_ICLASS) && NIL_P(RCLASS_REFINED_CLASS(klass)))) { // no subclasses // check only current class @@ -1334,6 +1338,16 @@ rb_add_refined_method_entry(VALUE refined_class, ID mid) static void check_override_opt_method_i(VALUE klass, VALUE arg) { + if (RB_TYPE_P(klass, T_ICLASS)) { + // ICLASS from a module's subclass list: check the includer and + // recurse into the includer's T_CLASS subclasses. + VALUE includer = RCLASS_INCLUDER(klass); + if (!UNDEF_P(includer) && includer) { + check_override_opt_method_i(includer, arg); + } + return; + } + ID mid = (ID)arg; const rb_method_entry_t *me, *newme;