mmtk/util/statistics/counter/latency_sampler.rs
1use super::*;
2use crate::util::statistics::stats::SharedStats;
3use std::sync::Arc;
4
5/// A [`Counter`] that records one latency sample per event (e.g. one per GC pause, for things
6/// like time-to-yield or pause time) and reports it as `p50`/`p9999` columns instead of a raw
7/// per-phase total.
8///
9/// This is a thin wrapper around an [`EventCounter`]: it reuses the inner counter's
10/// `start`/`stop`/`phase_change` machinery unchanged to accumulate one sample per pause into the
11/// per-phase array. It only overrides how the counter is
12/// named and printed, bending two parts of the [`Counter`] contract to do so:
13///
14/// - [`Counter::merge_phases`] always returns `true`.
15/// - [`Counter::name`] returns a compound, tab-separated pair of column names (e.g.
16/// `"pause-time.p50\tpause-time.p9999"`), so the single `print!("{}\t", c.name())` call site
17/// in [`crate::util::statistics::stats::Stats::print_column_names`] prints both headers.
18/// - [`Counter::print_total`] prints `"{p50}\t{p9999}"` (two tab-separated values, ignoring the
19/// `other` argument), so the single `c.print_total(None)` call site in
20/// [`crate::util::statistics::stats::Stats::print_stats`] prints both values.
21///
22/// This trick only works because `Counter::name()` has a single caller (`Stats`'s own printing
23/// code) anywhere in the codebase; if that changes, this would need revisiting.
24pub struct LatencySampler {
25 inner: EventCounter,
26 /// A compound `"{name}.p50\t{name}.p9999"` string, returned by `name()`.
27 display_name: String,
28}
29
30impl LatencySampler {
31 pub fn new(name: &str, stats: Arc<SharedStats>, implicitly_start: bool) -> Self {
32 LatencySampler {
33 inner: EventCounter::new(name.to_string(), stats, implicitly_start, false),
34 display_name: format!("{name}.p50\t{name}.p9999"),
35 }
36 }
37
38 /// Record one latency sample (e.g. a duration in nanoseconds).
39 pub fn record(&mut self, value: u64) {
40 self.inner.inc_by(value);
41 }
42
43 /// The recorded samples, one per pause. Every sample is recorded during the STW phase (see
44 /// `record`), so only the odd-indexed phase counts hold real values; the even-indexed
45 /// (mutator-phase) ones are always 0 and are skipped here.
46 fn samples(&self) -> Vec<u64> {
47 self.inner
48 .count
49 .iter()
50 .skip(1)
51 .step_by(2)
52 .copied()
53 .collect()
54 }
55}
56
57impl Counter for LatencySampler {
58 fn start(&mut self) {
59 self.inner.start();
60 }
61
62 fn stop(&mut self) {
63 self.inner.stop();
64 }
65
66 fn phase_change(&mut self, old_phase: usize) {
67 self.inner.phase_change(old_phase);
68 }
69
70 fn print_count(&self, phase: usize) {
71 self.inner.print_count(phase);
72 }
73
74 fn get_total(&self, other: Option<bool>) -> u64 {
75 self.inner.get_total(other)
76 }
77
78 fn print_total(&self, _other: Option<bool>) {
79 let mut samples = self.samples();
80 if samples.is_empty() {
81 print!("0\t0");
82 return;
83 }
84 // Exact percentiles, computed by sorting all samples and using the nearest-rank method.
85 // Note that with fewer than 10,000 samples, p9999 is guaranteed to just return the
86 // maximum.
87 samples.sort_unstable();
88 let percentile = |p: f64| {
89 let rank = ((p / 100.0) * samples.len() as f64).ceil() as usize;
90 samples[rank.clamp(1, samples.len()) - 1]
91 };
92 let p50_ns = percentile(50.0);
93 let p9999_ns = percentile(99.99);
94 print!("{:.2}\t{:.2}", p50_ns as f64 / 1e6, p9999_ns as f64 / 1e6);
95 }
96
97 fn print_min(&self, other: bool) {
98 self.inner.print_min(other);
99 }
100
101 fn print_max(&self, other: bool) {
102 self.inner.print_max(other);
103 }
104
105 fn print_last(&self) {
106 self.inner.print_last();
107 }
108
109 fn merge_phases(&self) -> bool {
110 true
111 }
112
113 fn implicitly_start(&self) -> bool {
114 self.inner.implicitly_start()
115 }
116
117 fn name(&self) -> &String {
118 &self.display_name
119 }
120}