diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 3d3f52b7ce..b7878e8136 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -15,7 +15,15 @@ crate-type = ["rlib", "staticlib"] # actually needs (see optimized_libs.rs), so the heavy subsystems below # (regex engine, Temporal, URL/IDNA, normalize, segmenter) are *opt-in per # app* and absent from binaries that never use them. -default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter"] +default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "diagnostics"] +# Cold-path diagnostic JSON serializers (~67 KB of code + the `serde_json` +# pulled only by them, which dead-strips when unreferenced): GC cycle telemetry +# (`PERRY_GC_DIAG`), typed-feedback trace dump (`PERRY_TYPED_FEEDBACK`), the v8 +# heap-snapshot builder (`v8.getHeapSnapshot`/`writeHeapSnapshot`), and +# `process.report`. None are on a hot path. The env-driven dev diagnostics +# degrade gracefully when off (auto-optimize leaves this off unless the program +# uses a heap-snapshot / `process.report` API, which the compiler detects). +diagnostics = [] # The user's regular-expression engine (`regex` + `fancy-regex`, ~1.2 MB of # DFA/NFA machinery). A program that never evaluates a regex literal, `RegExp`, # a regex-coercing string method, or a glob API can't produce a RegExp at diff --git a/crates/perry-runtime/src/arena/mod.rs b/crates/perry-runtime/src/arena/mod.rs index c8b32b9b0f..777a1fb155 100644 --- a/crates/perry-runtime/src/arena/mod.rs +++ b/crates/perry-runtime/src/arena/mod.rs @@ -53,6 +53,8 @@ pub use allocators::{ pub(crate) use allocators::{arena_alloc_gc_old_excluding_pages, arena_alloc_gc_survivor}; // walk.rs +#[cfg(feature = "diagnostics")] +pub(crate) use walk::ArenaRegionTelemetry; pub use walk::{ arena_block_count, arena_in_use_bytes, arena_total_bytes, arena_walk_objects, arena_walk_objects_addr_sorted, arena_walk_objects_filtered, @@ -61,8 +63,8 @@ pub use walk::{ }; pub(crate) use walk::{ arena_block_snapshots, arena_telemetry_snapshot, general_block_in_recent_window, - ArenaBlockSnapshot, ArenaObjectCursor, ArenaObjectCursorBuilder, ArenaRegionTelemetry, - ArenaTelemetrySnapshot, ArenaWalkOrder, + ArenaBlockSnapshot, ArenaObjectCursor, ArenaObjectCursorBuilder, ArenaTelemetrySnapshot, + ArenaWalkOrder, }; // reset.rs diff --git a/crates/perry-runtime/src/arena/walk.rs b/crates/perry-runtime/src/arena/walk.rs index 3942d09183..23c7b43ed4 100644 --- a/crates/perry-runtime/src/arena/walk.rs +++ b/crates/perry-runtime/src/arena/walk.rs @@ -376,6 +376,7 @@ pub(crate) struct ArenaRegionTelemetry { } #[derive(Clone, Copy, Default)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(crate) struct ArenaTelemetrySnapshot { pub(crate) arena: ArenaRegionTelemetry, pub(crate) survivor0: ArenaRegionTelemetry, diff --git a/crates/perry-runtime/src/gc/cycle.rs b/crates/perry-runtime/src/gc/cycle.rs index 6119c19ac4..8e48cdfd13 100644 --- a/crates/perry-runtime/src/gc/cycle.rs +++ b/crates/perry-runtime/src/gc/cycle.rs @@ -14,6 +14,7 @@ pub(super) enum GcCyclePhase { } impl GcCyclePhase { + #[cfg(feature = "diagnostics")] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { diff --git a/crates/perry-runtime/src/gc/malloc.rs b/crates/perry-runtime/src/gc/malloc.rs index 9d5427badd..ea84ae386f 100644 --- a/crates/perry-runtime/src/gc/malloc.rs +++ b/crates/perry-runtime/src/gc/malloc.rs @@ -30,6 +30,7 @@ impl MallocKindTelemetry { } } + #[cfg(feature = "diagnostics")] pub(super) fn reset_cycle_deltas(&mut self) { self.allocated_count = 0; self.allocated_bytes = 0; @@ -273,6 +274,7 @@ impl MallocState { counters.copied_minor_validation_lookups.saturating_add(1); } + #[cfg(feature = "diagnostics")] pub(super) fn take_kind_telemetry( &mut self, ) -> [MallocKindTelemetry; MALLOC_KIND_BUCKET_COUNT] { diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index d5516a92a5..ab6a35fa88 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -58,7 +58,9 @@ mod cycle; use cycle::*; mod verify; pub use verify::*; +#[cfg(feature = "diagnostics")] mod heap_snapshot; +#[cfg(feature = "diagnostics")] pub use heap_snapshot::gc_build_v8_heap_snapshot_json; pub fn gc_collect_minor() -> u64 { diff --git a/crates/perry-runtime/src/gc/oldgen.rs b/crates/perry-runtime/src/gc/oldgen.rs index 64f0dadc99..6f05d67272 100644 --- a/crates/perry-runtime/src/gc/oldgen.rs +++ b/crates/perry-runtime/src/gc/oldgen.rs @@ -107,6 +107,7 @@ impl Default for EvacuationPolicyDecision { } #[derive(Clone, Copy, Default)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct SweepTraceStats { pub(super) dead_bytes: u64, // Compatibility alias for dead_bytes. diff --git a/crates/perry-runtime/src/gc/policy.rs b/crates/perry-runtime/src/gc/policy.rs index 687283e3bd..b83dbd84f0 100644 --- a/crates/perry-runtime/src/gc/policy.rs +++ b/crates/perry-runtime/src/gc/policy.rs @@ -365,6 +365,7 @@ pub(super) enum GcCollectionKind { } impl GcCollectionKind { + #[cfg(feature = "diagnostics")] #[inline] pub(super) fn as_str(self) -> &'static str { match self { @@ -394,6 +395,7 @@ pub(super) enum GcTriggerKind { } impl GcTriggerKind { + #[cfg(feature = "diagnostics")] #[inline] pub(super) fn as_str(self) -> &'static str { match self { @@ -466,6 +468,7 @@ impl DeferredGcRequest { } #[derive(Clone, Copy)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct GcStepSnapshot { pub(super) arena_step_bytes: usize, pub(super) next_arena_trigger_bytes: usize, diff --git a/crates/perry-runtime/src/gc/roots.rs b/crates/perry-runtime/src/gc/roots.rs index b05d0d743b..696b428fd5 100644 --- a/crates/perry-runtime/src/gc/roots.rs +++ b/crates/perry-runtime/src/gc/roots.rs @@ -143,6 +143,7 @@ pub(super) enum ConservativeStackScanDecision { } impl ConservativeStackScanDecision { + #[cfg(feature = "diagnostics")] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { diff --git a/crates/perry-runtime/src/gc/telemetry.rs b/crates/perry-runtime/src/gc/telemetry.rs index f6941cd0ee..91b4499836 100644 --- a/crates/perry-runtime/src/gc/telemetry.rs +++ b/crates/perry-runtime/src/gc/telemetry.rs @@ -15,6 +15,7 @@ thread_local! { } #[derive(Clone, Copy, Default)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct RememberedSetTraceStats { pub(super) entries_scanned: usize, pub(super) valid_roots: usize, @@ -98,6 +99,7 @@ pub(super) enum CopiedMinorFallbackReason { } impl CopiedMinorFallbackReason { + #[cfg(feature = "diagnostics")] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -218,6 +220,7 @@ impl RootSourceSlotTraceStats { } #[derive(Clone, Copy, Default)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct NativeStackFallbackTraceStats { pub(super) decision: ConservativeStackScanDecision, pub(super) scanned: bool, @@ -524,6 +527,7 @@ pub(super) enum AllocatorMaintenanceStatus { } impl AllocatorMaintenanceStatus { + #[cfg(feature = "diagnostics")] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -543,6 +547,7 @@ pub(super) enum AllocatorMaintenanceReason { } impl AllocatorMaintenanceReason { + #[cfg(feature = "diagnostics")] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -554,6 +559,7 @@ impl AllocatorMaintenanceReason { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct AllocatorMaintenanceEvent { pub(super) status: AllocatorMaintenanceStatus, pub(super) reason: AllocatorMaintenanceReason, @@ -561,10 +567,12 @@ pub(super) struct AllocatorMaintenanceEvent { } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct AllocatorMaintenanceTrace { pub(super) malloc_trim: Option, } +#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] pub(super) struct GcCycleTrace { pub(super) collection_kind: GcCollectionKind, pub(super) trigger_kind: GcTriggerKind, @@ -719,6 +727,7 @@ impl GcCycleTrace { } } + #[cfg(feature = "diagnostics")] pub(super) fn into_json(mut self, steps_after: GcStepSnapshot) -> serde_json::Value { self.capture_layout_scans(); self.debt.record(GcDebtSnapshot::current()); @@ -964,6 +973,7 @@ impl GcCycleTrace { }) } + #[cfg(feature = "diagnostics")] pub(super) fn emit(self, steps_after: GcStepSnapshot) { let event = self.into_json(steps_after); #[cfg(test)] @@ -972,8 +982,16 @@ impl GcCycleTrace { eprintln!("{line}"); } } + + #[cfg(not(feature = "diagnostics"))] + pub(super) fn emit(self, _steps_after: GcStepSnapshot) { + eprintln!( + "[gc] cycle (diagnostics feature disabled — rebuild without --no-default-features for JSON trace)" + ); + } } +#[cfg(feature = "diagnostics")] pub(super) fn debt_snapshot_json(snapshot: GcDebtSnapshot) -> serde_json::Value { serde_json::json!({ "arena_debt_bytes": snapshot.arena_debt_bytes, @@ -982,6 +1000,7 @@ pub(super) fn debt_snapshot_json(snapshot: GcDebtSnapshot) -> serde_json::Value }) } +#[cfg(feature = "diagnostics")] pub(super) fn pause_budget_json( progress_kind: GcProgressKind, progress_budget: GcPauseBudget, @@ -999,6 +1018,7 @@ pub(super) fn pause_budget_json( }) } +#[cfg(feature = "diagnostics")] pub(super) fn pause_step_json(step: GcPauseStepTrace) -> serde_json::Value { let progress_budget = gc_progress_contract().budget_for(step.progress_kind); let within_soft_pause_target = progress_budget @@ -1026,6 +1046,7 @@ pub(super) fn pause_step_json(step: GcPauseStepTrace) -> serde_json::Value { }) } +#[cfg(feature = "diagnostics")] pub(super) fn allocator_maintenance_json( trace: AllocatorMaintenanceTrace, progress_kind: GcProgressKind, @@ -1045,6 +1066,7 @@ pub(super) fn allocator_maintenance_json( }) } +#[cfg(feature = "diagnostics")] fn default_malloc_trim_maintenance(progress_kind: GcProgressKind) -> AllocatorMaintenanceEvent { if progress_kind.is_budgeted() { return AllocatorMaintenanceEvent { @@ -1134,6 +1156,7 @@ pub(super) fn malloc_object_count() -> usize { MALLOC_STATE.with(|s| s.borrow().objects.len()) } +#[cfg(feature = "diagnostics")] pub(super) fn malloc_kind_telemetry_row( obj_type: u8, counters: MallocKindTelemetry, @@ -1154,6 +1177,7 @@ pub(super) fn malloc_kind_telemetry_row( }) } +#[cfg(feature = "diagnostics")] pub(super) fn root_source_slot_json(stats: RootSourceSlotTraceStats) -> serde_json::Value { serde_json::json!({ "registered_scanners": stats.registered_scanners, @@ -1164,6 +1188,7 @@ pub(super) fn root_source_slot_json(stats: RootSourceSlotTraceStats) -> serde_js }) } +#[cfg(feature = "diagnostics")] pub(super) fn root_sources_json(stats: RootSourcesTraceStats) -> serde_json::Value { serde_json::json!({ "compiled_shadow": root_source_slot_json(stats.compiled_shadow), @@ -1183,6 +1208,7 @@ pub(super) fn root_sources_json(stats: RootSourcesTraceStats) -> serde_json::Val }) } +#[cfg(feature = "diagnostics")] pub(super) fn malloc_kind_telemetry_json_from_snapshot( snapshot: [MallocKindTelemetry; MALLOC_KIND_BUCKET_COUNT], ) -> serde_json::Value { @@ -1201,11 +1227,13 @@ pub(super) fn malloc_kind_telemetry_json_from_snapshot( serde_json::Value::Array(rows) } +#[cfg(feature = "diagnostics")] pub(super) fn take_malloc_kind_telemetry_json() -> serde_json::Value { let snapshot = MALLOC_STATE.with(|s| s.borrow_mut().take_kind_telemetry()); malloc_kind_telemetry_json_from_snapshot(snapshot) } +#[cfg(feature = "diagnostics")] pub(super) fn arena_region_json(region: crate::arena::ArenaRegionTelemetry) -> serde_json::Value { serde_json::json!({ "in_use_bytes": region.in_use_bytes, @@ -1214,6 +1242,7 @@ pub(super) fn arena_region_json(region: crate::arena::ArenaRegionTelemetry) -> s }) } +#[cfg(feature = "diagnostics")] pub(super) fn arena_snapshot_json( snapshot: crate::arena::ArenaTelemetrySnapshot, ) -> serde_json::Value { @@ -1229,6 +1258,7 @@ pub(super) fn arena_snapshot_json( }) } +#[cfg(feature = "diagnostics")] pub(super) fn steps_json(before: GcStepSnapshot, after: GcStepSnapshot) -> serde_json::Value { serde_json::json!({ "arena_step_bytes": { diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index ced071c3e0..724a233779 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -623,6 +623,7 @@ pub(crate) unsafe fn gc_type_finalize_unmarked_payload(obj_type: u8, user_ptr: * } } +#[cfg(feature = "diagnostics")] #[inline] pub(super) fn gc_type_name(obj_type: u8) -> &'static str { gc_type_info(obj_type).map_or("unknown", |info| info.name) diff --git a/crates/perry-runtime/src/node_v8.rs b/crates/perry-runtime/src/node_v8.rs index 3d1de9e375..b6bdd53065 100644 --- a/crates/perry-runtime/src/node_v8.rs +++ b/crates/perry-runtime/src/node_v8.rs @@ -334,7 +334,12 @@ pub extern "C" fn js_v8_cached_data_version_tag() -> f64 { #[no_mangle] pub extern "C" fn js_v8_get_heap_snapshot(options: f64) -> f64 { validate_heap_snapshot_options(options); + #[cfg(feature = "diagnostics")] let json = crate::gc::gc_build_v8_heap_snapshot_json(); + // OFF stub: the compiler enables `diagnostics` whenever a program uses the + // v8 heap-snapshot APIs, so this branch is unreachable in practice. + #[cfg(not(feature = "diagnostics"))] + let json = String::from("{}"); snapshot_readable_stream(&json) } @@ -352,7 +357,12 @@ pub extern "C" fn js_v8_write_heap_snapshot(filename: f64, options: f64) -> f64 } }; validate_heap_snapshot_options(options); + #[cfg(feature = "diagnostics")] let json = crate::gc::gc_build_v8_heap_snapshot_json(); + // OFF stub: unreachable in practice (compiler enables `diagnostics` when a + // program uses the v8 heap-snapshot APIs). + #[cfg(not(feature = "diagnostics"))] + let json = String::from("{}"); match std::fs::write(&path, json.as_bytes()) { Ok(()) => string_value(&path), Err(err) => unsafe { diff --git a/crates/perry-runtime/src/process.rs b/crates/perry-runtime/src/process.rs index c741b2cea2..d4e881f582 100644 --- a/crates/perry-runtime/src/process.rs +++ b/crates/perry-runtime/src/process.rs @@ -801,7 +801,12 @@ extern "C" fn process_report_function_write_report( let filename = module_value_to_string(file_arg) .filter(|s| !s.is_empty()) .unwrap_or_else(process_report_default_filename); + // OFF stub: unreachable in practice (the compiler enables `diagnostics` + // whenever a program references `process.report`). + #[cfg(feature = "diagnostics")] let report_json = process_report_json_string("API", Some(&filename)); + #[cfg(not(feature = "diagnostics"))] + let report_json = String::from("{}"); if let Err(err) = std::fs::write(&filename, report_json) { crate::fs::validate::throw_type_error_with_code( &format!("Failed to write diagnostic report to {filename}: {err}"), @@ -1282,6 +1287,7 @@ fn node_platform_name() -> &'static str { } } +#[cfg(feature = "diagnostics")] fn process_report_json_string(trigger: &str, filename: Option<&str>) -> String { let args: Vec = std::env::args().collect(); let command_line = if args.is_empty() { diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 6b3b2ecae2..88385a1264 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -5,10 +5,11 @@ //! has actually seen at runtime. use std::collections::{BTreeMap, HashMap}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - LazyLock, Mutex, -}; +#[cfg(any(feature = "diagnostics", test))] +use std::sync::atomic::AtomicBool; +#[cfg(any(feature = "diagnostics", test))] +use std::sync::atomic::Ordering; +use std::sync::{LazyLock, Mutex}; use crate::array::ArrayHeader; use crate::object::ObjectHeader; @@ -21,6 +22,7 @@ const POLYMORPHIC_CAP: usize = 4; static REGISTRY: LazyLock> = LazyLock::new(|| Mutex::new(TypedFeedbackRegistry::default())); +#[cfg(any(feature = "diagnostics", test))] static TRACE_DUMPED: AtomicBool = AtomicBool::new(false); #[cfg(not(test))] @@ -961,9 +963,9 @@ pub use guards::{ #[path = "typed_feedback/trace.rs"] mod trace; -pub use trace::{ - js_typed_feedback_maybe_dump_trace, typed_feedback_snapshot, typed_feedback_trace_json, -}; +pub use trace::typed_feedback_snapshot; +#[cfg(feature = "diagnostics")] +pub use trace::{js_typed_feedback_maybe_dump_trace, typed_feedback_trace_json}; fn hash_bytes(bytes: &[u8]) -> u64 { let mut h = 0xcbf2_9ce4_8422_2325u64; diff --git a/crates/perry-runtime/src/typed_feedback/trace.rs b/crates/perry-runtime/src/typed_feedback/trace.rs index 3ae58a68dd..c67acbfb4f 100644 --- a/crates/perry-runtime/src/typed_feedback/trace.rs +++ b/crates/perry-runtime/src/typed_feedback/trace.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "diagnostics")] use std::path::{Path, PathBuf}; use super::*; @@ -225,6 +226,7 @@ pub fn typed_feedback_snapshot() -> TypedFeedbackSnapshot { snapshot } +#[cfg(feature = "diagnostics")] pub fn typed_feedback_trace_json() -> serde_json::Value { let snapshot = typed_feedback_snapshot(); serde_json::json!({ @@ -283,6 +285,7 @@ pub fn typed_feedback_trace_json() -> serde_json::Value { }) } +#[cfg(feature = "diagnostics")] fn typed_feedback_trace_path_from_env() -> Option { let value = std::env::var("PERRY_TYPED_FEEDBACK_TRACE").ok()?; if value.is_empty() || value == "0" { @@ -295,6 +298,7 @@ fn typed_feedback_trace_path_from_env() -> Option { } } +#[cfg(feature = "diagnostics")] fn ensure_parent_dir(path: &Path) -> std::io::Result<()> { if let Some(parent) = path.parent() { if !parent.as_os_str().is_empty() { @@ -304,28 +308,38 @@ fn ensure_parent_dir(path: &Path) -> std::io::Result<()> { Ok(()) } +// The extern symbol must ALWAYS be compiled: codegen emits an unconditional +// call to it from `main` (the program-exit trace-dump hook), so gating the +// whole function breaks the final link under auto-optimize (`Undefined symbols: +// _js_typed_feedback_maybe_dump_trace`). Only the JSON-building body is gated +// behind `diagnostics`; with the feature off this is a no-op (the +// `PERRY_TYPED_FEEDBACK` trace is a dev diagnostic, absent from size-optimized +// binaries). #[no_mangle] pub extern "C" fn js_typed_feedback_maybe_dump_trace() { - let Some(path) = typed_feedback_trace_path_from_env() else { - return; - }; - if TRACE_DUMPED.swap(true, Ordering::AcqRel) { - return; - } - - let json = typed_feedback_trace_json(); - let bytes = match serde_json::to_vec_pretty(&json) { - Ok(bytes) => bytes, - Err(err) => { - eprintln!("perry typed-feedback trace: failed to encode JSON: {err}"); + #[cfg(feature = "diagnostics")] + { + let Some(path) = typed_feedback_trace_path_from_env() else { + return; + }; + if TRACE_DUMPED.swap(true, Ordering::AcqRel) { return; } - }; - if let Err(err) = ensure_parent_dir(&path).and_then(|_| std::fs::write(&path, bytes)) { - eprintln!( - "perry typed-feedback trace: failed to write {}: {err}", - path.display() - ); + + let json = typed_feedback_trace_json(); + let bytes = match serde_json::to_vec_pretty(&json) { + Ok(bytes) => bytes, + Err(err) => { + eprintln!("perry typed-feedback trace: failed to encode JSON: {err}"); + return; + } + }; + if let Err(err) = ensure_parent_dir(&path).and_then(|_| std::fs::write(&path, bytes)) { + eprintln!( + "perry typed-feedback trace: failed to write {}: {err}", + path.display() + ); + } } } @@ -373,5 +387,6 @@ mod keep_typed_feedback { #[used] static K20: extern "C" fn(u64, i64, f64, f64) = js_typed_feedback_object_set_index_polymorphic; #[used] static K21: extern "C" fn(u64, *mut ObjectHeader, u32, *const crate::StringHeader, f64) = js_typed_feedback_object_set_unboxed_f64_field; #[used] static K22: extern "C" fn(u64, f64) -> f64 = js_typed_feedback_observe_helper_return; + #[cfg(feature = "diagnostics")] #[used] static K23: extern "C" fn() = js_typed_feedback_maybe_dump_trace; } diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index b3dbc254de..8ad98fb8a1 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -1927,6 +1927,23 @@ fn collect_module_finish( } } + // Detect heap-snapshot / `process.report` usage, the only user-facing APIs + // behind the `diagnostics` feature (~95 KB of cold-path JSON serializers + + // the `serde_json` pulled only by them). `v8.getHeapSnapshot` / + // `v8.writeHeapSnapshot` lower to `NativeMethodCall { method: "…" }`; + // `process.report.*` surfaces as `property: "report"`. The env-driven dev + // diagnostics (GC-diag / typed-feedback JSON) ride the same feature and + // degrade gracefully when off, so they need no detection. + { + let hir_debug: String = format!("{:?}{:?}", &hir_module.init, &hir_module.functions); + if hir_debug.contains("method: \"getHeapSnapshot\"") + || hir_debug.contains("method: \"writeHeapSnapshot\"") + || hir_debug.contains("property: \"report\"") + { + ctx.uses_diagnostics = true; + } + } + // Detect readline usage via process.stdin raw/lifecycle methods. These // don't go through an `import 'readline'` statement, so the import-based // needs_stdlib detection above misses them. diff --git a/crates/perry/src/commands/compile/link/mod.rs b/crates/perry/src/commands/compile/link/mod.rs index b4d1279c6b..34ccb0e1ba 100644 --- a/crates/perry/src/commands/compile/link/mod.rs +++ b/crates/perry/src/commands/compile/link/mod.rs @@ -882,7 +882,11 @@ pub(super) fn build_and_run_link( "{}/toolchains/llvm/prebuilt/{}/bin/aarch64-linux-android24-clang{}", ndk_home, host_tag, - if cfg!(target_os = "windows") { ".cmd" } else { "" } + if cfg!(target_os = "windows") { + ".cmd" + } else { + "" + } ); let stub_ok = Command::new(&ndk_clang) .args(["-c", "-fPIC", "-target", "aarch64-linux-android24"]) diff --git a/crates/perry/src/commands/compile/link/platform_cmd.rs b/crates/perry/src/commands/compile/link/platform_cmd.rs index 05ca762936..5d86946e01 100644 --- a/crates/perry/src/commands/compile/link/platform_cmd.rs +++ b/crates/perry/src/commands/compile/link/platform_cmd.rs @@ -576,7 +576,11 @@ pub fn select_linker_command( "{}/toolchains/llvm/prebuilt/{}/bin/aarch64-linux-android24-clang{}", ndk_home, host_tag, - if cfg!(target_os = "windows") { ".cmd" } else { "" } + if cfg!(target_os = "windows") { + ".cmd" + } else { + "" + } ); if !PathBuf::from(&clang).exists() { return Err(anyhow!("Android NDK clang not found at: {}", clang)); diff --git a/crates/perry/src/commands/compile/optimized_libs.rs b/crates/perry/src/commands/compile/optimized_libs.rs index 256e1cf47b..83d58ba849 100644 --- a/crates/perry/src/commands/compile/optimized_libs.rs +++ b/crates/perry/src/commands/compile/optimized_libs.rs @@ -670,7 +670,7 @@ pub(super) fn build_optimized_libs( // Cheap djb2 — no need for the SipHash overhead. let target_str = target.unwrap_or("host"); let key_input = format!( - "{}|{}|{}|wasm={}|regex={}|temporal={}|url={}|norm={}|seg={}|v={}", + "{}|{}|{}|wasm={}|regex={}|temporal={}|url={}|norm={}|seg={}|diag={}|v={}", feature_arg, panic_abort_safe, target_str, @@ -680,6 +680,7 @@ pub(super) fn build_optimized_libs( ctx.uses_url, ctx.uses_string_normalize, ctx.uses_intl_segmenter, + ctx.uses_diagnostics, env!("CARGO_PKG_VERSION"), ); let mut hash: u64 = 5381; @@ -796,6 +797,13 @@ pub(super) fn build_optimized_libs( if ctx.uses_intl_segmenter { cross_features.push("perry-runtime/intl-segmenter".to_string()); } + // Cold-path diagnostic JSON serializers (~95 KB incl. the `serde_json` + // pulled only by them) — enabled only when the program uses a heap-snapshot + // API or `process.report`. The env-driven GC/typed-feedback dev trace JSON + // ride this feature and stay off in size-optimized binaries. + if ctx.uses_diagnostics { + cross_features.push("perry-runtime/diagnostics".to_string()); + } if !cross_features.is_empty() { cargo_cmd.arg("--features").arg(cross_features.join(",")); } diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index 19338354d9..782cb99e6b 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -570,6 +570,14 @@ pub struct CompilationContext { /// `perry-runtime/intl-segmenter` (`unicode-segmentation`, ~73 KB of UAX #29 /// grapheme/word/sentence tables). Other `Intl.*` APIs don't need it. pub uses_intl_segmenter: bool, + /// Whether any TS module uses a heap-snapshot API (`v8.getHeapSnapshot` / + /// `v8.writeHeapSnapshot`) or `process.report`. Gates + /// `perry-runtime/diagnostics` (the cold-path JSON serializers + the + /// `serde_json` pulled only by them, ~95 KB). The env-driven dev + /// diagnostics (`PERRY_GC_DIAG` JSON trace, typed-feedback trace dump) ride + /// the same feature and degrade gracefully when it's off, so they're absent + /// from size-optimized binaries unless one of these APIs is also used. + pub uses_diagnostics: bool, /// Whether `perry/thread` is imported. When true, the runtime must /// keep `panic = "unwind"` so that worker-thread panics translate to /// promise rejections via `catch_unwind` in `perry-runtime/src/thread.rs` @@ -818,6 +826,7 @@ impl CompilationContext { uses_url: false, uses_string_normalize: false, uses_intl_segmenter: false, + uses_diagnostics: false, needs_thread: false, cross_module_class_field_types: HashMap::new(), min_windows_version: "10".to_string(),