Skip to content

M3X-1590: Optimize code for better performance - #9

Open
ragoolkr wants to merge 1 commit into
developfrom
M3X-1590-optimize-for-performance
Open

ragoolkr wants to merge 1 commit into
developfrom
M3X-1590-optimize-for-performance

Conversation

@ragoolkr

@ragoolkr ragoolkr commented Oct 9, 2026

Copy link
Copy Markdown

Ticket

Summary

Performance fixes for the structural table redlining introduced in #8 (v2.0.0). Output is unchanged: all specs pass and results are byte-identical to 2.0.0 on every edited input. The only difference is that identical input is now returned as-is, as in 1.6.0.

2.0.0 was ~3× slower than 1.6.0 on lightly edited and keyed tables, and spent 180 ms (200 rows) to 4.3 s (800 rows) on tables that had not changed at all. With this PR, 2.0.0 is faster than 1.6.0 on every table workload measured.

Changes

File Change Why it's better
src/htmldiff.ts Return the input as-is when before === after Unchanged sections, the common case in a document redline, go from 180 ms (4.3 s at 800 rows) to 0; same output as 1.6.0
src/core/tokens.ts Track the last < instead of rescanning the whole atomic token on every >; self-closing check by one character instead of a regex Large keyed elements (the merged table) no longer get quadratically slower with size; main reason "worst" went 139 → 16 ms
src/tables/RowAligner.ts Read each row once (words, blank, keys, integer exactKey from the sorted word set) Per-pair cost drops from regex word-splitting to an int compare or Set lookup; exactKey matches exactly when similarity === 1, so output stays byte-identical
src/tables/SequenceAligner.ts Optional isExact hook, flat Int32Array DP table, push instead of concat in a loop Cheap exact pass, one allocation instead of n, less GC; removes quadratic array copying
src/tables/similarity.ts Split cellSimilarity into signatureWords (parse once) + wordSimilarity (Set-based) Words are parsed once per cell, not per comparison; public API unchanged
src/tables/TableVersion.ts rowKeysByRow() builds the owner map once, then reads every row's keys Keyed tables 150 → 11 ms; no repeated owner lookups per comparison
src/tables/TableMerger.ts Precompute kept-row group signatures; early return when there's no group; find instead of filter()[0] Removes a changed × kept recomputation; stops at the first match
src/tables/html.ts Cache the compiled attribute regex per name No new RegExp in hot loops
src/tables/helpers.ts uniqueValues via Array.from(new Set(...)) O(k) instead of O(k²), same first-seen order
.gitignore Ignore generated htmldiff-cli.d.ts Keeps build output out of git

Results

Measured with the htmldiff perf bench (drive.sh, best of two round medians; Apple Silicon, Node 25, round spread ≤ 8%).

Case 1.6.0 2.0.0 (#8) This PR vs 1.6.0
table 200×6, identical ~0 ms 180 ms ~0 ms same
table 200×6, 10% cells + row add/del 57.6 ms 190 ms 12.2 ms 4.7× faster
keyed table 200×6, 10% cells 47.7 ms 150 ms 11.3 ms 4.2× faster
table 200×6, 60% cells edited 107 ms 20.7 ms 6.4 ms 17× faster
worst: table 300×8 long cells, 60% edited 828 ms 139 ms 16.0 ms 52× faster
document 20 tables 10×4, edits in each 34.8 ms 36.5 ms 16.2 ms 2× faster
text 506 KB, 5% words 1048 ms 953 ms 955 ms same core

Scaling, one table with 5% of cells edited: 800 rows took ~2.9 s on 2.0.0 and take 58 ms now (≈2× per doubling instead of ≈4×).

@ragoolkr ragoolkr self-assigned this Oct 9, 2026
@ragoolkr
ragoolkr requested a review from sirenevenkii October 9, 2026 09:42

@sirenevenkii sirenevenkii left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

fantastic!

});

describe("wordSimilarity", () => {
it("is cellSimilarity on the words read once", () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is it really testing what it states?

Comment on lines +79 to +88
it("lists the keys of every row at once", () => {
const table = readTable(
'<table><tbody><tr><td rowspan="2" data-htmldiff-id="SPEC-1">SPEC-1</td><td data-htmldiff-id="TC-1">TC-1</td></tr>' +
'<tr><td data-htmldiff-id="TC-2">TC-2</td></tr><tr><td>plain</td><td>row</td></tr></tbody></table>',
);
MergedCells.expand(table, "new");
const v = TableVersion.read(table);
expect(v.rowKeysByRow()).to.deep.equal([["SPEC-1", "TC-1"], ["SPEC-1", "TC-2"], []]);
expect(version("<table><tbody><tr><td>a</td></tr></tbody></table>").rowKeysByRow()).to.deep.equal([[]]);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

are those 2 tests? is the name correct?

Comment thread package.json
"description": "Diff and markup HTML with <ins> and <del> tags",
"companyname": "Matrix Requirements",
"version": "2.0.0",
"version": "2.0.1",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we never published it, so it can stay 2.0

@sirenevenkii sirenevenkii left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

forgot to approve

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