A custom OS for the xteink x4 ebook reader
at main 366 lines 11 kB view raw
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}