Skip to content

perf(diff): add timing logs and bail out of LCS on long lines to prevent freezes - #599

Merged
matt2e merged 2 commits into
mainfrom
slow-diffs
Apr 10, 2026
Merged

perf(diff): add timing logs and bail out of LCS on long lines to prevent freezes#599
matt2e merged 2 commits into
mainfrom
slow-diffs

Conversation

@matt2e

@matt2e matt2e commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add info-level timing logs across the entire diff pipeline (Rust backend, Svelte frontend state, syntax highlighting, inline diff computation) to make it easy to diagnose slow diffs
  • Bail out of LCS computation when line length product exceeds 1M characters, preventing UI freezes on files with very long lines (e.g. minified JS/CSS)

Test plan

  • Verify timing logs appear in console/log output when viewing diffs
  • Open a diff with very long lines (e.g. minified file) and confirm it no longer freezes
  • Normal diffs still compute inline highlights correctly

🤖 Generated with Claude Code

matt2e and others added 2 commits April 10, 2026 12:22
Add performance instrumentation to debug slow/freezing diffs when
viewing branches with long lines. Logs are added at every stage:

- Frontend: getDiffFiles/getFileDiff call timing, file/line stats,
  max line lengths, syntax highlighting duration
- Frontend: inline diff computation timing, large line pair warnings
  for O(m*n) LCS operations (similarity, computeCharHighlights)
- Rust backend: get_diff_files/get_file_diff timing with line counts
  and max line lengths per file
- Rust git-diff crate: file loading, hunk computation, and alignment
  timing breakdown
- Rust diff_cache: remote collection script exec, JSON parsing, and
  processing timing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip O(m*n) LCS operations in similarity() and computeCharHighlights()
when the product of input lengths exceeds 1,000,000. For similarity,
returns 0 (lines treated as unrelated). For char highlights, marks the
entire line as changed. Uses the same threshold that was previously
only used for logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt2e
matt2e requested review from baxen and wesbillman as code owners April 10, 2026 03:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a798e6b57e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

afterLines: string[],
): LineDiffResult {
const t0 = performance.now();
const beforeMaxLen = Math.max(0, ...beforeLines.map(l => l.length));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace spread-based max scan in line diff

Using Math.max(0, ...beforeLines.map(...)) expands one argument per line, which throws a RangeError in V8 once the file has a large number of lines (roughly 100k+). Because this runs at the start of computeLineDiff, a large but otherwise valid text diff can fail before any fallback logic, so the inline diff view breaks for exactly the large inputs this change is trying to protect.

Useful? React with 👍 / 👎.

const diff = await commands.getFileDiff(state.branchId, state.commitSha, state.scope, path);
const beforeLineCount = diff.before?.content?.type === 'Text' ? diff.before.content.lines.length : 0;
const afterLineCount = diff.after?.content?.type === 'Text' ? diff.after.content.lines.length : 0;
const beforeMaxLen = diff.before?.content?.type === 'Text' ? Math.max(0, ...diff.before.content.lines.map((l: string) => l.length)) : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid spreading all file lines in getFileDiff logging

The new logging computes max line length via Math.max(0, ...lines.map(...)), which can also raise RangeError: Maximum call stack size exceeded on large files due to argument explosion. In this function the exception is caught and treated as a diff load failure, so users can get null diffs for large files even when backend diff generation succeeded.

Useful? React with 👍 / 👎.

@matt2e
matt2e merged commit 8ed4f90 into main Apr 10, 2026
7 checks passed
@matt2e
matt2e deleted the slow-diffs branch April 10, 2026 03:52
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.

1 participant