Skip to main content

zenjxl_decoder/util/
profiling.rs

1// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6//! Simple profiling infrastructure for hot path timing.
7//!
8//! Enable with the `profiling` feature. When enabled, tracks cumulative time
9//! spent in instrumented functions and prints a report when `print_profile_report()`
10//! is called.
11
12#[cfg(feature = "profiling")]
13mod inner {
14    use std::sync::atomic::{AtomicU64, Ordering};
15    use std::time::Instant;
16
17    /// Profiling counters for different hot paths.
18    /// Each counter tracks cumulative nanoseconds.
19    pub struct ProfileCounters {
20        pub dequant_transform_ns: AtomicU64,
21        pub dequant_transform_calls: AtomicU64,
22        pub dct_ns: AtomicU64,
23        pub dct_calls: AtomicU64,
24        pub entropy_decode_ns: AtomicU64,
25        pub entropy_decode_calls: AtomicU64,
26        pub chroma_upsample_ns: AtomicU64,
27        pub chroma_upsample_calls: AtomicU64,
28        pub render_pipeline_ns: AtomicU64,
29        pub render_pipeline_calls: AtomicU64,
30        pub modular_decode_ns: AtomicU64,
31        pub modular_decode_calls: AtomicU64,
32    }
33
34    impl ProfileCounters {
35        pub const fn new() -> Self {
36            Self {
37                dequant_transform_ns: AtomicU64::new(0),
38                dequant_transform_calls: AtomicU64::new(0),
39                dct_ns: AtomicU64::new(0),
40                dct_calls: AtomicU64::new(0),
41                entropy_decode_ns: AtomicU64::new(0),
42                entropy_decode_calls: AtomicU64::new(0),
43                chroma_upsample_ns: AtomicU64::new(0),
44                chroma_upsample_calls: AtomicU64::new(0),
45                render_pipeline_ns: AtomicU64::new(0),
46                render_pipeline_calls: AtomicU64::new(0),
47                modular_decode_ns: AtomicU64::new(0),
48                modular_decode_calls: AtomicU64::new(0),
49            }
50        }
51
52        #[allow(dead_code)] // Available for manual profiling reset
53        pub fn reset(&self) {
54            self.dequant_transform_ns.store(0, Ordering::Relaxed);
55            self.dequant_transform_calls.store(0, Ordering::Relaxed);
56            self.dct_ns.store(0, Ordering::Relaxed);
57            self.dct_calls.store(0, Ordering::Relaxed);
58            self.entropy_decode_ns.store(0, Ordering::Relaxed);
59            self.entropy_decode_calls.store(0, Ordering::Relaxed);
60            self.chroma_upsample_ns.store(0, Ordering::Relaxed);
61            self.chroma_upsample_calls.store(0, Ordering::Relaxed);
62            self.render_pipeline_ns.store(0, Ordering::Relaxed);
63            self.render_pipeline_calls.store(0, Ordering::Relaxed);
64            self.modular_decode_ns.store(0, Ordering::Relaxed);
65            self.modular_decode_calls.store(0, Ordering::Relaxed);
66        }
67    }
68
69    pub static COUNTERS: ProfileCounters = ProfileCounters::new();
70
71    /// RAII guard that measures elapsed time and adds it to a counter.
72    pub struct ProfileGuard {
73        start: Instant,
74        ns_counter: &'static AtomicU64,
75        call_counter: &'static AtomicU64,
76    }
77
78    impl ProfileGuard {
79        #[inline]
80        pub fn new(ns_counter: &'static AtomicU64, call_counter: &'static AtomicU64) -> Self {
81            Self {
82                start: Instant::now(),
83                ns_counter,
84                call_counter,
85            }
86        }
87    }
88
89    impl Drop for ProfileGuard {
90        #[inline]
91        fn drop(&mut self) {
92            let elapsed = self.start.elapsed().as_nanos() as u64;
93            // Use separate load/store instead of fetch_add to avoid lock prefix.
94            // This generates plain `addq`/`incq` vs `lock addq`/`lock incq`.
95            // Racy but acceptable for profiling - we just want approximate counts.
96            let ns = self.ns_counter.load(Ordering::Relaxed);
97            self.ns_counter.store(ns + elapsed, Ordering::Relaxed);
98            let calls = self.call_counter.load(Ordering::Relaxed);
99            self.call_counter.store(calls + 1, Ordering::Relaxed);
100        }
101    }
102
103    /// Print a profile report to stderr.
104    pub fn print_profile_report() {
105        let c = &COUNTERS;
106
107        eprintln!("\n=== JXL-RS Profile Report ===\n");
108
109        let entries = [
110            (
111                "dequant_transform",
112                c.dequant_transform_ns.load(Ordering::Relaxed),
113                c.dequant_transform_calls.load(Ordering::Relaxed),
114            ),
115            (
116                "dct/idct",
117                c.dct_ns.load(Ordering::Relaxed),
118                c.dct_calls.load(Ordering::Relaxed),
119            ),
120            (
121                "entropy_decode",
122                c.entropy_decode_ns.load(Ordering::Relaxed),
123                c.entropy_decode_calls.load(Ordering::Relaxed),
124            ),
125            (
126                "chroma_upsample",
127                c.chroma_upsample_ns.load(Ordering::Relaxed),
128                c.chroma_upsample_calls.load(Ordering::Relaxed),
129            ),
130            (
131                "render_pipeline",
132                c.render_pipeline_ns.load(Ordering::Relaxed),
133                c.render_pipeline_calls.load(Ordering::Relaxed),
134            ),
135            (
136                "modular_decode",
137                c.modular_decode_ns.load(Ordering::Relaxed),
138                c.modular_decode_calls.load(Ordering::Relaxed),
139            ),
140        ];
141
142        let total_ns: u64 = entries.iter().map(|(_, ns, _)| ns).sum();
143
144        eprintln!(
145            "{:<20} {:>12} {:>12} {:>10} {:>8}",
146            "Function", "Time (ms)", "Calls", "Avg (µs)", "% Total"
147        );
148        eprintln!("{:-<66}", "");
149
150        for (name, ns, calls) in entries {
151            if calls > 0 {
152                let ms = ns as f64 / 1_000_000.0;
153                let avg_us = (ns as f64 / calls as f64) / 1_000.0;
154                let pct = if total_ns > 0 {
155                    (ns as f64 / total_ns as f64) * 100.0
156                } else {
157                    0.0
158                };
159                eprintln!(
160                    "{:<20} {:>12.2} {:>12} {:>10.2} {:>7.1}%",
161                    name, ms, calls, avg_us, pct
162                );
163            }
164        }
165
166        eprintln!("{:-<66}", "");
167        eprintln!("{:<20} {:>12.2}", "TOTAL", total_ns as f64 / 1_000_000.0);
168        eprintln!();
169    }
170
171    /// Reset all counters.
172    #[allow(dead_code)] // Available for manual profiling reset
173    pub fn reset_profile_counters() {
174        COUNTERS.reset();
175    }
176}
177
178#[cfg(not(feature = "profiling"))]
179#[allow(dead_code)]
180mod inner {
181    /// No-op guard when profiling is disabled.
182    #[derive(Default)]
183    pub struct ProfileGuard;
184
185    impl ProfileGuard {
186        #[inline(always)]
187        pub fn new() -> Self {
188            Self
189        }
190    }
191
192    #[inline(always)]
193    pub fn print_profile_report() {}
194
195    #[inline(always)]
196    pub fn reset_profile_counters() {}
197}
198
199#[allow(unused_imports)]
200pub use inner::*;
201
202/// Macro to create a profile guard for a specific counter.
203/// When profiling is disabled, this is a no-op.
204#[cfg(feature = "profiling")]
205#[macro_export]
206macro_rules! profile {
207    (dequant_transform) => {
208        let _guard = $crate::util::ProfileGuard::new(
209            &$crate::util::COUNTERS.dequant_transform_ns,
210            &$crate::util::COUNTERS.dequant_transform_calls,
211        );
212    };
213    (dct) => {
214        let _guard = $crate::util::ProfileGuard::new(
215            &$crate::util::COUNTERS.dct_ns,
216            &$crate::util::COUNTERS.dct_calls,
217        );
218    };
219    (entropy_decode) => {
220        let _guard = $crate::util::ProfileGuard::new(
221            &$crate::util::COUNTERS.entropy_decode_ns,
222            &$crate::util::COUNTERS.entropy_decode_calls,
223        );
224    };
225    (chroma_upsample) => {
226        let _guard = $crate::util::ProfileGuard::new(
227            &$crate::util::COUNTERS.chroma_upsample_ns,
228            &$crate::util::COUNTERS.chroma_upsample_calls,
229        );
230    };
231    (render_pipeline) => {
232        let _guard = $crate::util::ProfileGuard::new(
233            &$crate::util::COUNTERS.render_pipeline_ns,
234            &$crate::util::COUNTERS.render_pipeline_calls,
235        );
236    };
237    (modular_decode) => {
238        let _guard = $crate::util::ProfileGuard::new(
239            &$crate::util::COUNTERS.modular_decode_ns,
240            &$crate::util::COUNTERS.modular_decode_calls,
241        );
242    };
243}
244
245#[cfg(not(feature = "profiling"))]
246#[macro_export]
247macro_rules! profile {
248    ($name:ident) => {};
249}