rvsim-core 2.0.0

A cycle-level RISC-V 64-bit system simulator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
//! Hierarchical, path-addressed statistics.
//!
//! Every counter lives at a path like `core0.commit.insts` or
//! `memctrl0.ch0.bank3.row_hits`. Paths carry no class-name leakage — swapping
//! branch predictors or cache implementations doesn't renumber anything.
//!
//! ## What this module gives you
//!
//! - Path-addressed [`Counter`]s and [`Histogram`]s ([`Stats::counter`]).
//! - Per-stat [`Meta`] with unit + aggregation kind ([`Stats::register`]).
//! - Derived metrics with formulas ([`Stats::derive`]), evaluated on read.
//! - Wildcard queries ([`Stats::query`]) with `*` (one segment) and `**`
//!   (any depth).
//! - Auto-generated summary ([`Stats::summary`]) driven by metadata.
//!
//! See `docs/architecture/stats.md` for the design rationale.

use std::collections::{BTreeMap, HashMap};
use std::io::{self, Write};
use std::sync::{LazyLock, RwLock};

pub mod meta;
pub mod paths;
pub mod query;
pub mod summary;

pub use meta::{Kind, Meta, Unit};
pub use query::QueryResult;

use crate::common::{CoreId, HartId};
use paths::{CorePaths, HartPaths, SystemPaths};

/// A scalar counter.
#[derive(Clone, Copy, Debug, Default)]
pub struct Counter(u64);

impl Counter {
    /// Increments the counter by one.
    #[inline]
    pub const fn inc(&mut self) {
        self.0 = self.0.saturating_add(1);
    }

    /// Adds `n` to the counter.
    #[inline]
    pub const fn add(&mut self, n: u64) {
        self.0 = self.0.saturating_add(n);
    }

    /// Returns the current value.
    #[inline]
    pub const fn get(&self) -> u64 {
        self.0
    }

    /// Resets the counter to zero.
    #[inline]
    pub const fn reset(&mut self) {
        self.0 = 0;
    }

    /// What was counted after `earlier`, a snapshot of this counter.
    #[must_use]
    pub const fn since(self, earlier: Self) -> Self {
        Self(self.0.saturating_sub(earlier.0))
    }
}

/// A power-of-two-bucketed histogram for latency / queue-depth distributions.
///
/// Bucket `i` counts samples in `[2^i, 2^(i+1))`. Bucket 0 counts zeroes too.
#[derive(Clone, Debug, Default)]
pub struct Histogram {
    buckets: Vec<u64>,
    count: u64,
    sum: u64,
    /// The smallest and largest samples; `None` with no samples, or for a
    /// window between two snapshots, whose extremes cannot be recovered.
    extremes: Option<(u64, u64)>,
}

impl Histogram {
    /// Records a sample.
    pub fn record(&mut self, sample: u64) {
        let bucket = if sample <= 1 { 0 } else { (64 - sample.leading_zeros() - 1) as usize };
        if self.buckets.len() <= bucket {
            self.buckets.resize(bucket + 1, 0);
        }
        self.buckets[bucket] = self.buckets[bucket].saturating_add(1);
        self.extremes = Some(match self.extremes {
            Some((min, max)) => (min.min(sample), max.max(sample)),
            None => (sample, sample),
        });
        self.count = self.count.saturating_add(1);
        self.sum = self.sum.saturating_add(sample);
    }

    /// Returns the total number of recorded samples.
    pub const fn count(&self) -> u64 {
        self.count
    }

    /// Returns the sum of all samples.
    pub const fn sum(&self) -> u64 {
        self.sum
    }

    /// Returns the mean of recorded samples, or 0 if no samples were recorded.
    pub fn mean(&self) -> f64 {
        if self.count == 0 { 0.0 } else { self.sum as f64 / self.count as f64 }
    }

    /// The smallest recorded sample, when known.
    pub fn min(&self) -> Option<u64> {
        self.extremes.map(|(min, _)| min)
    }

    /// The largest recorded sample, when known.
    pub fn max(&self) -> Option<u64> {
        self.extremes.map(|(_, max)| max)
    }

    /// Clears the histogram for phase-based analysis.
    pub fn reset(&mut self) {
        self.buckets.clear();
        self.count = 0;
        self.sum = 0;
        self.extremes = None;
    }

    /// The samples recorded after `earlier`, a snapshot of this histogram:
    /// exact buckets, count, sum and mean, but unknown extremes.
    #[must_use]
    pub fn since(&self, earlier: &Self) -> Self {
        let buckets = self
            .buckets
            .iter()
            .enumerate()
            .map(|(i, &n)| n.saturating_sub(earlier.buckets.get(i).copied().unwrap_or(0)))
            .collect();
        Self {
            buckets,
            count: self.count.saturating_sub(earlier.count),
            sum: self.sum.saturating_sub(earlier.sum),
            extremes: None,
        }
    }
}

/// A stat path, interned for the whole process: its text is kept once and
/// its id indexes every [`Stats`]' storage directly, so a hot increment is
/// an array index.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StatId(u32);

/// Every interned path and the id it has.
#[derive(Default)]
struct Registry {
    ids: HashMap<&'static str, StatId>,
    paths: Vec<&'static str>,
}

static REGISTRY: LazyLock<RwLock<Registry>> = LazyLock::new(RwLock::default);

fn registry() -> std::sync::RwLockReadGuard<'static, Registry> {
    REGISTRY.read().unwrap_or_else(std::sync::PoisonError::into_inner)
}

impl StatId {
    /// The id of `path`, interning it on first use.
    #[must_use]
    pub fn of(path: &str) -> Self {
        if let Some(id) = Self::find(path) {
            return id;
        }
        let mut registry = REGISTRY.write().unwrap_or_else(std::sync::PoisonError::into_inner);
        if let Some(&id) = registry.ids.get(path) {
            return id;
        }
        let id = Self(u32::try_from(registry.paths.len()).unwrap_or(u32::MAX));
        let text: &'static str = Box::leak(path.to_owned().into_boxed_str());
        registry.paths.push(text);
        let _ = registry.ids.insert(text, id);
        id
    }

    /// The id of `path` if it was ever interned.
    #[must_use]
    pub fn find(path: &str) -> Option<Self> {
        registry().ids.get(path).copied()
    }

    /// The path's text.
    #[must_use]
    pub fn path(self) -> &'static str {
        registry().paths[self.0 as usize]
    }

    const fn index(self) -> usize {
        self.0 as usize
    }
}

impl From<&str> for StatId {
    fn from(path: &str) -> Self {
        Self::of(path)
    }
}

/// Something that names a stat for reading: an id, or a path's text.
pub trait StatKey {
    /// The stat's id, if the path was ever interned.
    fn stat_id(&self) -> Option<StatId>;
}

impl StatKey for StatId {
    fn stat_id(&self) -> Option<StatId> {
        Some(*self)
    }
}

impl StatKey for str {
    fn stat_id(&self) -> Option<StatId> {
        StatId::find(self)
    }
}

impl<T: StatKey + ?Sized> StatKey for &T {
    fn stat_id(&self) -> Option<StatId> {
        (**self).stat_id()
    }
}

impl StatKey for String {
    fn stat_id(&self) -> Option<StatId> {
        StatId::find(self)
    }
}

/// One kind of stat, indexed by [`StatId`]: which ids this tree holds, and
/// their values.
#[derive(Clone, Debug)]
struct Store<T> {
    values: Vec<T>,
    present: Vec<bool>,
}

impl<T> Default for Store<T> {
    fn default() -> Self {
        Self { values: Vec::new(), present: Vec::new() }
    }
}

impl<T: Default + Clone> Store<T> {
    /// The value of `id`, created on first use.
    #[inline]
    fn get_or_insert(&mut self, id: StatId) -> &mut T {
        let index = id.index();
        if index >= self.values.len() {
            self.values.resize(index + 1, T::default());
            self.present.resize(index + 1, false);
        }
        self.present[index] = true;
        &mut self.values[index]
    }
}

impl<T> Store<T> {
    fn get(&self, id: StatId) -> Option<&T> {
        let index = id.index();
        self.present.get(index).copied().unwrap_or(false).then(|| &self.values[index])
    }

    /// Every id this tree holds, in path order.
    fn ids(&self) -> Vec<StatId> {
        let registry = registry();
        let mut ids: Vec<StatId> = (0..self.values.len())
            .filter(|&index| self.present[index])
            .map(|index| StatId(index as u32))
            .collect();
        ids.sort_by_key(|id| registry.paths[id.index()]);
        ids
    }

    /// The same ids with each value mapped through `f`, given the value
    /// `earlier` holds for it, if any.
    fn map_since(&self, earlier: &Self, f: impl Fn(&T, Option<&T>) -> T) -> Self {
        let values = (0..self.values.len())
            .map(|index| f(&self.values[index], earlier.get(StatId(index as u32))))
            .collect();
        Self { values, present: self.present.clone() }
    }
}

/// Output format for `Stats::dump`.
#[derive(Clone, Copy, Debug)]
pub enum StatFormat {
    /// Human-readable `path.to.metric value` lines.
    Text,
}

/// A formula for a derived stat.
///
/// Deliberately narrow — enough for IPC/CPI/accuracy/miss-rate. Divide-by-zero
/// evaluates to `0.0`, not `NaN`, so runs with zero events don't poison
/// downstream analysis.
#[derive(Clone, Debug)]
pub enum Formula {
    /// `numerator / denominator`. Both are stats (raw or derived).
    Div(StatId, StatId),
    /// `numerator / (numerator + other)` — the common "accuracy" or
    /// "hit rate" shape.
    Ratio {
        /// The stat in the numerator and one term of the denominator.
        numerator: StatId,
        /// The second term of the denominator.
        other: StatId,
    },
    /// Sum of many stats. `Sum(&[])` is `0.0`.
    Sum(Vec<StatId>),
}

/// Top-level statistics tree.
#[derive(Clone, Debug, Default)]
pub struct Stats {
    counters: Store<Counter>,
    histograms: Store<Histogram>,
    /// Per-registered-stat metadata.
    pub(crate) meta: BTreeMap<StatId, Meta>,
    /// Registered derived stats and the formulas that compute them.
    pub(crate) derived: BTreeMap<StatId, Formula>,
}

/// A component whose stats are registered with the tree it writes to.
pub trait StatSource {
    /// Registers the component's counters and derived stats with `stats`.
    fn register(&self, stats: &mut Stats);
}

impl Stats {
    /// Creates an empty stats tree.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates a stats tree with every hart's and core's canonical metadata
    /// and derived formulas pre-registered, plus the `system.*` sums. Every
    /// writer path appears in [`Stats::query`] and [`Stats::summary`] output
    /// even before any counter has been incremented. `components` register
    /// their own stats, in order, between the cores' and the system sums.
    #[must_use]
    pub fn for_components(
        harts: &[HartPaths],
        cores: &[(CorePaths, HartId)],
        components: &[&dyn StatSource],
    ) -> Self {
        let mut s = Self::new();
        for hart in harts {
            register_hart(&mut s, hart);
        }
        for (core, first_hart) in cores {
            register_core(&mut s, core, &harts[first_hart.as_index()]);
        }
        for component in components {
            component.register(&mut s);
        }
        register_system(&mut s, harts);
        s
    }

    /// [`Stats::for_components`] for one core hosting one hart, with no
    /// caches.
    #[must_use]
    pub fn with_default_registrations() -> Self {
        let hart = HartId::new(0);
        Self::for_components(
            &[HartPaths::new(hart)],
            &[(CorePaths::new(CoreId::new(0)), hart)],
            &[],
        )
    }

    /// Returns a mutable reference to the counter at `path`.
    ///
    /// `path` uses `.` as a separator: `"core0.cache.l1d.hits"`.
    #[inline]
    pub fn counter(&mut self, stat: impl Into<StatId>) -> &mut Counter {
        self.counters.get_or_insert(stat.into())
    }

    /// Returns a mutable reference to the histogram at `path`.
    pub fn histogram(&mut self, stat: impl Into<StatId>) -> &mut Histogram {
        self.histograms.get_or_insert(stat.into())
    }

    /// Reads the histogram `stat` names without creating it.
    #[must_use]
    pub fn histogram_at(&self, stat: impl StatKey) -> Option<&Histogram> {
        self.histograms.get(stat.stat_id()?)
    }

    /// Registers metadata for a counter path. Idempotent — a repeat
    /// registration overwrites the previous [`Meta`].
    ///
    /// Registering also allocates the underlying [`Counter`] so the path
    /// appears in queries even before any writer has incremented it.
    pub fn register(&mut self, stat: impl Into<StatId>, meta: Meta) {
        let stat = stat.into();
        let _ = self.counters.get_or_insert(stat);
        let _ = self.meta.insert(stat, meta);
    }

    /// Describes a histogram. It holds nothing, and queries do not list it,
    /// until it records its first sample.
    pub fn register_histogram(&mut self, stat: impl Into<StatId>, meta: Meta) {
        let _ = self.meta.insert(stat.into(), meta);
    }

    /// Registers a derived stat with a formula. The value is computed on read
    /// via [`Stats::get`] and included in [`Stats::summary`].
    pub fn derive(&mut self, stat: impl Into<StatId>, formula: Formula, meta: Meta) {
        let stat = stat.into();
        let _ = self.derived.insert(stat, formula);
        let _ = self.meta.insert(stat, meta);
    }

    /// Reads a stat value by path. Works for both raw counters and derived
    /// stats. Returns `None` if the path isn't registered / hasn't been
    /// written and isn't a derived formula.
    #[must_use]
    pub fn get(&self, stat: impl StatKey) -> Option<f64> {
        let stat = stat.stat_id()?;
        if let Some(formula) = self.derived.get(&stat) {
            return Some(self.eval(formula));
        }
        self.counters.get(stat).map(|c| c.get() as f64)
    }

    /// Evaluates a wildcard query against every path in the tree (raw and
    /// derived).
    #[must_use]
    pub fn query(&self, pattern: &str) -> QueryResult {
        let mut matches = Vec::new();
        let mut visit = |path: String, value: f64| {
            if query::matches(pattern, &path) {
                matches.push((path, value));
            }
        };
        for id in self.counters.ids() {
            let value = self.counters.get(id).map_or(0.0, |c| c.get() as f64);
            visit(id.path().to_string(), value);
        }
        for (id, formula) in &self.derived {
            visit(id.path().to_string(), self.eval(formula));
        }
        QueryResult { matches }
    }

    /// Returns an auto-generated multi-section summary of the stats tree,
    /// grouped by subject and formatted according to each stat's [`Meta`].
    #[must_use]
    pub fn summary(&self, cycles: u64, instructions_retired: u64) -> String {
        summary::format(self, cycles, instructions_retired)
    }

    /// Same as [`Stats::summary`], but restricts output to a whitelist of
    /// top-level subjects (e.g. `["core0", "hart0"]`). An empty slice emits
    /// the header only.
    #[must_use]
    pub fn summary_sections(
        &self,
        cycles: u64,
        instructions_retired: u64,
        sections: &[&str],
    ) -> String {
        summary::format_sections(self, cycles, instructions_retired, Some(sections))
    }

    /// The set of top-level subjects that currently have at least one
    /// registered stat.
    #[must_use]
    pub fn subjects(&self) -> Vec<&'static str> {
        let mut out: Vec<&'static str> = self
            .meta
            .keys()
            .map(|id| id.path())
            .map(|p| p.split_once('.').map_or(p, |(head, _)| head))
            .collect();
        out.sort_unstable();
        out.dedup();
        out
    }

    /// The stats accumulated after `earlier`, a snapshot of this tree:
    /// counters and histograms are subtracted, and derived stats (ratios)
    /// are recomputed from the differences when read.
    #[must_use]
    pub fn since(&self, earlier: &Self) -> Self {
        Self {
            counters: self
                .counters
                .map_since(&earlier.counters, |c, e| c.since(e.copied().unwrap_or_default())),
            histograms: self
                .histograms
                .map_since(&earlier.histograms, |h, e| e.map_or_else(|| h.clone(), |e| h.since(e))),
            meta: self.meta.clone(),
            derived: self.derived.clone(),
        }
    }

    /// Resets every counter and histogram in the tree. Metadata and derived
    /// registrations are preserved so the tree shape survives phase resets.
    pub fn reset(&mut self) {
        for counter in &mut self.counters.values {
            counter.reset();
        }
        for histogram in &mut self.histograms.values {
            histogram.reset();
        }
    }

    /// Writes the stats tree to `out` in the requested format.
    ///
    /// # Errors
    ///
    /// Propagates any I/O error from the underlying writer.
    pub fn dump(&self, format: StatFormat, out: &mut dyn Write) -> io::Result<()> {
        match format {
            StatFormat::Text => self.dump_text(out),
        }
    }

    fn dump_text(&self, out: &mut dyn Write) -> io::Result<()> {
        for id in self.counters.ids() {
            let value = self.counters.get(id).map_or(0, Counter::get);
            writeln!(out, "{} {value}", id.path())?;
        }
        for id in self.histograms.ids() {
            let (path, Some(h)) = (id.path(), self.histograms.get(id)) else { continue };
            let known = |v: Option<u64>| v.map_or_else(|| "n/a".to_string(), |v| v.to_string());
            writeln!(
                out,
                "{path} count={} sum={} mean={:.4} min={} max={}",
                h.count(),
                h.sum(),
                h.mean(),
                known(h.min()),
                known(h.max())
            )?;
        }
        Ok(())
    }

    /// Evaluates a formula against the current tree state. Divide-by-zero
    /// returns `0.0`.
    pub(crate) fn eval(&self, formula: &Formula) -> f64 {
        match formula {
            Formula::Div(num, den) => {
                let n = self.get(*num).unwrap_or(0.0);
                let d = self.get(*den).unwrap_or(0.0);
                if d == 0.0 { 0.0 } else { n / d }
            }
            Formula::Ratio { numerator, other } => {
                let n = self.get(*numerator).unwrap_or(0.0);
                let o = self.get(*other).unwrap_or(0.0);
                let total = n + o;
                if total == 0.0 { 0.0 } else { n / total }
            }
            Formula::Sum(stats) => stats.iter().map(|&stat| self.get(stat).unwrap_or(0.0)).sum(),
        }
    }
}

/// Registers a hart's counters.
fn register_hart(s: &mut Stats, h: &HartPaths) {
    s.register(h.retired_insts, Meta::events("instructions retired"));
    s.register(h.traps, Meta::events("trap-taken events"));
    s.register(h.cycles_user, Meta::cycles("cycles spent in user (U) privilege"));
    s.register(h.cycles_kernel, Meta::cycles("cycles spent in supervisor (S) privilege"));
    s.register(h.cycles_machine, Meta::cycles("cycles spent in machine (M) privilege"));
}

/// Registers a core's counters and its derived rates (BP accuracies,
/// IPC/CPI against `first_hart`'s retired instructions).
fn register_core(s: &mut Stats, c: &CorePaths, first_hart: &HartPaths) {
    let commit = &c.commit;
    s.register(commit.op_load, Meta::events("integer load retired"));
    s.register(commit.op_store, Meta::events("integer store retired"));
    s.register(commit.op_branch, Meta::events("branch/jump retired"));
    s.register(commit.op_alu, Meta::events("integer ALU retired"));
    s.register(commit.op_system, Meta::events("system / CSR / ECALL retired"));
    s.register(commit.op_atomic, Meta::events("LR, SC or AMO retired"));
    s.register(commit.fp_load, Meta::events("FP load retired"));
    s.register(commit.fp_store, Meta::events("FP store retired"));
    s.register(commit.fp_arith, Meta::events("FP arithmetic retired"));
    s.register(commit.fp_fma, Meta::events("FP fused multiply-add retired"));
    s.register(commit.fp_div_sqrt, Meta::events("FP divide/sqrt retired"));
    s.register(commit.vec_int, Meta::events("vector integer op retired"));
    s.register(commit.vec_fp, Meta::events("vector FP op retired"));
    s.register(commit.vec_load, Meta::events("vector load retired"));
    s.register(commit.vec_store, Meta::events("vector store retired"));
    s.register(commit.vec_misc, Meta::events("vector misc (permute/mask/config) retired"));
    s.register(commit.vec_crypto, Meta::events("vector crypto (Zvk*) retired"));
    s.register(commit.retire_hist_zero, Meta::cycles("cycles where 0 insts retired"));
    s.register(commit.retire_hist_one, Meta::cycles("cycles where exactly 1 inst retired"));
    s.register(commit.retire_hist_two, Meta::cycles("cycles where exactly 2 insts retired"));
    s.register(commit.retire_hist_three_plus, Meta::cycles("cycles where 3+ insts retired"));

    let pipe = &c.pipeline;
    s.register(pipe.cycles_total, Meta::cycles("cycles the core was ticked"));
    s.register(pipe.cycles_wfi, Meta::cycles("cycles in WFI"));
    s.register(pipe.cycles_rob_empty, Meta::cycles("cycles with empty ROB"));
    s.register(pipe.stalls_control, Meta::cycles("recovering from a backend redirect"));
    s.register(pipe.stalls_fetch_wait, Meta::cycles("Fetch waited on an in-flight fetch"));
    s.register(pipe.stalls_data, Meta::cycles("issue stalled on data hazard"));
    s.register(pipe.stalls_ordering, Meta::cycles("issue held for program order"));
    s.register(pipe.stalls_fu_structural, Meta::cycles("issue stalled on FU structural"));
    s.register(pipe.stalls_backpressure, Meta::cycles("downstream backpressure stalls"));
    s.register(pipe.stalls_dispatch, Meta::cycles("dispatch stalls"));
    s.register(pipe.stalls_checkpoint, Meta::cycles("checkpoint allocation stalls"));
    s.register(
        pipe.stalls_serialize,
        Meta::cycles("rename stalls behind a serializing instruction"),
    );
    s.register(pipe.stalls_squash, Meta::cycles("squash-recovery cycles"));
    s.register(pipe.flushes_total, Meta::events("total pipeline flushes"));
    s.register(pipe.flushes_branch, Meta::events("flushes: branch mispredict"));
    s.register(pipe.flushes_system, Meta::events("flushes: system serialization"));
    s.register(pipe.flushes_mem_violations, Meta::events("flushes: memory ordering violations"));
    s.register(pipe.flushes_coherence, Meta::events("flushes: a value another hart overwrote"));
    s.register(pipe.flushes_trap, Meta::events("flushes: exceptions and interrupts at commit"));
    s.register(pipe.flushes_squashed_insns, Meta::events("ROB entries dropped by flushes"));

    let bp = &c.bp;
    s.register(bp.committed_hits, Meta::events("branch and jump predictions correct (committed)"));
    s.register(
        bp.committed_mispredicts,
        Meta::events("branch and jump predictions wrong (committed)"),
    );
    s.register(bp.spec_hits, Meta::events("branch predictions correct (speculative)"));
    s.register(bp.spec_mispredicts, Meta::events("branch predictions wrong (speculative)"));
    s.register(bp.decode_redirects, Meta::events("fetch redirects from decode"));

    let mdp = &c.mdp;
    s.register(mdp.predictions_bypass, Meta::events("loads predicted independent of older stores"));
    s.register(mdp.predictions_wait_all, Meta::events("loads made to wait for every older store"));
    s.register(
        mdp.predictions_wait_for,
        Meta::events("loads and stores made to wait for one store"),
    );
    s.register(mdp.violations, Meta::events("loads that read past an aliasing older store"));

    let lsq = &c.lsq;
    s.register(
        lsq.rescheduled_mem_ops,
        Meta::events(
            "memory ops that waited in memory1 for an older store, a device or the ROB head",
        ),
    );
    s.register(lsq.split_stores, Meta::events("Stores whose data issued after their address"));
    s.register(
        lsq.coherence_replays,
        Meta::events("LRs re-executed after a remote write to their line"),
    );
    s.register(
        lsq.coherence_violations,
        Meta::events("Loads squashed for reading a line before a remote write an older load saw"),
    );

    s.register(c.wcb.coalesces, Meta::events("stores merged into a line the WCB already held"));
    s.register(c.wcb.drains, Meta::events("WCB lines written to the L1D"));
    let pf = &c.load_prefetch;
    s.register(pf.l1, Meta::events("load prefetches sent to fill the L1D"));
    s.register(pf.l2, Meta::events("load prefetches sent to fill the L2 alone"));
    s.register(pf.page_boundary, Meta::events("load prefetches stopped at the page boundary"));
    s.register(pf.tlb_miss, Meta::events("load prefetches whose next page missed the DTLB"));
    s.register(pf.denied, Meta::events("load prefetches to a page the load may not read"));
    s.register(pf.not_ram, Meta::events("load prefetches to a line outside RAM"));
    for path in c.fu.all {
        s.register(path, Meta::cycles("cycles units of this type could take no other op"));
    }

    s.derive(
        bp.committed_accuracy,
        Formula::Ratio { numerator: bp.committed_hits, other: bp.committed_mispredicts },
        Meta::ratio("branch-prediction accuracy (committed)"),
    );
    s.derive(
        bp.spec_accuracy,
        Formula::Ratio { numerator: bp.spec_hits, other: bp.spec_mispredicts },
        Meta::ratio("branch-prediction accuracy (speculative)"),
    );
    s.derive(
        c.ipc,
        Formula::Div(first_hart.retired_insts, pipe.cycles_total),
        Meta::ratio("instructions per cycle"),
    );
    s.derive(
        c.cpi,
        Formula::Div(pipe.cycles_total, first_hart.retired_insts),
        Meta::ratio("cycles per instruction"),
    );
}

/// Registers the `system.*` sums over every hart.
fn register_system(s: &mut Stats, harts: &[HartPaths]) {
    let system = SystemPaths::new();
    let retired: Vec<StatId> = harts.iter().map(|h| h.retired_insts).collect();
    let traps: Vec<StatId> = harts.iter().map(|h| h.traps).collect();
    s.derive(
        system.retired_insts,
        Formula::Sum(retired),
        Meta::events("instructions retired by all harts"),
    );
    s.derive(system.traps, Formula::Sum(traps), Meta::events("traps taken by all harts"));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counter_path_resolves_and_increments() {
        let mut s = Stats::new();
        s.counter("system.cpu0.commit.insts").add(42);
        s.counter("system.cpu0.commit.insts").inc();
        assert_eq!(s.counter("system.cpu0.commit.insts").get(), 43);
    }

    #[test]
    fn histogram_records_samples() {
        let mut s = Stats::new();
        for v in [1u64, 2, 4, 8, 16, 16, 16] {
            s.histogram("system.memctrl.latency").record(v);
        }
        let h = s.histogram("system.memctrl.latency");
        assert_eq!(h.count(), 7);
        assert_eq!(h.min(), Some(1));
        assert_eq!(h.max(), Some(16));
    }

    #[test]
    fn reset_clears_everything() {
        let mut s = Stats::new();
        s.counter("a.b").add(10);
        s.histogram("a.b.h").record(5);
        s.reset();
        assert_eq!(s.counter("a.b").get(), 0);
        assert_eq!(s.histogram("a.b.h").count(), 0);
    }

    #[test]
    fn dump_text_emits_paths() {
        let mut s = Stats::new();
        s.counter("root.left").add(1);
        s.counter("root.right").add(2);
        let mut buf = Vec::new();
        s.dump(StatFormat::Text, &mut buf).unwrap();
        let out = String::from_utf8(buf).unwrap();
        assert!(out.contains("root.left 1"));
        assert!(out.contains("root.right 2"));
    }

    #[test]
    fn a_window_recomputes_ratios_from_the_counts_it_holds() {
        let mut s = Stats::new();
        s.derive(
            "core.ipc",
            Formula::Div("core.insts".into(), "core.cycles".into()),
            Meta::events("ipc"),
        );
        s.counter("core.insts").add(100);
        s.counter("core.cycles").add(400);
        let start = s.clone();
        s.counter("core.insts").add(300);
        s.counter("core.cycles").add(100);

        let window = s.since(&start);

        assert_eq!(window.get("core.insts"), Some(300.0));
        assert_eq!(window.get("core.ipc"), Some(3.0), "not 0.8 - 0.25");
    }

    #[test]
    fn a_window_keeps_exact_histogram_counts_but_not_extremes() {
        let mut s = Stats::new();
        s.histogram("mem.latency").record(10);
        let start = s.clone();
        s.histogram("mem.latency").record(20);
        s.histogram("mem.latency").record(40);

        let window = s.since(&start);
        let h = window.histogram_at("mem.latency").cloned().expect("recorded");

        assert_eq!((h.count(), h.sum(), h.mean()), (2, 60, 30.0));
        assert_eq!((h.min(), h.max()), (None, None));
    }
}