Skip to content

fix(worddiff): guard intra-line diff cost by LCS table size, not line length - #328

Merged
umputun merged 3 commits into
masterfrom
worddiff-cost-budget
Aug 20, 2026
Merged

fix(worddiff): guard intra-line diff cost by LCS table size, not line length#328
umputun merged 3 commits into
masterfrom
worddiff-cost-budget

Conversation

@umputun

@umputun umputun commented Aug 19, 2026

Copy link
Copy Markdown
Owner

word-diff skipped any add/remove pair where either line was over 500 bytes, bailing out before tokenization. On markdown where a paragraph is one physical line that is most of the interesting pairs, so the status bar showed ± while nothing highlighted, which looks exactly like the feature being broken.

Byte length was never the cost driver. The LCS table is, and it is quadratic in tokens, so the two are only loosely related: 500 repeated letters tokenize to a single token, 500 bytes of minified JSON to nearly 300. The old cap admitted at most 15,625 cells.

Measured here, prose input:

pair tokens cells DP alloc time
500 x 500 B 125 x 125 15k 0.1 MB 0.1 ms
3602 x 5525 B (the reported pair) 937 x 1419 1.3M 12 MB 5.5 ms
8 x 8 KB 2073 x 2073 4.3M 38 MB 17 ms
20 x 20 KB 5105 x 5105 26M 209 MB 91 ms
50 x 50 KB 12765 x 12765 163M 1.36 GB 566 ms

So the guard now budgets the table instead of the line: maxDiffCells = 4_000_000 (~32 MB, ~17 ms per pair), with the byte cap kept only as a cheap pre-filter at 20000 bytes so minified input never reaches the tokenizer. ComputeIntraRanges keeps its signature, nothing is configurable, and no doc or plugin surface mentions the cap.

recomputeIntraRanges runs once per file load and once per W press, not per keystroke, so this is not on the render path.

Tests pin each guard on its own: deleting the byte pre-filter fails one subtest, deleting the cell budget fails another, and TestComputeIntraRanges_LongProsePairIsDiffed fails against the old 500-byte cap. The previous fixture for the byte gate used two identical prose lines, which three separate branches rejected independently, so it would have passed with the gate deleted.

Not done here, both worth a separate decision:

  • --word-diff-max-line as asked in the issue. Precedent exists (--compact-context, --page-overlap), but the knob is in bytes while the cost is quadratic in tokens, so there is no value a user could pick sensibly.
  • a hint when a pair is skipped for cost. Needs ComputeIntraRanges to report why it returned nil, which changes the wordDiffer interface.

One thing this widens without creating: recomputeIntraRanges has no budget across pairs, so a file with many long changed lines pays the per-pair cost repeatedly on the update goroutine. Reaching a visible stall needs roughly half a megabyte of changed content in one file with word-diff on.

Related to #323

… length

The 500-byte per-line cap skipped word-diff on any pair where either side was longer, so a
markdown paragraph living on one physical line silently got no highlighting while the status
bar still showed the mode as on. Byte length is a poor proxy for the cost it was guarding:
500 repeated letters tokenize to a single token, 500 bytes of minified JSON to nearly 300.

Gate on the LCS table size instead — minus tokens times plus tokens, budget 4M cells, about
32MB and 17ms for one pair, paid once per file load or word-diff toggle. The byte cap stays
only as a cheap pre-filter at 20000 bytes so minified input never reaches the tokenizer.

Related to #323
…s godoc

The byte pre-filter subtest used two identical prose lines, which the cell budget and the
identical-lines branch both reject on their own — it passed with the pre-filter deleted. It now
uses a single-token pair that clears every other gate, with a control pair sized to fit the
pre-filter, so the assertion can only be answered by the byte gate.

The maxDiffCells godoc said the budget was paid once per file load. It bounds one pair;
recomputeIntraRanges calls ComputeIntraRanges once per paired remove/add line, so a diff holding
many long pairs pays it for each.

Related to #323
Copilot AI lite review requested due to automatic review settings August 19, 2026 17:54

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

TestChangedRanges_SkipsVeryLongLines still carried the pre-change rationale, a near-copy of the
godoc this branch replaced, so the package held two contradictory reasons for one constant. The
byte cap is a tokenizer pre-filter; maxDiffCells guards the LCS cost. The fixture below the
comment is its own counter-example: 20001 repeated letters is a single token.

Also correct prose's godoc, which returns exactly nbytes rather than roughly.

Related to #323
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploying revdiff with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8264806
Status: ✅  Deploy successful!
Preview URL: https://f630873e.revdiff.pages.dev
Branch Preview URL: https://worddiff-cost-budget.revdiff.pages.dev

View logs

@umputun
umputun merged commit 09e5b07 into master Aug 20, 2026
5 checks passed
@umputun
umputun deleted the worddiff-cost-budget branch August 20, 2026 03:35
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