rusty_opus/prof.rs
1//! Feature-gated encode/decode **stage profiler** — the instrument for perf work.
2//!
3//! Zero cost unless the `profile` cargo feature is enabled: with it off,
4//! [`scope`] is a no-op returning a ZST guard the optimizer elides entirely, so
5//! release builds are byte-identical and the hot path is untouched. With it on,
6//! each stage times itself into an atomic tick bucket; [`dump`] prints the
7//! per-stage breakdown and [`snapshot`] returns a calibrated reading so a driver
8//! can run many passes and take per-stage medians.
9//!
10//! Design mirrors `rusty_h264-common/src/prof.rs` (rdtsc ticks + wall-clock
11//! anchor calibration). A [`Stage::Total`] scope wraps `OpusEncoder::encode()`;
12//! the **`mgmt/other`** line is the residue (`Total − Σ stages`) — decompose it
13//! until every line is named, or prove it equals the timer overhead
14//! (`Σ calls × ~2 × tick cost`).
15
16/// A timed pipeline stage. Order matters: everything before [`Total`](Stage::Total)
17/// is a sub-component summed for the `mgmt/other` residue.
18#[derive(Clone, Copy)]
19pub enum Stage {
20 // --- top-level (lib.rs encode()) ---
21 /// hp_cutoff / f32→i16 conversion + SILK input resampling (down2 / down2_3).
22 Resample = 0,
23 // --- SILK encoder ---
24 /// silk_vad_get_sa_q8 (voice activity detection).
25 SilkVad = 1,
26 /// silk_find_pitch_lags_fix (open-loop pitch analysis).
27 SilkPitch = 2,
28 /// silk_noise_shape_analysis_fix (shaping filter derivation).
29 SilkNoise = 3,
30 /// silk_find_pred_coefs_fix (LPC/LTP analysis + NLSF quantization).
31 SilkPred = 4,
32 /// silk_nsq / silk_nsq_del_dec (noise-shaping quantizer, incl. rate-loop reruns).
33 SilkNsq = 5,
34 /// silk_encode_indices + silk_encode_pulses (range coding of SILK symbols).
35 SilkCode = 6,
36 // --- CELT encoder ---
37 /// Pre-emphasis + input/overlap buffer plumbing.
38 CeltPreemph = 7,
39 /// transient_analysis (short/long block decision).
40 CeltTransient = 8,
41 /// run_prefilter (pitch pre-filter incl. its pitch search).
42 CeltPrefilter = 9,
43 /// mode.mdct.forward calls (the forward MDCT(s)).
44 CeltMdct = 10,
45 /// compute_band_energies + normalise_bands.
46 CeltBands = 11,
47 /// quant_coarse_energy (coarse energy quantization + laplace coding).
48 CeltCoarse = 12,
49 /// tf_analysis + tf_encode (time-frequency resolution switching).
50 CeltTf = 13,
51 /// dynalloc_analysis + alloc_trim_analysis + clt_compute_allocation.
52 CeltAlloc = 14,
53 /// quant_fine_energy.
54 CeltFine = 15,
55 /// quant_all_bands (PVQ search + encode — the expected workhorse).
56 CeltPvq = 16,
57 /// Encoder-side synthesis after coding (denormalise/IMDCT for prefilter memory).
58 CeltSynth = 17,
59 // --- info-tier diagnostic scopes (nested inside SilkNsq; EXCLUDED from the
60 // residue sum via INFO_FIRST). Remove call sites after reading — at n_states
61 // × length calls their own rdtsc overhead inflates the enclosing stage. ---
62 /// silk_noise_shape_quantizer_short_prediction (16-tap LPC dot product).
63 SilkNsqLpc = 18,
64 /// Warped shaping AR filter (serial recurrence) + RD decision, per state.
65 SilkNsqShape = 19,
66 /// Wraps the whole `OpusEncoder::encode()` call — the denominator.
67 Total = 20,
68}
69
70/// Number of buckets.
71pub const N: usize = 21;
72
73/// Index of the first info-tier stage — buckets `INFO_FIRST..Total` are nested
74/// diagnostics excluded from the `mgmt/other` residue sum.
75pub const INFO_FIRST: usize = Stage::SilkNsqLpc as usize;
76
77#[cfg(feature = "profile")]
78mod imp {
79 use super::{Stage, N};
80 use std::sync::atomic::{AtomicU64, Ordering};
81 use std::sync::Mutex;
82 use std::time::Instant;
83
84 /// Index of the first non-`Total` stage — the residue sum runs `0..SUB`.
85 const SUB: usize = Stage::Total as usize;
86
87 /// A cheap monotonic tick. On x86_64 this is `rdtsc` (~5-10 ns, ~3-5× cheaper
88 /// than `Instant::now()` on Windows). Buckets accumulate *ticks*; `dump()`
89 /// converts via a run-length TSC calibration (invariant TSC → ticks are
90 /// wall-time-proportional). Elsewhere we fall back to `Instant` nanos.
91 #[cfg(target_arch = "x86_64")]
92 #[inline(always)]
93 fn ticks() -> u64 {
94 // SAFETY: `_rdtsc` is a pure timestamp read with no memory effects; it is
95 // `unsafe` only because it is a target intrinsic. Dev-only (profile feature).
96 unsafe { core::arch::x86_64::_rdtsc() }
97 }
98 #[cfg(not(target_arch = "x86_64"))]
99 #[inline(always)]
100 fn ticks() -> u64 {
101 use std::sync::OnceLock;
102 static EPOCH: OnceLock<Instant> = OnceLock::new();
103 EPOCH.get_or_init(Instant::now).elapsed().as_nanos() as u64
104 }
105
106 /// (wall-clock, tick-count) sampled at `reset()` — the calibration anchor.
107 static ANCHOR: Mutex<Option<(Instant, u64)>> = Mutex::new(None);
108
109 const NAMES: [&str; N] = [
110 "resample/hp",
111 "silk-vad",
112 "silk-pitch",
113 "silk-noise-shape",
114 "silk-pred-coefs",
115 "silk-nsq",
116 "silk-range-code",
117 "celt-preemph",
118 "celt-transient",
119 "celt-prefilter",
120 "celt-mdct",
121 "celt-bands",
122 "celt-coarse-q",
123 "celt-tf",
124 "celt-alloc",
125 "celt-fine-q",
126 "celt-pvq",
127 "celt-synth",
128 " ↳nsq-lpc-pred",
129 " ↳nsq-shape+rd",
130 "TOTAL encode()",
131 ];
132
133 static NS: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
134 static CALLS: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
135
136 /// RAII timer: accumulates `ticks()..drop` into the stage's bucket.
137 pub struct Guard {
138 stage: usize,
139 start: u64,
140 }
141
142 impl Drop for Guard {
143 #[inline]
144 fn drop(&mut self) {
145 let d = ticks().wrapping_sub(self.start);
146 NS[self.stage].fetch_add(d, Ordering::Relaxed);
147 CALLS[self.stage].fetch_add(1, Ordering::Relaxed);
148 }
149 }
150
151 #[inline]
152 pub fn scope(s: Stage) -> Guard {
153 Guard {
154 stage: s as usize,
155 start: ticks(),
156 }
157 }
158
159 /// Zero all buckets and sample the calibration anchor — call before a clean run.
160 pub fn reset() {
161 for a in NS.iter().chain(CALLS.iter()) {
162 a.store(0, Ordering::Relaxed);
163 }
164 *ANCHOR.lock().unwrap() = Some((Instant::now(), ticks()));
165 }
166
167 /// Human-readable name for stage index `i` (`SUB` = the `TOTAL` row).
168 pub fn name(i: usize) -> &'static str {
169 NAMES.get(i).copied().unwrap_or("?")
170 }
171
172 /// One calibrated reading: `(ms, calls)` per stage index `0..N`.
173 pub fn snapshot() -> [(f64, u64); N] {
174 let load = |i: usize| NS[i].load(Ordering::Relaxed);
175 let ns_per_tick = ANCHOR
176 .lock()
177 .unwrap()
178 .map(|(t0, c0)| {
179 let wall = t0.elapsed().as_nanos() as f64;
180 let cyc = ticks().wrapping_sub(c0) as f64;
181 if cyc > 0.0 {
182 wall / cyc
183 } else {
184 1.0
185 }
186 })
187 .unwrap_or(1.0);
188 let mut out = [(0.0f64, 0u64); N];
189 for (i, o) in out.iter_mut().enumerate() {
190 *o = (
191 load(i) as f64 * ns_per_tick / 1e6,
192 CALLS[i].load(Ordering::Relaxed),
193 );
194 }
195 out
196 }
197
198 /// Print the per-stage breakdown (does not reset).
199 pub fn dump() {
200 let s = snapshot();
201 let total = s[SUB].0.max(1e-9);
202 let sub_sum: f64 = (0..super::INFO_FIRST).map(|i| s[i].0).sum();
203 let mgmt = (total - sub_sum).max(0.0);
204 let pct = |ms: f64| 100.0 * ms / total;
205
206 eprintln!("\n--- encode stage profile (encode() wall = {total:.1} ms) ---");
207 for i in 0..SUB {
208 if s[i].1 == 0 {
209 continue;
210 }
211 eprintln!(
212 " {:<18} {:>8.1} ms {:>5.1}% ({} calls)",
213 NAMES[i],
214 s[i].0,
215 pct(s[i].0),
216 s[i].1,
217 );
218 }
219 eprintln!(
220 " {:<18} {:>8.1} ms {:>5.1}% <- residue: mode select / control / glue (or timer overhead)",
221 "mgmt/other",
222 mgmt,
223 pct(mgmt),
224 );
225 eprintln!(" {:<18} {:>8.1} ms 100.0%", NAMES[SUB], total);
226 }
227}
228
229#[cfg(not(feature = "profile"))]
230mod imp {
231 use super::{Stage, N};
232
233 /// No-op guard (ZST) — elided in release.
234 pub struct Guard;
235
236 #[inline(always)]
237 pub fn scope(_s: Stage) -> Guard {
238 Guard
239 }
240 #[inline(always)]
241 pub fn reset() {}
242 #[inline(always)]
243 pub fn dump() {}
244 #[inline(always)]
245 pub fn snapshot() -> [(f64, u64); N] {
246 [(0.0, 0); N]
247 }
248 #[inline(always)]
249 pub fn name(_i: usize) -> &'static str {
250 ""
251 }
252}
253
254pub use imp::{dump, name, reset, scope, snapshot, Guard};