Skip to main content

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};