Synced from monorepo Changes: - Temporarily disable session share link creation in the TUI - Do not approve plan on empty Enter from the revise prompt - Expose chat product Skills via ACP available_commands_update - Return immediately from a blocking wait on an already-completed ACP task - Split headless pager module for clearer structure - Stop git worktree prune from removing user registrations on resume - Use compaction sampler tokenizer for item token counts - Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE - Cancel all session subagents when the user stops - Let the session persistence actor exit when its session ends - Make fullscreen terminal resize much cheaper on long sessions - Report honestly from kill_task when an ACP task does not exist - Hide /usage for external-auth deployments - Forward the history-load trailer’s computer_reason to the client - Remove ineffective no-op tool reminder - Declare slash-command screen-mode support in one place - Keep settings enum picker on the committed value until Enter - Reap a PTY’s full process tree - Stream tool calls from headless mode over ACP - Bridge gateway task lifecycle to ACP for chat session background tasks - Don’t warn about truncated history on a suppressed replay - Fit full-replace summarizer input and recover on context-length errors - Stop dropping agents over an unrecognized frontmatter color - Add /undo as a slash alias for /rewind - Harden sleep/wake token-refresh paths against forced re-login - Add session/list ACP method - Give each sampling backend its own conversion module - Treat an unenrolled child process as a lint error - Suppress the cancelled marker on send-now wake turns - Stop tearing down Roslyn on every edit, and read C# diagnostics Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
195 lines
6.6 KiB
Rust
195 lines
6.6 KiB
Rust
//! Criterion benchmarks for the terminal-RESIZE path.
|
|
//!
|
|
//! Dragging a terminal edge sends a stream of `Event::Resize`, and the
|
|
//! reported symptom is that the drag gets laggier the longer a session runs.
|
|
//!
|
|
//! Regressions these guard against, each of which was measured on a real
|
|
//! session and removed:
|
|
//! - re-deriving an entry's source text per width instead of reusing its
|
|
//! cached line-width profile (makes the estimate pass O(conversation bytes)),
|
|
//! - building or cloning an `AppearanceConfig` per entry,
|
|
//! - running `warm_measure_pages_above` on every resize instead of once the
|
|
//! width settles.
|
|
|
|
use std::time::Duration;
|
|
|
|
use criterion::{Criterion, criterion_group, criterion_main};
|
|
|
|
use xai_grok_pager::scrollback::{RenderBlock, ScrollbackState};
|
|
|
|
/// Roughly a VS Code editor pane maximized on a laptop screen.
|
|
const VIEWPORT_WIDTH: u16 = 120;
|
|
const VIEWPORT_HEIGHT: u16 = 50;
|
|
|
|
/// ~3,200 entries / ~5 MB of text — the scale of a multi-hour session.
|
|
const TURNS: usize = 400;
|
|
|
|
fn agent_markdown(i: usize) -> String {
|
|
format!(
|
|
"Here is what I found for step {i}.\n\n\
|
|
The `ScrollbackState` keeps a layout cache keyed by width, so a resize \
|
|
invalidates every entry. That matters because the estimate pass has to \
|
|
re-derive each block's source text before it can compute a height.\n\n\
|
|
- first observation about entry {i}\n\
|
|
- second observation, slightly longer, about how the wrap cache is keyed \
|
|
on `(width, generation, theme)` and therefore misses after a drag\n\
|
|
- third observation\n\n\
|
|
```rust\n\
|
|
fn rebuild_layout_cache(&mut self, width: u16) {{\n\
|
|
\x20 for entry in self.entries.values() {{\n\
|
|
\x20 let renderer = EntryRenderer::new(entry, &theme)\n\
|
|
\x20 .with_appearance(self.appearance.clone());\n\
|
|
\x20 let height = renderer.estimate_height(width);\n\
|
|
\x20 }}\n\
|
|
}}\n\
|
|
```\n\n\
|
|
In short: the {i}th response re-wraps on every width change, and the \
|
|
syntax highlighting of the fence above is recomputed with it. \
|
|
{}\n",
|
|
"Additional prose so the message spans several wrapped rows. ".repeat(6)
|
|
)
|
|
}
|
|
|
|
fn thinking_text(i: usize) -> String {
|
|
format!(
|
|
"Considering approach {i}. {}",
|
|
"The user asked about resize latency, so I should look at the layout cache. ".repeat(8)
|
|
)
|
|
}
|
|
|
|
fn edit_texts(i: usize) -> (String, String) {
|
|
let old = format!(
|
|
"fn handler_{i}(req: Request) -> Response {{\n\
|
|
\x20 let body = req.body();\n\
|
|
\x20 let parsed = serde_json::from_slice(body)?;\n\
|
|
\x20 Response::ok(parsed)\n\
|
|
}}\n"
|
|
);
|
|
let new = format!(
|
|
"fn handler_{i}(req: Request) -> Response {{\n\
|
|
\x20 let body = req.body();\n\
|
|
\x20 let parsed: Payload = serde_json::from_slice(body)\n\
|
|
\x20 .map_err(|e| Error::BadRequest(e.to_string()))?;\n\
|
|
\x20 Response::ok(parsed)\n\
|
|
}}\n"
|
|
);
|
|
(old, new)
|
|
}
|
|
|
|
fn bash_output(i: usize) -> String {
|
|
(0..40)
|
|
.map(|l| format!("crates/codegen/xai-grok-pager/src/file_{i}_{l}.rs:{l}: match found"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
/// Block mix and proportions follow what a real coding session produces.
|
|
fn build_session() -> (ScrollbackState, usize) {
|
|
let mut state = ScrollbackState::new();
|
|
let mut bytes = 0usize;
|
|
let mut push = |state: &mut ScrollbackState, block: RenderBlock, n: usize| {
|
|
bytes += n;
|
|
state.push_block(block);
|
|
};
|
|
for i in 0..TURNS {
|
|
let p = format!("please investigate issue {i} and report back with a plan");
|
|
push(&mut state, RenderBlock::user_prompt(p.clone()), p.len());
|
|
let t = thinking_text(i);
|
|
push(&mut state, RenderBlock::thinking(t.clone()), t.len());
|
|
let out = bash_output(i);
|
|
push(
|
|
&mut state,
|
|
RenderBlock::execute_with_output(
|
|
format!("rg -n 'pattern{i}' crates/"),
|
|
out.clone(),
|
|
None::<String>,
|
|
),
|
|
out.len(),
|
|
);
|
|
push(
|
|
&mut state,
|
|
RenderBlock::read(
|
|
format!("crates/codegen/xai-grok-pager/src/mod_{i}.rs"),
|
|
None,
|
|
),
|
|
64,
|
|
);
|
|
let (old, new) = edit_texts(i);
|
|
push(
|
|
&mut state,
|
|
RenderBlock::edit_with_hunks(
|
|
format!("crates/codegen/xai-grok-pager/src/mod_{i}.rs"),
|
|
xai_grok_pager::diff::diff_hunks_from_strings(&old, &new, 1),
|
|
),
|
|
old.len() + new.len(),
|
|
);
|
|
push(
|
|
&mut state,
|
|
RenderBlock::search(format!("fn handler_{i}"), 12, Vec::new()),
|
|
48,
|
|
);
|
|
let md = agent_markdown(i);
|
|
push(&mut state, RenderBlock::agent_message(md.clone()), md.len());
|
|
let t2 = thinking_text(i + 1);
|
|
push(&mut state, RenderBlock::thinking(t2.clone()), t2.len());
|
|
}
|
|
state.prepare_layout(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
|
(state, bytes)
|
|
}
|
|
|
|
fn bench_resize_step(c: &mut Criterion) {
|
|
let (mut state, bytes) = build_session();
|
|
eprintln!(
|
|
"resize corpus: {} entries, ~{:.1} MB text",
|
|
state.len(),
|
|
bytes as f64 / (1024.0 * 1024.0)
|
|
);
|
|
let mut g = c.benchmark_group("resize");
|
|
g.sample_size(20).warm_up_time(Duration::from_millis(500));
|
|
g.bench_function("width_step", |b| {
|
|
let mut w = VIEWPORT_WIDTH;
|
|
b.iter(|| {
|
|
w = if w == VIEWPORT_WIDTH {
|
|
VIEWPORT_WIDTH - 1
|
|
} else {
|
|
VIEWPORT_WIDTH
|
|
};
|
|
state.prepare_layout(w, VIEWPORT_HEIGHT);
|
|
});
|
|
});
|
|
g.finish();
|
|
}
|
|
|
|
fn bench_resize_drag(c: &mut Criterion) {
|
|
let (mut state, _) = build_session();
|
|
let mut g = c.benchmark_group("resize");
|
|
g.sample_size(10).warm_up_time(Duration::from_millis(500));
|
|
g.bench_function("drag_20_steps", |b| {
|
|
b.iter(|| {
|
|
for step in 0..20u16 {
|
|
state.prepare_layout(VIEWPORT_WIDTH - step, VIEWPORT_HEIGHT);
|
|
}
|
|
state.prepare_layout(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
|
});
|
|
});
|
|
g.finish();
|
|
}
|
|
|
|
fn bench_resize_noop(c: &mut Criterion) {
|
|
let (mut state, _) = build_session();
|
|
let mut g = c.benchmark_group("resize");
|
|
g.bench_function("same_width_noop", |b| {
|
|
b.iter(|| {
|
|
state.prepare_layout(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
|
});
|
|
});
|
|
g.finish();
|
|
}
|
|
|
|
criterion_group!(
|
|
benches,
|
|
bench_resize_step,
|
|
bench_resize_drag,
|
|
bench_resize_noop
|
|
);
|
|
criterion_main!(benches);
|