Skip to content

Commit 82ac7a3

Browse files
committed
ZJIT: Centralize recompilation decisions in a state machine
Replace 5 scattered fields on IseqPayload (side_exit_count, defer_count, deferred_stub_hits, no_profile_send_hits, has_inline_feedback) with a RecompileState struct that explicitly models the lifecycle phases: Monitoring, Deferred, and Complete. All recompilation decisions now flow through two methods: - on_signal(): handles runtime events (side exits, inline sends, ivar fallbacks, interpreter/stub entries during deferral) - post_hir_check(): handles compile-time deferral decisions after HIR The three runtime callbacks and two deferral gates in codegen.rs become thin wrappers that report events to the state machine and execute the returned actions. trigger_recompilation becomes execution-only with no decision logic. No behavioral changes — same transitions, same thresholds, same tests.
1 parent b1730ad commit 82ac7a3

2 files changed

Lines changed: 245 additions & 113 deletions

File tree

zjit/src/codegen.rs

Lines changed: 44 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::invariants::{
1515
track_root_box_assumption
1616
};
1717
use crate::gc::append_gc_offsets;
18-
use crate::payload::{get_or_create_iseq_payload, IseqCodePtrs, IseqPayload, IseqVersion, IseqVersionRef, IseqStatus};
18+
use crate::payload::{get_or_create_iseq_payload, IseqCodePtrs, IseqPayload, IseqVersion, IseqVersionRef, IseqStatus, RecompileSignal, RecompileAction, RecompileState};
1919
use crate::state::ZJITState;
2020
use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type};
2121
use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ns, exit_compile_error}};
@@ -24,7 +24,7 @@ use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NAT
2424
use crate::hir::{iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType};
2525
use crate::hir::{Const, FrameState, Function, Insn, InsnId, SendFallbackReason};
2626
use crate::hir_type::{types, Type};
27-
use crate::options::{get_option, rb_zjit_call_threshold};
27+
use crate::options::get_option;
2828
use crate::profile::ProfiledType;
2929
use crate::cast::IntoUsize;
3030

@@ -36,32 +36,24 @@ pub const MAX_ISEQ_VERSIONS: usize = 2;
3636
pub extern "C" fn rb_zjit_count_side_exit(payload_raw: *mut std::ffi::c_void) {
3737
if payload_raw.is_null() { return; }
3838
let payload = unsafe { &mut *(payload_raw as *mut IseqPayload) };
39-
let threshold = get_option!(recompile_threshold) as u64;
40-
if threshold == 0 || payload.side_exit_count >= threshold { return; }
41-
payload.side_exit_count += 1;
42-
if payload.side_exit_count == threshold && payload.versions.len() < MAX_ISEQ_VERSIONS {
39+
if let RecompileAction::Recompile { preserve_profiles } =
40+
payload.recompile.on_signal(RecompileSignal::SideExit, payload.versions.len())
41+
{
4342
let iseq = match payload.versions.last() {
4443
Some(version_ref) => unsafe { version_ref.as_ref() }.iseq,
4544
None => return,
4645
};
4746
with_vm_lock(src_loc!(), || {
48-
trigger_recompilation(payload_raw, iseq, true);
47+
trigger_recompilation(payload_raw, iseq, preserve_profiles);
4948
});
5049
}
5150
}
5251

5352
static GLOBAL_RECOMPILE_COUNT: AtomicU64 = AtomicU64::new(0);
5453

55-
/// Escalating threshold for deferred re-profiling. Higher deferral levels
56-
/// give cold branches progressively more time to warm up.
57-
fn deferred_threshold(defer_count: u32) -> u32 {
58-
match defer_count {
59-
1 => unsafe { rb_zjit_call_threshold as u32 },
60-
2 => 1_000,
61-
_ => 100_000,
62-
}
63-
}
64-
54+
/// Execute a recompilation: check global cap, reset profiles, invalidate version,
55+
/// reset JIT func, re-enable profiling. All decisions about *whether* to recompile
56+
/// are made by RecompileState before this is called.
6557
/// When `preserve_profiles` is true, only counters are reset (type distributions survive).
6658
/// When false, both counters and type distributions are cleared.
6759
fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr, preserve_profiles: bool) {
@@ -84,10 +76,8 @@ fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr, pres
8476
payload.profile.reset_for_recompile();
8577
}
8678

87-
// Reset deferral state so V2 compilation goes straight to building the HIR.
88-
// If the HIR still has unresolved issues, the post-HIR deferral trigger handles escalation.
89-
payload.defer_count = 0;
90-
payload.deferred_stub_hits = 0;
79+
// Reset the state machine so V2 compilation goes straight to the HIR check.
80+
payload.recompile.reset_after_trigger();
9181

9282
if let Some(version) = payload.versions.last_mut() {
9383
let version = unsafe { version.as_mut() };
@@ -98,9 +88,6 @@ fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr, pres
9888
}
9989

10090
/// Runtime helper called from JIT code to collect inline type feedback for NoProfile sends.
101-
/// When a NoProfile send executes, this records the receiver's class into the profiling data
102-
/// structure. After enough observations, triggers recompilation so the previously-NoProfile
103-
/// sends compile to direct calls using the collected type data.
10491
#[unsafe(no_mangle)]
10592
pub extern "C" fn rb_zjit_inline_profile_send(
10693
payload_raw: *mut std::ffi::c_void,
@@ -112,26 +99,27 @@ pub extern "C" fn rb_zjit_inline_profile_send(
11299
let payload = unsafe { &mut *(payload_raw as *mut IseqPayload) };
113100
let insn_idx = insn_idx as usize;
114101

115-
let threshold = (get_option!(recompile_threshold) as u64) / 2;
116-
if threshold == 0 || payload.no_profile_send_hits >= threshold { return; }
117-
118-
payload.no_profile_send_hits += 1;
119-
120-
if payload.no_profile_send_hits == threshold && payload.versions.len() < MAX_ISEQ_VERSIONS {
121-
if !payload.profile.inline_feedback_is_high_quality() {
102+
let quality_ok = payload.profile.inline_feedback_is_high_quality();
103+
let action = payload.recompile.on_signal(
104+
RecompileSignal::InlineSend { quality_ok },
105+
payload.versions.len(),
106+
);
107+
match action {
108+
RecompileAction::Recompile { preserve_profiles } => {
109+
let iseq = match payload.versions.last() {
110+
Some(version_ref) => unsafe { version_ref.as_ref() }.iseq,
111+
None => return,
112+
};
113+
with_vm_lock(src_loc!(), || {
114+
trigger_recompilation(payload_raw, iseq, preserve_profiles);
115+
});
122116
return;
123117
}
124-
payload.has_inline_feedback = true;
125-
let iseq = match payload.versions.last() {
126-
Some(version_ref) => unsafe { version_ref.as_ref() }.iseq,
127-
None => return,
128-
};
129-
with_vm_lock(src_loc!(), || {
130-
trigger_recompilation(payload_raw, iseq, true);
131-
});
132-
return;
118+
RecompileAction::Ignore => return,
119+
_ => {}
133120
}
134121

122+
// Type recording stays outside the state machine (data collection, not a decision).
135123
const INLINE_PROFILE_LIMIT: u32 = 5;
136124
if payload.profile.num_profiles_for(insn_idx) >= INLINE_PROFILE_LIMIT { return; }
137125

@@ -145,24 +133,20 @@ pub extern "C" fn rb_zjit_inline_profile_send(
145133
}
146134

147135
/// Lightweight runtime helper for not_monomorphic ivar fallbacks.
148-
/// Only increments the recompilation trigger counter — no type recording.
149136
#[unsafe(no_mangle)]
150137
pub extern "C" fn rb_zjit_count_ivar_fallback(payload_raw: *mut std::ffi::c_void) {
151138
if payload_raw.is_null() { return; }
152139
let payload = unsafe { &mut *(payload_raw as *mut IseqPayload) };
153140

154-
let threshold = get_option!(recompile_threshold) as u64;
155-
if threshold == 0 || payload.no_profile_send_hits >= threshold { return; }
156-
157-
payload.no_profile_send_hits += 1;
158-
159-
if payload.no_profile_send_hits == threshold && payload.versions.len() < MAX_ISEQ_VERSIONS {
141+
if let RecompileAction::Recompile { preserve_profiles } =
142+
payload.recompile.on_signal(RecompileSignal::IvarFallback, payload.versions.len())
143+
{
160144
let iseq = match payload.versions.last() {
161145
Some(version_ref) => unsafe { version_ref.as_ref() }.iseq,
162146
None => return,
163147
};
164148
with_vm_lock(src_loc!(), || {
165-
trigger_recompilation(payload_raw, iseq, true);
149+
trigger_recompilation(payload_raw, iseq, preserve_profiles);
166150
});
167151
}
168152
}
@@ -333,20 +317,16 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
333317
return Err(CompileError::ExceptionHandler);
334318
}
335319

336-
// If this ISEQ is in a deferred re-profiling window, don't compile yet.
337-
// Count this interpreter entry toward the threshold and keep the ISEQ
338-
// running in the interpreter with profiling active. Both interpreter
339-
// entries and stub fallbacks count toward the same escalating threshold.
320+
// Check the recompilation state machine for deferral.
340321
{
341322
let payload = get_or_create_iseq_payload(iseq);
342-
if payload.defer_count > 0 {
343-
let threshold = deferred_threshold(payload.defer_count);
344-
if payload.deferred_stub_hits < threshold {
345-
let call_threshold = unsafe { rb_zjit_call_threshold as u32 };
346-
payload.deferred_stub_hits += call_threshold;
323+
let call_threshold = unsafe { crate::options::rb_zjit_call_threshold as u32 };
324+
match payload.recompile.on_signal(RecompileSignal::Entry { credit: call_threshold }, payload.versions.len()) {
325+
RecompileAction::DeferToInterpreter => {
347326
unsafe { rb_iseq_reset_jit_func(iseq) };
348327
return Err(CompileError::DeferredForReprofiling);
349328
}
329+
_ => {}
350330
}
351331
}
352332

@@ -355,29 +335,18 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
355335
incr_counter!(failed_iseq_count);
356336
}))?;
357337

358-
// Adaptive deferral for recompilations. First compilations never defer.
359-
// For recompilations (latest version invalidated), if the HIR has a
360-
// significant fraction of unresolved sends or any unresolved ivars,
361-
// defer for 1K interpreter calls to exercise cold branches.
362-
// A single dead-branch NoProfile send does NOT trigger deferral —
363-
// ISEQs where most sends are well-profiled compile immediately.
338+
// Post-HIR quality check: decide if recompilation should be deferred.
364339
if get_option!(recompile_threshold) > 0 {
365340
let payload = get_or_create_iseq_payload(iseq);
366341
let is_recompile = payload
367342
.versions
368343
.last()
369344
.map(|v| unsafe { v.as_ref() }.status == IseqStatus::Invalidated)
370345
.unwrap_or(false);
371-
// Use ratio-based check for sends: only defer if >25% of sends lack profiles.
372346
let (no_profile_sends, total_sends) = function.count_no_profile_sends();
373347
let sends_need_deferral = total_sends > 0 && no_profile_sends * 4 > total_sends;
374348
let has_unresolved = sends_need_deferral || function.has_not_monomorphic_ivars();
375-
let skip_deferral = payload.has_inline_feedback;
376-
if is_recompile && payload.defer_count < 2 && has_unresolved && !skip_deferral {
377-
payload.defer_count = 2; // level 2: deferred_threshold(2) = 1K calls
378-
payload.deferred_stub_hits = 0;
379-
// Preserve inline feedback — only reset counters so the interpreter
380-
// adds observations on top during the 1K-call deferral window.
349+
if let RecompileAction::Defer = payload.recompile.post_hir_check(is_recompile, has_unresolved) {
381350
payload.profile.reset_counters_for_recompile();
382351
unsafe { rb_zjit_profile_enable(iseq) };
383352
unsafe { rb_iseq_reset_jit_func(iseq) };
@@ -1607,7 +1576,7 @@ fn gen_guarded_inline_profile(
16071576

16081577
asm_comment!(asm, "guard: skip inline profiling if self-disabled");
16091578
let payload_addr = asm.load(Opnd::UImm(jit.payload_ptr as u64));
1610-
let offset = std::mem::offset_of!(crate::payload::IseqPayload, no_profile_send_hits) as i32;
1579+
let offset = (std::mem::offset_of!(crate::payload::IseqPayload, recompile) + std::mem::offset_of!(RecompileState, no_profile_send_hits)) as i32;
16111580
let hits = asm.load(Opnd::mem(64, payload_addr, offset));
16121581
asm.cmp(hits, Opnd::UImm(threshold));
16131582
asm.jge(jit, skip_edge());
@@ -3100,32 +3069,14 @@ c_callable! {
31003069
let cb = ZJITState::get_code_block();
31013070
let payload = get_or_create_iseq_payload(iseq);
31023071

3103-
// If this ISEQ is being re-profiled after deferral, fall back to
3104-
// the interpreter — the zjit_* profiling instructions are active
3105-
// and collect type data on each fallback. The threshold escalates
3106-
// with each deferral level to give cold branches progressively more
3107-
// time to warm up. This gate fires for both first-compilation deferrals
3108-
// (versions empty) and inline-triggered recompilation deferrals
3109-
// (latest version invalidated).
3110-
let latest_invalidated = payload.versions.last()
3111-
.map(|v| unsafe { v.as_ref() }.status == IseqStatus::Invalidated)
3112-
.unwrap_or(false);
3113-
if payload.defer_count > 0 && (payload.versions.is_empty() || latest_invalidated) {
3114-
// Count stub hits toward the deferral threshold for BOTH initial
3115-
// deferrals (versions empty) and recompilation deferrals (latest
3116-
// invalidated). Previously, recompilation deferrals returned the
3117-
// exit trampoline unconditionally without counting, causing the
3118-
// method to stay in the interpreter indefinitely — a catastrophic
3119-
// overhead for hot methods (addressable-merge lost 2.5s).
3120-
let threshold = deferred_threshold(payload.defer_count);
3121-
payload.deferred_stub_hits += 1;
3122-
if payload.deferred_stub_hits <= threshold {
3123-
// Still collecting profile data — fall back to interpreter
3072+
// Check the recompilation state machine for deferral.
3073+
match payload.recompile.on_signal(RecompileSignal::Entry { credit: 1 }, payload.versions.len()) {
3074+
RecompileAction::DeferToInterpreter => {
31243075
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
31253076
prepare_for_exit(iseq, cfp, sp, &CompileError::DeferredForReprofiling);
31263077
return ZJITState::get_exit_trampoline().raw_ptr(cb);
31273078
}
3128-
// Enough profile data collected — fall through to compile
3079+
_ => {}
31293080
}
31303081

31313082
let last_status = payload.versions.last().map(|version| &unsafe { version.as_ref() }.status);

0 commit comments

Comments
 (0)