Skip to main content

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// =============================================================================