ktstr/stats/
metric_id.rs

1//! Typed metric identifiers — a discoverable, typo-proof handle for the built-in
2//! metric registry ([`super::METRICS`]), plus a first-class string escape for
3//! scheduler-runtime / payload-supplied keys.
4//!
5//! [`BuiltinMetric`] is one variant per [`super::METRICS`] entry: the entire
6//! built-in metric vocabulary in one readable, completable enum. A misspelled
7//! built-in metric is a COMPILE error, not a silent runtime `None` — closing the
8//! silent-wrong-answer class where a typo'd metric name yields `None` and a test
9//! vacuously passes having asserted nothing.
10//!
11//! [`MetricId`] unifies the typed built-in path with the open scheduler-runtime
12//! string path behind one `impl Into<MetricId>` accessor argument:
13//! `phase_metric(BuiltinMetric::TaobenchTotalQps)` and
14//! `phase_metric("scx_custom_runtime_key")` both compile through the same
15//! signature — the known one typed and discoverable, the dynamic one a first-class
16//! string escape (existing `&str` call sites keep compiling, canonicalized to the
17//! `Builtin` variant when the string names a registered metric).
18//!
19//! GUARDRAIL: a `MetricId::Dynamic` whose string is NOT a registered name has no
20//! declared [`super::MetricKind`] ([`MetricId::def`] returns `None`); callers must
21//! never aggregate such a metric as a guessed kind (the math-protocol silent-lie
22//! class). Resolve-by-name or fold conservatively — never assume `Counter`.
23//!
24//! The 1:1 correspondence with [`super::METRICS`] is pinned by
25//! `tests::builtin_metric_is_one_to_one_with_registry` (both directions), so a
26//! registry entry without an enum variant — or an enum variant without a registry
27//! entry — fails the build.
28
29use std::borrow::Cow;
30
31use super::{MetricDef, metric_def};
32
33/// Define [`BuiltinMetric`] from the single `(variant => wire_name)` source of
34/// truth, generating the enum, `wire_name()`, `from_wire_name()`, and `ALL` so the
35/// three can never drift from each other (the registry-vs-enum drift is caught
36/// separately by the pin-test).
37macro_rules! builtin_metrics {
38    ($($variant:ident => $wire:literal),* $(,)?) => {
39        /// One variant per `super::METRICS` entry — the discoverable built-in
40        /// metric vocabulary. [`Self::wire_name`] is the stable registry / sidecar
41        /// / CI string; [`Self::def`] resolves the full `MetricDef`. Pinned 1:1
42        /// to `METRICS`.
43        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44        pub enum BuiltinMetric { $($variant),* }
45
46        impl BuiltinMetric {
47            /// The stable registry / sidecar / CI wire name — the `&str` key into
48            /// `super::METRICS` and the ext-metrics maps.
49            pub const fn wire_name(self) -> &'static str {
50                match self { $(Self::$variant => $wire,)* }
51            }
52
53            /// Classify a wire name back to its variant, or `None` for a
54            /// non-built-in (scheduler-runtime / payload) key.
55            pub fn from_wire_name(name: &str) -> Option<Self> {
56                match name { $($wire => Some(Self::$variant),)* _ => None }
57            }
58
59            /// Every built-in metric (registry-order-independent). Drives typed
60            /// all-metrics sweeps and the 1:1 pin-test.
61            pub const ALL: &'static [BuiltinMetric] = &[$(Self::$variant),*];
62        }
63    };
64}
65
66builtin_metrics! {
67    // Run-level typed / derived metrics (Gauge / Peak / Counter / Rate /
68    // Distribution / WorstLowest / WakeLatencyTailRatio / WorstCrossNodeRatio).
69    WorstSpread => "worst_spread",
70    WorstGapMs => "worst_gap_ms",
71    TotalMigrations => "total_migrations",
72    WorstMigrationRatio => "worst_migration_ratio",
73    MaxImbalanceRatio => "max_imbalance_ratio",
74    AvgImbalanceRatio => "avg_imbalance_ratio",
75    MaxDsqDepth => "max_dsq_depth",
76    AvgDsqDepth => "avg_dsq_depth",
77    StuckCount => "stuck_count",
78    TotalFallback => "total_fallback",
79    TotalKeepLast => "total_keep_last",
80    TotalRunDelay => "total_run_delay",
81    TotalPcount => "total_pcount",
82    TotalSchedCount => "total_sched_count",
83    TotalYldCount => "total_yld_count",
84    TotalSchedGoidle => "total_sched_goidle",
85    TotalTtwuCount => "total_ttwu_count",
86    TotalTtwuLocal => "total_ttwu_local",
87    TotalRunDelayNsPerSched => "total_run_delay_ns_per_sched",
88    TtwuLocalFraction => "ttwu_local_fraction",
89    SchedGoidleFraction => "sched_goidle_fraction",
90    // Per-second schedstat rates (total_X / total_schedstat_wall_sec) + the
91    // shared window-seconds denominator.
92    TotalSchedstatWallSec => "total_schedstat_wall_sec",
93    RunDelayPerSec => "run_delay_per_sec",
94    PcountPerSec => "pcount_per_sec",
95    SchedCountPerSec => "sched_count_per_sec",
96    YldCountPerSec => "yld_count_per_sec",
97    TtwuCountPerSec => "ttwu_count_per_sec",
98    SchedGoidlePerSec => "sched_goidle_per_sec",
99    AvgNrRunning => "avg_nr_running",
100    WorstP99WakeLatencyUs => "worst_p99_wake_latency_us",
101    WorstMedianWakeLatencyUs => "worst_median_wake_latency_us",
102    WorstWakeLatencyCv => "worst_wake_latency_cv",
103    WorstP99TimerLatencyUs => "worst_p99_timer_latency_us",
104    WorstMedianTimerLatencyUs => "worst_median_timer_latency_us",
105    WorstP999TimerLatencyUs => "worst_p999_timer_latency_us",
106    WorstTimerLatencyUs => "worst_timer_latency_us",
107    IterationRate => "iteration_rate",
108    TotalIterations => "total_iterations",
109    TotalPhaseIterations => "total_phase_iterations",
110    TotalPhaseDurationSec => "total_phase_duration_sec",
111    TotalCpuTimeSec => "total_cpu_time_sec",
112    TotalIterationsPooled => "total_iterations_pooled",
113    IterationsPerCpuSec => "iterations_per_cpu_sec",
114    // Whole-run taobench qps + hit (run-level pool of the WorkType::Taobench
115    // engine, derived by populate_run_pooled_taobench). 4 Counter components
116    // (RENDER_SUPPRESSED_COMPONENTS) + 4 derived Rates. Distinct from the
117    // per-phase taobench_*_qps below (MetricKind::PerPhase).
118    TotalTaobenchOps => "total_taobench_ops",
119    TotalTaobenchFastOps => "total_taobench_fast_ops",
120    TotalTaobenchSlowOps => "total_taobench_slow_ops",
121    TotalTaobenchWallSec => "total_taobench_wall_sec",
122    TaobenchTotalOpsPerSec => "taobench_total_ops_per_sec",
123    TaobenchFastOpsPerSec => "taobench_fast_ops_per_sec",
124    TaobenchSlowOpsPerSec => "taobench_slow_ops_per_sec",
125    TaobenchHitFraction => "taobench_hit_fraction",
126    // taobench whole-run open-loop serve-latency percentiles (PerRunDistribution).
127    TaobenchServeP50UsWhole => "taobench_serve_p50_us_whole",
128    TaobenchServeP90UsWhole => "taobench_serve_p90_us_whole",
129    TaobenchServeP99UsWhole => "taobench_serve_p99_us_whole",
130    TaobenchServeP999UsWhole => "taobench_serve_p999_us_whole",
131    TaobenchServeMinUsWhole => "taobench_serve_min_us_whole",
132    TaobenchServeMaxUsWhole => "taobench_serve_max_us_whole",
133    // taobench whole-run command-time hit: get_cmds + get_hits Counter components
134    // → taobench_command_hit_rate (Σhits/Σcmds) Rate.
135    TotalTaobenchGetCmds => "total_taobench_get_cmds",
136    TotalTaobenchGetHits => "total_taobench_get_hits",
137    TaobenchCommandHitRate => "taobench_command_hit_rate",
138    // schbench whole-run Class-3 (loop Counter + role-separate run-delay gate
139    // Rates + their Counter components), re-pooled by populate_run_pooled_schbench.
140    TotalSchbenchMsgRunDelayNs => "total_schbench_msg_run_delay_ns",
141    TotalSchbenchMsgPcount => "total_schbench_msg_pcount",
142    TotalSchbenchWorkerRunDelayNs => "total_schbench_worker_run_delay_ns",
143    TotalSchbenchWorkerPcount => "total_schbench_worker_pcount",
144    TotalSchbenchLoops => "total_schbench_loops",
145    SchbenchMsgRunDelayNsPerSched => "schbench_msg_run_delay_ns_per_sched",
146    SchbenchWorkerRunDelayNsPerSched => "schbench_worker_run_delay_ns_per_sched",
147    // schbench whole-run distributional metrics (MetricKind::PerRunDistribution),
148    // union-recomputed by populate_run_pooled_schbench_distribution; *_whole names
149    // distinct from the per-phase percentile keys.
150    WakeupP50LatencyUsWhole => "wakeup_p50_latency_us_whole",
151    WakeupP90LatencyUsWhole => "wakeup_p90_latency_us_whole",
152    WakeupP99LatencyUsWhole => "wakeup_p99_latency_us_whole",
153    WakeupP999LatencyUsWhole => "wakeup_p999_latency_us_whole",
154    WakeupMinLatencyUsWhole => "wakeup_min_latency_us_whole",
155    WakeupMaxLatencyUsWhole => "wakeup_max_latency_us_whole",
156    RequestP50LatencyUsWhole => "request_p50_latency_us_whole",
157    RequestP90LatencyUsWhole => "request_p90_latency_us_whole",
158    RequestP99LatencyUsWhole => "request_p99_latency_us_whole",
159    RequestP999LatencyUsWhole => "request_p999_latency_us_whole",
160    RequestMinLatencyUsWhole => "request_min_latency_us_whole",
161    RequestMaxLatencyUsWhole => "request_max_latency_us_whole",
162    RpsP20Whole => "rps_p20_whole",
163    RpsP50Whole => "rps_p50_whole",
164    RpsP90Whole => "rps_p90_whole",
165    RpsMinWhole => "rps_min_whole",
166    RpsMaxWhole => "rps_max_whole",
167    SystemTimeNs => "system_time_ns",
168    UserTimeNs => "user_time_ns",
169    WorstMeanRunDelayUs => "worst_mean_run_delay_us",
170    WorstRunDelayUs => "worst_run_delay_us",
171    WorstWakeLatencyTailRatio => "worst_wake_latency_tail_ratio",
172    WorstIterationsPerWorker => "worst_iterations_per_worker",
173    WorstIterationsPerCpuSec => "worst_iterations_per_cpu_sec",
174    WorstPageLocality => "worst_page_locality",
175    WorstCrossNodeMigrationRatio => "worst_cross_node_migration_ratio",
176    TotalCpuTimeNs => "total_cpu_time_ns",
177
178    // IRQ observability — host-side observer-free signals, run-level
179    // ext-only (accessor |_| None; populated via the read_sample fold like
180    // system_time_ns). Counters / Gauge(Avg) / Peak + 3 derived rates + 2
181    // hidden capture-window duration components. Require num_snapshots >= 2
182    // (per_cpu_time freeze capture) + CONFIG_IRQ_TIME_ACCOUNTING for the
183    // time signals; loud-absent (None), never false-zero, when off. System-wide
184    // PSI-irq (psi_irq_full_avg10 / total_irq_pressure_us) is host-walked from
185    // the global psi_system per monitor sample and folded run-level, NOT a guest
186    // /proc read; see its own block below.
187    TotalHardirqs => "total_hardirqs",
188    TotalSoftirqNetRx => "total_softirq_net_rx",
189    TotalSoftirqNetTx => "total_softirq_net_tx",
190    TotalSoftirqTimer => "total_softirq_timer",
191    TotalSoftirqSched => "total_softirq_sched",
192    TotalIrqTimeNs => "total_irq_time_ns",
193    TotalSoftirqTimeNs => "total_softirq_time_ns",
194    TotalStealTimeNs => "total_steal_time_ns",
195    AvgIrqUtil => "avg_irq_util",
196    MaxAvgIrqUtil => "max_avg_irq_util",
197    HardirqRate => "hardirq_rate",
198    NetRxSoftirqRate => "net_rx_softirq_rate",
199    IrqTimeFraction => "irq_time_fraction",
200    // Hidden rate-denominator components (accessor |_| None, bucket-derived):
201    // the capture-WINDOW duration (first→last freeze span), NOT the full
202    // phase wall-time. _sec backs the count-rates, _ns backs irq_time_fraction.
203    TotalPhaseWallSec => "total_phase_wall_sec",
204    TotalPhaseWallNs => "total_phase_wall_ns",
205
206    // Per-CPU IRQ spatial axis (the busiest-CPU dimension; the IRQ counters
207    // above are the cross-CPU SUM). Custom per-CPU-delta bucket-fold, NOT a
208    // read_sample arm (read_sample yields one f64 per freeze, no per-CPU
209    // vector). max_cpu_hardirqs = max over CPUs of each CPU's hardirq delta;
210    // _concentration = max / mean over the reporting CPUs. Both Peak.
211    MaxCpuHardirqs => "max_cpu_hardirqs",
212    MaxCpuHardirqConcentration => "max_cpu_hardirq_concentration",
213    // Per-CPU NET_RX softirq spatial axis (the softirq sibling of the hardirq
214    // axis above): max_cpu_softirq_net_rx = busiest CPU's NET_RX softirq-run
215    // delta; _concentration = max / mean over reporting CPUs. Both Peak.
216    MaxCpuSoftirqNetRx => "max_cpu_softirq_net_rx",
217    MaxCpuSoftirqNetRxConcentration => "max_cpu_softirq_net_rx_concentration",
218    // scx_layered util-compensation scale: per-CPU first→last cpustat-delta
219    // clamped scale, then mean across CPUs. Gauge(Avg)/LowerBetter; same
220    // per-CPU-delta bucket-fold family as the per-CPU IRQ axis above
221    // (assert::phase_build::fold_util_comp_scale), NOT a read_sample arm.
222    AvgCpuUtilCompScale => "avg_cpu_util_comp_scale",
223
224    // scx_lavd per-task latency-criticality (normalized_lat_cri, [0,1024])
225    // host-read from the sdt_alloc arena each freeze, folded over (freeze, task):
226    // mean -> avg_task_lat_cri (Gauge(Avg)), max -> max_task_lat_cri (Peak). Both
227    // Informational; computed in assert::phase_build::fold_lat_cri, NOT a
228    // read_sample arm. lavd-only (loud-absent for schedulers whose arena payload
229    // has no normalized_lat_cri member).
230    AvgTaskLatCri => "avg_task_lat_cri",
231    MaxTaskLatCri => "max_task_lat_cri",
232
233    // Per-cgroup PSI-irq spatial axis (host cgroup-walk capture → per-phase fold):
234    // the busiest workload-leaf cgroup dimension. max_cgroup_irq_pressure = the busiest leaf's
235    // IRQ-full stall delta (µs); _concentration = max / mean over the reporting
236    // leaves (the cgroup-isolation signal); max_cgroup_psi_irq_avg10 = the worst
237    // leaf's avg10 gauge (%). All Peak; folded per-phase in assert::phase_build
238    // (fold_per_cgroup_psi) from the freeze cgroup_psi capture.
239    MaxCgroupIrqPressure => "max_cgroup_irq_pressure",
240    MaxCgroupIrqPressureConcentration => "max_cgroup_irq_pressure_concentration",
241    MaxCgroupPsiIrqAvg10 => "max_cgroup_psi_irq_avg10",
242
243    // System-wide PSI-irq pressure (host-walked from the global psi_system per
244    // monitor sample, folded run-level in MonitorSummary). avg10 = mean
245    // 10s-EWMA full-pressure percent (Gauge); total = cumulative full-stall µs
246    // over the window (Counter). Gated on CONFIG_PSI + CONFIG_IRQ_TIME_ACCOUNTING
247    // (PSI_IRQ_FULL in BTF); loud-absent (None) when off.
248    PsiIrqFullAvg10 => "psi_irq_full_avg10",
249    TotalIrqPressureUs => "total_irq_pressure_us",
250
251    // Per-phase scalars (MetricKind::PerPhase) — schbench / taobench engine
252    // metrics, surfaced via the per-phase PhaseBucket path (const-named).
253    WakeupP50LatencyUs => "wakeup_p50_latency_us",
254    WakeupP90LatencyUs => "wakeup_p90_latency_us",
255    WakeupP99LatencyUs => "wakeup_p99_latency_us",
256    WakeupP999LatencyUs => "wakeup_p999_latency_us",
257    RequestP50LatencyUs => "request_p50_latency_us",
258    RequestP90LatencyUs => "request_p90_latency_us",
259    RequestP99LatencyUs => "request_p99_latency_us",
260    RequestP999LatencyUs => "request_p999_latency_us",
261    SchedDelayMsgUs => "sched_delay_msg_us",
262    SchedDelayWorkerUs => "sched_delay_worker_us",
263    SchbenchLoopCount => "schbench_loop_count",
264    TaobenchTotalQps => "taobench_total_qps",
265    TaobenchFastQps => "taobench_fast_qps",
266    TaobenchSlowQps => "taobench_slow_qps",
267    TaobenchHitRatio => "taobench_hit_ratio",
268    TaobenchHitRate => "taobench_hit_rate",
269    // taobench per-phase open-loop serve-latency percentiles (PerPhase, µs).
270    TaobenchServeP50Us => "taobench_serve_p50_us",
271    TaobenchServeP90Us => "taobench_serve_p90_us",
272    TaobenchServeP99Us => "taobench_serve_p99_us",
273    TaobenchServeP999Us => "taobench_serve_p999_us",
274    TaobenchServeMinUs => "taobench_serve_min_us",
275    TaobenchServeMaxUs => "taobench_serve_max_us",
276    WakeupMinLatencyUs => "wakeup_min_latency_us",
277    WakeupMaxLatencyUs => "wakeup_max_latency_us",
278    RequestMinLatencyUs => "request_min_latency_us",
279    RequestMaxLatencyUs => "request_max_latency_us",
280    RpsP20 => "rps_p20",
281    RpsP50 => "rps_p50",
282    RpsP90 => "rps_p90",
283    RpsMin => "rps_min",
284    RpsMax => "rps_max",
285
286    // Per-phase scalars inserted as bare literals by the non-schbench carrier
287    // derivation (run_metrics.rs write_carrier_scalars) — the per-cgroup
288    // wake/run-delay/off-cpu/migration/locality family.
289    P99WakeLatencyUs => "p99_wake_latency_us",
290    MedianWakeLatencyUs => "median_wake_latency_us",
291    WakeLatencyCv => "wake_latency_cv",
292    P99TimerLatencyUs => "p99_timer_latency_us",
293    MedianTimerLatencyUs => "median_timer_latency_us",
294    P999TimerLatencyUs => "p999_timer_latency_us",
295    MeanRunDelayUs => "mean_run_delay_us",
296    MaxRunDelayUs => "max_run_delay_us",
297    AvgOffCpuPct => "avg_off_cpu_pct",
298    MinOffCpuPct => "min_off_cpu_pct",
299    MaxOffCpuPct => "max_off_cpu_pct",
300    OffCpuSpreadPct => "off_cpu_spread_pct",
301    MigrationRatio => "migration_ratio",
302    IterationsPerWorker => "iterations_per_worker",
303    PageLocality => "page_locality",
304    CrossNodeMigrationRatio => "cross_node_migration_ratio",
305}
306
307impl BuiltinMetric {
308    /// The full registry definition (polarity / kind / unit / tolerances /
309    /// accessor). Total — the pin-test guarantees every variant resolves in
310    /// `super::METRICS`.
311    pub fn def(self) -> &'static MetricDef {
312        metric_def(self.wire_name())
313            .expect("BuiltinMetric variant must resolve in METRICS (pinned by test)")
314    }
315}
316
317/// A metric identifier accepted by every metric accessor via `impl Into<MetricId>`:
318/// a typed [`BuiltinMetric`] (the common, discoverable, typo-proof case) or a
319/// dynamic scheduler-runtime / payload string (the open keyspace). One call shape
320/// for both — `phase_metric(BuiltinMetric::X)` and `phase_metric("runtime_key")`.
321#[derive(Debug, Clone, PartialEq, Eq, Hash)]
322pub enum MetricId {
323    /// A compile-time-known built-in metric (carries its registry kind via
324    /// [`BuiltinMetric::def`]).
325    Builtin(BuiltinMetric),
326    /// A scheduler-runtime / payload-supplied key not in the built-in registry.
327    Dynamic(Cow<'static, str>),
328}
329
330impl MetricId {
331    /// The wire-name key — into `super::METRICS` and the ext-metrics maps. For
332    /// a `Builtin` this is the inner [`BuiltinMetric::wire_name`]; for a `Dynamic`
333    /// it is the raw key string.
334    pub fn as_str(&self) -> &str {
335        match self {
336            MetricId::Builtin(b) => b.wire_name(),
337            MetricId::Dynamic(s) => s.as_ref(),
338        }
339    }
340
341    /// The registry definition if this id names a REGISTERED metric — `Some` for
342    /// every `Builtin`; `None` for a genuine scheduler-runtime key. Via the `From`
343    /// impls a registered name canonicalizes to `Builtin`, so a `Dynamic` reached
344    /// through the public `impl Into<MetricId>` path never holds a registered name
345    /// and resolves to `None` here; the `Dynamic` registry lookup is a defensive
346    /// fallback for a directly-constructed `MetricId::Dynamic`. The GUARDRAIL: a
347    /// `None` here means no declared kind, so the caller must not aggregate the
348    /// value as a guessed kind (resolve-by-name or fold conservatively).
349    pub fn def(&self) -> Option<&'static MetricDef> {
350        match self {
351            MetricId::Builtin(b) => Some(b.def()),
352            MetricId::Dynamic(s) => metric_def(s.as_ref()),
353        }
354    }
355}
356
357impl From<BuiltinMetric> for MetricId {
358    fn from(b: BuiltinMetric) -> Self {
359        MetricId::Builtin(b)
360    }
361}
362
363impl From<&BuiltinMetric> for MetricId {
364    /// Borrowed-`BuiltinMetric` convenience — sweeping [`BuiltinMetric::ALL`]
365    /// yields `&BuiltinMetric`; it is `Copy`, so this is a trivial copy-then-wrap.
366    fn from(b: &BuiltinMetric) -> Self {
367        MetricId::Builtin(*b)
368    }
369}
370
371impl From<&str> for MetricId {
372    /// Canonicalize a string: if it names a built-in, yield the typed `Builtin`
373    /// (so a known reached by string still carries the typed identity + kind);
374    /// otherwise `Dynamic`. A non-built-in `&str` is owned (the dynamic path is
375    /// not hot; correctness over the micro-alloc).
376    fn from(s: &str) -> Self {
377        match BuiltinMetric::from_wire_name(s) {
378            Some(b) => MetricId::Builtin(b),
379            None => MetricId::Dynamic(Cow::Owned(s.to_owned())),
380        }
381    }
382}
383
384impl From<String> for MetricId {
385    fn from(s: String) -> Self {
386        match BuiltinMetric::from_wire_name(&s) {
387            Some(b) => MetricId::Builtin(b),
388            None => MetricId::Dynamic(Cow::Owned(s)),
389        }
390    }
391}
392
393impl From<&String> for MetricId {
394    /// Borrowed-`String` convenience — iterating owned metric names (a
395    /// `HashMap<String, _>`'s keys, a `Vec<String>`) yields `&String`;
396    /// delegates to the `&str` canonicalization.
397    fn from(s: &String) -> Self {
398        Self::from(s.as_str())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use crate::stats::METRICS;
406
407    /// The enum is pinned 1:1 to the registry, both directions, so adding a
408    /// METRICS entry without a BuiltinMetric variant (or vice versa) fails here.
409    #[test]
410    fn builtin_metric_is_one_to_one_with_registry() {
411        // (1) every variant resolves to a registry entry.
412        for b in BuiltinMetric::ALL {
413            assert!(
414                metric_def(b.wire_name()).is_some(),
415                "BuiltinMetric::{b:?} (wire {:?}) is not in METRICS",
416                b.wire_name()
417            );
418        }
419        // (2) every registry entry has a variant (no entry without a typed id).
420        for m in METRICS {
421            assert!(
422                BuiltinMetric::from_wire_name(m.name).is_some(),
423                "METRICS entry {:?} has no BuiltinMetric variant",
424                m.name
425            );
426        }
427        // (3) cardinality: catches a duplicate variant or a duplicate registry
428        // name that (1)+(2) would individually miss.
429        assert_eq!(
430            BuiltinMetric::ALL.len(),
431            METRICS.len(),
432            "BuiltinMetric::ALL ({}) != METRICS ({})",
433            BuiltinMetric::ALL.len(),
434            METRICS.len()
435        );
436    }
437
438    /// wire_name <-> from_wire_name roundtrips for every variant.
439    #[test]
440    fn wire_name_roundtrips() {
441        for b in BuiltinMetric::ALL {
442            assert_eq!(BuiltinMetric::from_wire_name(b.wire_name()), Some(*b));
443        }
444    }
445
446    /// The MetricId hybrid: a built-in string canonicalizes to Builtin (typed +
447    /// def() resolves); a non-built-in string stays Dynamic with def() == None
448    /// (the no-guessed-kind guardrail).
449    #[test]
450    fn metric_id_canonicalizes_builtin_and_quarantines_dynamic() {
451        let typed: MetricId = BuiltinMetric::TaobenchTotalQps.into();
452        let from_str: MetricId = "taobench_total_qps".into();
453        assert_eq!(
454            typed, from_str,
455            "a built-in string canonicalizes to Builtin"
456        );
457        assert_eq!(typed.as_str(), "taobench_total_qps");
458        assert!(typed.def().is_some(), "a built-in carries its registry def");
459
460        let dynamic: MetricId = "scx_layered_layer0_depth".into();
461        assert!(
462            matches!(dynamic, MetricId::Dynamic(_)),
463            "a non-registered key stays Dynamic"
464        );
465        assert!(
466            dynamic.def().is_none(),
467            "a Dynamic key has no def() -> caller must not guess its kind"
468        );
469    }
470}