diff --git a/majit/majit-backend-wasm/src/codegen.rs b/majit/majit-backend-wasm/src/codegen.rs index 5841fe2a066..5a5ad1643e9 100644 --- a/majit/majit-backend-wasm/src/codegen.rs +++ b/majit/majit-backend-wasm/src/codegen.rs @@ -1035,16 +1035,38 @@ impl LabelResumeData { self.capture_by_id.get(r.raw() as usize).copied().flatten() } + fn shortage(&self, frame: FrameGeometry) -> Option { + if self.ref_slots > frame.label_ref_slots { + return Some(super::FrameShortage::new( + super::FrameShortageKind::LabelResumeRefSlots, + self.ref_slots, + frame.label_ref_slots, + )); + } + for storage in self.capture_by_id.iter().flatten() { + match storage { + LabelCaptureStorage::ValueSlot(slot) if *slot >= frame.value_slots => { + return Some(super::FrameShortage::new( + super::FrameShortageKind::LabelResumeCaptureSlots, + slot + 1, + frame.value_slots, + )); + } + LabelCaptureStorage::RefSlot(slot) if *slot >= frame.label_ref_slots => { + return Some(super::FrameShortage::new( + super::FrameShortageKind::LabelResumeCaptureSlots, + slot + 1, + frame.label_ref_slots, + )); + } + LabelCaptureStorage::ValueSlot(_) | LabelCaptureStorage::RefSlot(_) => {} + } + } + None + } + fn supported_by(&self, frame: FrameGeometry) -> bool { - self.ref_slots <= frame.label_ref_slots - && self - .capture_by_id - .iter() - .flatten() - .all(|storage| match storage { - LabelCaptureStorage::ValueSlot(slot) => *slot < frame.value_slots, - LabelCaptureStorage::RefSlot(slot) => *slot < frame.label_ref_slots, - }) + self.shortage(frame).is_none() } fn frame_offset(&self, storage: LabelCaptureStorage, frame: FrameGeometry) -> u64 { @@ -2365,13 +2387,17 @@ pub fn build_wasm_module( let max_value_slots = normal_frame_value_slots(&analysis_inputargs, &analysis_ops) + label_resume.scalar_slots; if max_value_slots > frame.value_slots { + let shortage = super::FrameShortage::new( + super::FrameShortageKind::FrameValueSlots, + max_value_slots, + frame.value_slots, + ); if !inlined_bridges.is_empty() { - super::record_inline_geometry(max_value_slots, frame.value_slots); + super::record_inline_geometry(shortage.kind, shortage.needed, shortage.available); } return Err(BackendError::Unsupported(format!( - "wasm backend: {max_value_slots} frame value slots exceed frozen frame layout \ - ({})", - frame.value_slots, + "wasm backend: {} frame value slots exceed frozen frame layout ({})", + shortage.needed, shortage.available, ))); } @@ -2398,16 +2424,37 @@ pub fn build_wasm_module( &label_resume.captured_refs, ); let num_ref_homes = ref_homes.len(); - if num_ref_homes > frame.ordinary_home_slots() || !label_resume.supported_by(*frame) { + let shortage = if num_ref_homes > frame.ordinary_home_slots() { + Some(super::FrameShortage::new( + super::FrameShortageKind::OrdinaryRefHomes, + num_ref_homes, + frame.ordinary_home_slots(), + )) + } else { + label_resume.shortage(*frame) + }; + if let Some(shortage) = shortage { if !inlined_bridges.is_empty() { - super::record_inline_geometry(num_ref_homes, frame.ordinary_home_slots()); + super::record_inline_geometry(shortage.kind, shortage.needed, shortage.available); } - return Err(BackendError::Unsupported(format!( - "wasm backend: {num_ref_homes} ordinary ref homes and {} LABEL ref captures exceed frozen frame layout ({}, {})", - label_resume.ref_slots, - frame.ordinary_home_slots(), - frame.label_ref_slots, - ))); + let reason = match shortage.kind { + super::FrameShortageKind::OrdinaryRefHomes => format!( + "wasm backend: {} ordinary ref homes exceed frozen frame layout ({})", + shortage.needed, shortage.available, + ), + super::FrameShortageKind::LabelResumeRefSlots => format!( + "wasm backend: {} LABEL ref captures exceed label resume layout ({} label ref slots)", + shortage.needed, shortage.available, + ), + super::FrameShortageKind::LabelResumeCaptureSlots => format!( + "wasm backend: {} LABEL capture slots exceed label resume layout ({})", + shortage.needed, shortage.available, + ), + super::FrameShortageKind::FrameValueSlots => { + unreachable!("value-slot shortage was checked above") + } + }; + return Err(BackendError::Unsupported(reason)); } // Self-recursive CALL_ASSEMBLER arm (`PYRE_WASM_CA`): `bridge_finish_fi` is diff --git a/majit/majit-backend-wasm/src/lib.rs b/majit/majit-backend-wasm/src/lib.rs index 8bef60f6d3e..7bdc2969ff2 100644 --- a/majit/majit-backend-wasm/src/lib.rs +++ b/majit/majit-backend-wasm/src/lib.rs @@ -83,31 +83,69 @@ use std::sync::{Arc, Mutex}; /// because the source module has frame-only dispatch; 46 = parameter entry /// declined because the source guard and bridge input arities disagree; 47 = /// LABEL publication suppressed because the bridge entry has nonzero parameters. -pub static BRIDGE_DIAG: [AtomicU64; 48] = [const { AtomicU64::new(0) }; 48]; +/// 48 = an inline trial's LABEL-resume storage exceeds the frozen frame. +pub static BRIDGE_DIAG: [AtomicU64; 49] = [const { AtomicU64::new(0) }; 49]; -/// The first three inline geometry failures, packed as `(needed, available)`. -/// They expose a frozen-layout shortage without changing the compile result. +#[repr(u8)] +#[derive(Clone, Copy)] +pub(crate) enum FrameShortageKind { + FrameValueSlots = 1, + OrdinaryRefHomes = 2, + LabelResumeRefSlots = 3, + LabelResumeCaptureSlots = 4, +} + +#[derive(Clone, Copy)] +pub(crate) struct FrameShortage { + pub(crate) kind: FrameShortageKind, + pub(crate) needed: usize, + pub(crate) available: usize, +} + +impl FrameShortage { + pub(crate) const fn new(kind: FrameShortageKind, needed: usize, available: usize) -> Self { + Self { + kind, + needed, + available, + } + } +} + +/// The first three inline geometry failures, packed as +/// `(kind: u8, needed: u24, available: u24)`. They expose a frozen-layout +/// shortage without changing the compile result. static INLINE_GEOMETRY: [AtomicU64; 3] = [const { AtomicU64::new(0) }; 3]; static INLINE_GEOMETRY_COUNT: AtomicU64 = AtomicU64::new(0); static INLINE_TRIAL_ERRORS: Mutex> = Mutex::new(Vec::new()); -pub(crate) fn record_inline_geometry(needed: usize, available: usize) { +pub(crate) fn record_inline_geometry(kind: FrameShortageKind, needed: usize, available: usize) { + const FIELD_MASK: u64 = (1 << 24) - 1; + let index = INLINE_GEOMETRY_COUNT.fetch_add(1, Ordering::Relaxed) as usize; if let Some(slot) = INLINE_GEOMETRY.get(index) { slot.store( - ((needed as u64) << 32) | available as u64, + ((kind as u64) << 48) + | ((needed as u64).min(FIELD_MASK) << 24) + | (available as u64).min(FIELD_MASK), Ordering::Relaxed, ); } } -/// Read a packed `(needed, available)` inline geometry failure. +/// Read a packed `(kind, needed, available)` inline geometry failure. pub fn inline_geometry_diag(index: usize) -> u64 { INLINE_GEOMETRY .get(index) .map_or(0, |slot| slot.load(Ordering::Relaxed)) } +/// Number of inline geometry failures, including records beyond the three +/// diagnostics retained in [`INLINE_GEOMETRY`]. +pub fn inline_geometry_count() -> u64 { + INLINE_GEOMETRY_COUNT.load(Ordering::Relaxed) +} + pub fn inline_trial_errors() -> String { INLINE_TRIAL_ERRORS.lock().unwrap().join(" | ") } @@ -228,11 +266,9 @@ fn reemit_enabled() -> bool { REEMIT_ENABLED.load(Ordering::Relaxed) } -/// Arm loop-closing bridge inlining. Inlining rebuilds the owning loop, so it -/// also enables the replacement path. +/// Arm loop-closing bridge inlining from the host before guest execution starts. pub fn inline_bridge_enable() { INLINE_BRIDGE_ENABLED.store(true, Ordering::Relaxed); - reemit_enable(); } fn inline_bridge_enabled() -> bool { @@ -3301,6 +3337,8 @@ impl majit_backend::Backend for WasmBackend { diag_bump(40); } else if reason.contains("ordinary ref homes") { diag_bump(41); + } else if reason.contains("label resume layout") { + diag_bump(48); } else if reason .contains("inlined bridge stream has no local loop LABEL") { @@ -3606,10 +3644,9 @@ impl majit_backend::Backend for WasmBackend { unsafe { core::ptr::write(cell, bridge_slot); } - // Only retained module replacement needs to restore this cell - // after allocating a fresh dispatch array. Without replacement, - // the live cell is already the sole dispatch state. - if is_direct && reemit_enabled() { + // Retained module replacement and loop-closing bridge inlining + // restore this cell after allocating a fresh dispatch array. + if is_direct && (reemit_enabled() || inline_bridge_enabled()) { if let Some(source_loop) = original_token .compiled .get() diff --git a/majit/majit-backend/src/lib.rs b/majit/majit-backend/src/lib.rs index 8e031790c71..fb1f4bdcfa2 100644 --- a/majit/majit-backend/src/lib.rs +++ b/majit/majit-backend/src/lib.rs @@ -1304,12 +1304,13 @@ pub struct JitCellToken { /// (`LoopTargetDescr` Arc; `TargetToken IS-A AbstractDescr` in /// PyPy, so a `DescrRef` is the matching identity). Each /// successful loop / retrace populates this through - /// `record_target_token`. Its one reader is - /// [`Self::first_target_token`], the descr a bridge closes onto: - /// neither `has_compiled_loop` (token presence) nor pyre's - /// `has_compiled_targets` (the `compiled_loops` side table) reads - /// this list, so it is not pyre's `has_compiled_targets` signal - /// despite mirroring what upstream's reads. The metainterp-side + /// `record_target_token`. Its one reader with a caller is + /// [`Self::first_target_token`], the descr a bridge closes onto; + /// [`Self::has_target_tokens`] reads it too but is called from + /// nowhere. Neither `has_compiled_loop` (token presence) nor + /// pyre's `has_compiled_targets` (the `compiled_loops` side table) + /// reads this list, so it is not pyre's `has_compiled_targets` + /// signal despite mirroring what upstream's reads. The metainterp-side /// `TargetToken` value (with `virtual_state` / `short_preamble`) /// stays on the `CompiledEntry::front_target_tokens` list per /// the F.6 retirement plan — the per-target descr identity is the diff --git a/majit/majit-metainterp/src/optimizeopt/mod.rs b/majit/majit-metainterp/src/optimizeopt/mod.rs index 37ed2b8753f..4621b2fb1a6 100644 --- a/majit/majit-metainterp/src/optimizeopt/mod.rs +++ b/majit/majit-metainterp/src/optimizeopt/mod.rs @@ -4347,6 +4347,17 @@ impl OptContext { self.active_short_preamble_producer.as_mut() } + /// Address of the active producer's storage for the MetaInterp root + /// walker. This field has the same + /// `Option` type as + /// `Optimizer.short_preamble_producer`, so either address is valid for + /// the walker's cast while the builder is moved between them. + pub(crate) fn active_short_preamble_producer_slot_addr(&mut self) -> usize { + (&mut self.active_short_preamble_producer + as *mut Option) + as usize + } + pub fn build_active_short_preamble( &self, ) -> Option { diff --git a/majit/majit-metainterp/src/optimizeopt/optimizer.rs b/majit/majit-metainterp/src/optimizeopt/optimizer.rs index 4a108418a9b..61a3727faef 100644 --- a/majit/majit-metainterp/src/optimizeopt/optimizer.rs +++ b/majit/majit-metainterp/src/optimizeopt/optimizer.rs @@ -408,6 +408,11 @@ pub struct Optimizer { /// the extended builder's home without merging those stages. pub short_preamble_producer: Option, + /// MetaInterp's `Option` publication slot for this producer. While + /// the builder is on loan to OptContext, the slot is re-pointed at + /// `OptContext.active_short_preamble_producer` so the root walker always + /// follows the builder's current home. + pub(crate) published_short_preamble_producer_slot: Option, /// RPython unroll.py: `label_args = import_state(...)`. /// The peeled loop's LABEL must use these args, not the phase-1 end_args. pub imported_label_args: Option>, @@ -1490,6 +1495,7 @@ impl Optimizer { imported_short_preamble: None, imported_short_preamble_builder: None, short_preamble_producer: None, + published_short_preamble_producer_slot: None, imported_label_args: None, patchguardop: None, skip_flush: false, diff --git a/majit/majit-metainterp/src/optimizeopt/unroll.rs b/majit/majit-metainterp/src/optimizeopt/unroll.rs index ef4005b45a7..3379bd4e7ff 100644 --- a/majit/majit-metainterp/src/optimizeopt/unroll.rs +++ b/majit/majit-metainterp/src/optimizeopt/unroll.rs @@ -393,17 +393,61 @@ pub struct UnrollOptimizer { /// `MetaInterp.compile_short_preamble_producer`. pub(crate) struct PublishedShortPreambleProducer { slot: Option, + previous: Option, } impl Drop for PublishedShortPreambleProducer { fn drop(&mut self) { if let Some(addr) = self.slot { // SAFETY: the same address pyjitpl installed for this compile, on - // the same thread as the registered root walker. Writing `None` - // here is what keeps the walker from reading the optimizer local - // after it is dropped. + // the same thread as the registered root walker. Restoring the + // previous value preserves an outer publication and keeps the + // walker from reading the optimizer local after it is dropped. unsafe { - *(addr as *mut Option) = None; + *(addr as *mut Option) = self.previous; + } + } + } +} + +/// Temporarily publishes the context storage while an extended short-preamble +/// builder is on loan from its optimizer. The two storage fields have the same +/// `Option` type, which is required by the root +/// walker's cast. +struct ActiveShortPreambleProducerPublication { + slot: Option, + optimizer_producer_slot: usize, +} + +impl ActiveShortPreambleProducerPublication { + fn new(optimizer: &mut crate::optimizeopt::optimizer::Optimizer, ctx: &mut OptContext) -> Self { + let optimizer_producer_slot = (&mut optimizer.short_preamble_producer + as *mut Option) + as usize; + let slot = optimizer.published_short_preamble_producer_slot; + if let Some(addr) = slot { + // SAFETY: `publish_short_preamble_producer` installed this + // MetaInterp-owned slot for the current optimizer on the same + // thread as the registered root walker. + unsafe { + *(addr as *mut Option) = + Some(ctx.active_short_preamble_producer_slot_addr()); + } + } + Self { + slot, + optimizer_producer_slot, + } + } +} + +impl Drop for ActiveShortPreambleProducerPublication { + fn drop(&mut self) { + if let Some(addr) = self.slot { + // SAFETY: the builder has returned to the optimizer before this + // guard drops, so the published address again names its storage. + unsafe { + *(addr as *mut Option) = Some(self.optimizer_producer_slot); } } } @@ -506,7 +550,8 @@ impl UnrollOptimizer { } /// Publish the address of `optimizer.short_preamble_producer` to the - /// registered root walker, and withdraw it when the returned guard drops. + /// registered root walker, and restore the prior publication when the + /// returned guard drops. /// /// The guard is not optional. The published address points into the /// caller's `Optimizer` local, which dies when the unroll call returns; @@ -519,13 +564,16 @@ impl UnrollOptimizer { &self, optimizer: &mut crate::optimizeopt::optimizer::Optimizer, ) -> PublishedShortPreambleProducer { + let mut previous = None; if let Some(addr) = self.compile_short_preamble_producer_slot { // SAFETY: pyjitpl installs this address from // `MetaInterp.compile_short_preamble_producer` for the duration // of one compile. Unroll phases run on the same thread as the // registered root walker. unsafe { - *(addr as *mut Option) = Some( + let slot = &mut *(addr as *mut Option); + previous = *slot; + *slot = Some( (&mut optimizer.short_preamble_producer as *mut Option< crate::optimizeopt::shortpreamble::ExtendedShortPreambleBuilder, @@ -533,8 +581,11 @@ impl UnrollOptimizer { ); } } + optimizer.published_short_preamble_producer_slot = + self.compile_short_preamble_producer_slot; PublishedShortPreambleProducer { slot: self.compile_short_preamble_producer_slot, + previous, } } @@ -3711,6 +3762,7 @@ impl OptUnroll { return None; } ctx.activate_short_preamble_producer(builder); + let publication = ActiveShortPreambleProducerPublication::new(optimizer, ctx); extra = Self::inline_short_preamble( &short_jump_args, &target_args, @@ -3718,20 +3770,31 @@ impl OptUnroll { optimizer, ctx, ); + // Return the builder to its optimizer storage before + // anything reads it. Dropping the guard below re-points + // the walker there, and `build_short_preamble_struct` + // must run with the builder rooted where the walker looks. if let Some(builder) = ctx.take_active_short_preamble_producer() { - // history.py:227/268/314 — `Const{Int,Float,Ptr}.value` - // rides inline on the OpRef. Production no longer - // seeds `ctx.const_pool` - // (`merge_backend_constants_from_ctx` asserts the - // pool is empty at export), so the cross-compile - // `loop_constants` snapshot is no longer built: - // short-preamble ops embed the Const value - // directly in `op.args`, mirroring RPython's - // `shortpreamble.py` which has no parallel side - // table. - target_token.short_preamble = Some(builder.build_short_preamble_struct()); optimizer.short_preamble_producer = Some(builder); } + drop(publication); + // history.py:227/268/314 — `Const{Int,Float,Ptr}.value` + // rides inline on the OpRef. Production no longer seeds + // `ctx.const_pool` (`merge_backend_constants_from_ctx` + // asserts the pool is empty at export), so the + // cross-compile `loop_constants` snapshot is no longer + // built: short-preamble ops embed the Const value + // directly in `op.args`, mirroring RPython's + // `shortpreamble.py` which has no parallel side table. + // + // Replay can abort mid-way and leave the builder partial, + // so publishing it would persist a short preamble later + // bridges and retraces consume. + if !ctx.has_pending_invalid_loop() + && let Some(builder) = optimizer.short_preamble_producer.as_ref() + { + target_token.short_preamble = Some(builder.build_short_preamble_struct()); + } } else { extra = Self::inline_short_preamble( &short_jump_args, diff --git a/majit/majit-metainterp/src/pyjitpl.rs b/majit/majit-metainterp/src/pyjitpl.rs index b66f6f8858e..6c8d62109e2 100644 --- a/majit/majit-metainterp/src/pyjitpl.rs +++ b/majit/majit-metainterp/src/pyjitpl.rs @@ -8306,25 +8306,50 @@ impl MetaInterp { // compile.py:362-367: optimize using UnrolledLoopData with start_state. // - // `compile.py:355-356` resolves the retrace's token with - // `get_procedure_token(greenkey)` and asserts it, so a retrace runs - // against a token that already owns the accumulated target tokens and - // carrying them is that token's own state. The arm below without a - // resumekey has no such token: it mints one at `compile.py:1013`, and - // `ResumeFromInterpDescr.compile_and_attach` (`:1006-1022`) never - // assigns that token's `target_tokens` at all — `target_tokens` has - // exactly three writers upstream, the `history.py:440` class default - // `None` and the two assignments at `compile.py:245` and `:290`, both - // of which are inside `compile_simple_loop` / `compile_loop` and so - // are not on this route. The minted token therefore starts owning - // nothing. Drain the parked tokens on both arms — `swap_remove` is - // what spends them — and hand them on only where a token already owns - // them. + // Upstream seeds candidate visibility unconditionally. + // `compile.py:355-356` resolves `loop_jitcell_token = + // metainterp.get_procedure_token(greenkey)` before any resumekey + // is consulted, `:359` records the closing JUMP under that token, + // and `unroll.py:321-325` walks its whole `target_tokens` list. + // `unroll.py:297` + // `jitcelltoken.target_tokens.append(target_token)` then adds the + // retrace's own token to that same list, so the accumulation + // happens inside the optimizer, not in either `compile_and_attach` + // arm; the resumekey is first read at `:393`. The arm without one + // is in fact the arm that wants those candidates most: + // `compile.py:1007-1009` describes what it installs as "a bridge + // going from the interpreter to previously-compiled code ... not a + // loop at all but ends in a jump to the target loop". // - // The binding, not the seed argument, is emptied: on the minting arm - // it also feeds the ownership rebind and the republication fallback - // further down, and that fallback fires precisely when the optimizer - // produced no tokens of its own. + // Emptying the seed on that arm is therefore a deliberate + // deviation, not a port. It is deliberate because pyre does not + // implement that install: the minting arm replaces the front door + // with a standalone artifact, and the close gate in + // `jump_to_existing_trace_impl` admits a foreign target only when + // it belongs to the artifact this compile attaches to — which on + // this arm is none, so every seeded candidate is inadmissible + // however it got here. A seed can thus never buy an admitted + // close. Its only live consumers are the loop that rebinds each + // token through `set_original_jitcell_token_number` and the + // republication that hands them on as the next entry's + // `front_target_tokens` — the route by which a retired loop lends + // its labels to the loop that replaced it. + // + // Two further consumers change with it, and are meant to. The + // virtual-state pick falls back to the exported state rather than + // a prior token's, and the `jump_to_preamble` fallback targets + // this compile's own token rather than the prior preamble. Both + // follow from the same install semantics: a replacement front door + // has no business reusing the state of a close it will never make. + // + // The binding, not the seed argument, is emptied: on the minting + // arm it also feeds the ownership rebind and the republication + // fallback further down, and that fallback fires precisely when + // the optimizer produced no tokens of its own. Both arms still + // drain the park, but only nominally — this function has already + // required a live `compiled_loops` entry, and the park is filled + // only when no such entry exists, so the `or_else` can drain + // nothing a live entry does not already shadow. let prior_front_target_tokens = { let prior_front_target_tokens = self .compiled_loops diff --git a/pyre/pyre-wasm-runner/src/main.rs b/pyre/pyre-wasm-runner/src/main.rs index b4da1a4ec60..7ec8a4e1eba 100644 --- a/pyre/pyre-wasm-runner/src/main.rs +++ b/pyre/pyre-wasm-runner/src/main.rs @@ -684,7 +684,8 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { // compile_bridge outcome tallies (diagnostic). 0=entered 1=declCALL_ASM // 2=declMultiPeel 3=declNotDirect 4=declRefHome 5=BRIDGE_OK // 6=loopClosing 7=srcHasPreamble 15=declCAHostTrampoline, - // 16=forced terminal-decline regression hook. + // 16=forced terminal-decline regression hook. 48=inline LABEL-resume + // layout decline. if let Ok(diag) = instance.get_typed_func::(&mut store, "pyre_jit_bridge_diag") { let labels = [ "entered", @@ -735,6 +736,7 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { "bridge_param_decl_source_frame", "bridge_param_decl_arity", "bridge_param_label_suppressed", + "inline_decl_label_resume_layout", ]; let mut parts = Vec::new(); for (i, lbl) in labels.iter().enumerate() { @@ -750,11 +752,32 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { for i in 0..3 { let packed = geometry.call(&mut store, i).unwrap_or(0); if packed != 0 { - parts.push(format!("{}:{}/{}", i + 1, packed >> 32, packed as u32)); + let kind = match (packed >> 48) as u8 { + 1 => "frame_value_slots", + 2 => "ordinary_ref_homes", + 3 => "label_resume_ref_slots", + 4 => "label_resume_capture_slots", + _ => "unknown", + }; + let needed = (packed >> 24) & 0x00ff_ffff; + let available = packed & 0x00ff_ffff; + parts.push(format!("{}:{kind}={needed}/{available}", i + 1)); } } if !parts.is_empty() { - eprintln!("[jit-stats] inline_geometry {}", parts.join(" ")); + let count = instance + .get_typed_func::<(), u64>(&mut store, "pyre_jit_inline_geometry_count") + .ok() + .and_then(|count| count.call(&mut store, ()).ok()); + match count { + Some(count) => eprintln!( + "[jit-stats] inline_geometry records={}/{} {}", + parts.len(), + count, + parts.join(" ") + ), + None => eprintln!("[jit-stats] inline_geometry {}", parts.join(" ")), + } } } // Per-walk full-body-walk census (diagnostic). Prints the same record diff --git a/pyre/pyre-wasm/src/lib.rs b/pyre/pyre-wasm/src/lib.rs index c329947c98d..18bd5e1a79a 100644 --- a/pyre/pyre-wasm/src/lib.rs +++ b/pyre/pyre-wasm/src/lib.rs @@ -351,14 +351,22 @@ pub extern "C" fn pyre_jit_bridge_diag(i: u32) -> u64 { majit_backend_wasm::bridge_diag(i as usize) } -/// Packed `(needed, available)` geometry for an inline-module trial that did -/// not fit its owner's frozen frame. The host formats this diagnostic only. +/// Packed `(kind, needed, available)` geometry for an inline-module trial that +/// did not fit its owner's frozen frame. The host formats this diagnostic only. #[cfg(all(target_arch = "wasm32", feature = "wasm-host"))] #[unsafe(no_mangle)] pub extern "C" fn pyre_jit_inline_geometry_diag(i: u32) -> u64 { majit_backend_wasm::inline_geometry_diag(i as usize) } +/// Number of inline geometry failures, including entries beyond the retained +/// diagnostic records. +#[cfg(all(target_arch = "wasm32", feature = "wasm-host"))] +#[unsafe(no_mangle)] +pub extern "C" fn pyre_jit_inline_geometry_count() -> u64 { + majit_backend_wasm::inline_geometry_count() +} + /// Test-only control plane for the terminal-declined CALL_ASSEMBLER regression. /// Exported rather than imported so it cannot perturb table/function indices /// used by JIT-emitted modules. Zero disables it; see