use std::cell::RefCell;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
static ENABLED: AtomicBool = AtomicBool::new(false);
pub fn init() {
if std::env::var("SUI_EVAL_PERF").ok().as_deref() == Some("1") {
ENABLED.store(true, Ordering::Relaxed);
}
}
pub fn set_enabled(on: bool) {
ENABLED.store(on, Ordering::Relaxed);
}
#[inline(always)]
pub fn enabled() -> bool {
ENABLED.load(Ordering::Relaxed)
}
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum Counter {
EvalExpr = 0,
ForceValue = 1,
ThunkForce = 2,
ThunkHit = 3,
Import = 4,
ImportHit = 5,
Apply = 6,
Select = 7,
Attrset = 8,
EnvClone = 9,
EnvLookup = 10,
EnvLookupDepth = 11,
ExprIdent = 12,
ExprLiteral = 13,
ExprStr = 14,
ExprList = 15,
ExprAttrs = 16,
ExprSelect = 17,
ExprApply = 18,
ExprLetIn = 19,
ExprIfElse = 20,
ExprWith = 21,
ExprLambda = 22,
ExprOther = 23,
DeadBindingsSkipped = 24,
ExprBinOp = 25,
ExprHasAttr = 26,
ExprUnaryOp = 27,
ExprAssert = 28,
ExprPath = 29,
OverlayFlattenAttempt = 30,
OverlayFlattenBuild = 31,
OverlayFlattenEntries = 32,
OverlayCreated = 33,
SortedEntriesCalls = 34,
SortedEntriesRows = 35,
ListConcatCalls = 36,
ListConcatElemsCopied = 37,
ListConcatElemsReused = 38,
AttrsEqStructuralCalls = 39,
AttrsEqEntriesCloneElided = 40,
WithScopeCacheClone = 41,
SlashDeferredTailClone = 42,
ThunkStoreWrites = 43,
ThunkStoreLoopMutated = 44,
ThunkStoreRedundant = 45,
SelfRecWalkCalls = 46,
SelfRecWalkNodes = 47,
ThunkSiteMaybeOther = 48,
ThunkSiteApplyArg = 49,
ThunkSiteMaybeIdent = 50,
ThunkSiteLetForward = 51,
ThunkSiteOther = 52,
ThunkSiteInheritSrc = 53,
ThunkSiteNative = 54,
ThunkSiteEvaluated = 55,
}
const NUM_COUNTERS: usize = 56;
const COUNTER_NAMES: [&str; NUM_COUNTERS] = [
"eval_expr",
"force_value",
"thunk_forces",
"thunk_hits",
"imports",
"import_hits",
"apply",
"select",
"attrsets",
"env_clones",
"env_lookups",
"env_lookup_depth",
"expr_ident",
"expr_literal",
"expr_str",
"expr_list",
"expr_attrs",
"expr_select",
"expr_apply",
"expr_letin",
"expr_ifelse",
"expr_with",
"expr_lambda",
"expr_other",
"dead_bindings_skipped",
"expr_binop",
"expr_hasattr",
"expr_unaryop",
"expr_assert",
"expr_path",
"overlay_flatten_attempt",
"overlay_flatten_build",
"overlay_flatten_entries",
"overlay_created",
"sorted_entries_calls",
"sorted_entries_rows",
"list_concat_calls",
"list_concat_elems_copied",
"list_concat_elems_reused",
"attrs_eq_structural_calls",
"attrs_eq_entries_clone_elided",
"with_scope_cache_clone",
"slash_deferred_tail_clone",
"thunk_store_writes",
"thunk_store_loop_mutated",
"thunk_store_redundant",
"self_rec_walk_calls",
"self_rec_walk_nodes",
"thunk_site_maybe_other",
"thunk_site_apply_arg",
"thunk_site_maybe_ident",
"thunk_site_let_forward",
"thunk_site_other",
"thunk_site_inherit_src",
"thunk_site_native",
"thunk_site_evaluated",
];
struct PerfCounters {
counts: [u64; NUM_COUNTERS],
}
impl Default for PerfCounters {
fn default() -> Self {
Self {
counts: [0; NUM_COUNTERS],
}
}
}
impl PerfCounters {
#[inline(always)]
fn inc(&mut self, counter: Counter) {
self.counts[counter as usize] += 1;
}
#[inline(always)]
fn add(&mut self, counter: Counter, n: u64) {
self.counts[counter as usize] += n;
}
#[inline(always)]
fn get(&self, counter: Counter) -> u64 {
self.counts[counter as usize]
}
}
thread_local! {
static COUNTERS: RefCell<PerfCounters> = RefCell::new(PerfCounters::default());
static START: RefCell<Option<Instant>> = RefCell::new(None);
}
pub fn start() {
if enabled() {
START.with(|s| *s.borrow_mut() = Some(Instant::now()));
}
}
const PROGRESS_INTERVAL: u64 = 1_000_000;
#[inline(always)]
pub fn inc(counter: Counter) {
if !enabled() {
return;
}
COUNTERS.with(|c| {
let mut c = c.borrow_mut();
c.inc(counter);
if matches!(counter, Counter::EvalExpr)
&& c.get(Counter::EvalExpr) % PROGRESS_INTERVAL == 0
{
let elapsed = START.with(|s| {
s.borrow()
.map(|s| s.elapsed().as_secs_f64())
.unwrap_or(0.0)
});
eprintln!(
"[perf] {:.1}s | eval:{} force:{} thunk_f:{} thunk_h:{} import:{}({}) apply:{} select:{} attrset:{} env_c:{} env_l:{}",
elapsed,
c.get(Counter::EvalExpr),
c.get(Counter::ForceValue),
c.get(Counter::ThunkForce),
c.get(Counter::ThunkHit),
c.get(Counter::Import),
c.get(Counter::ImportHit),
c.get(Counter::Apply),
c.get(Counter::Select),
c.get(Counter::Attrset),
c.get(Counter::EnvClone),
c.get(Counter::EnvLookup),
);
eprintln!(
" [id:{} ap:{} if:{} let:{} sel:{} at:{} w:{} lam:{} lit:{} str:{} list:{} ot:{}]",
c.get(Counter::ExprIdent),
c.get(Counter::ExprApply),
c.get(Counter::ExprIfElse),
c.get(Counter::ExprLetIn),
c.get(Counter::ExprSelect),
c.get(Counter::ExprAttrs),
c.get(Counter::ExprWith),
c.get(Counter::ExprLambda),
c.get(Counter::ExprLiteral),
c.get(Counter::ExprStr),
c.get(Counter::ExprList),
c.get(Counter::ExprOther),
);
let binop = c.get(Counter::ExprBinOp);
let hasattr = c.get(Counter::ExprHasAttr);
let unary = c.get(Counter::ExprUnaryOp);
let assert = c.get(Counter::ExprAssert);
let path = c.get(Counter::ExprPath);
if binop + hasattr + unary + assert + path > 0 {
eprintln!(
" [binop:{binop} hasattr:{hasattr} unary:{unary} assert:{assert} path:{path}]",
);
}
let dead = c.get(Counter::DeadBindingsSkipped);
let created = crate::trace::get_thunks_created();
let forced = crate::trace::get_thunks_forced();
if created > 0 {
let waste = (1.0 - forced as f64 / created as f64) * 100.0;
eprintln!(" [thunks created:{created} forced:{forced} waste:{waste:.0}% dead_skipped:{dead}]");
}
crate::eval::dump_force_sites();
}
});
}
#[inline(always)]
pub fn add(counter: Counter, n: u64) {
if !enabled() {
return;
}
COUNTERS.with(|c| {
c.borrow_mut().add(counter, n);
});
}
pub fn report() {
if !enabled() {
return;
}
COUNTERS.with(|c| {
let c = c.borrow();
let elapsed = START.with(|s| {
s.borrow()
.map(|s| s.elapsed().as_secs_f64())
.unwrap_or(0.0)
});
let lookups = c.get(Counter::EnvLookup);
let depth_total = c.get(Counter::EnvLookupDepth);
let avg_lookup = if lookups > 0 {
depth_total as f64 / lookups as f64
} else {
0.0
};
eprintln!("\n=== sui-eval performance ===");
eprintln!("elapsed: {elapsed:.2}s");
eprintln!("eval_expr: {}", c.get(Counter::EvalExpr));
eprintln!("force_value: {}", c.get(Counter::ForceValue));
eprintln!("thunk_forces: {}", c.get(Counter::ThunkForce));
eprintln!("thunk_hits: {}", c.get(Counter::ThunkHit));
eprintln!(
"imports: {} ({} cached)",
c.get(Counter::Import),
c.get(Counter::ImportHit)
);
eprintln!("apply: {}", c.get(Counter::Apply));
eprintln!("select: {}", c.get(Counter::Select));
eprintln!("attrsets: {}", c.get(Counter::Attrset));
eprintln!("env_clones: {}", c.get(Counter::EnvClone));
eprintln!(
"env_lookups: {} (avg depth {avg_lookup:.1})",
lookups
);
let total = c.get(Counter::EvalExpr);
if total > 0 {
eprintln!("--- expression breakdown ---");
for (counter, name) in [
(Counter::ExprIdent, "ident"),
(Counter::ExprApply, "apply"),
(Counter::ExprLetIn, "let-in"),
(Counter::ExprIfElse, "if-else"),
(Counter::ExprSelect, "select"),
(Counter::ExprAttrs, "attrset"),
(Counter::ExprWith, "with"),
(Counter::ExprLambda, "lambda"),
(Counter::ExprLiteral, "literal"),
(Counter::ExprStr, "string"),
(Counter::ExprList, "list"),
(Counter::ExprBinOp, "binop"),
(Counter::ExprHasAttr, "hasattr"),
(Counter::ExprUnaryOp, "unaryop"),
(Counter::ExprAssert, "assert"),
(Counter::ExprPath, "path"),
(Counter::ExprOther, "other"),
] {
let n = c.get(counter);
if n > 0 {
let pct = (n as f64 / total as f64) * 100.0;
eprintln!(" {name:<12} {n:>12} ({pct:.1}%)");
}
}
}
let dead = c.get(Counter::DeadBindingsSkipped);
if dead > 0 {
eprintln!("dead_skipped: {dead}");
}
let ov_created = c.get(Counter::OverlayCreated);
let ov_attempt = c.get(Counter::OverlayFlattenAttempt);
let ov_build = c.get(Counter::OverlayFlattenBuild);
let ov_entries = c.get(Counter::OverlayFlattenEntries);
if ov_created > 0 || ov_attempt > 0 {
let hit = ov_attempt.saturating_sub(ov_build);
let hit_rate = if ov_attempt > 0 {
(hit as f64 / ov_attempt as f64) * 100.0
} else {
0.0
};
eprintln!("--- overlay (`//`) flatten ---");
eprintln!(" overlays_created: {ov_created}");
eprintln!(" flatten_attempts: {ov_attempt}");
eprintln!(" flatten_builds: {ov_build} (cache-miss = real O(n) merge)");
eprintln!(" cache_hit_rate: {hit_rate:.1}%");
eprintln!(" entries_merged: {ov_entries} (sum of left+right over all builds)");
if ov_created > 0 {
let builds_per_overlay = ov_build as f64 / ov_created as f64;
eprintln!(" builds_per_overlay:{builds_per_overlay:.2}");
}
let flatten_ms = crate::trace::get_overlay_flatten_nanos() as f64 / 1_000_000.0;
let pct = if elapsed > 0.0 {
(flatten_ms / 1000.0 / elapsed) * 100.0
} else {
0.0
};
eprintln!(" flatten_walltime: {flatten_ms:.1}ms ({pct:.1}% of eval, incl. nested)");
}
let se_calls = c.get(Counter::SortedEntriesCalls);
let se_rows = c.get(Counter::SortedEntriesRows);
if se_calls > 0 {
let se_ms = crate::trace::get_sorted_entries_nanos() as f64 / 1_000_000.0;
let se_pct = if elapsed > 0.0 {
(se_ms / 1000.0 / elapsed) * 100.0
} else {
0.0
};
eprintln!("--- sorted_entries (attrNames/iter) ---");
eprintln!(" calls: {se_calls}");
eprintln!(" rows_sorted: {se_rows}");
eprintln!(" walltime: {se_ms:.1}ms ({se_pct:.1}% of eval)");
}
let lc_calls = c.get(Counter::ListConcatCalls);
let lc_copied = c.get(Counter::ListConcatElemsCopied);
let lc_reused = c.get(Counter::ListConcatElemsReused);
if lc_calls > 0 {
let total = lc_copied + lc_reused;
let reuse_pct = if total > 0 {
(lc_reused as f64 / total as f64) * 100.0
} else {
0.0
};
eprintln!("--- list concat (`++` / concatLists) ---");
eprintln!(" calls: {lc_calls}");
eprintln!(" elems_copied: {lc_copied}");
eprintln!(" elems_reused: {lc_reused} (in-place, Rc uniquely owned)");
eprintln!(" reuse_rate: {reuse_pct:.1}%");
}
let eq_calls = c.get(Counter::AttrsEqStructuralCalls);
let eq_elided = c.get(Counter::AttrsEqEntriesCloneElided);
if eq_calls > 0 {
eprintln!("--- attrs structural eq (`==` fallback) ---");
eprintln!(" structural_calls: {eq_calls}");
eprintln!(
" map_clones_elided: {} ({eq_elided} entries not cloned — was 2 FxHashMap clones/call)",
eq_calls * 2
);
}
let wc = c.get(Counter::WithScopeCacheClone);
let sc = c.get(Counter::SlashDeferredTailClone);
let ts = c.get(Counter::ThunkStoreWrites);
if wc + sc + ts > 0 {
eprintln!("--- M2 RISKY-tier waste probes ---");
eprintln!(" with_scope_cache_clone: {wc} (C-with; O(1) HAMT clone each)");
eprintln!(" slash_deferred_tail_clone:{sc} (C-slash; O(1) HAMT clone, then COW-merged)");
eprintln!(" thunk_store_writes: {ts} (C-store; repr+cache double-store per force)");
let tm = c.get(Counter::ThunkStoreLoopMutated);
eprintln!(" thunk_store_loop_mutated: {tm} (C-store; Store#2 content ≠ Store#1 — collapse NOT content-neutral if >0)");
let tr = c.get(Counter::ThunkStoreRedundant);
eprintln!(" thunk_store_redundant: {tr} (C-store; Store#2 = pure redundant rewrite — provably skippable)");
}
let sr_calls = c.get(Counter::SelfRecWalkCalls);
let sr_nodes = c.get(Counter::SelfRecWalkNodes);
if sr_calls > 0 {
let nodes_per_call = sr_nodes as f64 / sr_calls as f64;
let sr_ms = crate::trace::get_self_rec_walk_nanos() as f64 / 1_000_000.0;
let sr_pct = if elapsed > 0.0 {
(sr_ms / 1000.0 / elapsed) * 100.0
} else {
0.0
};
eprintln!("--- Storm A: referenced_idents (self/mutual-rec walk) ---");
eprintln!(" walk_calls: {sr_calls} (= binding RHS subtree walks)");
eprintln!(" nodes_walked: {sr_nodes} (total rnix descendants visited)");
eprintln!(" nodes_per_call: {nodes_per_call:.1}");
eprintln!(" walltime: {sr_ms:.1}ms ({sr_pct:.1}% of eval)");
}
let s_maybe = c.get(Counter::ThunkSiteMaybeOther);
let s_apply = c.get(Counter::ThunkSiteApplyArg);
let s_ident = c.get(Counter::ThunkSiteMaybeIdent);
let s_recfwd = c.get(Counter::ThunkSiteLetForward);
let s_withid = c.get(Counter::ThunkSiteOther);
let s_tagged = s_maybe + s_apply + s_ident + s_recfwd + s_withid;
let created = crate::trace::get_thunks_created();
let s_rest = created.saturating_sub(s_tagged);
if s_tagged > 0 {
eprintln!("--- thunk-creation site attribution ---");
eprintln!(" maybe_thunk `_` arm: {s_maybe}");
eprintln!(" apply lambda-arg: {s_apply}");
eprintln!(" maybe_thunk ident fb: {s_ident}");
eprintln!(" recursive let/rec: {s_recfwd}");
eprintln!(" with-ident deferred: {s_withid}");
let s_inh = c.get(Counter::ThunkSiteInheritSrc);
let s_nat = c.get(Counter::ThunkSiteNative);
let s_ev = c.get(Counter::ThunkSiteEvaluated);
eprintln!(" inherit-select: {s_inh}");
eprintln!(" native (flake input): {s_nat}");
eprintln!(" evaluated (pre-done): {s_ev}");
let s_rest2 = s_rest.saturating_sub(s_inh + s_nat + s_ev);
eprintln!(" rest (select-src/…): {s_rest2}");
eprintln!(" thunks_created: {created}");
}
crate::trace::report_maybe_other_kinds();
crate::trace::report_thunk_stats();
eprintln!("===========================\n");
});
}
#[allow(dead_code)]
pub fn counter_name(counter: Counter) -> &'static str {
COUNTER_NAMES[counter as usize]
}
#[derive(Clone, Debug)]
pub struct PerfSnapshot {
pub elapsed: Option<std::time::Duration>,
pub counters: [u64; NUM_COUNTERS],
pub thunks_created: u64,
pub thunks_forced: u64,
}
impl PerfSnapshot {
#[must_use]
pub fn zero() -> Self {
Self {
elapsed: None,
counters: [0; NUM_COUNTERS],
thunks_created: 0,
thunks_forced: 0,
}
}
#[must_use]
pub fn get(&self, counter: Counter) -> u64 {
self.counters[counter as usize]
}
#[must_use]
pub fn delta_from(&self, other: &PerfSnapshot) -> PerfSnapshot {
let mut counters = [0u64; NUM_COUNTERS];
for i in 0..NUM_COUNTERS {
counters[i] = self.counters[i].saturating_sub(other.counters[i]);
}
let elapsed = match (self.elapsed, other.elapsed) {
(Some(a), Some(b)) => Some(a.saturating_sub(b)),
_ => self.elapsed,
};
PerfSnapshot {
elapsed,
counters,
thunks_created: self.thunks_created.saturating_sub(other.thunks_created),
thunks_forced: self.thunks_forced.saturating_sub(other.thunks_forced),
}
}
#[must_use]
pub fn thunk_hit_rate(&self) -> Option<f64> {
let created = self.thunks_created;
if created == 0 {
return None;
}
#[allow(clippy::cast_precision_loss)]
let r = self.thunks_forced as f64 / created as f64;
Some(r)
}
#[must_use]
pub fn dominant_expr_kind(&self) -> Option<(Counter, u64)> {
let kinds = [
Counter::ExprIdent,
Counter::ExprLiteral,
Counter::ExprStr,
Counter::ExprList,
Counter::ExprAttrs,
Counter::ExprSelect,
Counter::ExprApply,
Counter::ExprLetIn,
Counter::ExprIfElse,
Counter::ExprWith,
Counter::ExprLambda,
Counter::ExprBinOp,
Counter::ExprHasAttr,
Counter::ExprUnaryOp,
Counter::ExprAssert,
Counter::ExprPath,
Counter::ExprOther,
];
kinds
.iter()
.map(|&k| (k, self.get(k)))
.filter(|&(_, n)| n > 0)
.max_by_key(|&(_, n)| n)
}
}
#[must_use]
pub fn snapshot() -> PerfSnapshot {
let mut out = [0u64; NUM_COUNTERS];
COUNTERS.with(|c| {
let c = c.borrow();
out.copy_from_slice(&c.counts);
});
let elapsed = START.with(|s| s.borrow().map(|s| s.elapsed()));
PerfSnapshot {
elapsed,
counters: out,
thunks_created: crate::trace::get_thunks_created(),
thunks_forced: crate::trace::get_thunks_forced(),
}
}
pub fn reset() {
COUNTERS.with(|c| {
let mut c = c.borrow_mut();
*c = PerfCounters::default();
});
START.with(|s| *s.borrow_mut() = Some(Instant::now()));
crate::trace::reset_thunk_stats();
}
pub fn with_scope<F, R>(f: F) -> (R, PerfSnapshot)
where
F: FnOnce() -> R,
{
let prev_enabled = enabled();
set_enabled(true);
reset();
let before = snapshot();
let result = f();
let after = snapshot();
let delta = after.delta_from(&before);
set_enabled(prev_enabled);
(result, delta)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_enum_has_30_variants() {
assert_eq!(NUM_COUNTERS, 56);
assert_eq!(COUNTER_NAMES.len(), NUM_COUNTERS);
assert_eq!(Counter::OverlayFlattenAttempt as usize, 30);
assert_eq!(Counter::OverlayCreated as usize, 33);
assert_eq!(Counter::SortedEntriesCalls as usize, 34);
assert_eq!(Counter::SortedEntriesRows as usize, 35);
assert_eq!(Counter::ListConcatCalls as usize, 36);
assert_eq!(Counter::ListConcatElemsCopied as usize, 37);
assert_eq!(Counter::ListConcatElemsReused as usize, 38);
assert_eq!(Counter::AttrsEqStructuralCalls as usize, 39);
assert_eq!(Counter::AttrsEqEntriesCloneElided as usize, 40);
assert_eq!(Counter::WithScopeCacheClone as usize, 41);
assert_eq!(Counter::SlashDeferredTailClone as usize, 42);
assert_eq!(Counter::ThunkStoreWrites as usize, 43);
assert_eq!(Counter::ThunkStoreLoopMutated as usize, 44);
assert_eq!(Counter::ThunkStoreRedundant as usize, 45);
assert_eq!(Counter::SelfRecWalkCalls as usize, 46);
assert_eq!(Counter::SelfRecWalkNodes as usize, 47);
assert_eq!(Counter::ThunkSiteMaybeOther as usize, 48);
assert_eq!(Counter::ThunkSiteApplyArg as usize, 49);
assert_eq!(Counter::ThunkSiteMaybeIdent as usize, 50);
assert_eq!(Counter::ThunkSiteLetForward as usize, 51);
assert_eq!(Counter::ThunkSiteOther as usize, 52);
assert_eq!(Counter::ThunkSiteInheritSrc as usize, 53);
assert_eq!(Counter::ThunkSiteNative as usize, 54);
assert_eq!(Counter::ThunkSiteEvaluated as usize, 55);
assert_eq!(Counter::EvalExpr as usize, 0);
assert_eq!(Counter::ForceValue as usize, 1);
assert_eq!(Counter::ThunkForce as usize, 2);
assert_eq!(Counter::ThunkHit as usize, 3);
assert_eq!(Counter::Import as usize, 4);
assert_eq!(Counter::ImportHit as usize, 5);
assert_eq!(Counter::Apply as usize, 6);
assert_eq!(Counter::Select as usize, 7);
assert_eq!(Counter::Attrset as usize, 8);
assert_eq!(Counter::EnvClone as usize, 9);
assert_eq!(Counter::EnvLookup as usize, 10);
assert_eq!(Counter::EnvLookupDepth as usize, 11);
assert_eq!(Counter::DeadBindingsSkipped as usize, 24);
assert_eq!(Counter::ExprBinOp as usize, 25);
assert_eq!(Counter::ExprPath as usize, 29);
}
#[test]
fn inc_does_not_panic_when_disabled() {
ENABLED.store(false, Ordering::Relaxed);
inc(Counter::EvalExpr);
inc(Counter::ForceValue);
inc(Counter::ThunkForce);
}
#[test]
fn counter_variant_maps_to_correct_index() {
assert_eq!(counter_name(Counter::EvalExpr), "eval_expr");
assert_eq!(counter_name(Counter::ForceValue), "force_value");
assert_eq!(counter_name(Counter::ThunkForce), "thunk_forces");
assert_eq!(counter_name(Counter::ThunkHit), "thunk_hits");
assert_eq!(counter_name(Counter::Import), "imports");
assert_eq!(counter_name(Counter::ImportHit), "import_hits");
assert_eq!(counter_name(Counter::Apply), "apply");
assert_eq!(counter_name(Counter::Select), "select");
assert_eq!(counter_name(Counter::Attrset), "attrsets");
assert_eq!(counter_name(Counter::EnvClone), "env_clones");
assert_eq!(counter_name(Counter::EnvLookup), "env_lookups");
assert_eq!(counter_name(Counter::EnvLookupDepth), "env_lookup_depth");
}
#[test]
fn add_increments_by_given_amount() {
let mut counters = PerfCounters::default();
assert_eq!(counters.get(Counter::EvalExpr), 0);
counters.add(Counter::EvalExpr, 5);
assert_eq!(counters.get(Counter::EvalExpr), 5);
counters.add(Counter::EvalExpr, 3);
assert_eq!(counters.get(Counter::EvalExpr), 8);
}
}