From 6d15e7be7c6319919825cd1eb48f1bf7d67cd4b9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 9 Sep 2026 10:41:36 -0400 Subject: [PATCH 1/8] ZJIT: Add support for once instruction (#18617) Much like `opt_getconstant_path`, check the cache and see if the interpreter has done the hard work for us already. If it has, just re-use the result. For example, support regexp modifier `o` , which runs the code to create the regular expression... once: ```ruby def test = /#{'a'.upcase}/o ``` --- vm_insnhelper.c | 14 ++++++++ zjit.c | 1 + zjit/bindgen/src/main.rs | 1 + zjit/src/cruby_bindings.inc.rs | 2 ++ zjit/src/hir.rs | 15 +++++++++ zjit/src/hir/tests.rs | 61 ++++++++++++++++++++++++++++++++++ zjit/src/stats.rs | 2 ++ 7 files changed, 96 insertions(+) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 05c9bf75b7753f..fa9df3eb9dfc10 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6783,6 +6783,20 @@ rb_vm_opt_getconstant_path(rb_execution_context_t *ec, rb_control_frame_t *const return val; } +// Return true if the once value is already computed and set *result to the value. +// Otherwise, return false. +// Used for ZJIT. Keep in sync with `vm_once_dispatch` below. +bool +rb_vm_once_done_value(ISE is, VALUE *result) +{ + rb_thread_t *running_th = rbimpl_atomic_ptr_load((void**)&is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th == RUNNING_THREAD_ONCE_DONE) { + *result = is->once.value; + return true; + } + return false; +} + static VALUE vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) { diff --git a/zjit.c b/zjit.c index 3e3c6e9c6dce5a..741c99c12a0b64 100644 --- a/zjit.c +++ b/zjit.c @@ -333,6 +333,7 @@ rb_zjit_class_has_default_allocator(VALUE klass) VALUE rb_vm_untag_block_handler(VALUE block_handler); VALUE rb_vm_get_untagged_block_handler(rb_control_frame_t *reg_cfp); +bool rb_vm_once_done_value(ISE is, VALUE *result); // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_enable(rb_execution_context_t *ec, VALUE self); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 9463da3070bdc4..6609d20761471e 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -276,6 +276,7 @@ fn main() { .allowlist_function("rb_callable_method_entry") .allowlist_function("rb_callable_method_entry_or_negative") .allowlist_function("rb_vm_frame_method_entry") + .allowlist_function("rb_vm_once_done_value") .allowlist_type("IVC") // pointer to iseq_inline_iv_cache_entry .allowlist_type("IC") // pointer to iseq_inline_constant_cache .allowlist_type("iseq_inline_constant_cache_entry") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 1a0532ceb77390..372a86f1d415ae 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1649,6 +1649,7 @@ pub type vm_special_object_type = u32; pub type IC = *mut iseq_inline_constant_cache; pub type IVC = *mut iseq_inline_iv_cache_entry; pub type ICVARC = *mut iseq_inline_cvar_cache_entry; +pub type ISE = *mut iseq_inline_storage_entry; pub const VM_FRAME_MAGIC_METHOD: vm_frame_env_flags = 286326785; pub const VM_FRAME_MAGIC_BLOCK: vm_frame_env_flags = 572653569; pub const VM_FRAME_MAGIC_CLASS: vm_frame_env_flags = 858980353; @@ -2476,6 +2477,7 @@ unsafe extern "C" { pub fn rb_zjit_class_has_default_allocator(klass: VALUE) -> bool; pub fn rb_vm_untag_block_handler(block_handler: VALUE) -> VALUE; pub fn rb_vm_get_untagged_block_handler(reg_cfp: *mut rb_control_frame_t) -> VALUE; + pub fn rb_vm_once_done_value(is: ISE, result: *mut VALUE) -> bool; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e8df6678717c1c..23dddf35e6133f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -673,6 +673,7 @@ pub enum SideExitReason { NoProfileGetIvar, NoProfileSetIvar, InvokeBlockNotIfunc, + OnceNotDone, } /// Marks a side exit as triggering profiling and recompilation. @@ -9404,6 +9405,20 @@ fn add_iseq_to_hir( } } } + YARVINSN_once => { + let iseq: *const rb_iseq_t = get_arg(pc, 0).as_ptr(); + let ise: *mut iseq_inline_storage_entry = get_arg(pc, 1).as_mut_ptr(); + debug_assert!(!iseq.is_null()); + debug_assert!(!ise.is_null()); + let mut value = Qnil; + if unsafe { rb_vm_once_done_value(ise, &mut value) } { + let val = fun.push_insn(block, Insn::Const { val: Const::Value(value) }); + state.stack_push(val); + } else { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: Box::new(SideExitReason::OnceNotDone), recompile: Some(Recompile) }); + break; // End the block + } + } YARVINSN_branchunless | YARVINSN_branchunless_without_ints => { let offset = get_arg(pc, 0).as_i64(); if opcode == YARVINSN_branchunless && offset < 0 { diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index bbd0dfadb2e140..9758a5763c95a7 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -6273,6 +6273,67 @@ pub(crate) mod hir_build_tests { Return v22 "); } + + #[test] + fn test_once_not_done_side_exits() { + eval(" + def test = /#{'a'.upcase}/o + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + SideExit OnceNotDone recompile + "); + // Running the method fills in the once cache, so a recompile sees the + // cached value instead of side-exiting. + eval("test"); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:RegexpExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_once_done_returns_value() { + eval(" + def test = /#{'a'.upcase}/o + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:RegexpExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v10 + "); + } } /// Test successor and predecessor set computations. diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 6bf4b673f32638..23eb2acd4650f5 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -252,6 +252,7 @@ make_counters! { exit_directive_induced, exit_send_while_tracing, exit_invokeblock_not_ifunc, + exit_once_not_done, } // Send fallback counters that are summed as dynamic_send_count @@ -674,6 +675,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { NoProfileGetIvar => exit_no_profile_getivar, NoProfileSetIvar => exit_no_profile_setivar, InvokeBlockNotIfunc => exit_invokeblock_not_ifunc, + OnceNotDone => exit_once_not_done, } } From d279f27d63c7190b14c1d9d7cd4dbc0e8e52da69 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 2 Sep 2026 16:48:37 -0700 Subject: [PATCH 2/8] Generate ISEQ_TRANSLATED with bindgen for YJIT ISEQ_TRANSLATED is defined through a chain of macros that ends in an enum value (IMEMO_FL_USER3 -> FL_USER8 -> RUBY_FL_USER8), which bindgen cannot evaluate, so YJIT and ZJIT had been defining the constant manually in cruby.rs. ZJIT doesn't even use the constant, so drop it there, and re-expose it for YJIT through enum yjit_bindgen_constants in yjit.c so that the Rust side can no longer go out of sync with iseq.h. https://github.com/ruby/ruby/pull/18597#discussion_r3914719597 --- yjit.c | 6 ++++++ yjit/bindgen/src/main.rs | 1 + yjit/src/cruby.rs | 3 --- yjit/src/cruby_bindings.inc.rs | 2 ++ yjit/src/invariants.rs | 2 +- zjit/src/cruby.rs | 3 --- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/yjit.c b/yjit.c index d59bfaa38108ee..5e08b31afc880b 100644 --- a/yjit.c +++ b/yjit.c @@ -49,6 +49,12 @@ STATIC_ASSERT(size_t_no_padding_bits, sizeof(size_t) == sizeof(uint64_t)); // support one scheme for simplicity. STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM); +enum yjit_bindgen_constants { + // ISEQ_TRANSLATED expands to an enum value through a chain of macros, + // which bindgen cannot evaluate, so it needs to be re-exposed here. + YJIT_ISEQ_TRANSLATED = ISEQ_TRANSLATED, +}; + // NOTE: We can trust that uint8_t has no "padding bits" since the C spec // guarantees it. Wording about padding bits is more explicit in C11 compared // to C99. See C11 7.20.1.1p2. All this is to say we have _some_ standards backing to diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index ff9e587484a1e3..d8230ee5f8ba20 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -284,6 +284,7 @@ fn main() { .allowlist_function("rb_jit_vm_unlock") .allowlist_function("rb_jit_for_each_iseq") .allowlist_type("jit_bindgen_constants") + .allowlist_type("yjit_bindgen_constants") .allowlist_function("rb_vm_barrier") .allowlist_function("rb_yjit_cdhash_all_fixnum_p") .allowlist_function("rb_yjit_cdhash_lookup") diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index dc8b3200aa9fd5..55b0eec92f2d44 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -753,9 +753,6 @@ mod manual_defs { // From internal/struct.h - in anonymous enum, so we can't easily import it pub const RSTRUCT_EMBED_LEN_MASK: usize = (RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 |RUBY_FL_USER2 | RUBY_FL_USER1) as usize; - // From iseq.h - via a different constant, which seems to confuse bindgen - pub const ISEQ_TRANSLATED: usize = RUBY_FL_USER8 as usize; - // We'll need to encode a lot of Ruby struct/field offsets as constants unless we want to // redeclare all the Ruby C structs and write our own offsetof macro. For now, we use constants. pub const RUBY_OFFSET_RBASIC_FLAGS: i32 = 0; // struct RBasic, field "flags" diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index a5302a9a56b17d..9cba9ab73489d0 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1060,6 +1060,8 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; +pub const YJIT_ISEQ_TRANSLATED: yjit_bindgen_constants = 1048576; +pub type yjit_bindgen_constants = u32; pub type rb_seq_param_keyword_struct = rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; diff --git a/yjit/src/invariants.rs b/yjit/src/invariants.rs index 68eb84604259ff..784629b2e26a61 100644 --- a/yjit/src/invariants.rs +++ b/yjit/src/invariants.rs @@ -504,7 +504,7 @@ pub extern "C" fn rb_yjit_constant_ic_update(iseq: *const rb_iseq_t, ic: IC, ins // This should come from a running iseq, so direct threading translation // should have been done - assert!(unsafe { FL_TEST(iseq.into(), VALUE(ISEQ_TRANSLATED)) } != VALUE(0)); + assert!(unsafe { FL_TEST(iseq.into(), VALUE(YJIT_ISEQ_TRANSLATED as usize)) } != VALUE(0)); assert!(u32::from(insn_idx) < unsafe { get_iseq_encoded_size(iseq) }); // Ensure that the instruction the insn_idx is pointing to is in diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 2111266500df4a..6bc7629819bd42 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1223,9 +1223,6 @@ mod manual_defs { // From internal/struct.h - in anonymous enum, so we can't easily import it pub const RSTRUCT_EMBED_LEN_MASK: usize = (RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 |RUBY_FL_USER2 | RUBY_FL_USER1) as usize; - // From iseq.h - via a different constant, which seems to confuse bindgen - pub const ISEQ_TRANSLATED: usize = RUBY_FL_USER8 as usize; - // We'll need to encode a lot of Ruby struct/field offsets as constants unless we want to // redeclare all the Ruby C structs and write our own offsetof macro. For now, we use constants. pub const RUBY_OFFSET_RBASIC_FLAGS: i32 = 0; // struct RBasic, field "flags" From 0c6ae574be422771d27200ce16553d5a522971f6 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 3 Sep 2026 09:53:15 -0700 Subject: [PATCH 3/8] Generate RSTRUCT_EMBED_LEN_MASK with bindgen for YJIT and ZJIT --- internal/struct.h | 2 +- jit.c | 1 + yjit/bindgen/src/main.rs | 1 + yjit/src/codegen.rs | 2 +- yjit/src/cruby.rs | 3 --- yjit/src/cruby_bindings.inc.rs | 3 +++ zjit/bindgen/src/main.rs | 1 + zjit/src/cruby.rs | 5 +---- zjit/src/cruby_bindings.inc.rs | 3 +++ 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/struct.h b/internal/struct.h index fc1041bf1852da..c081bdba377ac5 100644 --- a/internal/struct.h +++ b/internal/struct.h @@ -18,7 +18,7 @@ * header, rather than being on a separately allocated buffer) and * these bits are the length of the Struct. */ -enum { +enum ruby_rstruct_flags { RSTRUCT_EMBED_LEN_MASK = RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 | RUBY_FL_USER2 | RUBY_FL_USER1, RSTRUCT_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+1), diff --git a/jit.c b/jit.c index e0866817f35c45..7f6f161f2f4cdf 100644 --- a/jit.c +++ b/jit.c @@ -20,6 +20,7 @@ #include "internal/string.h" #include "internal/class.h" #include "internal/imemo.h" +#include "internal/struct.h" #include "ruby/internal/core/rtypeddata.h" #include "zjit.h" diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index d8230ee5f8ba20..f81394a40c530a 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -76,6 +76,7 @@ fn main() { .allowlist_type("RBasic") .allowlist_type("ruby_rstring_flags") + .allowlist_type("ruby_rstruct_flags") // This function prints info about a value and is useful for debugging .allowlist_function("rb_obj_info_dump") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index a1ee87e2f29e9e..ac1af28d11097a 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -8990,7 +8990,7 @@ fn gen_struct_aref( // length. So if our comptime_recv is embedded all runtime // structs of the same class and shape_id should be as well, and the same is // true of the converse. - let embedded = unsafe { FL_TEST_RAW(comptime_recv, VALUE(RSTRUCT_EMBED_LEN_MASK)) }; + let embedded = unsafe { FL_TEST_RAW(comptime_recv, VALUE(RSTRUCT_EMBED_LEN_MASK as usize)) }; asm_comment!(asm, "struct aref"); diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index 55b0eec92f2d44..7f1826fa0cbf8a 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -750,9 +750,6 @@ mod manual_defs { pub const VM_CALL_ZSUPER : u32 = 1 << VM_CALL_ZSUPER_bit; pub const VM_CALL_OPT_SEND : u32 = 1 << VM_CALL_OPT_SEND_bit; - // From internal/struct.h - in anonymous enum, so we can't easily import it - pub const RSTRUCT_EMBED_LEN_MASK: usize = (RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 |RUBY_FL_USER2 | RUBY_FL_USER1) as usize; - // We'll need to encode a lot of Ruby struct/field offsets as constants unless we want to // redeclare all the Ruby C structs and write our own offsetof macro. For now, we use constants. pub const RUBY_OFFSET_RBASIC_FLAGS: i32 = 0; // struct RBasic, field "flags" diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 9cba9ab73489d0..df022439bb8e19 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1064,6 +1064,9 @@ pub const YJIT_ISEQ_TRANSLATED: yjit_bindgen_constants = 1048576; pub type yjit_bindgen_constants = u32; pub type rb_seq_param_keyword_struct = rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword; +pub const RSTRUCT_EMBED_LEN_MASK: ruby_rstruct_flags = 1040384; +pub const RSTRUCT_EMBED_LEN_SHIFT: ruby_rstruct_flags = 13; +pub type ruby_rstruct_flags = u32; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 6609d20761471e..fdb2e3bf6640a1 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -100,6 +100,7 @@ fn main() { .allowlist_var("RB_GC_ZJIT_FASTPATH_.*") .allowlist_type("ruby_rstring_flags") + .allowlist_type("ruby_rstruct_flags") // This function prints info about a value and is useful for debugging .allowlist_function("rb_raw_obj_info") diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 6bc7629819bd42..1a7a17d674006a 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -678,7 +678,7 @@ impl VALUE { pub fn struct_embedded_p(self) -> bool { unsafe { RB_TYPE_P(self, RUBY_T_STRUCT) && - FL_TEST_RAW(self, VALUE(RSTRUCT_EMBED_LEN_MASK)) != VALUE(0) + FL_TEST_RAW(self, VALUE(RSTRUCT_EMBED_LEN_MASK as usize)) != VALUE(0) } } @@ -1220,9 +1220,6 @@ mod manual_defs { pub const VM_CALL_ZSUPER : u32 = 1 << VM_CALL_ZSUPER_bit; pub const VM_CALL_OPT_SEND : u32 = 1 << VM_CALL_OPT_SEND_bit; - // From internal/struct.h - in anonymous enum, so we can't easily import it - pub const RSTRUCT_EMBED_LEN_MASK: usize = (RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 |RUBY_FL_USER2 | RUBY_FL_USER1) as usize; - // We'll need to encode a lot of Ruby struct/field offsets as constants unless we want to // redeclare all the Ruby C structs and write our own offsetof macro. For now, we use constants. pub const RUBY_OFFSET_RBASIC_FLAGS: i32 = 0; // struct RBasic, field "flags" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 372a86f1d415ae..7e64476d1aadb9 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2114,6 +2114,9 @@ pub struct rb_zjit_runtime_offsets { pub ractor_newobj_cache: i32, pub ractor_objspace: i32, } +pub const RSTRUCT_EMBED_LEN_MASK: ruby_rstruct_flags = 1040384; +pub const RSTRUCT_EMBED_LEN_SHIFT: ruby_rstruct_flags = 13; +pub type ruby_rstruct_flags = u32; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; From ab75c6508f7890fd0f09980a853a45e443ebca97 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 9 Sep 2026 09:46:20 -0700 Subject: [PATCH 4/8] Bindgen RSTRUCT_EMBED_LEN_MASK as usize --- yjit/bindgen/src/main.rs | 23 ++++++++++++++++++++--- yjit/src/codegen.rs | 2 +- yjit/src/cruby_bindings.inc.rs | 2 +- zjit/bindgen/src/main.rs | 23 ++++++++++++++--------- zjit/src/cruby.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- 6 files changed, 38 insertions(+), 16 deletions(-) diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index f81394a40c530a..e871924a7e4878 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -409,12 +409,29 @@ fn main() { // Unwrap the Result and panic on failure. .expect("Unable to generate bindings"); + // Write to a Vec for post-processing + let mut bindings_string = Vec::new(); + bindings.write(Box::new(&mut bindings_string)).expect("Couldn't write bindings!"); + let mut bindings_string = String::from_utf8(bindings_string).expect("bindings should be UTF-8"); + + // Give some generated type aliases an integer type that is nicer to use from + // Rust than the one bindgen derives from C. + const TYPE_REPLACEMENTS: &[(&str, &str)] = &[ + // usize is what VALUE() takes, so flag masks need no cast at their use sites + ("pub type ruby_rstruct_flags = u32;", "pub type ruby_rstruct_flags = usize;"), + ]; + // Each needle is a whole line of the output, so plain replacement is unambiguous. + // Yes, this search-and-replace could be faster, but it's a small file. + for (needle, replacement) in TYPE_REPLACEMENTS { + assert!(bindings_string.contains(needle), "no line to replace: {needle}"); + bindings_string = bindings_string.replace(needle, replacement); + } + + // Write out to file let mut out_path: PathBuf = src_root; out_path.push("yjit"); out_path.push("src"); out_path.push("cruby_bindings.inc.rs"); - bindings - .write_to_file(out_path) - .expect("Couldn't write bindings!"); + std::fs::write(out_path, bindings_string).expect("file output failed"); } diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index ac1af28d11097a..a1ee87e2f29e9e 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -8990,7 +8990,7 @@ fn gen_struct_aref( // length. So if our comptime_recv is embedded all runtime // structs of the same class and shape_id should be as well, and the same is // true of the converse. - let embedded = unsafe { FL_TEST_RAW(comptime_recv, VALUE(RSTRUCT_EMBED_LEN_MASK as usize)) }; + let embedded = unsafe { FL_TEST_RAW(comptime_recv, VALUE(RSTRUCT_EMBED_LEN_MASK)) }; asm_comment!(asm, "struct aref"); diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index df022439bb8e19..2b688521c80936 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1066,7 +1066,7 @@ pub type rb_seq_param_keyword_struct = rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword; pub const RSTRUCT_EMBED_LEN_MASK: ruby_rstruct_flags = 1040384; pub const RSTRUCT_EMBED_LEN_SHIFT: ruby_rstruct_flags = 13; -pub type ruby_rstruct_flags = u32; +pub type ruby_rstruct_flags = usize; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index fdb2e3bf6640a1..f1495d92a6f289 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -500,16 +500,21 @@ fn main() { // Write to a Vec for post-processing let mut bindings_string = Vec::new(); bindings.write(Box::new(&mut bindings_string)).expect("Couldn't write bindings!"); - - // Use i32 for this type since that's what the assembler APIs expect - const JIT_CONSTANTS_NEEDLE: &[u8] = b"pub type jit_bindgen_constants = u32;"; - const JIT_CONSTANTS_REPLACEMENT: &[u8] = b"pub type jit_bindgen_constants = i32;"; + let mut bindings_string = String::from_utf8(bindings_string).expect("bindings should be UTF-8"); + + // Give some generated type aliases an integer type that is nicer to use from + // Rust than the one bindgen derives from C. + const TYPE_REPLACEMENTS: &[(&str, &str)] = &[ + // i32 is what the assembler APIs expect + ("pub type jit_bindgen_constants = u32;", "pub type jit_bindgen_constants = i32;"), + // usize is what VALUE() takes, so flag masks need no cast at their use sites + ("pub type ruby_rstruct_flags = u32;", "pub type ruby_rstruct_flags = usize;"), + ]; + // Each needle is a whole line of the output, so plain replacement is unambiguous. // Yes, this search-and-replace could be faster, but it's a small file. - for line in bindings_string.as_mut_slice().split_mut(|&byte| byte == b'\n') { - if line == JIT_CONSTANTS_NEEDLE { - line.copy_from_slice(JIT_CONSTANTS_REPLACEMENT); - break; - } + for (needle, replacement) in TYPE_REPLACEMENTS { + assert!(bindings_string.contains(needle), "no line to replace: {needle}"); + bindings_string = bindings_string.replace(needle, replacement); } // Write out to file diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 1a7a17d674006a..9212fbc89f9b3f 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -678,7 +678,7 @@ impl VALUE { pub fn struct_embedded_p(self) -> bool { unsafe { RB_TYPE_P(self, RUBY_T_STRUCT) && - FL_TEST_RAW(self, VALUE(RSTRUCT_EMBED_LEN_MASK as usize)) != VALUE(0) + FL_TEST_RAW(self, VALUE(RSTRUCT_EMBED_LEN_MASK)) != VALUE(0) } } diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 7e64476d1aadb9..a6a826514a65cd 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2116,7 +2116,7 @@ pub struct rb_zjit_runtime_offsets { } pub const RSTRUCT_EMBED_LEN_MASK: ruby_rstruct_flags = 1040384; pub const RSTRUCT_EMBED_LEN_SHIFT: ruby_rstruct_flags = 13; -pub type ruby_rstruct_flags = u32; +pub type ruby_rstruct_flags = usize; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; From 949a631bd03fb6354a8470699df02b5b0c172254 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Fri, 17 Apr 2026 17:15:16 +0100 Subject: [PATCH 5/8] [ruby/rubygems] Implement exact-match flag for bundle info / show / open https://github.com/ruby/rubygems/commit/16bfc52f9d --- lib/bundler/cli.rb | 3 +++ lib/bundler/cli/common.rb | 4 ++++ lib/bundler/cli/info.rb | 2 +- lib/bundler/cli/open.rb | 8 +++++++- lib/bundler/cli/show.rb | 6 +++++- lib/bundler/man/bundle-info.1 | 5 ++++- lib/bundler/man/bundle-info.1.ronn | 4 ++++ lib/bundler/man/bundle-open.1 | 5 ++++- lib/bundler/man/bundle-open.1.ronn | 5 ++++- lib/bundler/man/bundle-show.1 | 5 ++++- lib/bundler/man/bundle-show.1.ronn | 4 ++++ spec/bundler/commands/info_spec.rb | 30 ++++++++++++++++++++++++++++-- spec/bundler/commands/open_spec.rb | 29 +++++++++++++++++++++++++++++ spec/bundler/commands/show_spec.rb | 29 +++++++++++++++++++++++++++-- 14 files changed, 128 insertions(+), 11 deletions(-) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 9655a8ad1b7de1..d7c61b3066e69e 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -342,6 +342,7 @@ def update(*gems) D method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile." method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def show(gem_name = nil) if ARGV.include?("--outdated") removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`" @@ -367,6 +368,7 @@ def list desc "info GEM [OPTIONS]", "Show information for the given gem" method_option "path", type: :boolean, banner: "Print full path to gem" method_option "version", type: :boolean, banner: "Print gem version" + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def info(gem_name) require_relative "cli/info" Info.new(options, gem_name).run @@ -530,6 +532,7 @@ def exec(*args) desc "open GEM", "Opens the source directory of the given bundled gem" method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def open(name) require_relative "cli/open" Open.new(options, name).run diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 1f66a113c2abdc..4e5d85e903b2df 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -122,6 +122,10 @@ def self.select_spec(name, regex_match = nil) raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) end + def self.select_spec_with_match_type(name, options) + select_spec(name, options["exact-match"] ? nil : :regex_match) + end + def self.default_gem_spec(name) gem_spec = Gem::Specification.find_all_by_name(name).last gem_spec if gem_spec&.default_gem? diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index cd01d4949bc98b..a0dd5a54876a1c 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -26,7 +26,7 @@ def run private def spec_for_gem(name) - Bundler::CLI::Common.select_spec(name, :regex_match) + Bundler::CLI::Common.select_spec_with_match_type(name, options) end def print_gem_version(spec) diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb index 64d44e6a0b7c0e..f6aeecf8139a0f 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -13,7 +13,7 @@ def run raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty? editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor - return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) + return unless spec = spec_for_gem(name) if spec.default_gem? Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist." else @@ -35,5 +35,11 @@ def editor_command(editor) require "shellwords" Shellwords.split(editor) end + + private + + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end end end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 67fdcc797e8592..6e88bd476515da 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -20,7 +20,7 @@ def run if gem_name == "bundler" path = File.expand_path("../../..", __dir__) else - spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) + spec = spec_for_gem(gem_name) return unless spec path = spec.full_gem_path unless File.directory?(path) @@ -55,6 +55,10 @@ def run private + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end + def fetch_latest_specs definition = Bundler.definition(true) Bundler.ui.info "Fetching remote specs for outdated check...\n\n" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index 49c2295f8c981f..85bd2dd0c011a6 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" -\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] +\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] [\-\-exact\-match] .SH "DESCRIPTION" Given a gem name present in your bundle, print the basic information about it such as homepage, version, path and summary\. .SH "OPTIONS" @@ -14,4 +14,7 @@ Print the path of the given gem .TP \fB\-\-version\fR Print gem version +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-info.1.ronn b/lib/bundler/man/bundle-info.1.ronn index e99db8c6149c8d..38cd19f4312cb6 100644 --- a/lib/bundler/man/bundle-info.1.ronn +++ b/lib/bundler/man/bundle-info.1.ronn @@ -6,6 +6,7 @@ bundle-info(1) -- Show information for the given gem in your bundle `bundle info` [GEM_NAME] [--path] [--version] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ Given a gem name present in your bundle, print the basic information about it * `--version`: Print gem version + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index 2aab59f14b9b16..1ecb3e4c77a747 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" -\fBbundle open\fR [GEM] [\-\-path=PATH] +\fBbundle open\fR [GEM] [\-\-path=PATH] [\-\-exact\-match] .SH "DESCRIPTION" Opens the source directory of the provided GEM in your editor\. .P @@ -29,4 +29,7 @@ Will open the README\.md file of the 'rack' gem source in your bundle\. .TP \fB\-\-path[=PATH]\fR Specify GEM source relative path to open\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-open.1.ronn b/lib/bundler/man/bundle-open.1.ronn index 24dbe97e44563a..c345b88adc586a 100644 --- a/lib/bundler/man/bundle-open.1.ronn +++ b/lib/bundler/man/bundle-open.1.ronn @@ -3,7 +3,7 @@ bundle-open(1) -- Opens the source directory for a gem in your bundle ## SYNOPSIS -`bundle open` [GEM] [--path=PATH] +`bundle open` [GEM] [--path=PATH] [--exact-match] ## DESCRIPTION @@ -26,3 +26,6 @@ Will open the README.md file of the 'rack' gem source in your bundle. * `--path[=PATH]`: Specify GEM source relative path to open. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index a2142694b8d317..3afbc2c06750d2 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" -\fBbundle show\fR [GEM] [\-\-paths] +\fBbundle show\fR [GEM] [\-\-paths] [\-\-exact\-match] .SH "DESCRIPTION" Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\. .P @@ -13,4 +13,7 @@ Calling show with [GEM] will list the exact location of that gem on your machine .TP \fB\-\-paths\fR List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-show.1.ronn b/lib/bundler/man/bundle-show.1.ronn index a6a59a1445dcd3..2f679e5f7d1f82 100644 --- a/lib/bundler/man/bundle-show.1.ronn +++ b/lib/bundler/man/bundle-show.1.ronn @@ -5,6 +5,7 @@ bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem `bundle show` [GEM] [--paths] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ machine. * `--paths`: List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by gem name. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb index a26b1696fbdf99..317a88863c9eb0 100644 --- a/spec/bundler/commands/info_spec.rb +++ b/spec/bundler/commands/info_spec.rb @@ -207,7 +207,31 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info myrack --exact-match" + expect(out).to include("* myrack (1.0.0)") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -215,7 +239,9 @@ G bundle "info rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb index 664dc58919cf62..085105b8c19bcc 100644 --- a/spec/bundler/commands/open_spec.rb +++ b/spec/bundler/commands/open_spec.rb @@ -172,4 +172,33 @@ expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.") end end + + context "with a valid regexp for gem name" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + end + + it "returns the exact match without prompting when requested" do + bundle "open myrack --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "myrack-1.0.0")}") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + bundle "open rac --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do + bundle "open rac", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") + end + end end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb index b2475f91e37edf..75d46587e4ca4c 100644 --- a/spec/bundler/commands/show_spec.rb +++ b/spec/bundler/commands/show_spec.rb @@ -191,7 +191,30 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show myrack --exact-match" + expect(out).to include(default_bundle_path("gems", "myrack-1.0.0").to_s) + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -199,7 +222,9 @@ G bundle "show rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end From 3345cedf99d319e4337d78cd76c9279e68b65d41 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 27 Aug 2026 01:51:27 +0000 Subject: [PATCH 6/8] gc: time a collection on the collecting thread, not the whole process gc_clock_start/end read CLOCK_PROCESS_CPUTIME_ID, which is the sum of every thread's cpu time. A local GC runs while the other Ractors keep going, so their work is counted as collection: the same ten collections of the same objects in one Ractor are reported as 3ms alone and 146ms with eight unrelated busy Ractors -- 96% of the wall clock that Ractor ran in, and in another run more than the wall clock. Read the collecting thread's own cpu instead. A local GC runs to its end on one thread, and a global one stops the others, so this is what the number was always meant to be. With the fix the same measurement reads 3 / 4 / 6 / 18ms, the remaining growth being collections that genuinely cost more on a busy machine. The read also gets cheaper. GC.measure_total_time is on by default, so every program pays for four of these reads per collection, and neither cpu clock is served by the vDSO: both are real syscalls. Reading CLOCK_PROCESS_CPUTIME_ID makes the kernel walk every thread in the process, so it costs 477ns with one thread and 3.6us with 256, while CLOCK_THREAD_CPUTIME_ID is 454ns whatever the count (idle Ryzen 9 8945HS; the threads were asleep, they only have to exist, not run). Co-Authored-By: Claude Opus 5 --- gc/default/default.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 41417f13f377b2..139fb142c0b24b 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -8586,10 +8586,32 @@ gc_enter_count(enum gc_enter_event event) static bool current_process_time(struct timespec *ts); +/* A gc phase must be timed on the collecting thread's own cpu. A local gc runs + * while the other ractors keep going, and process cpu time counts their work as + * gc: with eight busy ractors the same ten collections were reported as 131ms + * instead of 3ms, more than the wall clock they ran in. The kernel also answers + * this one without walking every thread in the process. */ +static bool +current_thread_time(struct timespec *ts) +{ +#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID) + { + static int try_clock_gettime = 1; + if (try_clock_gettime && clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) == 0) { + return true; + } + else { + try_clock_gettime = 0; + } + } +#endif + return current_process_time(ts); +} + static void gc_clock_start(struct timespec *ts) { - if (!current_process_time(ts)) { + if (!current_thread_time(ts)) { ts->tv_sec = 0; ts->tv_nsec = 0; } @@ -8601,7 +8623,7 @@ gc_clock_end(struct timespec *ts) struct timespec end_time; if ((ts->tv_sec > 0 || ts->tv_nsec > 0) && - current_process_time(&end_time) && + current_thread_time(&end_time) && end_time.tv_sec >= ts->tv_sec) { return (unsigned long long)(end_time.tv_sec - ts->tv_sec) * (1000 * 1000 * 1000) + (end_time.tv_nsec - ts->tv_nsec); From 3300f1e9097c038da4c06b657b6885e468a8097d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 9 Sep 2026 11:13:12 -0700 Subject: [PATCH 7/8] ZJIT: Spill the block handler in function stubs (#18686) --- zjit/src/codegen.rs | 21 +++++++++++++++++---- zjit/src/codegen_tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index f58f150a4a90bb..ad62deba76cac9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3976,12 +3976,25 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result reg, CArgLocation::StackSlot(slot) => { // The stub runs before any frame setup, so stack-passed arguments @@ -3993,7 +4006,7 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result Date: Tue, 8 Sep 2026 17:35:59 -0700 Subject: [PATCH 8/8] Lower number of threads in a ractor in a bootstrap test OpenBSD CI now runs inside a qemu virtual machine inside a Ubuntu virtual machine, without hardware virtualization support. It is limited to 2.5GB of RAM and only 2 vCPUs. There is a bootstrap test that fails on it due to resource issues. ``` bootstraptest.test_ractor.rb_1536_1361.rb:8:in 'Thread#initialize': can't create Thread: Cannot allocate memory (ThreadError) ``` Fix this by using 100 threads instead of 1000. --- bootstraptest/test_ractor.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index e263f34f09ac66..3fb76118aea3c4 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1534,10 +1534,10 @@ class C } assert_equal '1', %q{ - N = 1_000 + N = 100 Ractor.new{ a = [] - 1_000.times.map{|i| + 100.times.map{|i| Thread.new(i){|i| Thread.pass if i < N a << Ractor.store_if_absent(:i){ i }