reovim_kernel/debug/profiler.rs
1//! Profiling infrastructure.
2//!
3//! Linux equivalent: `kernel/trace/ring_buffer.c`
4//!
5//! Provides trait-based profiling following the `Logger` pattern:
6//! - Kernel defines the `Profiler` trait (mechanism)
7//! - Drivers implement it with tracing ecosystem (policy)
8//!
9//! # Architecture
10//!
11//! ```text
12//! ┌─────────────────────────────────────────────────────────────┐
13//! │ Runner/Modules (use profile_scope! macro) │
14//! └─────────────────────────┬───────────────────────────────────┘
15//! │
16//! v
17//! ┌─────────────────────────────────────────────────────────────┐
18//! │ Kernel (profiler.rs) Profiler trait, ProfileScope RAII │
19//! └─────────────────────────┬───────────────────────────────────┘
20//! │
21//! v
22//! ┌─────────────────────────────────────────────────────────────┐
23//! │ Drivers (trace/) TracingProfiler -> tracing crate │
24//! └─────────────────────────────────────────────────────────────┘
25//! ```
26//!
27//! # Zero-Overhead Default
28//!
29//! When no profiler is set, `NopProfiler` is used. The `enabled()` check
30//! allows early exit before any allocation or timing occurs.
31//!
32//! # Example
33//!
34//! ```ignore
35//! use reovim_kernel::{profile_scope, profile_counter};
36//!
37//! fn process_key(key: KeyEvent) {
38//! profile_scope!("process_key", "runner::input");
39//!
40//! // ... process the key ...
41//! profile_counter!("keys_processed");
42//! }
43//! ```
44
45use std::{
46 error::Error,
47 fmt,
48 sync::{
49 OnceLock,
50 atomic::{AtomicU64, Ordering},
51 },
52 time::Instant,
53};
54
55use super::metrics;
56
57// =============================================================================
58// Span Identifier
59// =============================================================================
60
61/// Unique identifier for a profiling span.
62///
63/// Used to track hierarchical spans for flame graph generation.
64/// Drivers use this to correlate `enter()` and `exit()` calls.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct SpanId(u64);
67
68impl SpanId {
69 /// Create a new unique span ID.
70 ///
71 /// IDs are globally unique within a process (uses atomic counter).
72 #[must_use]
73 pub fn new() -> Self {
74 static COUNTER: AtomicU64 = AtomicU64::new(1);
75 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
76 }
77
78 /// Null span ID (represents no parent or inactive span).
79 #[must_use]
80 pub const fn null() -> Self {
81 Self(0)
82 }
83
84 /// Check if this is the null span.
85 #[must_use]
86 pub const fn is_null(&self) -> bool {
87 self.0 == 0
88 }
89
90 /// Get the raw ID value.
91 #[must_use]
92 pub const fn as_u64(&self) -> u64 {
93 self.0
94 }
95}
96
97impl Default for SpanId {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103// =============================================================================
104// Span Data
105// =============================================================================
106
107/// Metadata for a profiling span.
108///
109/// Contains all information needed for the driver to create a tracing span.
110/// Uses `&'static str` for zero-allocation in hot paths.
111#[derive(Debug, Clone)]
112pub struct SpanData {
113 /// Unique identifier for this span.
114 pub id: SpanId,
115 /// Parent span ID (null if top-level).
116 pub parent: SpanId,
117 /// Span name (e.g., `process_key`, `keymap_lookup`).
118 pub name: &'static str,
119 /// Target/module path (e.g., `runner::input`, `mm::buffer`).
120 pub target: &'static str,
121 /// Start timestamp in nanoseconds since process start.
122 pub start_ns: u64,
123}
124
125impl SpanData {
126 /// Create a new span data with the given name and target.
127 #[must_use]
128 pub fn new(name: &'static str, target: &'static str) -> Self {
129 Self {
130 id: SpanId::new(),
131 parent: SpanId::null(),
132 name,
133 target,
134 start_ns: timestamp_ns(),
135 }
136 }
137}
138
139/// Get current timestamp in nanoseconds (monotonic, process-relative).
140#[must_use]
141#[allow(clippy::cast_possible_truncation)] // Nanosecond truncation acceptable for profiling
142pub(super) fn timestamp_ns() -> u64 {
143 static START: OnceLock<Instant> = OnceLock::new();
144 let start = START.get_or_init(Instant::now);
145 start.elapsed().as_nanos() as u64
146}
147
148// =============================================================================
149// Profiler Trait
150// =============================================================================
151
152/// Profiler trait - kernel defines mechanism, drivers implement policy.
153///
154/// Following the `Logger` pattern: the kernel provides the trait interface,
155/// and drivers (e.g., `shared/trace/`) implement it with the tracing
156/// ecosystem.
157///
158/// # Thread Safety
159///
160/// Implementations must be `Send + Sync` as the profiler is accessed from
161/// multiple threads concurrently. Implementations should minimize locking
162/// to avoid blocking hot paths.
163///
164/// # Example
165///
166/// ```
167/// use reovim_kernel::api::v1::*;
168///
169/// struct MyProfiler;
170///
171/// impl Profiler for MyProfiler {
172/// fn enabled(&self, _target: &str) -> bool {
173/// true // Always enabled
174/// }
175///
176/// fn enter(&self, data: &SpanData) -> SpanId {
177/// println!("Entering span: {}", data.name);
178/// data.id
179/// }
180///
181/// fn exit(&self, _id: SpanId, elapsed_ns: u64) {
182/// println!("Exiting span, elapsed: {}ns", elapsed_ns);
183/// }
184///
185/// fn counter(&self, name: &'static str, value: u64) {
186/// println!("Counter {}: {}", name, value);
187/// }
188///
189/// fn histogram(&self, name: &'static str, value_us: u64) {
190/// println!("Histogram {}: {}us", name, value_us);
191/// }
192/// }
193/// ```
194pub trait Profiler: Send + Sync {
195 /// Check if profiling is enabled for the given target.
196 ///
197 /// Called before creating a span to avoid allocation overhead.
198 /// If this returns `false`, `ProfileScope` will be a no-op.
199 ///
200 /// # Arguments
201 ///
202 /// * `target` - The module/target path (e.g., `runner::input`)
203 fn enabled(&self, target: &str) -> bool;
204
205 /// Enter a new span.
206 ///
207 /// Called when a profiling scope begins. The driver should record
208 /// the span and return the span ID for later correlation.
209 ///
210 /// # Arguments
211 ///
212 /// * `data` - Span metadata including name, target, and timestamps
213 ///
214 /// # Returns
215 ///
216 /// The span ID to use for `exit()` (usually `data.id`).
217 fn enter(&self, data: &SpanData) -> SpanId;
218
219 /// Exit a span with timing information.
220 ///
221 /// Called when a profiling scope ends. The driver should record
222 /// the elapsed time and close the span.
223 ///
224 /// # Arguments
225 ///
226 /// * `id` - The span ID returned from `enter()`
227 /// * `elapsed_ns` - Elapsed time in nanoseconds
228 fn exit(&self, id: SpanId, elapsed_ns: u64);
229
230 /// Record a counter increment.
231 ///
232 /// Used for counting events like keys processed, commands executed, etc.
233 ///
234 /// # Arguments
235 ///
236 /// * `name` - Counter name (static for zero allocation)
237 /// * `value` - Value to add (typically 1)
238 fn counter(&self, name: &'static str, value: u64);
239
240 /// Record a histogram sample.
241 ///
242 /// Used for timing distributions (e.g., command execution latency).
243 ///
244 /// # Arguments
245 ///
246 /// * `name` - Histogram name (static for zero allocation)
247 /// * `value_us` - Sample value in microseconds
248 fn histogram(&self, name: &'static str, value_us: u64);
249}
250
251// =============================================================================
252// No-Op Profiler (Default)
253// =============================================================================
254
255/// No-op profiler used when no profiler is set.
256///
257/// All operations are no-ops. `enabled()` returns `false`, causing
258/// `ProfileScope` to skip all work. This provides zero overhead when
259/// profiling is disabled.
260#[derive(Debug, Clone, Copy, Default)]
261pub struct NopProfiler;
262
263impl Profiler for NopProfiler {
264 #[inline]
265 fn enabled(&self, _target: &str) -> bool {
266 false
267 }
268
269 #[inline]
270 fn enter(&self, _data: &SpanData) -> SpanId {
271 SpanId::null()
272 }
273
274 #[inline]
275 fn exit(&self, _id: SpanId, _elapsed_ns: u64) {}
276
277 #[inline]
278 fn counter(&self, _name: &'static str, _value: u64) {}
279
280 #[inline]
281 fn histogram(&self, _name: &'static str, _value_us: u64) {}
282}
283
284// =============================================================================
285// Global Profiler
286// =============================================================================
287
288/// Error returned when attempting to set the profiler more than once.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub struct SetProfilerError;
291
292impl fmt::Display for SetProfilerError {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(f, "profiler already set")
295 }
296}
297
298impl Error for SetProfilerError {}
299
300/// Global profiler storage.
301static PROFILER: OnceLock<&'static dyn Profiler> = OnceLock::new();
302
303/// Static no-op profiler instance.
304static NOP_PROFILER: NopProfiler = NopProfiler;
305
306/// Set the global profiler.
307///
308/// This function can only be called once. Subsequent calls will
309/// return `Err(SetProfilerError)`.
310///
311/// # Errors
312///
313/// Returns `Err(SetProfilerError)` if a profiler has already been set.
314///
315/// # Example
316///
317/// ```
318/// use reovim_kernel::api::v1::*;
319///
320/// static MY_PROFILER: NopProfiler = NopProfiler;
321///
322/// // First call succeeds (in practice, only call once during init)
323/// // set_profiler(&MY_PROFILER).expect("profiler not yet set");
324/// ```
325pub fn set_profiler(profiler: &'static dyn Profiler) -> Result<(), SetProfilerError> {
326 PROFILER.set(profiler).map_err(|_| SetProfilerError)
327}
328
329/// Get the global profiler.
330///
331/// Returns the registered profiler, or `NopProfiler` if none was set.
332#[must_use]
333pub fn profiler() -> &'static dyn Profiler {
334 PROFILER.get().copied().unwrap_or(&NOP_PROFILER)
335}
336
337// =============================================================================
338// Profile Scope (RAII Guard)
339// =============================================================================
340
341/// RAII guard for profiling a scope.
342///
343/// Creates a span on construction and records timing on drop.
344/// When profiling is disabled (`enabled()` returns false), this is a no-op.
345///
346/// # Example
347///
348/// ```ignore
349/// use reovim_kernel::debug::ProfileScope;
350///
351/// fn expensive_operation() {
352/// let _scope = ProfileScope::new("expensive_operation", "mymodule");
353/// // ... do work ...
354/// } // timing recorded when _scope drops
355/// ```
356pub struct ProfileScope {
357 id: SpanId,
358 start: Instant,
359 active: bool,
360}
361
362#[cfg_attr(coverage_nightly, coverage(off))]
363impl ProfileScope {
364 /// Create a new profile scope.
365 ///
366 /// If profiling is disabled for the target, returns an inactive scope
367 /// that does nothing on drop.
368 #[must_use]
369 pub fn new(name: &'static str, target: &'static str) -> Self {
370 let prof = profiler();
371 if !prof.enabled(target) {
372 return Self {
373 id: SpanId::null(),
374 start: Instant::now(),
375 active: false,
376 };
377 }
378
379 let start = Instant::now();
380 let data = SpanData::new(name, target);
381 let id = prof.enter(&data);
382
383 Self {
384 id,
385 start,
386 active: true,
387 }
388 }
389
390 /// Check if this scope is active (profiling enabled).
391 #[must_use]
392 pub const fn is_active(&self) -> bool {
393 self.active
394 }
395}
396
397#[cfg_attr(coverage_nightly, coverage(off))]
398impl Drop for ProfileScope {
399 #[allow(clippy::cast_possible_truncation)] // Nanosecond truncation acceptable for profiling
400 fn drop(&mut self) {
401 if self.active {
402 let elapsed_ns = self.start.elapsed().as_nanos() as u64;
403 profiler().exit(self.id, elapsed_ns);
404 }
405 }
406}
407
408// =============================================================================
409// Legacy Profile Guard (Backward Compatibility)
410// =============================================================================
411
412/// RAII guard for timing a scope (legacy API).
413///
414/// Records the elapsed time to a histogram when dropped.
415/// This is the original profiling mechanism that records directly to
416/// the `MetricsRegistry`. For new code, prefer `ProfileScope` with
417/// the `Profiler` trait.
418///
419/// # Example
420///
421/// ```ignore
422/// use reovim_kernel::debug::ProfileGuard;
423///
424/// fn expensive_operation() {
425/// let _guard = ProfileGuard::new("expensive_operation");
426/// // ... do work ...
427/// } // time recorded to histogram when guard drops
428/// ```
429#[deprecated(since = "0.9.5", note = "Use profile_scope!() macro instead")]
430pub struct ProfileGuard {
431 name: &'static str,
432 start: Instant,
433}
434
435#[allow(deprecated)]
436#[cfg_attr(coverage_nightly, coverage(off))]
437impl ProfileGuard {
438 /// Create a new profile guard that will record to the named histogram.
439 #[must_use]
440 pub fn new(name: &'static str) -> Self {
441 Self {
442 name,
443 start: Instant::now(),
444 }
445 }
446}
447
448#[allow(deprecated)]
449#[cfg_attr(coverage_nightly, coverage(off))]
450impl Drop for ProfileGuard {
451 #[allow(clippy::cast_possible_truncation)] // Microsecond truncation acceptable
452 fn drop(&mut self) {
453 let elapsed = self.start.elapsed();
454 let histogram = metrics().histogram(self.name);
455 histogram.record(elapsed.as_micros() as u64);
456 }
457}
458
459// =============================================================================
460// Profiling Macros
461// =============================================================================
462
463/// Profile a scope with the `Profiler` trait.
464///
465/// Zero overhead when profiling is disabled (checked at runtime).
466///
467/// # Example
468///
469/// ```ignore
470/// use reovim_kernel::profile_scope;
471///
472/// fn process_buffer() {
473/// profile_scope!("process_buffer", "mm");
474/// // ... work ...
475/// }
476/// ```
477#[macro_export]
478macro_rules! profile_scope {
479 ($name:expr, $target:expr) => {
480 let _profile_scope_guard = $crate::api::v1::ProfileScope::new($name, $target);
481 };
482}
483
484/// Profile a function (uses module path as target).
485///
486/// # Example
487///
488/// ```ignore
489/// use reovim_kernel::profile_fn;
490///
491/// fn my_function() {
492/// profile_fn!("my_function");
493/// // ... work ...
494/// }
495/// ```
496#[macro_export]
497macro_rules! profile_fn {
498 ($name:expr) => {
499 $crate::profile_scope!($name, module_path!())
500 };
501}
502
503/// Increment a counter metric via the global profiler.
504///
505/// # Example
506///
507/// ```ignore
508/// use reovim_kernel::profile_counter;
509///
510/// fn handle_key() {
511/// profile_counter!("keys_processed");
512/// // or with explicit value:
513/// profile_counter!("bytes_read", 1024);
514/// }
515/// ```
516#[macro_export]
517macro_rules! profile_counter {
518 ($name:expr) => {
519 $crate::api::v1::profiler().counter($name, 1)
520 };
521 ($name:expr, $value:expr) => {
522 $crate::api::v1::profiler().counter($name, $value)
523 };
524}
525
526/// Record a histogram sample via the global profiler.
527///
528/// # Example
529///
530/// ```ignore
531/// use reovim_kernel::profile_histogram;
532///
533/// fn measure_latency() {
534/// let latency_us = 42;
535/// profile_histogram!("request_latency", latency_us);
536/// }
537/// ```
538#[macro_export]
539macro_rules! profile_histogram {
540 ($name:expr, $value_us:expr) => {
541 $crate::api::v1::profiler().histogram($name, $value_us)
542 };
543}
544
545/// Profile a scope and record to histogram (legacy macro).
546///
547/// Uses `ProfileGuard` which records directly to `MetricsRegistry`.
548///
549/// # Example
550///
551/// ```ignore
552/// use reovim_kernel::profile;
553///
554/// fn process_buffer() {
555/// profile!("buffer_processing");
556/// // ... processing code ...
557/// }
558/// ```
559#[macro_export]
560macro_rules! profile {
561 ($name:expr) => {
562 let _guard = $crate::api::v1::ProfileGuard::new($name);
563 };
564}
565
566// =============================================================================
567// Tests
568// =============================================================================