vor 0.2.1

Cross-platform performance instrumentation with an in-app egui panel and live system and GPU metrics.
Documentation
use std::collections::VecDeque;

use puffin::GlobalFrameView;
use web_time::Instant;

use crate::system::{SystemSample, sample_now};

/// Persistent state for [`show`](super::show).
///
/// Owns:
/// - puffin's `GlobalFrameView` (flame-chart frame buffer);
/// - vor's own [`SystemSample`] ring (frame_ms / memory_mb /
///   io_ms / io_MB), pushed each [`tick`](Self::tick);
/// - cursor pin state;
/// - flame-chart time window (zoom + pan).
///
/// Caller maintains their own `VecDeque<R>` of workload records;
/// vor's system ring runs in parallel.
pub struct PanelState {
    pub(super) view: GlobalFrameView,
    pub(super) pinned: Option<usize>,
    pub(super) system: VecDeque<SystemSample>,
    pub(super) last_tick: Option<Instant>,
    pub(super) config: PanelConfig,
    /// Visible time slice of the pinned/latest frame in the flame
    /// chart, normalized to `[0.0, 1.0]`. `FULL` = whole frame.
    pub(super) flame_view: FlameView,
    /// `(lo, hi)` inclusive system-ring indices selected by a
    /// shift+drag on the bars row. `Some(_)` keeps the panel paused
    /// so the indices don't shift under the user.
    pub(super) selection: Option<(usize, usize)>,
    /// Wall time of the most recent [`tick`](Self::tick) body.
    pub(super) last_tick_ns: u64,
    /// EMA of vor's own main-thread cost per frame
    /// (`tick` + panel render), in ns. The frame budget the profiler
    /// itself spends; rendered as the corner load chip.
    pub(super) profiler_load_ns: f64,
    /// Whether the frame_ms row annotates the profiler's own
    /// per-frame contribution next to its stat.
    pub(super) show_profiler_load: bool,
}

/// Visible time slice of the flame chart.
///
/// `start` and `end` are normalized fractions of the displayed
/// frame's duration; `(0.0, 1.0)` = whole frame, narrower windows
/// zoom in. `end > start` is invariant.
#[derive(Clone, Copy)]
pub(super) struct FlameView {
    pub start: f32,
    pub end: f32,
}

impl FlameView {
    pub(super) const FULL: Self = Self {
        start: 0.0,
        end: 1.0,
    };

    pub(super) fn width(self) -> f32 {
        self.end - self.start
    }
}

impl PanelState {
    pub fn new(config: PanelConfig) -> Self {
        crate::gpu::ensure_collector();
        Self {
            view: GlobalFrameView::default(),
            pinned: None,
            system: VecDeque::with_capacity(config.history_capacity),
            last_tick: None,
            config,
            flame_view: FlameView::FULL,
            selection: None,
            last_tick_ns: 0,
            profiler_load_ns: 0.0,
            show_profiler_load: false,
        }
    }

    /// Record one displayed frame: mark a puffin frame boundary
    /// (so its scope ring slices the just-completed frame) and
    /// sample system metrics into the ring.
    ///
    /// Caller should push their `R` workload record exactly once
    /// per `tick` to keep the workload ring aligned with vor's
    /// system ring.
    ///
    /// While paused ([`is_paused`](Self::is_paused)) the caller
    /// should skip both `tick` and that push, so every graph freezes
    /// together on a stable frame instead of scrolling under the
    /// pinned cursor:
    ///
    /// ```ignore
    /// if !state.is_paused() {
    ///     state.tick();
    ///     history.push_back(record);
    /// }
    /// ```
    pub fn tick(&mut self) {
        let started = Instant::now();
        puffin::GlobalProfiler::lock().new_frame();
        let s = sample_now(&mut self.last_tick);
        if self.system.len() >= self.config.history_capacity {
            self.system.pop_front();
        }
        self.system.push_back(s);
        self.last_tick_ns = started.elapsed().as_nanos() as u64;
    }

    /// Fold this frame's panel-render cost into the profiler-load
    /// EMA. `tick` is already timed; together they are vor's
    /// own per-frame main-thread budget.
    pub(super) fn record_render_ns(&mut self, render_ns: u64) {
        let load = (self.last_tick_ns + render_ns) as f64;
        self.profiler_load_ns = if self.profiler_load_ns == 0.0 {
            load
        } else {
            0.9 * self.profiler_load_ns + 0.1 * load
        };
    }

    /// Toggle the frame_ms row's annotation of vor's own
    /// per-frame cost. Mirror of the in-panel chip for hotkeys.
    pub const fn toggle_profiler_load(&mut self) {
        self.show_profiler_load = !self.show_profiler_load;
    }

    /// Index into the history ring (front-to-back order, latest at
    /// the back) the user has frozen on, or `None` if following the
    /// latest frame. `Some(_)` means paused.
    pub const fn pinned(&self) -> Option<usize> {
        self.pinned
    }

    pub const fn is_paused(&self) -> bool {
        self.pinned.is_some()
    }

    /// Freeze on the latest system sample. No-op if the ring is
    /// empty.
    pub fn pin_latest(&mut self) {
        if !self.system.is_empty() {
            self.pinned = Some(self.system.len() - 1);
        }
    }

    pub const fn unpin(&mut self) {
        self.pinned = None;
        self.selection = None;
    }

    pub const fn selection(&self) -> Option<(usize, usize)> {
        self.selection
    }

    /// Toggle between "live" (`unpin`) and "paused on latest"
    /// (`pin_latest`). Bind to a hotkey to give callers a quick
    /// pause/resume.
    pub fn toggle_pause(&mut self) {
        if self.pinned.is_some() {
            self.unpin();
        } else {
            self.pin_latest();
        }
    }

    /// Restore the flame chart's time slice to the whole frame.
    pub const fn reset_flame_zoom(&mut self) {
        self.flame_view = FlameView::FULL;
    }
}

/// Layout + threshold parameters for the panel.
///
/// Construct via a preset (e.g. [`PanelConfig::FRAME_MS`]) or build
/// manually. No `Default` impl — bar thresholds are workload-specific
/// and silently picking ms thresholds for a non-ms workload would
/// hide bugs.
#[derive(Clone)]
pub struct PanelConfig {
    /// Capacity of both the system ring (owned by `PanelState`) and
    /// the bars' slot grid. Caller's workload ring should match.
    pub history_capacity: usize,
    /// `frame_ms` value below which the bars render green.
    pub bar_good_threshold: f64,
    /// `frame_ms` value above which the bars render red. Between
    /// good and warn renders yellow.
    pub bar_warn_threshold: f64,
}

impl PanelConfig {
    /// Standard 60-FPS / 30-FPS thresholds for an `ms`-typed bar
    /// metric over a 256-frame ring.
    pub const FRAME_MS: Self = Self {
        history_capacity: 256,
        bar_good_threshold: 16.667,
        bar_warn_threshold: 33.333,
    };
}