diff --git a/majit/majit-ir/src/descr.rs b/majit/majit-ir/src/descr.rs index ef78ad8b0c0..af84018987b 100644 --- a/majit/majit-ir/src/descr.rs +++ b/majit/majit-ir/src/descr.rs @@ -357,6 +357,22 @@ impl StructId { StructId(path_hash(canonical_path)) } + /// Derive the identity of one concrete generic instantiation from the + /// defining type's identity and its balanced `<...>` argument spelling. + /// + /// Rust monomorphizations do not share a physical layout: `Option` + /// and `Option` may differ in both size and field representation. + /// RPython's analogue is one distinct low-level `Struct` object per + /// specialized representation. Folding the argument list away therefore + /// collapses distinct layout/descr identities. The template id keeps path + /// aliases converged; the argument suffix keeps monomorphizations apart. + pub fn instantiate(self, generic_args: &str) -> Self { + StructId(path_hash(&format!( + "__majit_generic_struct__::{:016x}{generic_args}", + self.0 + ))) + } + /// The underlying path-stable `u64`, identical to the descr layer's /// `LLType::Struct(_)` key for the same canonical path. pub fn as_u64(self) -> u64 { @@ -408,17 +424,66 @@ pub fn struct_id_for_name(raw: &str) -> Option { .trim_start_matches("&mut ") .trim_start_matches('&') .trim(); - // Generic nominal ADT instantiations share the defining TypeDecl's one - // physical layout. Tuples are the exception (see [`is_shaped_tuple_name`]): - // preserve the full tuple shape while continuing to collapse - // `Result::Ok` and other nominal generics to their template. - let s = if is_shaped_tuple_name(s) { + let guard = STRUCT_ID_BY_NAME.lock().unwrap(); + if let Some(id) = guard.get(s).copied().flatten() { + return Some(id); + } + if is_shaped_tuple_name(s) { + return None; + } + let generic_args = generic_args_span(s)?; + let template = strip_generic_args(s); + guard + .get(template.as_ref()) + .copied() + .flatten() + .map(|id| id.instantiate(generic_args)) +} + +/// Resolve the defining/template identity of a name, intentionally erasing a +/// concrete generic argument list while retaining an enum-variant tail. +/// Annotation metadata such as `_immutable_fields_` belongs to the declared +/// class/template and uses this lookup; physical layouts use +/// [`struct_id_for_name`] instead. +pub fn struct_template_id_for_name(raw: &str) -> Option { + let s = raw + .trim_start_matches("*const ") + .trim_start_matches("*mut ") + .trim_start_matches("&mut ") + .trim_start_matches('&') + .trim(); + let template = if is_shaped_tuple_name(s) { std::borrow::Cow::Borrowed(s) } else { strip_generic_args(s) }; - let guard = STRUCT_ID_BY_NAME.lock().unwrap(); - guard.get(s.as_ref()).copied().flatten() + STRUCT_ID_BY_NAME + .lock() + .unwrap() + .get(template.as_ref()) + .copied() + .flatten() +} + +/// The first balanced generic argument group in `name`, including brackets. +/// A trailing enum variant (`Result::Ok`) is deliberately excluded: the +/// template [`StructId`] already distinguishes the base and each variant. +fn generic_args_span(name: &str) -> Option<&str> { + let start = name.find('<')?; + let mut depth = 0usize; + for (offset, byte) in name.as_bytes()[start..].iter().copied().enumerate() { + match byte { + b'<' => depth += 1, + b'>' => { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(&name[start..=start + offset]); + } + } + _ => {} + } + } + None } /// Use-import resolver / module-aware canonicalisation table. @@ -689,6 +754,202 @@ static FIELD_INDEX_REDERIVED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); static FIELD_INDEX_UNRESOLVED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_DISAGREE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_OFFSET: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_SIZE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_TYPE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_IMMUTABILITY: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_VIRTUALIZABLE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_CACHE_HIT_INDEX_IN_PARENT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +static FIELD_OFFSET_LAYOUT_HIT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_OFFSET_ACCUMULATOR_FALLBACK: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static COMPUTE_STRUCT_SIZE_LAYOUT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static COMPUTE_STRUCT_SIZE_HEURISTIC: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static COMPUTE_STRUCT_SIZE_FIELDS_MISSING: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELD_OWNER_ID_REGISTRY_MISS: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +static FIELDLESS_SIZE_SHELL_MINTS: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static FIELDLESS_SIZE_SHELL_UPGRADES: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +static EI_DESCR_MINT_DIFFERING: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_IDENTICAL: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_STRUCT_SIZE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_OFFSET: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_FIELD_SIZE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_FIELD_TYPE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_FLAG: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_INDEX_IN_PARENT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_IMMUTABLE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); +static EI_DESCR_MINT_QUASI_IMMUTABLE: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +static FIELD_MINT_TRACE_ENABLED: OnceLock = OnceLock::new(); +static FIELD_MINT_BACKTRACE_ENABLED: OnceLock = OnceLock::new(); + +/// Which exit supplied one `compute_struct_size` result. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StructSizePath { + Layout, + Heuristic, + FieldsMissing, +} + +/// Release-safe field-mint census snapshotted across the build/runtime process +/// boundary by `pyre-jit-trace`'s build script. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct FieldMintCensus { + pub cache_hit_disagree: usize, + pub cache_hit_offset: usize, + pub cache_hit_size: usize, + pub cache_hit_type: usize, + pub cache_hit_immutability: usize, + pub cache_hit_virtualizable: usize, + pub cache_hit_index_in_parent: usize, + pub offset_layout_hit: usize, + pub offset_accumulator_fallback: usize, + pub struct_size_layout: usize, + pub struct_size_heuristic: usize, + pub struct_size_fields_missing: usize, + pub owner_id_registry_miss: usize, + pub fieldless_size_shell_mints: usize, + pub fieldless_size_shell_upgrades: usize, + pub ei_differing: usize, + pub ei_identical: usize, + pub ei_struct_size: usize, + pub ei_offset: usize, + pub ei_field_size: usize, + pub ei_field_type: usize, + pub ei_flag: usize, + pub ei_index_in_parent: usize, + pub ei_immutable: usize, + pub ei_quasi_immutable: usize, +} + +impl std::ops::Add for FieldMintCensus { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self { + cache_hit_disagree: self.cache_hit_disagree + rhs.cache_hit_disagree, + cache_hit_offset: self.cache_hit_offset + rhs.cache_hit_offset, + cache_hit_size: self.cache_hit_size + rhs.cache_hit_size, + cache_hit_type: self.cache_hit_type + rhs.cache_hit_type, + cache_hit_immutability: self.cache_hit_immutability + rhs.cache_hit_immutability, + cache_hit_virtualizable: self.cache_hit_virtualizable + rhs.cache_hit_virtualizable, + cache_hit_index_in_parent: self.cache_hit_index_in_parent + + rhs.cache_hit_index_in_parent, + offset_layout_hit: self.offset_layout_hit + rhs.offset_layout_hit, + offset_accumulator_fallback: self.offset_accumulator_fallback + + rhs.offset_accumulator_fallback, + struct_size_layout: self.struct_size_layout + rhs.struct_size_layout, + struct_size_heuristic: self.struct_size_heuristic + rhs.struct_size_heuristic, + struct_size_fields_missing: self.struct_size_fields_missing + + rhs.struct_size_fields_missing, + owner_id_registry_miss: self.owner_id_registry_miss + rhs.owner_id_registry_miss, + fieldless_size_shell_mints: self.fieldless_size_shell_mints + + rhs.fieldless_size_shell_mints, + fieldless_size_shell_upgrades: self.fieldless_size_shell_upgrades + + rhs.fieldless_size_shell_upgrades, + ei_differing: self.ei_differing + rhs.ei_differing, + ei_identical: self.ei_identical + rhs.ei_identical, + ei_struct_size: self.ei_struct_size + rhs.ei_struct_size, + ei_offset: self.ei_offset + rhs.ei_offset, + ei_field_size: self.ei_field_size + rhs.ei_field_size, + ei_field_type: self.ei_field_type + rhs.ei_field_type, + ei_flag: self.ei_flag + rhs.ei_flag, + ei_index_in_parent: self.ei_index_in_parent + rhs.ei_index_in_parent, + ei_immutable: self.ei_immutable + rhs.ei_immutable, + ei_quasi_immutable: self.ei_quasi_immutable + rhs.ei_quasi_immutable, + } + } +} + +pub fn field_mint_trace_enabled() -> bool { + *FIELD_MINT_TRACE_ENABLED.get_or_init(|| { + std::env::var_os("MAJIT_FIELD_MINT_TRACE").as_deref() == Some(std::ffi::OsStr::new("1")) + }) +} + +fn field_mint_backtrace_enabled() -> bool { + *FIELD_MINT_BACKTRACE_ENABLED.get_or_init(|| std::env::var_os("RUST_BACKTRACE").is_some()) +} + +pub fn record_field_offset_source(layout_hit: bool) { + use std::sync::atomic::Ordering::Relaxed; + if layout_hit { + FIELD_OFFSET_LAYOUT_HIT.fetch_add(1, Relaxed); + } else { + FIELD_OFFSET_ACCUMULATOR_FALLBACK.fetch_add(1, Relaxed); + } +} + +pub fn record_compute_struct_size_path(path: StructSizePath) { + use std::sync::atomic::Ordering::Relaxed; + match path { + StructSizePath::Layout => COMPUTE_STRUCT_SIZE_LAYOUT.fetch_add(1, Relaxed), + StructSizePath::Heuristic => COMPUTE_STRUCT_SIZE_HEURISTIC.fetch_add(1, Relaxed), + StructSizePath::FieldsMissing => COMPUTE_STRUCT_SIZE_FIELDS_MISSING.fetch_add(1, Relaxed), + }; +} + +pub fn record_field_owner_id_registry_miss() { + FIELD_OWNER_ID_REGISTRY_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); +} + +pub fn field_mint_census_snapshot() -> FieldMintCensus { + use std::sync::atomic::Ordering::Relaxed; + FieldMintCensus { + cache_hit_disagree: FIELD_CACHE_HIT_DISAGREE.load(Relaxed), + cache_hit_offset: FIELD_CACHE_HIT_OFFSET.load(Relaxed), + cache_hit_size: FIELD_CACHE_HIT_SIZE.load(Relaxed), + cache_hit_type: FIELD_CACHE_HIT_TYPE.load(Relaxed), + cache_hit_immutability: FIELD_CACHE_HIT_IMMUTABILITY.load(Relaxed), + cache_hit_virtualizable: FIELD_CACHE_HIT_VIRTUALIZABLE.load(Relaxed), + cache_hit_index_in_parent: FIELD_CACHE_HIT_INDEX_IN_PARENT.load(Relaxed), + offset_layout_hit: FIELD_OFFSET_LAYOUT_HIT.load(Relaxed), + offset_accumulator_fallback: FIELD_OFFSET_ACCUMULATOR_FALLBACK.load(Relaxed), + struct_size_layout: COMPUTE_STRUCT_SIZE_LAYOUT.load(Relaxed), + struct_size_heuristic: COMPUTE_STRUCT_SIZE_HEURISTIC.load(Relaxed), + struct_size_fields_missing: COMPUTE_STRUCT_SIZE_FIELDS_MISSING.load(Relaxed), + owner_id_registry_miss: FIELD_OWNER_ID_REGISTRY_MISS.load(Relaxed), + fieldless_size_shell_mints: FIELDLESS_SIZE_SHELL_MINTS.load(Relaxed), + fieldless_size_shell_upgrades: FIELDLESS_SIZE_SHELL_UPGRADES.load(Relaxed), + ei_differing: EI_DESCR_MINT_DIFFERING.load(Relaxed), + ei_identical: EI_DESCR_MINT_IDENTICAL.load(Relaxed), + ei_struct_size: EI_DESCR_MINT_STRUCT_SIZE.load(Relaxed), + ei_offset: EI_DESCR_MINT_OFFSET.load(Relaxed), + ei_field_size: EI_DESCR_MINT_FIELD_SIZE.load(Relaxed), + ei_field_type: EI_DESCR_MINT_FIELD_TYPE.load(Relaxed), + ei_flag: EI_DESCR_MINT_FLAG.load(Relaxed), + ei_index_in_parent: EI_DESCR_MINT_INDEX_IN_PARENT.load(Relaxed), + ei_immutable: EI_DESCR_MINT_IMMUTABLE.load(Relaxed), + ei_quasi_immutable: EI_DESCR_MINT_QUASI_IMMUTABLE.load(Relaxed), + } +} /// One `FIELD_INDEX_UNRESOLVED` event, named, keyed so identical mints fold /// into one row with a count. @@ -1139,6 +1400,8 @@ impl GcCache { if let LLType::Struct(k) = &key { sd.set_cache_key(*k); } + sd.mark_fieldless_shell_mint(); + FIELDLESS_SIZE_SHELL_MINTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // descr.py:119: gccache.init_size_descr(STRUCT, sizedescr) // gc.py:536-542: sets descr.tid — must happen BEFORE Arc wrap. self.init_size_descr(&key, &mut sd); @@ -1811,6 +2074,75 @@ impl GcCache { } else { descr.index_in_parent }; + let offset_disagrees = descr.offset != offset; + let size_disagrees = descr.field_size != field_size; + let type_disagrees = descr.field_type != field_type; + let immutability_disagrees = descr.is_immutable != expected_immutable + || descr.is_quasi_immutable() != expected_quasi_immutable; + let virtualizable_disagrees = descr.virtualizable != virtualizable; + let index_disagrees = descr.index_in_parent != expected_index_in_parent; + let disagrees = offset_disagrees + || size_disagrees + || type_disagrees + || immutability_disagrees + || virtualizable_disagrees + || index_disagrees; + if disagrees { + use std::sync::atomic::Ordering::Relaxed; + FIELD_CACHE_HIT_DISAGREE.fetch_add(1, Relaxed); + if offset_disagrees { + FIELD_CACHE_HIT_OFFSET.fetch_add(1, Relaxed); + } + if size_disagrees { + FIELD_CACHE_HIT_SIZE.fetch_add(1, Relaxed); + } + if type_disagrees { + FIELD_CACHE_HIT_TYPE.fetch_add(1, Relaxed); + } + if immutability_disagrees { + FIELD_CACHE_HIT_IMMUTABILITY.fetch_add(1, Relaxed); + } + if virtualizable_disagrees { + FIELD_CACHE_HIT_VIRTUALIZABLE.fetch_add(1, Relaxed); + } + if index_disagrees { + FIELD_CACHE_HIT_INDEX_IN_PARENT.fetch_add(1, Relaxed); + } + if field_mint_trace_enabled() { + let cached_parent_has_positional_list = descr + .get_parent_descr() + .and_then(|p| p.as_size_descr().map(|sd| !sd.all_fielddescrs().is_empty())) + .unwrap_or(false); + eprintln!( + "MAJIT_FIELD_MINT_TRACE cache_hit_disagree field_key={field_name:?} \ + display_name={display_name:?} cached_display_name={:?} \ + struct_key={struct_key:?} cached_offset={} requested_offset={offset} \ + cached_size={} requested_size={field_size} cached_type={:?} \ + requested_type={field_type:?} cached_flag={:?} requested_flag={flag:?} \ + cached_immutable={} requested_immutable={is_immutable} \ + effective_requested_immutable={expected_immutable} \ + cached_quasi_immutable={} \ + requested_quasi_immutable={is_quasi_immutable} \ + effective_requested_quasi_immutable={expected_quasi_immutable} \ + cached_virtualizable={} requested_virtualizable={virtualizable} \ + cached_index_in_parent={} caller_index_in_parent={index_in_parent:?} \ + expected_index_in_parent={expected_index_in_parent} \ + cached_parent_has_positional_list={cached_parent_has_positional_list}", + descr.name, + descr.offset, + descr.field_size, + descr.field_type, + descr.flag, + descr.is_immutable, + descr.is_quasi_immutable(), + descr.virtualizable, + descr.index_in_parent, + ); + if field_mint_backtrace_enabled() { + eprintln!("{}", std::backtrace::Backtrace::force_capture()); + } + } + } debug_assert!( descr.describes_same_field( offset, @@ -2213,6 +2545,18 @@ impl GcCache { } }; if should_insert { + let upgrades_fieldless_shell = self + ._cache_size + .get(&key) + .and_then(|old| old.as_any()) + .and_then(|old| old.downcast_ref::()) + .is_some_and(|old| old.fieldless_shell_mint) + && descr + .as_size_descr() + .is_some_and(|sd| !sd.all_fielddescrs().is_empty()); + if upgrades_fieldless_shell { + FIELDLESS_SIZE_SHELL_UPGRADES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } if let Some(old) = self._cache_size.get(&key) && !arc_in_vec(&self._size_keepalive, old) { @@ -2546,11 +2890,97 @@ fn ei_descr_mints() -> &'static Mutex (stored.as_ref(), rejected.as_ref()), + (stored @ DescrMintSpec::Field { .. }, rejected @ DescrMintSpec::Field { .. }) => { + (stored, rejected) + } + _ => return, + }; + let ( + DescrMintSpec::Field { + struct_size: stored_struct_size, + offset: stored_offset, + field_size: stored_field_size, + field_type: stored_field_type, + flag: stored_flag, + is_immutable: stored_immutable, + is_quasi_immutable: stored_quasi_immutable, + index_in_parent: stored_index_in_parent, + }, + DescrMintSpec::Field { + struct_size: rejected_struct_size, + offset: rejected_offset, + field_size: rejected_field_size, + field_type: rejected_field_type, + flag: rejected_flag, + is_immutable: rejected_immutable, + is_quasi_immutable: rejected_quasi_immutable, + index_in_parent: rejected_index_in_parent, + }, + ) = (stored, rejected) + else { + return; + }; + if stored_struct_size != rejected_struct_size { + EI_DESCR_MINT_STRUCT_SIZE.fetch_add(1, Relaxed); + } + if stored_offset != rejected_offset { + EI_DESCR_MINT_OFFSET.fetch_add(1, Relaxed); + } + if stored_field_size != rejected_field_size { + EI_DESCR_MINT_FIELD_SIZE.fetch_add(1, Relaxed); + } + if stored_field_type != rejected_field_type { + EI_DESCR_MINT_FIELD_TYPE.fetch_add(1, Relaxed); + } + if stored_flag != rejected_flag { + EI_DESCR_MINT_FLAG.fetch_add(1, Relaxed); + } + if stored_index_in_parent != rejected_index_in_parent { + EI_DESCR_MINT_INDEX_IN_PARENT.fetch_add(1, Relaxed); + } + if stored_immutable != rejected_immutable { + EI_DESCR_MINT_IMMUTABLE.fetch_add(1, Relaxed); + } + if stored_quasi_immutable != rejected_quasi_immutable { + EI_DESCR_MINT_QUASI_IMMUTABLE.fetch_add(1, Relaxed); + } + } + + let mut guard = ei_descr_mints().lock().unwrap_or_else(|e| e.into_inner()); + match guard.entry(member) { + indexmap::map::Entry::Vacant(entry) => { + entry.insert(spec); + } + indexmap::map::Entry::Occupied(entry) => { + let stored = entry.get(); + if stored == &spec { + EI_DESCR_MINT_IDENTICAL.fetch_add(1, Relaxed); + } else { + EI_DESCR_MINT_DIFFERING.fetch_add(1, Relaxed); + count_field_axes(stored, &spec); + if field_mint_trace_enabled() { + eprintln!( + "MAJIT_FIELD_MINT_TRACE ei_descr_mint_disagree member={:?} \ + stored_spec={stored:?} rejected_spec={spec:?}", + entry.key(), + ); + if field_mint_backtrace_enabled() { + eprintln!("{}", std::backtrace::Backtrace::force_capture()); + } + } + } + } + } } /// Every recorded `(member, mint spec)` pair, in mint order. @@ -5118,6 +5548,9 @@ pub struct SimpleSizeDescr { /// landing on a different cache slot and breaking round-trip /// identity). cache_key: u64, + /// Census marker for a fieldless shell minted by `get_size_descr` before + /// any producer published the struct's positional field list. + fieldless_shell_mint: bool, /// descr.py:64,112: SizeDescr.immutable_flag pub is_immutable: bool, vtable: usize, @@ -5156,6 +5589,7 @@ impl Clone for SimpleSizeDescr { size: self.size, type_id: self.type_id, cache_key: self.cache_key, + fieldless_shell_mint: self.fieldless_shell_mint, is_immutable: self.is_immutable, vtable: self.vtable, is_gc_managed: self.is_gc_managed, @@ -5175,6 +5609,7 @@ impl SimpleSizeDescr { size, type_id, cache_key: 0, + fieldless_shell_mint: false, is_immutable: false, vtable: 0, is_gc_managed: true, @@ -5192,6 +5627,7 @@ impl SimpleSizeDescr { size, type_id, cache_key: 0, + fieldless_shell_mint: false, is_immutable: false, vtable, is_gc_managed: true, @@ -5209,6 +5645,10 @@ impl SimpleSizeDescr { self.cache_key = key; } + fn mark_fieldless_shell_mint(&mut self) { + self.fieldless_shell_mint = true; + } + /// Override the GC-header flag (default `true` from the constructors). /// Set `false` for a natively-allocated raw struct registered via /// `register_struct_layout` (no `ref - 8` type-id word, so @@ -6879,6 +7319,31 @@ mod tests { )); } + #[test] + fn generic_struct_identity_keeps_monomorphizations_distinct() { + let template = StructId::from_canonical("option::Option::Some"); + assert_eq!(template.instantiate(""), template.instantiate("")); + assert_ne!( + template.instantiate(""), + template.instantiate("") + ); + assert_ne!(template.instantiate(""), template); + } + + #[test] + fn generic_args_span_is_balanced_and_excludes_variant_tail() { + assert_eq!( + generic_args_span("Result::Ok"), + Some("") + ); + assert_eq!( + generic_args_span("Option>::Some"), + Some(">") + ); + assert_eq!(generic_args_span("Result::Ok"), None); + assert_eq!(generic_args_span("Result"); let parent_type_id = |owner: &str| { let field = FieldDescriptor::new("__discriminant", Some(owner.to_string())) .with_owner_id(Some(owner_id)); diff --git a/majit/majit-translate/src/codewriter/call.rs b/majit/majit-translate/src/codewriter/call.rs index 6188e22819a..d50fdee642f 100644 --- a/majit/majit-translate/src/codewriter/call.rs +++ b/majit/majit-translate/src/codewriter/call.rs @@ -2487,13 +2487,19 @@ impl CallControl { // publish under the same `struct_key` carries the real // vtable on its PyreSizeDescr — cache-hit returns // *that* Arc here unchanged). - let struct_size = compute_struct_size(self, owner_root); - let field_offset = owner_id - .or_else(|| majit_ir::descr::struct_id_for_name(owner_root)) + let registry_struct_id = majit_ir::descr::struct_id_for_name(owner_root); + if owner_id.is_some() && registry_struct_id.is_none() { + majit_ir::descr::record_field_owner_id_registry_miss(); + } + let (struct_size, struct_size_path) = + compute_struct_size_with_path(self, owner_root); + let exact_field_offset = owner_id + .or(registry_struct_id) .and_then(|sid| self.struct_layouts.get(&sid)) .and_then(|l| l.fields.iter().find(|f| f.name.as_str() == field_name)) - .map(|f| f.offset) - .unwrap_or(offset); + .map(|f| f.offset); + majit_ir::descr::record_field_offset_source(exact_field_offset.is_some()); + let field_offset = exact_field_offset.unwrap_or(offset); let rank = self.field_immutability(Some(owner_root), field_name); let is_immutable = rank.map(|r| r.is_immutable()).unwrap_or(false); let is_quasi_immutable = rank.map(|r| r.is_quasi_immutable()).unwrap_or(false); @@ -2529,6 +2535,13 @@ impl CallControl { // layout still has to travel. Read it back off the // descr rather than off the locals below, which // describe the mint that did not happen. + trace_field_ei_descr_mint( + "parent_field", + owner_root, + owner_id.is_some(), + registry_struct_id, + struct_size_path, + ); majit_ir::descr::record_ei_descr_mint( member.clone(), majit_ir::effectinfo::DescrMintSpec::Field { @@ -2585,6 +2598,13 @@ impl CallControl { // Same arguments this `get_field_descr` miss just used, kept so // the runtime's own cache can take the same miss branch // (`descr.py:224-238`) instead of finding an empty slot. + trace_field_ei_descr_mint( + "analyzer_field", + owner_root, + owner_id.is_some(), + registry_struct_id, + struct_size_path, + ); majit_ir::descr::record_ei_descr_mint( member.clone(), majit_ir::effectinfo::DescrMintSpec::Field { @@ -8065,16 +8085,29 @@ fn field_pos_in(cc: &CallControl, owner: &str, field_name: &str) -> usize { /// 1. `cc.struct_layouts[struct_name].size` — actual layout /// 2. Type-string heuristic fallback fn compute_struct_size(cc: &CallControl, struct_name: &str) -> usize { + compute_struct_size_with_path(cc, struct_name).0 +} + +fn compute_struct_size_with_path( + cc: &CallControl, + struct_name: &str, +) -> (usize, majit_ir::descr::StructSizePath) { // Path 1: actual layout from runtime (RPython: symbolic.get_size(STRUCT)) if let Some(layout) = cc.struct_layout_for(struct_name) { - return layout.size; + let path = majit_ir::descr::StructSizePath::Layout; + majit_ir::descr::record_compute_struct_size_path(path); + return (layout.size, path); } // Path 2: heuristic fallback — RPython: symbolic always computes the full // struct size, even with nested structs. Nested struct sizes are looked up // recursively from struct_layouts. let fields = match cc.struct_fields.fields.get(struct_name) { Some(f) => f, - None => return 0, + None => { + let path = majit_ir::descr::StructSizePath::FieldsMissing; + majit_ir::descr::record_compute_struct_size_path(path); + return (0, path); + } }; let mut offset: usize = 0; for (_, field_type_str) in fields.iter() { @@ -8109,10 +8142,30 @@ fn compute_struct_size(cc: &CallControl, struct_name: &str) -> usize { .filter(|s| *s > 0) .max() .unwrap_or_else(crate::layout::target_word_size); - if offset > 0 { + let size = if offset > 0 { (offset + max_align - 1) & !(max_align - 1) } else { 0 + }; + let path = majit_ir::descr::StructSizePath::Heuristic; + majit_ir::descr::record_compute_struct_size_path(path); + (size, path) +} + +fn trace_field_ei_descr_mint( + site: &str, + owner_root: &str, + owner_id_is_some: bool, + registry_struct_id: Option, + struct_size_path: majit_ir::descr::StructSizePath, +) { + if majit_ir::descr::field_mint_trace_enabled() { + eprintln!( + "MAJIT_FIELD_MINT_TRACE ei_descr_mint site={site} owner_root={owner_root:?} \ + owner_id_is_some={owner_id_is_some} \ + struct_id_for_name={registry_struct_id:?} \ + compute_struct_size_path={struct_size_path:?}" + ); } } diff --git a/majit/majit-translate/src/front/mir.rs b/majit/majit-translate/src/front/mir.rs index 53774a7d569..04a5ee102c4 100644 --- a/majit/majit-translate/src/front/mir.rs +++ b/majit/majit-translate/src/front/mir.rs @@ -411,6 +411,27 @@ struct RefEnumInst { suffix: String, } +/// Physical layout identity for a concrete ADT use. The defining path supplies +/// the template identity; all rendered type arguments supply the +/// monomorphization identity. This is deliberately broader than +/// `adt_head_instantiation_suffix`, whose reference-payload predicate controls +/// annotator class splitting rather than Rust memory layout. +fn concrete_adt_struct_id( + template: majit_ir::descr::StructId, + adt: Option<&serde_json::Map>, + llbc: &Llbc, +) -> majit_ir::descr::StructId { + let Some(adt) = adt else { + return template; + }; + let args = render_adt_type_args(adt, llbc, 0); + if args.is_empty() { + template + } else { + template.instantiate(&format!("<{}>", args.join(","))) + } +} + /// The [`RefEnumInst`] for the ADT descriptor `adt` (`{"id": …, /// "generics": …}`) when it names a split-eligible reference-payload /// generic enum, else `None`. Shared by both instantiation scans below so @@ -4783,9 +4804,9 @@ impl<'a> Lowering<'a> { // use `variant_idx = null`, enum variants index into the // `TypeDeclKind::Enum` variant list. let resolved = self.resolve_aggregate_adt(&kind); - let (owner_path, ctor_name, field_names) = match resolved { - Some((owner_path, ctor_name, field_names)) => { - (owner_path, ctor_name, field_names) + let (owner_path, ctor_name, field_names, aggregate_owner_id) = match resolved { + Some((owner_path, ctor_name, field_names, owner_id)) => { + (owner_path, ctor_name, field_names, Some(owner_id)) } None => { // Synthetic placeholders for non-Adt aggregates @@ -4811,7 +4832,7 @@ impl<'a> Lowering<'a> { ); let positional = (0..arg_vars.len()).map(|i| format!("__pos_{i}")).collect(); - (Vec::new(), leaf, positional) + (Vec::new(), leaf, positional, None) } }; let result_ty_owner = if owner_path.is_empty() { @@ -4899,7 +4920,7 @@ impl<'a> Lowering<'a> { field: crate::model::FieldDescriptor { name, owner_root: Some(result_ty_owner.clone()), - owner_id: None, + owner_id: aggregate_owner_id, base_is_deref: None, taken_by_address: false, }, @@ -4931,15 +4952,16 @@ impl<'a> Lowering<'a> { // sites project, so the `__discriminant` read and the // constructor's `setattr` land on ONE classdef per // instantiation (`enum_variant_narrowing_knowntypedata` - // then mints matching variant subclasses). `owner_id` (the - // layout-side `StructId`) stays on the bare template name so - // every instantiation shares the one template tag layout — - // `from_canonical` keys the un-suffixed path. + // then mints matching variant subclasses). `owner_id` is the + // independent physical-layout identity and preserves every + // concrete generic argument. let (owner_root, owner_id) = match self.tyref_adt_class_root(&place.ty) { Some(class_root) => { let canon = strip_crate_prefix(&class_root); - let bare = majit_ir::descr::strip_instantiation_suffix(&canon); - let sid = majit_ir::descr::StructId::from_canonical(bare); + let sid = self.tyref_adt_layout_id(&place.ty).unwrap_or_else(|| { + let bare = majit_ir::descr::strip_instantiation_suffix(&canon); + majit_ir::descr::StructId::from_canonical(bare) + }); (Some(canon), Some(sid)) } None => (None, None), @@ -5924,7 +5946,7 @@ impl<'a> Lowering<'a> { fn resolve_aggregate_adt( &self, kind: &serde_json::Value, - ) -> Option<(Vec, String, Vec)> { + ) -> Option<(Vec, String, Vec, majit_ir::descr::StructId)> { let adt = kind.as_object()?.get("Adt")?.as_array()?; // `AggregateKind::Adt` head: either a bare `type_id` u64 or a // full `TypeDeclRef` object `{"generics": …, "id": {"Adt": @@ -5944,6 +5966,7 @@ impl<'a> Lowering<'a> { let variant_idx = adt.get(1).and_then(serde_json::Value::as_u64); let td = self.llbc.type_by_id(type_id)?; let name_path = td.item_meta.name_path(); + let head_adt = head.as_object(); let mut segments: Vec = name_path.split("::").map(str::to_string).collect(); let type_leaf = segments.pop().unwrap_or_default(); let owner_path = segments; @@ -5954,7 +5977,14 @@ impl<'a> Lowering<'a> { .enumerate() .map(|(i, f)| f.name.clone().unwrap_or_else(|| format!("__pos_{i}"))) .collect(); - Some((owner_path, type_leaf, field_names)) + let template = + majit_ir::descr::StructId::from_canonical(&strip_crate_prefix(&name_path)); + Some(( + owner_path, + type_leaf, + field_names, + concrete_adt_struct_id(template, head_adt, self.llbc), + )) } (TypeDeclKind::Enum(variants), Some(idx)) => { let v = variants.get(idx as usize)?; @@ -5980,7 +6010,17 @@ impl<'a> Lowering<'a> { .enumerate() .map(|(i, f)| f.name.clone().unwrap_or_else(|| format!("__pos_{i}"))) .collect(); - Some((variant_owner, v.name.clone(), field_names)) + let template = majit_ir::descr::StructId::from_canonical(&format!( + "{}::{}", + strip_crate_prefix(&name_path), + v.name + )); + Some(( + variant_owner, + v.name.clone(), + field_names, + concrete_adt_struct_id(template, head_adt, self.llbc), + )) } _ => None, } @@ -6058,6 +6098,7 @@ impl<'a> Lowering<'a> { // variant field read, so the per-instantiation variant class the // constructor and receiver project had no matching field read. let head = adt.first()?; + let head_adt = head.as_object(); let type_id = match head.as_u64() { Some(id) => id, None => head.get("id")?.get("Adt")?.as_u64()?, @@ -6075,11 +6116,10 @@ impl<'a> Lowering<'a> { // `owner_root` is the annotation-side classdef key: a // reference-payload workspace enum instantiation reads its field // off the per-instantiation variant class (`Result::Ok`), - // matching the receiver / constructor projection. `owner_id` - // (the layout-side `StructId` minted below) stays on the bare - // template name so every instantiation shares the one template - // variant layout — sound because the split is scoped to - // reference payloads, which all share that word-slot layout. + // matching the receiver / constructor projection. `owner_id` is the + // independent layout-side identity and preserves every concrete type + // argument, including primitive payloads for which the annotator does + // not split classdefs. let owner_leaf = name_path.rsplit("::").next().unwrap_or("").to_string(); let owner_root = match head .as_object() @@ -6096,9 +6136,9 @@ impl<'a> Lowering<'a> { .clone() .unwrap_or_else(|| format!("__pos_{field_idx}")); let ty = clone_tyref(&f.ty); - let owner_id = Some(majit_ir::descr::StructId::from_canonical( - &strip_crate_prefix(&name_path), - )); + let template = + majit_ir::descr::StructId::from_canonical(&strip_crate_prefix(&name_path)); + let owner_id = Some(concrete_adt_struct_id(template, head_adt, self.llbc)); Some((owner_root, name, ty, owner_id)) } (TypeDeclKind::Enum(variants), Some(vidx)) => { @@ -6115,11 +6155,12 @@ impl<'a> Lowering<'a> { // registered under this key). The downcast statically fixes // the variant. let variant_owner = format!("{owner_root}::{}", variant.name); - let owner_id = Some(majit_ir::descr::StructId::from_canonical(&format!( + let template = majit_ir::descr::StructId::from_canonical(&format!( "{}::{}", strip_crate_prefix(&name_path), variant.name - ))); + )); + let owner_id = Some(concrete_adt_struct_id(template, head_adt, self.llbc)); Some((variant_owner, name, ty, owner_id)) } _ => None, @@ -12883,6 +12924,19 @@ impl<'a> Lowering<'a> { } } + /// Concrete physical-layout identity of an ADT type. Unlike + /// [`Self::tyref_adt_class_root`], this always preserves generic arguments: + /// annotator class splitting is selective, while Rust monomorphization is + /// not. + fn tyref_adt_layout_id(&self, ty: &TyRef) -> Option { + let value = self.tyref_adt_body(ty)?; + let def_id = inline_adt_def_id(value)?; + let name_path = self.llbc.type_by_id(def_id)?.item_meta.name_path(); + let template = majit_ir::descr::StructId::from_canonical(&strip_crate_prefix(&name_path)); + let adt = value.as_object()?.get("Adt")?.as_object(); + Some(concrete_adt_struct_id(template, adt, self.llbc)) + } + /// `true` when `ty` resolves to a FIELDLESS enum whose discriminant /// tag sits at the value's base (byte 0). Mirrors the /// [`Lowering::tyref_adt_name_path`] resolution (dedup / hash-consed diff --git a/majit/majit-translate/src/lib.rs b/majit/majit-translate/src/lib.rs index bf783afd01e..1c7ea704b0e 100644 --- a/majit/majit-translate/src/lib.rs +++ b/majit/majit-translate/src/lib.rs @@ -842,7 +842,7 @@ fn expand_immutable_fields_to_all_spellings( let mut declared_names: Vec<&String> = declared.keys().collect(); declared_names.sort(); for name in declared_names { - if let Some(sid) = majit_ir::descr::struct_id_for_name(name) { + if let Some(sid) = majit_ir::descr::struct_template_id_for_name(name) { by_struct_id.entry(sid).or_insert(&declared[name]); } } @@ -851,8 +851,8 @@ fn expand_immutable_fields_to_all_spellings( if out.contains_key(name) { continue; } - let Some(entries) = - majit_ir::descr::struct_id_for_name(name).and_then(|sid| by_struct_id.get(&sid)) + let Some(entries) = majit_ir::descr::struct_template_id_for_name(name) + .and_then(|sid| by_struct_id.get(&sid)) else { continue; }; @@ -861,6 +861,23 @@ fn expand_immutable_fields_to_all_spellings( out } +fn struct_layout_fields_equal(left: &[StructFieldLayout], right: &[StructFieldLayout]) -> bool { + left.len() == right.len() + && left.iter().zip(right).all(|(left, right)| { + left.name == right.name + && left.offset == right.offset + && left.size == right.size + && left.flag == right.flag + && left.field_type == right.field_type + && left.rank == right.rank + }) +} + +fn struct_layout_census_enabled() -> bool { + std::env::var_os("MAJIT_STRUCT_LAYOUT_CENSUS") + .is_some_and(|value| value == std::ffi::OsStr::new("1")) +} + #[expect( clippy::arc_with_non_send_sync, reason = "Arc preserves shared runtime descriptor/JitCode identity while non-Send translator payload remains confined to the single-threaded build phase" @@ -1249,6 +1266,18 @@ fn analyze_pipeline_from_module_paths( // registers each as an opaque external so the residual `FunctionPath` // form resolves; see `cutover::register_foreign_opaque_method_externals`. call_control.foreign_opaque_method_externals = program.foreign_opaque_method_externals.clone(); + // Diagnostic-only spelling census. Do not sort the production loop or + // alter its last-writer-wins behaviour: this pass measures whether two + // registry spellings that resolve to one StructId produce different + // layouts before choosing the canonical producer. The side table groups + // equal layouts instead of comparing every spelling pair, keeping the + // census linear in the number of spellings times the (small) number of + // distinct layouts. + let census_struct_layouts = struct_layout_census_enabled(); + let mut struct_layout_variants: std::collections::HashMap< + majit_ir::descr::StructId, + Vec<(StructLayout, Vec)>, + > = std::collections::HashMap::new(); // Populate CallControl with layouts from the provider. Where Charon // resolved an exact layout (`program.exact_layouts`), use the true Rust // offsets and total size — `#[repr(Rust)]` reorders/repacks fields, so @@ -1265,16 +1294,90 @@ fn analyze_pipeline_from_module_paths( let Some(sid) = majit_ir::descr::struct_id_for_name(struct_name) else { continue; }; - let layout = match program.exact_layouts.get(&sid) { + // `exact_layouts` originates on Charon's defining TypeDecl. Concrete + // generic identities are derived later from use-site type arguments, + // so inherit the declaration's exact offsets when it has no concrete + // entry (notably the explicit translated `Result` shell). + let exact = program.exact_layouts.get(&sid).or_else(|| { + majit_ir::descr::struct_template_id_for_name(struct_name) + .and_then(|template| program.exact_layouts.get(&template)) + }); + // An opaque/shadow declaration can share the runtime identity of its + // defining type (e.g. pyre-interpreter's zero-field GetSetProperty + // marker versus pyre-object's real definition). If Charon's exact + // definition carries fields but this spelling has no rows, it is an + // alias consumer, not a layout producer; letting it write would erase + // the real field layout according to HashMap iteration order. + let incomplete_shadow = exact.is_some_and(|layout| !layout.field_offsets.is_empty()) + && program + .struct_fields + .fields + .get(struct_name) + .is_some_and(Vec::is_empty); + if incomplete_shadow { + continue; + } + let layout = match exact { Some(exact) => { provider.get_struct_layout_exact(struct_name, &exact.field_offsets, exact.size) } None => provider.get_struct_layout(struct_name), }; if let Some(layout) = layout { + if census_struct_layouts { + let variants = struct_layout_variants.entry(sid).or_default(); + if let Some((_, spellings)) = variants.iter_mut().find(|(observed, _)| { + observed.size == layout.size + && struct_layout_fields_equal(&observed.fields, &layout.fields) + }) { + spellings.push(struct_name.clone()); + } else { + variants.push((layout.clone(), vec![struct_name.clone()])); + } + } call_control.set_struct_layout(sid, layout); } } + if census_struct_layouts { + let struct_id_count = struct_layout_variants.len(); + let aliased_struct_ids = struct_layout_variants + .values() + .filter(|variants| variants.iter().map(|(_, names)| names.len()).sum::() > 1) + .count(); + let mut conflicts: Vec<_> = struct_layout_variants + .iter_mut() + .filter(|(_, variants)| variants.len() > 1) + .collect(); + conflicts.sort_by_key(|(sid, _)| **sid); + for (sid, variants) in &mut conflicts { + for (_, spellings) in variants.iter_mut() { + spellings.sort(); + } + variants.sort_by(|(_, left), (_, right)| left[0].cmp(&right[0])); + let spelling_count = variants + .iter() + .map(|(_, spellings)| spellings.len()) + .sum::(); + eprintln!( + "MAJIT_STRUCT_LAYOUT_CENSUS conflict sid={sid:?} \ + spellings={spelling_count} layout_variants={}", + variants.len(), + ); + for (index, (layout, spellings)) in variants.iter().enumerate() { + eprintln!( + "MAJIT_STRUCT_LAYOUT_CENSUS variant sid={sid:?} index={index} \ + spelling_count={} spellings={spellings:?} layout={layout:?}", + spellings.len(), + ); + } + } + eprintln!( + "MAJIT_STRUCT_LAYOUT_CENSUS summary struct_ids={} \ + aliased_struct_ids={aliased_struct_ids} conflicting_struct_ids={}", + struct_id_count, + conflicts.len(), + ); + } // Register graphs collected above (free functions only — trait // methods are handled separately via register_trait_method). prof.mark(" struct layouts"); diff --git a/pyre/pyre-jit-trace/build.rs b/pyre/pyre-jit-trace/build.rs index 39d1b3d24c7..a7766bad0a8 100644 --- a/pyre/pyre-jit-trace/build.rs +++ b/pyre/pyre-jit-trace/build.rs @@ -13,7 +13,7 @@ use walkdir::WalkDir; #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; -const CODEGEN_CACHE_VERSION: &str = "pyre-jit-trace-codegen-cache-v5"; +const CODEGEN_CACHE_VERSION: &str = "pyre-jit-trace-codegen-cache-v6"; /// Retained cache entries. Each is ~6 MB, and a handful covers the /// configurations one checkout switches between (native/wasm × release/dev). const CODEGEN_CACHE_MAX_ENTRIES: usize = 8; @@ -29,6 +29,7 @@ const CODEGEN_OUTPUTS: &[&str] = &[ "insns.bin", "descrs.bin", "ei_descr_mints.bin", + "field_mint_census.bin", "liveness.bin", "fnaddr_bindings.bin", "static_pytype_bindings.bin", @@ -239,6 +240,11 @@ fn emit_llbc_extraction_placeholders() { bincode::serialize(&Vec::::new()).unwrap(), ) .unwrap(); + std::fs::write( + format!("{out_dir}/field_mint_census.bin"), + bincode::serialize(&majit_ir::descr::FieldMintCensus::default()).unwrap(), + ) + .unwrap(); std::fs::write( format!("{out_dir}/liveness.bin"), bincode::serialize(&Vec::::new()).unwrap(), @@ -895,8 +901,15 @@ fn real_main() { // Restoring the outputs leaves the self-check nothing to compare, so it // bypasses the cache for the same reason the verbose prepass does. let determinism_check = DeterminismCheck::from_env(); + // These diagnostics are emitted while the analyzer is running. A cache + // restore would make an enabled census look empty. + let field_mint_trace = majit_ir::descr::field_mint_trace_enabled(); + let struct_layout_census = + std::env::var_os("MAJIT_STRUCT_LAYOUT_CENSUS").is_some_and(|value| value == "1"); if !verbose_prepass && !callee_census + && !field_mint_trace + && !struct_layout_census && determinism_check == DeterminismCheck::Off && restore_codegen_cache(&cache_dir, &out_dir) { @@ -1020,6 +1033,17 @@ fn real_main() { let ei_descr_mints_bin = bincode::serialize(&pipeline.ei_descr_mints).unwrap(); std::fs::write(format!("{out_dir}/ei_descr_mints.bin"), &ei_descr_mints_bin).unwrap(); + // Analyzer-side descriptor producers run in this build-script process, + // while the runtime formats the field-position report. Persist the + // producer census beside the mint ledger. + let field_mint_census_bin = + bincode::serialize(&majit_ir::descr::field_mint_census_snapshot()).unwrap(); + std::fs::write( + format!("{out_dir}/field_mint_census.bin"), + &field_mint_census_bin, + ) + .unwrap(); + // RPython `pyjitpl.py:2264 self.liveness_info = "".join(asm.all_liveness)`. // Persist the build-time assembler's shared `all_liveness` byte stream so a // runtime consumer re-tracing a build-time jitcode (whose `BC_LIVE` ops @@ -1252,6 +1276,8 @@ fn emit_rerun_directives(repo_root: &str, source_paths: &[String]) { println!("cargo::rerun-if-env-changed=PYRE_RTYPER_VERBOSE"); println!("cargo::rerun-if-env-changed=PYRE_CALLEE_CENSUS"); println!("cargo::rerun-if-env-changed=PYRE_CALLEE_CENSUS_ROWS"); + println!("cargo::rerun-if-env-changed=MAJIT_FIELD_MINT_TRACE"); + println!("cargo::rerun-if-env-changed=MAJIT_STRUCT_LAYOUT_CENSUS"); println!("cargo::rerun-if-env-changed={DETERMINISM_CHECK_ENV}"); // Re-runs this script without changing anything it hashes. That is the // only way to exercise `DeterminismCheck::AgainstCache`, which needs two diff --git a/pyre/pyre-jit-trace/src/jitcode_runtime.rs b/pyre/pyre-jit-trace/src/jitcode_runtime.rs index 13c2f50ad9c..aeb25719df0 100644 --- a/pyre/pyre-jit-trace/src/jitcode_runtime.rs +++ b/pyre/pyre-jit-trace/src/jitcode_runtime.rs @@ -552,6 +552,20 @@ static ALL_EI_DESCR_MINTS: LazyLock> = }) }); +/// Analyzer-side release census captured at the end of the build-script +/// translation. The live process adds its own counters before formatting the +/// existing field-position stats line. +static BUILD_FIELD_MINT_CENSUS: LazyLock = LazyLock::new(|| { + const BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/field_mint_census.bin")); + bincode::deserialize(BYTES).unwrap_or_else(|e| { + panic!( + "pyre-jit-trace: failed to deserialize field_mint_census.bin \ + ({} bytes): {e}", + BYTES.len(), + ) + }) +}); + /// Deserialized `pipeline.all_liveness` — RPython `Assembler.all_liveness` /// (assembler.py), the target of `pyjitpl.py:2264 self.liveness_info = /// "".join(asm.all_liveness)`. @@ -779,6 +793,7 @@ pub fn field_position_jit_stats() -> String { attached_checked, attached_misplaced, } = field_position_counts(); + let mint = *BUILD_FIELD_MINT_CENSUS + majit_ir::descr::field_mint_census_snapshot(); let ( [published, fieldless, shadowing, aliased, aliased_multi], [slots, misplaced], @@ -806,11 +821,49 @@ pub fn field_position_jit_stats() -> String { field_pos_spec_checked={spec_checked} field_pos_spec_misplaced={spec_misplaced} \ field_pos_attached_checked={attached_checked} \ field_pos_attached_misplaced={attached_misplaced} \ + field_cache_hit_disagree={} field_cache_hit_offset={} \ + field_cache_hit_size={} field_cache_hit_type={} \ + field_cache_hit_immutability={} field_cache_hit_virtualizable={} \ + field_cache_hit_index_in_parent={} field_offset_layout_hit={} \ + field_offset_accumulator_fallback={} compute_struct_size_layout={} \ + compute_struct_size_heuristic={} compute_struct_size_fields_missing={} \ + field_owner_id_registry_miss={} fieldless_size_shell_mints={} \ + fieldless_size_shell_upgrades={} ei_descr_mint_differing={} \ + ei_descr_mint_identical={} ei_descr_mint_struct_size={} \ + ei_descr_mint_offset={} ei_descr_mint_field_size={} \ + ei_descr_mint_field_type={} ei_descr_mint_flag={} \ + ei_descr_mint_index_in_parent={} ei_descr_mint_immutable={} \ + ei_descr_mint_quasi_immutable={} \ size_shell_published={published} size_shell_fieldless={fieldless} \ size_shell_shadowing={shadowing} size_shell_aliased={aliased} \ size_shell_aliased_multi={aliased_multi} \ positional_slots={slots} positional_misplaced={misplaced} \ - key_compared={key_compared} key_conflicting={key_conflicting}{sample}" + key_compared={key_compared} key_conflicting={key_conflicting}{sample}", + mint.cache_hit_disagree, + mint.cache_hit_offset, + mint.cache_hit_size, + mint.cache_hit_type, + mint.cache_hit_immutability, + mint.cache_hit_virtualizable, + mint.cache_hit_index_in_parent, + mint.offset_layout_hit, + mint.offset_accumulator_fallback, + mint.struct_size_layout, + mint.struct_size_heuristic, + mint.struct_size_fields_missing, + mint.owner_id_registry_miss, + mint.fieldless_size_shell_mints, + mint.fieldless_size_shell_upgrades, + mint.ei_differing, + mint.ei_identical, + mint.ei_struct_size, + mint.ei_offset, + mint.ei_field_size, + mint.ei_field_type, + mint.ei_flag, + mint.ei_index_in_parent, + mint.ei_immutable, + mint.ei_quasi_immutable, ) }