Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
265
crates/codegen/xai-grok-pager/src/views/fps_hud.rs
Normal file
265
crates/codegen/xai-grok-pager/src/views/fps_hud.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
//! Release-safe FPS readout — `/debug fps`, `GROK_FPS` on release builds.
|
||||
//!
|
||||
//! The full frame profiler (`render::frame_metrics`, `GROK_FPS`) is compiled
|
||||
//! only in debug/dev builds because it threads per-phase timings
|
||||
//! through `draw_frame`. This HUD measures the one thing that needs no
|
||||
//! pipeline change — the wall-clock duration of the whole `draw_frame` call
|
||||
//! (render + flush + writer handoff) — so it compiles into release builds
|
||||
//! behind a runtime toggle (the scroll-debug HUD precedent) and profiles the
|
||||
//! production render path with zero fidelity gap.
|
||||
//!
|
||||
//! `GROK_FPS` ownership: in debug/dev builds the env feeds `FrameMetrics` as
|
||||
//! always and this HUD stays toggle-only (no double overlay); on release
|
||||
//! binaries — where that overlay does not exist — the same env enables this
|
||||
//! HUD from startup, so `GROK_FPS=1` is never a silent no-op
|
||||
//! ([`HONORS_GROK_FPS_ENV`]).
|
||||
//!
|
||||
//! "fps" here is render throughput (1 / mean frame cost), not paint
|
||||
//! frequency: the pager draws on demand, so an idle UI paints nothing and a
|
||||
//! busy one is bounded by this number.
|
||||
//!
|
||||
//! Invariant (shared with the scroll HUD): pure observation. Disabled cost
|
||||
//! is one bool check per frame; rendering only paints buffer cells.
|
||||
use super::debug_style;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
/// Frame-duration ring buffer capacity (~4s at 30fps).
|
||||
const SAMPLE_CAP: usize = 120;
|
||||
/// Overlay text refresh cadence; avoids re-sorting/formatting every frame.
|
||||
const REFRESH: Duration = Duration::from_millis(250);
|
||||
/// Panel width in cells; each line is padded/truncated to this.
|
||||
const PANEL_WIDTH: u16 = 32;
|
||||
/// Whether this HUD owns the `GROK_FPS` env gate: only where the dev
|
||||
/// `FrameMetrics` overlay is compiled out. In debug/dev builds the env keeps
|
||||
/// feeding that overlay alone.
|
||||
const HONORS_GROK_FPS_ENV: bool = true;
|
||||
/// Runtime state for the FPS HUD. `GROK_FPS` enables it at startup on
|
||||
/// release binaries ([`HONORS_GROK_FPS_ENV`]); `/debug fps` toggles it
|
||||
/// live everywhere. Deliberately NOT a settings-registry entry: it is a
|
||||
/// diagnostic, not a preference to persist.
|
||||
pub struct FpsHud {
|
||||
enabled: bool,
|
||||
samples: VecDeque<Duration>,
|
||||
/// Cached stats line, rewritten at most every [`REFRESH`].
|
||||
body: String,
|
||||
last_refresh: Option<Instant>,
|
||||
}
|
||||
impl Default for FpsHud {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl FpsHud {
|
||||
pub fn new() -> Self {
|
||||
Self::with_env(std::env::var("GROK_FPS").ok())
|
||||
}
|
||||
/// `env` is the raw `GROK_FPS` value; the truthiness rule (nonempty and
|
||||
/// not `"0"`) matches `FrameMetrics` and `GROK_SCROLL_DEBUG`.
|
||||
fn with_env(env: Option<String>) -> Self {
|
||||
let env_on = HONORS_GROK_FPS_ENV && env.is_some_and(|v| !v.is_empty() && v != "0");
|
||||
Self {
|
||||
enabled: env_on,
|
||||
samples: VecDeque::with_capacity(SAMPLE_CAP),
|
||||
body: String::new(),
|
||||
last_refresh: None,
|
||||
}
|
||||
}
|
||||
/// Whether the HUD is currently enabled.
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
/// `/debug fps` runtime toggle.
|
||||
pub fn toggle(&mut self) {
|
||||
self.enabled = !self.enabled;
|
||||
self.samples.clear();
|
||||
self.body.clear();
|
||||
self.last_refresh = None;
|
||||
}
|
||||
/// Rows the overlay occupies when enabled (for stacking overlays).
|
||||
pub fn overlay_height(&self) -> u16 {
|
||||
if self.enabled { 2 } else { 0 }
|
||||
}
|
||||
/// Record one frame's `draw_frame` wall duration.
|
||||
pub fn record(&mut self, frame: Duration) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
if self.samples.len() >= SAMPLE_CAP {
|
||||
self.samples.pop_front();
|
||||
}
|
||||
self.samples.push_back(frame);
|
||||
}
|
||||
/// Owned per-frame render params (`None` unless enabled), assembled by
|
||||
/// `AppView::draw` BEFORE the frame closure — the `ScrollDebugPanel`
|
||||
/// pattern. `top_offset` leaves rows for overlays stacked above.
|
||||
pub fn overlay(&mut self, top_offset: u16) -> Option<FpsOverlay> {
|
||||
if !self.enabled {
|
||||
return None;
|
||||
}
|
||||
if self.last_refresh.is_none_or(|at| at.elapsed() >= REFRESH) {
|
||||
self.body = format_stats(&self.samples);
|
||||
self.last_refresh = Some(Instant::now());
|
||||
}
|
||||
Some(FpsOverlay {
|
||||
body: self.body.clone(),
|
||||
top_offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Mean/percentile line from the ring buffer; placeholder before samples.
|
||||
fn format_stats(samples: &VecDeque<Duration>) -> String {
|
||||
if samples.is_empty() {
|
||||
return "fps:- p50:- p95:-".to_string();
|
||||
}
|
||||
let mut ms: Vec<f64> = samples.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
|
||||
ms.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let mean_ms = ms.iter().sum::<f64>() / ms.len() as f64;
|
||||
let fps = if mean_ms > 1e-6 {
|
||||
1000.0 / mean_ms
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let p50 = percentile(&ms, 50.0);
|
||||
let p95 = percentile(&ms, 95.0);
|
||||
format!("fps:{fps:.0} p50:{p50:.1}ms p95:{p95:.1}ms")
|
||||
}
|
||||
/// Linear-interpolation percentile from a sorted slice.
|
||||
fn percentile(sorted: &[f64], pct: f64) -> f64 {
|
||||
if sorted.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let rank = (pct / 100.0) * (sorted.len() - 1) as f64;
|
||||
let lower = rank.floor() as usize;
|
||||
if lower + 1 >= sorted.len() {
|
||||
return sorted[lower];
|
||||
}
|
||||
let frac = rank - lower as f64;
|
||||
sorted[lower] + (sorted[lower + 1] - sorted[lower]) * frac
|
||||
}
|
||||
/// Owned render params for one frame (title + stats line, top-right).
|
||||
pub struct FpsOverlay {
|
||||
body: String,
|
||||
/// Rows left free for overlays above (the dev `GROK_FPS` line).
|
||||
pub top_offset: u16,
|
||||
}
|
||||
impl FpsOverlay {
|
||||
/// Paint the two-line panel in the top-right corner of `area`, in the
|
||||
/// shared theme-agnostic debug chrome (every cell, padding included).
|
||||
pub fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
debug_style::render_panel(
|
||||
area,
|
||||
buf,
|
||||
self.top_offset,
|
||||
PANEL_WIDTH,
|
||||
&["fps debug (/debug fps)", self.body.as_str()],
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
#[test]
|
||||
fn disabled_by_default_and_toggle_round_trips() {
|
||||
let mut hud = FpsHud::with_env(None);
|
||||
assert!(!hud.enabled());
|
||||
assert_eq!(hud.overlay_height(), 0);
|
||||
assert!(hud.overlay(0).is_none());
|
||||
hud.toggle();
|
||||
assert!(hud.enabled());
|
||||
assert_eq!(hud.overlay_height(), 2);
|
||||
assert!(hud.overlay(0).is_some());
|
||||
hud.toggle();
|
||||
assert!(!hud.enabled());
|
||||
assert!(hud.overlay(0).is_none());
|
||||
}
|
||||
/// Default test builds compile without dev instrumentation — release-shaped
|
||||
/// for this gate — so a truthy env must construct enabled. A
|
||||
/// debug/dev test build hands the env to `FrameMetrics` instead;
|
||||
/// asserting against [`HONORS_GROK_FPS_ENV`] keeps the test true under
|
||||
/// both cfgs (the dev half is pinned by the constant's shape, the same
|
||||
/// limitation as the `/debug` visibility test).
|
||||
#[test]
|
||||
fn grok_fps_env_enables_hud_where_dev_overlay_absent() {
|
||||
for truthy in ["1", "full", " "] {
|
||||
assert_eq!(
|
||||
FpsHud::with_env(Some(truthy.into())).enabled(),
|
||||
HONORS_GROK_FPS_ENV,
|
||||
"GROK_FPS={truthy:?} must track the env-gate owner"
|
||||
);
|
||||
}
|
||||
for falsy in [None, Some(String::new()), Some("0".into())] {
|
||||
assert!(!FpsHud::with_env(falsy).enabled());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn record_caps_ring_buffer_and_toggle_clears_stale_samples() {
|
||||
let mut hud = FpsHud::with_env(None);
|
||||
hud.toggle();
|
||||
for _ in 0..SAMPLE_CAP + 30 {
|
||||
hud.record(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(hud.samples.len(), SAMPLE_CAP);
|
||||
hud.toggle();
|
||||
hud.toggle();
|
||||
assert!(hud.samples.is_empty());
|
||||
assert!(
|
||||
hud.overlay(0).unwrap().body.contains("fps:-"),
|
||||
"fresh enablement must show placeholders"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn stats_line_reports_mean_fps_and_percentiles() {
|
||||
let mut hud = FpsHud::with_env(None);
|
||||
hud.toggle();
|
||||
for _ in 0..100 {
|
||||
hud.record(Duration::from_millis(10));
|
||||
}
|
||||
let overlay = hud.overlay(0).expect("enabled");
|
||||
assert_eq!(overlay.body, "fps:100 p50:10.0ms p95:10.0ms");
|
||||
}
|
||||
#[test]
|
||||
fn record_is_a_noop_while_disabled() {
|
||||
let mut hud = FpsHud::with_env(None);
|
||||
hud.record(Duration::from_millis(10));
|
||||
assert!(hud.samples.is_empty());
|
||||
}
|
||||
/// Every cell of the panel rect — trailing padding included — must carry
|
||||
/// the explicit debug chrome, not the theme style underneath.
|
||||
#[test]
|
||||
fn render_paints_theme_agnostic_style_over_every_panel_cell() {
|
||||
let area = Rect::new(0, 0, 60, 6);
|
||||
let mut buf = Buffer::empty(area);
|
||||
let theme = Style::default()
|
||||
.fg(Color::Rgb(228, 228, 228))
|
||||
.bg(Color::Rgb(3, 3, 4))
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
buf.set_style(area, theme);
|
||||
let overlay = FpsOverlay {
|
||||
body: "fps:100 p50:10.0ms p95:10.0ms".to_string(),
|
||||
top_offset: 1,
|
||||
};
|
||||
overlay.render(area, &mut buf);
|
||||
let x0 = area.width - PANEL_WIDTH;
|
||||
for y in 1..3u16 {
|
||||
for x in x0..area.width {
|
||||
let cell = &buf[(x, y)];
|
||||
assert_eq!(cell.bg, Color::Black, "cell ({x},{y}) bg");
|
||||
assert!(
|
||||
cell.fg == Color::White || cell.fg == Color::Yellow,
|
||||
"cell ({x},{y}) fg must be debug chrome, got {:?}",
|
||||
cell.fg
|
||||
);
|
||||
assert_eq!(
|
||||
cell.modifier,
|
||||
Modifier::empty(),
|
||||
"cell ({x},{y}) must shed themed modifiers"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(buf[(0, 1)].bg, Color::Rgb(3, 3, 4));
|
||||
assert_eq!(buf[(area.width - 1, 0)].bg, Color::Rgb(3, 3, 4));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue