Skip to content

fix(session-ui): skip redundant diffs when grouping patch files - #46768

Merged
Hona merged 2 commits into
anomalyco:v2from
Hona:patch-group-work
Sep 2, 2026
Merged

fix(session-ui): skip redundant diffs when grouping patch files#46768
Hona merged 2 commits into
anomalyco:v2from
Hona:patch-group-work

Conversation

@Hona

@Hona Hona commented Sep 2, 2026

Copy link
Copy Markdown
Member

patchFileGroups runs for every edit/patch tool group in the transcript, including collapsed headers. It parsed each file's patch eagerly, then for complete/chained groups ran a second diffLines(before, after) only to get +/- counts, then normalized the composed contents a third time. toolErrorSubtitle also re-ran the whole grouping just to count paths.

  • Make ApplyPatchFile.view a lazy memoized getter so per-file normalization only runs for groups that show individual views (partial/disconnected patches).
  • Take chained group counts from the parsed hunks (hunk.additionLines/hunk.deletionLines) instead of a separate diffLines pass.
  • Reuse the cached original view for one-file groups instead of normalizing the same contents again.
  • Count distinct changed paths directly in the patch error subtitle.
// before: normalize every file, diff again for counts, normalize the result again
const counts = diffLines(before, after).reduce(...)
views: [normalize({ file: path, before, after, status, ...counts })]

// after: one view per group; counts come from its parsed hunks
const view = files.length === 1 ? first.view : normalize({ file: path, before, after, status, additions: 0, deletions: 0 })
const counts = view.fileDiff.hunks.reduce((r, h) => ({ additions: r.additions + h.additionLines, deletions: r.deletions + h.deletionLines }), { additions: 0, deletions: 0 })
views: [{ ...view, ...counts }]
flowchart LR
  subgraph Before
    a1[patchFile x N<br/>normalize each] --> a2[group by path] --> a3[diffLines before/after] --> a4[normalize composed] --> a5[views + counts]
  end
  subgraph After
    b1[patchFile x N<br/>view deferred] --> b2[group by path] --> b3[normalize once<br/>or reuse cached view] --> b4[counts from hunks] --> b5[views + counts]
  end
Loading

Benchmark

packages/app/e2e/performance/patch-groups mounts the production CurrentFileToolGroup / ToolDisplay / File components in a production Vite build (Chromium via Playwright, 1366x768, fresh context per sample, fonts-ready gate). Each sample: cold collapsed mount through layout, unmount, warm remount, then expand edit.ts until the file renderer's onRendered fires. Payloads are real Core tool sources with deterministic identifier renames.

Case Shape Payload
complete 1 edit tool, full-context patch 13.0 KB
partial 1 edit tool, 3-line context patch 4.1 KB
chained 2 edit tools on the same file 26.4 KB
multi 4 edit tools, 4 files 57.8 KB
direct ToolDisplay patch tool, 1 file 13.0 KB

Baseline = 335e4ca56f production modules (same harness), candidate = this branch. n = 20 serial samples per run, order B1 → C1 → B2 → C2, values are median / p95 ms. Machine drift is visible across runs (compare partial, which this change does not touch), so read adjacent columns.

Warm remount (collapsed)

Case Baseline #1 Candidate #1 Baseline #2 Candidate #2
complete 13.0 / 20.1 10.4 / 13.7 10.9 / 15.2 7.8 / 9.7
partial 8.9 / 13.1 9.7 / 13.4 8.1 / 11.5 7.2 / 9.6
chained 13.9 / 20.5 14.0 / 17.8 11.6 / 15.5 9.5 / 12.8
multi 29.3 / 46.4 20.4 / 27.9 27.3 / 34.2 15.5 / 19.4
direct 12.4 / 18.6 9.7 / 14.3 12.1 / 19.5 7.0 / 8.9

Cold mount (collapsed)

Case Baseline #1 Candidate #1 Baseline #2 Candidate #2
complete 51.2 / 63.3 47.6 / 64.1 44.4 / 53.7 40.1 / 46.4
partial 41.6 / 54.1 43.2 / 55.4 36.5 / 53.4 34.5 / 49.4
chained 49.1 / 69.5 49.3 / 56.0 47.9 / 58.9 39.3 / 45.2
multi 72.1 / 106.8 67.5 / 80.0 69.0 / 90.1 55.5 / 66.3
direct 49.0 / 76.7 47.4 / 54.8 46.2 / 62.0 36.6 / 47.5

patchFileGroups alone (warm, collapsed)

Case Baseline #1 Candidate #1 Baseline #2 Candidate #2
complete 2.2 / 3.1 0.5 / 0.7 2.0 / 2.8 0.4 / 0.8
partial 0.3 / 0.4 0.4 / 0.8 0.3 / 0.4 0.3 / 0.5
chained 4.0 / 5.5 3.6 / 5.1 3.8 / 5.3 2.9 / 4.0
multi 8.6 / 13.5 1.1 / 1.3 7.8 / 11.6 0.7 / 1.1
direct 2.4 / 3.0 0.4 / 0.6 2.1 / 2.8 0.3 / 0.6

Mechanism (separate build-only instrumented bundle, calls during cold mount / warm remount)

Case diffLines normalize patchFileGroups
complete 3/2 → 1/0 2/2 → 1/1 1/1 → 1/1
partial 0/0 → 0/0 1/1 → 1/1 1/1 → 1/1
chained 4/2 → 1/1 3/3 → 1/1 1/1 → 1/1
multi 12/8 → 4/0 8/8 → 4/4 1/1 → 1/1
direct 5/4 → 1/0 4/4 → 1/1 2/2 → 1/1

Expansion (click → file renderer onRendered) is dominated by the file renderer and highlighting, which this PR does not touch. Run-to-run variance of the same bundle (chained: C1 271.9 vs C2 223.9; baselines 240.5 / 247.2) exceeds any between-build difference, so no change is claimed there.

Case Baseline #1 Candidate #1 Baseline #2 Candidate #2
complete 230.7 / 290.0 245.4 / 313.9 213.3 / 346.6 204.1 / 243.0
partial 204.2 / 250.1 214.5 / 274.4 206.3 / 253.6 179.1 / 202.6
chained 240.5 / 329.4 271.9 / 323.4 247.2 / 292.3 223.9 / 263.8
multi 232.3 / 302.0 240.1 / 301.7 217.7 / 292.3 208.6 / 238.6
direct 246.7 / 344.1 255.1 / 327.0 242.0 / 262.2 221.5 / 260.6

Rendering parity

Collapsed screenshots (including +/- counts) are byte-identical between builds. Expanded four-file case, before / after (only the fixture's own timing readout differs):

Before After

Limitations: browser component benchmark on one Windows machine; not a desktop process memory measurement. The harness is manual (not part of normal test discovery) and enforces completion, not thresholds.

Hona added 2 commits September 2, 2026 18:06
Defer per-file view normalization until a group needs it, derive chained group counts from the parsed hunks instead of a second line diff, reuse the cached original view for one-file groups, and count distinct changed paths directly for the patch error subtitle.
@Hona
Hona marked this pull request as ready for review September 2, 2026 09:23
@Hona
Hona requested a review from Brendonovich as a code owner September 2, 2026 09:23
Copilot AI lite review requested due to automatic review settings September 2, 2026 09:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new benchmark static server has a confirmed path-escape issue due to path.join with an absolute pathname, and there is also an ineffective test assertion that should be fixed/removed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR optimizes patch grouping/rendering in session-ui to avoid redundant normalization and diff passes when collapsing or chaining patch tool outputs, and adds a manual browser benchmark harness to measure the impact.

Changes:

  • Make per-file ApplyPatchFile.view lazy/memoized and reuse normalized views for single-file chained groups.
  • Compute +/- counts for chained complete groups from parsed hunk metadata rather than re-running diffLines.
  • Avoid re-running grouping logic in the patch tool error subtitle by counting distinct changed paths directly.
  • Add a dedicated Playwright/Vite-based manual benchmark fixture under packages/app/e2e/performance/patch-groups.
File summaries
File Description
packages/session-ui/src/tools/tool-renderer.tsx Avoids calling patchFileGroups just to compute a patch file-count subtitle.
packages/session-ui/src/components/apply-patch-file.ts Lazies view normalization and derives chained counts from parsed hunks to reduce redundant work.
packages/session-ui/src/components/apply-patch-file.test.ts Adds coverage for chained/complete patch composition and count derivation behavior.
packages/app/e2e/performance/patch-groups/vite.config.ts Adds a benchmark build config with optional instrumentation/baseline loading.
packages/app/e2e/performance/patch-groups/serve.ts Adds a small Bun static server for the benchmark build output.
packages/app/e2e/performance/patch-groups/README.md Documents how to run the manual benchmark and interpret outputs.
packages/app/e2e/performance/patch-groups/playwright.config.ts Adds a dedicated Playwright config for the benchmark runner.
packages/app/e2e/performance/patch-groups/patch-groups.bench.ts Implements the benchmark scenarios and measurements collection.
packages/app/e2e/performance/patch-groups/index.html Adds the benchmark HTML entrypoint.
packages/app/e2e/performance/patch-groups/fixture.tsx Adds the benchmark fixture mounting production components and exposing grouping metrics.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +8 to +11
async fetch(request) {
const pathname = new URL(request.url).pathname
const file = Bun.file(path.join(directory, pathname === "/" ? "index.html" : pathname))
return (await file.exists()) ? new Response(file) : new Response("Not found", { status: 404 })
Comment on lines 34 to +38
additions: value.additions,
deletions: value.deletions,
view: normalize(value),
get view() {
return (view ??= normalize(value))
},
expect(groups[0]!.views).toHaveLength(1)
expect(text(groups[0]!.views[0]!, "deletions")).toBe(before)
expect(text(groups[0]!.views[0]!, "additions")).toBe(after)
expect(groups[0]!.views).toBe(groups[0]!.views)
@Hona
Hona merged commit 651cdd2 into anomalyco:v2 Sep 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants