A custom OS for the xteink x4 ebook reader
1//! Feature-gated performance instrumentation for plump.
2//!
3//! All macros compile to nothing when the `perf` cargo feature is disabled.
4//! When enabled, they emit structured `key=value` logs via `log::info!`
5//! using targets prefixed with `perf::` for filtering.
6//!
7//! # ESP_LOG filtering
8//!
9//! `esp-println` compiles the log filter from `ESP_LOG` at build time.
10//! The target prefix `perf::` allows selective filtering:
11//!
12//! - `ESP_LOG="debug,esp_hal=info"` — default dev logging
13//! - `ESP_LOG="info"` — info-level logging including perf
14//! - `ESP_LOG="warn,perf=info"` — suppress normal info, show only perf
15//! - `ESP_LOG="info,perf=off"` — normal logging, hide perf output
16//!
17//! Note: target filtering only suppresses output. To remove instrumentation
18//! cost entirely, build without `--features perf`.
19//!
20//! # Output format
21//!
22//! Since `esp-println` doesn't include the log target in output, perf macros
23//! prepend `perf::{target}` to the message for easy grepping:
24//!
25//! ```text
26//! INFO - perf::reader NeedInit elapsed_ms=42
27//! INFO - perf::storage read bytes=4096
28//! ```
29//!
30//! # Macros
31//!
32//! ## `perf_event!` — structured point event
33//!
34//! ```ignore
35//! perf_event!("reader", "state_change from={:?} to={:?}", old, new);
36//! perf_event!("storage", "read bytes={}", n);
37//! ```
38//!
39//! ## `perf_scope!` — RAII timing guard
40//!
41//! ```ignore
42//! let _g = perf_scope!("reader", "NeedInit");
43//! // ... synchronous work only — do NOT hold across .await ...
44//! // on drop: INFO - perf::reader NeedInit elapsed_ms=42
45//! ```
46//!
47//! ## `perf_begin!` — capture timestamp for manual timing
48//!
49//! ```ignore
50//! perf_begin!(t0);
51//! // ... work ...
52//! perf_event!("reader", "manual elapsed_ms={}", t0.elapsed().as_millis());
53//! ```
54//!
55//! When `perf` is off, `perf_begin!` produces no binding and
56//! `perf_event!` discards its arguments without type-checking,
57//! so references to `t0` inside `perf_event!` compile cleanly.
58//!
59//! # Storage counters
60//!
61//! Critical-section-protected counters for SD I/O operations. Always callable
62//! (no-op stubs when `perf` is off). Use [`counters::snapshot`] and
63//! [`counters::delta`] for
64//! per-scope summaries inside `perf_event!`:
65//!
66//! ```ignore
67//! let snap = plump_kernel::perf::counters::snapshot();
68//! // ... work ...
69//! let d = plump_kernel::perf::counters::delta(&snap);
70//! perf_event!("storage", "page_load reads={} bytes_r={}", d.sd_reads, d.sd_bytes_read);
71//! ```
72
73// ---------------------------------------------------------------------------
74// perf_event! — structured point event
75// ---------------------------------------------------------------------------
76
77/// Log a structured performance event with `key=value` fields.
78///
79/// Compiles to nothing when the `perf` feature is off.
80/// The first argument is a target literal (e.g. `"reader"`, `"storage"`).
81/// The rest is a `format_args!`-style format string with arguments.
82///
83/// Output includes the `perf::{target}` prefix in the message for grepability.
84#[cfg(feature = "perf")]
85#[macro_export]
86macro_rules! perf_event {
87 ($target:literal, $fmt:literal) => {
88 log::info!(
89 target: concat!("perf::", $target),
90 concat!("perf::", $target, " ", $fmt),
91 )
92 };
93 ($target:literal, $fmt:literal, $($args:expr),+ $(,)?) => {
94 log::info!(
95 target: concat!("perf::", $target),
96 concat!("perf::", $target, " ", $fmt),
97 $($args),+
98 )
99 };
100}
101
102#[cfg(not(feature = "perf"))]
103#[macro_export]
104macro_rules! perf_event {
105 ($target:literal, $($arg:tt)*) => {};
106}
107
108// ---------------------------------------------------------------------------
109// perf_scope! — RAII timing guard
110// ---------------------------------------------------------------------------
111
112/// Create an RAII timing guard that logs `elapsed_ms` on drop.
113///
114/// Returns a [`TimingGuard`] when `perf` is on, `()` when off.
115/// **Synchronous only** — do NOT hold across `.await` points. This is a
116/// usage rule; the helper does not enforce it for you.
117///
118/// ```ignore
119/// let _g = perf_scope!("reader", "NeedInit");
120/// // on drop: INFO - perf::reader NeedInit elapsed_ms=42
121/// ```
122#[cfg(feature = "perf")]
123#[macro_export]
124macro_rules! perf_scope {
125 ($target:literal, $name:expr) => {
126 $crate::perf::TimingGuard::new(concat!("perf::", $target), $name)
127 };
128}
129
130#[cfg(not(feature = "perf"))]
131#[macro_export]
132macro_rules! perf_scope {
133 ($target:literal, $name:expr) => {
134 ()
135 };
136}
137
138// ---------------------------------------------------------------------------
139// perf_begin! — capture timestamp for manual timing
140// ---------------------------------------------------------------------------
141
142/// Bind a local variable to the current `embassy_time::Instant`.
143///
144/// Compiles to nothing when `perf` is off. References to the binding
145/// inside [`perf_event!`] are safe because that macro also compiles out.
146///
147/// ```ignore
148/// perf_begin!(t0);
149/// // ... work ...
150/// perf_event!("reader", "done elapsed_ms={}", t0.elapsed().as_millis());
151/// ```
152#[cfg(feature = "perf")]
153#[macro_export]
154macro_rules! perf_begin {
155 ($name:ident) => {
156 let $name = embassy_time::Instant::now();
157 };
158}
159
160#[cfg(not(feature = "perf"))]
161#[macro_export]
162macro_rules! perf_begin {
163 ($name:ident) => {};
164}
165
166// ---------------------------------------------------------------------------
167// TimingGuard
168// ---------------------------------------------------------------------------
169
170#[cfg(feature = "perf")]
171pub struct TimingGuard {
172 target: &'static str,
173 name: &'static str,
174 start: embassy_time::Instant,
175}
176
177#[cfg(feature = "perf")]
178impl TimingGuard {
179 /// Create a new timing guard. Prefer the [`perf_scope!`] macro.
180 #[inline]
181 pub fn new(target: &'static str, name: &'static str) -> Self {
182 Self {
183 target,
184 name,
185 start: embassy_time::Instant::now(),
186 }
187 }
188}
189
190#[cfg(feature = "perf")]
191impl Drop for TimingGuard {
192 fn drop(&mut self) {
193 let ms = self.start.elapsed().as_millis();
194 log::info!(
195 target: self.target,
196 "{} {} elapsed_ms={}",
197 self.target,
198 self.name,
199 ms,
200 );
201 }
202}
203
204// ---------------------------------------------------------------------------
205// Storage I/O counters
206// ---------------------------------------------------------------------------
207
208/// Critical-section-protected SD I/O counters for per-scope profiling
209/// summaries.
210///
211/// All functions are always callable. When `perf` is off, they are no-ops
212/// and the compiler optimises them away entirely.
213pub mod counters {
214 /// Snapshot of counter values at a point in time.
215 ///
216 /// When `perf` is off this is a ZST. Field access only compiles
217 /// inside [`perf_event!`] (which also compiles out), so this is safe.
218 #[cfg(feature = "perf")]
219 #[derive(Clone, Copy, Debug)]
220 pub struct Snapshot {
221 pub sd_reads: u32,
222 pub sd_writes: u32,
223 pub sd_bytes_read: u32,
224 pub sd_bytes_written: u32,
225 }
226
227 #[cfg(not(feature = "perf"))]
228 #[derive(Clone, Copy, Debug)]
229 pub struct Snapshot;
230
231 // -- active counters (perf on) -----------------------------------------
232 //
233 // ESP32-C3 is RV32IMC (no 'A' atomic extension), so we use
234 // critical_section::Mutex<Cell<u32>> instead of AtomicU32.
235 // The critical section is ~free on single-core (just disables interrupts).
236
237 #[cfg(feature = "perf")]
238 #[allow(dead_code)]
239 mod inner {
240 use core::cell::Cell;
241 use critical_section::Mutex;
242
243 pub(super) static SD_READS: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
244 pub(super) static SD_WRITES: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
245 pub(super) static SD_BYTES_READ: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
246 pub(super) static SD_BYTES_WRITTEN: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
247
248 #[inline]
249 pub(super) fn add(counter: &Mutex<Cell<u32>>, n: u32) {
250 critical_section::with(|cs| {
251 let c = counter.borrow(cs);
252 c.set(c.get().wrapping_add(n));
253 });
254 }
255
256 #[inline]
257 pub(super) fn load(counter: &Mutex<Cell<u32>>) -> u32 {
258 critical_section::with(|cs| counter.borrow(cs).get())
259 }
260
261 #[inline]
262 pub(super) fn store(counter: &Mutex<Cell<u32>>, val: u32) {
263 critical_section::with(|cs| counter.borrow(cs).set(val));
264 }
265 }
266
267 /// Record one SD read operation.
268 #[cfg(feature = "perf")]
269 #[inline]
270 pub fn inc_sd_reads() {
271 inner::add(&inner::SD_READS, 1);
272 }
273
274 /// Record one SD write operation.
275 #[cfg(feature = "perf")]
276 #[inline]
277 pub fn inc_sd_writes() {
278 inner::add(&inner::SD_WRITES, 1);
279 }
280
281 /// Record bytes read from SD.
282 #[cfg(feature = "perf")]
283 #[inline]
284 pub fn add_sd_bytes_read(n: u32) {
285 inner::add(&inner::SD_BYTES_READ, n);
286 }
287
288 /// Record bytes written to SD.
289 #[cfg(feature = "perf")]
290 #[inline]
291 pub fn add_sd_bytes_written(n: u32) {
292 inner::add(&inner::SD_BYTES_WRITTEN, n);
293 }
294
295 /// Take a snapshot of current counter values.
296 #[cfg(feature = "perf")]
297 #[inline]
298 pub fn snapshot() -> Snapshot {
299 // single CS to get a consistent snapshot
300 critical_section::with(|cs| Snapshot {
301 sd_reads: inner::SD_READS.borrow(cs).get(),
302 sd_writes: inner::SD_WRITES.borrow(cs).get(),
303 sd_bytes_read: inner::SD_BYTES_READ.borrow(cs).get(),
304 sd_bytes_written: inner::SD_BYTES_WRITTEN.borrow(cs).get(),
305 })
306 }
307
308 /// Compute the delta between a previous snapshot and now.
309 #[cfg(feature = "perf")]
310 #[inline]
311 pub fn delta(before: &Snapshot) -> Snapshot {
312 let now = snapshot();
313 Snapshot {
314 sd_reads: now.sd_reads.wrapping_sub(before.sd_reads),
315 sd_writes: now.sd_writes.wrapping_sub(before.sd_writes),
316 sd_bytes_read: now.sd_bytes_read.wrapping_sub(before.sd_bytes_read),
317 sd_bytes_written: now.sd_bytes_written.wrapping_sub(before.sd_bytes_written),
318 }
319 }
320
321 /// Reset all counters to zero.
322 #[cfg(feature = "perf")]
323 #[inline]
324 pub fn reset() {
325 critical_section::with(|cs| {
326 inner::SD_READS.borrow(cs).set(0);
327 inner::SD_WRITES.borrow(cs).set(0);
328 inner::SD_BYTES_READ.borrow(cs).set(0);
329 inner::SD_BYTES_WRITTEN.borrow(cs).set(0);
330 });
331 }
332
333 // -- stubs (perf off) --------------------------------------------------
334
335 #[cfg(not(feature = "perf"))]
336 #[inline]
337 pub fn inc_sd_reads() {}
338
339 #[cfg(not(feature = "perf"))]
340 #[inline]
341 pub fn inc_sd_writes() {}
342
343 #[cfg(not(feature = "perf"))]
344 #[inline]
345 pub fn add_sd_bytes_read(_n: u32) {}
346
347 #[cfg(not(feature = "perf"))]
348 #[inline]
349 pub fn add_sd_bytes_written(_n: u32) {}
350
351 #[cfg(not(feature = "perf"))]
352 #[inline]
353 pub fn snapshot() -> Snapshot {
354 Snapshot
355 }
356
357 #[cfg(not(feature = "perf"))]
358 #[inline]
359 pub fn delta(_before: &Snapshot) -> Snapshot {
360 Snapshot
361 }
362
363 #[cfg(not(feature = "perf"))]
364 #[inline]
365 pub fn reset() {}
366}