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
363
crates/codegen/xai-grok-pager/benches/render.rs
Normal file
363
crates/codegen/xai-grok-pager/benches/render.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
//! Criterion benchmarks for the xai-grok-pager rendering pipeline.
|
||||
//!
|
||||
//! Measures the per-frame cost of rendering a rich markdown document
|
||||
//! into a ratatui `Buffer`. This isolates the render hot path (entry
|
||||
//! rendering, scratch buffer copies, layout computation) from one-time
|
||||
//! setup (markdown parsing, syntax highlighting, word wrapping).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
use xai_grok_pager::appearance::AppearanceConfig;
|
||||
use xai_grok_pager::render::Renderable;
|
||||
use xai_grok_pager::scrollback::entry::ScrollbackEntry;
|
||||
use xai_grok_pager::scrollback::render::render_scrolled_entries_with_scratch;
|
||||
use xai_grok_pager::scrollback::wrappers::EntryRenderer;
|
||||
use xai_grok_pager::scrollback::{
|
||||
EntryId, EntryLayoutInfo, HorizontalLayout, RenderBlock, ScrollbackState,
|
||||
};
|
||||
use xai_grok_pager::theme::Theme;
|
||||
|
||||
static BENCH_MD: &str = include_str!("bench.md");
|
||||
|
||||
/// Viewport dimensions for the benchmark.
|
||||
const VIEWPORT_WIDTH: u16 = 120;
|
||||
const VIEWPORT_HEIGHT: u16 = 50;
|
||||
|
||||
/// How many lines to advance per step in full_scroll.
|
||||
const SCROLL_STEP: u16 = 10;
|
||||
|
||||
/// Entry count for the reveal benchmarks. Approximates the ~3,200-entry
|
||||
/// scrollback of a long real session (~5 MB of searchable text).
|
||||
const REVEAL_ENTRIES: usize = 3000;
|
||||
|
||||
/// Build the entries used by every benchmark iteration.
|
||||
///
|
||||
/// This is called once in the setup closure so that markdown parsing,
|
||||
/// syntax highlighting, and word-wrap caching are **not** measured.
|
||||
fn build_entries() -> Vec<ScrollbackEntry> {
|
||||
vec![
|
||||
// A user prompt to exercise multi-entry layout
|
||||
ScrollbackEntry::new(RenderBlock::user_prompt(
|
||||
"Explain the rendering pipeline architecture in detail",
|
||||
)),
|
||||
// The main payload — a large markdown agent response
|
||||
ScrollbackEntry::new(RenderBlock::agent_message(BENCH_MD)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Pre-compute entry layout info (same as prepare_layout does in production).
|
||||
fn compute_layouts(
|
||||
entries: &[ScrollbackEntry],
|
||||
appearance: &AppearanceConfig,
|
||||
) -> Vec<EntryLayoutInfo> {
|
||||
let theme = Theme::current();
|
||||
let viewport = Rect::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
let layout = HorizontalLayout::new(viewport, &appearance.scrollback.layout);
|
||||
let entry_width = layout.entry_content_area().width;
|
||||
|
||||
entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let height = EntryRenderer::new(e, &theme)
|
||||
.with_appearance(appearance.clone())
|
||||
.desired_height(entry_width);
|
||||
EntryLayoutInfo {
|
||||
height,
|
||||
gap_after: 1,
|
||||
group_header_count: 0,
|
||||
group_collapse_header: false,
|
||||
verb_group_header: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ─── Benchmarks ────────────────────────────────────────────────────
|
||||
|
||||
/// Render a single frame at scroll offset 0 (top of document).
|
||||
///
|
||||
/// The per-frame baseline with no top clipping — only bottom-clipped.
|
||||
fn bench_single_frame(c: &mut Criterion) {
|
||||
let entries = build_entries();
|
||||
let entry_refs: Vec<&ScrollbackEntry> = entries.iter().collect();
|
||||
let viewport = Rect::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
let theme = Theme::current();
|
||||
let appearance = AppearanceConfig::default();
|
||||
let layouts = compute_layouts(&entries, &appearance);
|
||||
|
||||
// Prime the wrap cache
|
||||
{
|
||||
let mut buf = Buffer::empty(viewport);
|
||||
render_scrolled_entries_with_scratch(
|
||||
&mut buf,
|
||||
viewport,
|
||||
&entry_refs,
|
||||
0,
|
||||
None,
|
||||
&theme,
|
||||
&appearance,
|
||||
&layouts,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
c.bench_function("render/single_frame", |b| {
|
||||
let mut buf = Buffer::empty(viewport);
|
||||
b.iter(|| {
|
||||
buf.reset();
|
||||
render_scrolled_entries_with_scratch(
|
||||
&mut buf,
|
||||
viewport,
|
||||
&entry_refs,
|
||||
0,
|
||||
None,
|
||||
&theme,
|
||||
&appearance,
|
||||
&layouts,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Scroll through the entire document, stepping by SCROLL_STEP lines.
|
||||
///
|
||||
/// Simulates a user paging through the document, exercising the full
|
||||
/// render pipeline at many offsets including partial entry rendering.
|
||||
fn bench_full_scroll(c: &mut Criterion) {
|
||||
let entries = build_entries();
|
||||
let entry_refs: Vec<&ScrollbackEntry> = entries.iter().collect();
|
||||
let viewport = Rect::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
let theme = Theme::current();
|
||||
let appearance = AppearanceConfig::default();
|
||||
let layouts = compute_layouts(&entries, &appearance);
|
||||
|
||||
// usize: scroll offset is usize in the render path.
|
||||
let total: usize = layouts
|
||||
.iter()
|
||||
.map(|l| l.height as usize + l.gap_after as usize)
|
||||
.sum(); // heights + gaps
|
||||
let max_scroll = total.saturating_sub(VIEWPORT_HEIGHT as usize);
|
||||
|
||||
// Prime the wrap cache
|
||||
{
|
||||
let mut buf = Buffer::empty(viewport);
|
||||
render_scrolled_entries_with_scratch(
|
||||
&mut buf,
|
||||
viewport,
|
||||
&entry_refs,
|
||||
0,
|
||||
None,
|
||||
&theme,
|
||||
&appearance,
|
||||
&layouts,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
c.bench_function("render/full_scroll", |b| {
|
||||
let mut buf = Buffer::empty(viewport);
|
||||
b.iter(|| {
|
||||
let mut offset = 0usize;
|
||||
while offset <= max_scroll {
|
||||
buf.reset();
|
||||
render_scrolled_entries_with_scratch(
|
||||
&mut buf,
|
||||
viewport,
|
||||
&entry_refs,
|
||||
offset,
|
||||
None,
|
||||
&theme,
|
||||
&appearance,
|
||||
&layouts,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
offset += SCROLL_STEP as usize;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Scroll through a large scrollback the way production does: per step,
|
||||
/// locate the paint window via `ScrollbackState::paint_window` (partition
|
||||
/// point over the cached virtual-y prefix sum) and render only that slice
|
||||
/// with `content_y0`/`entry_index_base` — mirroring
|
||||
/// `ScrollbackPane::render_content`. `full_scroll` above measures the
|
||||
/// renderer's full-list walk; this measures the shipped windowed path, so
|
||||
/// regressions in the window computation show up here.
|
||||
fn bench_windowed_scroll(c: &mut Criterion) {
|
||||
let (state, _think_id) = build_reveal_state();
|
||||
let viewport = Rect::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
let theme = Theme::current();
|
||||
let n = state.len();
|
||||
let virtual_y = state.get_cached_virtual_y().expect("layout cache");
|
||||
let layouts = state.get_cached_entry_layouts().expect("layout cache");
|
||||
let total: usize =
|
||||
virtual_y[n - 1] + layouts[n - 1].height as usize + layouts[n - 1].gap_after as usize;
|
||||
let max_scroll = total.saturating_sub(VIEWPORT_HEIGHT as usize);
|
||||
|
||||
let mut g = c.benchmark_group("render");
|
||||
// Each iteration pages through the whole corpus; cap samples to keep the
|
||||
// run short (same treatment as reveal/navigate_rebuild).
|
||||
g.sample_size(10).warm_up_time(Duration::from_millis(500));
|
||||
g.bench_function("windowed_scroll", |b| {
|
||||
let mut buf = Buffer::empty(viewport);
|
||||
b.iter(|| {
|
||||
let mut offset = 0usize;
|
||||
while offset <= max_scroll {
|
||||
buf.reset();
|
||||
let (paint_range, content_y0) =
|
||||
state.paint_window(0..n, offset, VIEWPORT_HEIGHT as usize);
|
||||
let window = state.entries_in_range(paint_range.clone());
|
||||
render_scrolled_entries_with_scratch(
|
||||
&mut buf,
|
||||
viewport,
|
||||
&window,
|
||||
offset,
|
||||
None,
|
||||
&theme,
|
||||
state.appearance(),
|
||||
&layouts[paint_range.clone()],
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
content_y0,
|
||||
paint_range.start,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
);
|
||||
offset += SCROLL_STEP as usize;
|
||||
}
|
||||
});
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
// ─── Reveal (scrollback-search n/N navigation) ─────────────────────
|
||||
|
||||
/// One paragraph of lorem-style body per entry (~1.7 KB), so the
|
||||
/// `REVEAL_ENTRIES`-entry corpus is on the order of the motivating session's
|
||||
/// searchable text (a few MB; the exact size is logged).
|
||||
fn reveal_body(i: usize) -> String {
|
||||
let lorem = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do \
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua ut \
|
||||
enim ad minim veniam quis nostrud exercitation ullamco laboris ";
|
||||
format!(
|
||||
"Entry {i}: the quick brown fox jumps over the lazy dog. {} done.",
|
||||
lorem.repeat(9)
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a large scrollback approximating a long search session, returning it
|
||||
/// with the `EntryId` of a thinking block placed in the middle (the rebuild
|
||||
/// bench dirties that entry's height to force reveal's rebuild branch).
|
||||
fn build_reveal_state() -> (ScrollbackState, EntryId) {
|
||||
let mut state = ScrollbackState::new();
|
||||
let mut total_bytes = 0usize;
|
||||
let middle = REVEAL_ENTRIES / 2;
|
||||
let mut think_id = None;
|
||||
for i in 0..REVEAL_ENTRIES {
|
||||
if i == middle {
|
||||
let body = "reasoning about the matched entry and the nearby context";
|
||||
total_bytes += body.len();
|
||||
think_id = Some(state.push_block(RenderBlock::thinking(body)));
|
||||
} else {
|
||||
let body = reveal_body(i);
|
||||
total_bytes += body.len();
|
||||
state.push_block(RenderBlock::user_prompt(body));
|
||||
}
|
||||
}
|
||||
// ~1-2 KB prompts are foldable, so they default to Collapsed; expand them so
|
||||
// a reveal over an already-visible match doesn't change display state — the
|
||||
// steady n/N case the skip path optimizes. (Setup only; not measured.)
|
||||
state.expand_all();
|
||||
// Settle the layout cache and clear dirty heights so the measured reveals
|
||||
// start from a clean cache.
|
||||
state.prepare_layout(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
eprintln!(
|
||||
"reveal corpus: {REVEAL_ENTRIES} entries, ~{:.1} MB text",
|
||||
total_bytes as f64 / (1024.0 * 1024.0)
|
||||
);
|
||||
(state, think_id.expect("thinking block pushed"))
|
||||
}
|
||||
|
||||
/// New per-`n` cost: revealing an already-visible match reuses the settled
|
||||
/// cache and only refreshes the cheap total-height sum — no O(history) rebuild.
|
||||
fn bench_reveal_skip(c: &mut Criterion) {
|
||||
let (mut state, _think_id) = build_reveal_state();
|
||||
c.bench_function("reveal/navigate_skip", |b| {
|
||||
let mut i = 0usize;
|
||||
b.iter(|| {
|
||||
state.reveal_entry_line(i % REVEAL_ENTRIES, 0);
|
||||
i += 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Old per-`n` cost: the pre-fix reveal ran a full layout rebuild unconditionally.
|
||||
/// The O(1) `push_chunk_to_thinking` dirties one entry's height, negligible next
|
||||
/// to the O(N) `rebuild_layout` it forces reveal to take on every call.
|
||||
fn bench_reveal_rebuild(c: &mut Criterion) {
|
||||
let (mut state, think_id) = build_reveal_state();
|
||||
let mut g = c.benchmark_group("reveal");
|
||||
// Each iteration rebuilds the whole layout (O(N)); cap samples so the run
|
||||
// doesn't take minutes.
|
||||
g.sample_size(20).warm_up_time(Duration::from_millis(500));
|
||||
g.bench_function("navigate_rebuild", |b| {
|
||||
let mut i = 0usize;
|
||||
b.iter(|| {
|
||||
state.push_chunk_to_thinking(think_id, "x");
|
||||
state.reveal_entry_line(i % REVEAL_ENTRIES, 0);
|
||||
i += 1;
|
||||
});
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_single_frame,
|
||||
bench_full_scroll,
|
||||
bench_windowed_scroll,
|
||||
bench_reveal_skip,
|
||||
bench_reveal_rebuild
|
||||
);
|
||||
criterion_main!(benches);
|
||||
Loading…
Reference in a new issue