perf(tui): hand tool-output rows back as a shared handle (#6213 T1) - #6265
Conversation
The rows cache exists so a finalized tool cell is not re-wrapped on every frame, but a hit still deep-copied every `String` and styled span in the payload — the one thing the cache was supposed to make free. The rows now live behind an `Arc` and a hit hands back a refcount bump. The caller only iterates or indexes the rows, so it derefs unchanged: `all_lines.iter()`, `&all_lines[idx]`, and `&all_lines` into `selected_output_indices` all still resolve. Part of #6213. T1 is not finished: the other half is keying the cache by tool-result id so the per-frame `hash_str` over the whole output disappears, and that needs the id plumbed down to the render call. Verification: cargo check -p codewhale-tui --all-targets --all-features --locked (clean) test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 12799 filtered out (tui::output_rows_cache) test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 12800 filtered out (tui::history::tool_output) Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @Hmbown's task in 1m 57s —— View job Review: PR #6265 — hand tool-output rows back as a shared handleTodo list
SummaryScoped, well-documented change. I verified the PR's central claim myself: Findings1. Correctness — none found. The 2. Provider/model/route safety — not applicable; this diff touches only the render-side rows cache. 3. Reuse/simplification (minor) — 4. Tests — existing tests ( 5. Security — not applicable. Notes on the PR descriptionAccurate and appropriately scoped — it correctly states what's in vs. out of T1 (id-keying is explicitly deferred, not silently dropped), and the "no benchmark, win argued from the removed allocation" framing matches AGENTS.md's evidence policy (claims should match the evidence actually gathered). No blocking issues. The |
There was a problem hiding this comment.
Codewhale review
Complete review coverage: 1/1 passes, 1 file patches, sha256:00519d3473a66731069bf2b4c8f45ef8d2ccb4fd7846a7d7c2f0fe15e06826b1.
Pass 1: Single-file change in crates/tui/src/tui/output_rows_cache.rs switching CacheEntry::rows and the public get_or_compute_rows* returns from Vec<OutputRow> to Arc<Vec<OutputRow>>. By inspection of the supplied lines the change is semantics-preserving: the miss path constructs one Arc, stores one clone and returns the original (previously one deep clone() was stored), and the hit path returns Arc::clone(&entry.rows) instead of entry.rows.clone(). No mutation of the cached rows exists on either path (get_or_compute_indices only mutates the separate selected_by_limit map), so shared immutable aliasing introduces no behavioral change. No defect was demonstrated in the changed lines.
Assessment
Pass 1: I inspected every changed line and the surrounding unchanged control flow that is supplied (lines 94–174 for OutputRowsCacheInner, 176–236 for the thread-local wrapper and public entry points). The refactor is a pure ownership/aliasing change: (1) the hit path Arc::clone(&entry.rows) is a refcount bump equivalent to the old deep clone for all read-only uses; (2) the miss path let rows = Arc::new(compute()); let entry = CacheEntry::new(Arc::clone(&rows)); ... rows stores and returns handles to the same single allocation, exactly as the old rows.clone() + return-original did (in fact it removes the only remaining copy); (3) nothing in the module mutates CacheEntry::rows after insertion — get_or_compute_indices touches only selected_by_limit — so Arc's immutable sharing is safe and the #[derive(Clone)] on CacheEntry becoming shallow does not change any visible behavior (no clone site of CacheEntry exists in the supplied source). Arc rather than Rc is slightly redundant given the cache is thread-local, but the atomic refcount is negligible and the return type is public, so I do not treat it as a defect. Open items I could not verify here (no build, no tests, and the files were not supplied): the production consumer crates/tui/src/tui/history/tool_output.rs and the test modules of both files are outside the provided context, and the PR's cargo check/test results are untrusted, unreproduced claims. A return-type change from an owned Vec to Arc<Vec<...>> would fail to compile for any consumer that mutates the rows in place or writes assert_eq!(rows, vec![...]) (no PartialEq<Vec<T>> for Arc<Vec<T>>); auto-deref keeps rows.iter(), &rows[idx], and &rows → &[OutputRow] coercion working, so read-only callers are unaffected. That residual risk is a compile-time one in code I was not given, not a defect I can assert from the diff, and should be confirmed by the reported cargo check --all-targets on the head commit.
Advisory review by Codewhale (codewhale review --pr 6265 --post, head 6447e6342ba2b1617335cd6182032a02652a8507). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
Summary
Part of #6213 (item T1). The points below are what changed and what did not.
No-Issue: partial work on #6213 — T1's shared-handle half only; the id-keying half is not implemented, so that issue must stay open.
What changed
The tool-output rows cache exists so a finalized tool cell is not re-wrapped on every frame. A cache hit still deep-copied every
Stringand styled span in the payload — the one thing the cache was supposed to make free, on the 120 FPS path the module's own docs describe.The rows now live behind an
Arc<Vec<OutputRow>>, and a hit hands back a refcount bump instead of a copy. The cache still stores one owned copy per(content_hash, width); only the hand-off changed.The single production caller derefs unchanged:
all_lines.iter(),&all_lines[idx], and&all_linesintoselected_output_indicesall still resolve throughArc'sDereftoVecto slice, socrates/tui/src/tui/history/tool_output.rsneeded no edit at all. The existing cache tests also pass untouched, includingcache_hit_returns_cached_rows, which asserts the second call does not recompute — the property this change is about.What deliberately did not change
T1's second half — keying the cache by tool-result id so the per-frame
hash_str(output)over the whole output disappears — is not here. It needs the tool-result id plumbed down to the render call, and doing it half-way would leave two keying schemes in the same cache. I am not claiming T1 is done.Verification
Local only, on macOS/aarch64 at
ecbec3e44. No hosted CI claim. No benchmark was run, so the size of the win is argued from the removed allocation, not measured.