Skip to content

perf(renderer): diff code lines in O(n) via a prev-line map#706

Open
ly-wang19 wants to merge 1 commit into
THU-MAIC:mainfrom
ly-wang19:perf/renderer-code-line-diff
Open

perf(renderer): diff code lines in O(n) via a prev-line map#706
ly-wang19 wants to merge 1 commit into
THU-MAIC:mainfrom
ly-wang19:perf/renderer-code-line-diff

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

Problem

BaseCodeElement's line-diff useEffect runs on every [lines, animate] change — i.e. on each streaming update while a code block types out. It built a Set for the insert check (O(1)), then did a second pass with a linear prevLinesRef.current.find(...) per line for the content/replace check:

const prevIds = new Set(prevLinesRef.current.map((l) => l.id));
for (const line of lines) {
  if (!prevIds.has(line.id)) states.set(line.id, { type: "inserted", timestamp: 0 });
}
for (const line of lines) {
  const prev = prevLinesRef.current.find((p) => p.id === line.id);   // O(prevLines) per line
  if (prev && prev.content !== line.content) states.set(line.id, { type: "replaced", timestamp: 0 });
}

That second loop is O(lines × prevLines) = O(n²). Code blocks routinely run 50-300 lines and this fires repeatedly during the typing animation, so it's an on-render hot path.

Fix

One Map<id, prevLine>, one O(n) pass. Insert and replace were already mutually exclusive (a line either isn't in prev → inserted, or is in prev with changed content → replaced), so the merged if/else if is behavior-identical and also drops the redundant second iteration. Consistent with the Set the first loop already used.

const prevById = new Map(prevLinesRef.current.map((l) => [l.id, l]));
for (const line of lines) {
  const prev = prevById.get(line.id);
  if (!prev) states.set(line.id, { type: "inserted", timestamp: 0 });
  else if (prev.content !== line.content) states.set(line.id, { type: "replaced", timestamp: 0 });
}

Verification

  • prettier clean, eslint clean, tsc --noEmit error count unchanged (the only errors are pre-existing unresolved-workspace-module noise; the edited region type-checks clean).
  • Behavior preserved: identical states map for any (prev, current) line pair — typing on first render, inserted for new ids, replaced for same id + changed content, unchanged otherwise.

Closes #705

BaseCodeElement diffed each render against the previous one with a
linear prevLinesRef.current.find() per line, on top of a separate Set
pass — O(lines x prevLines). This effect runs on every streaming update
while a code block types out, and code blocks routinely run dozens to
hundreds of lines, so the quadratic cost lands on a render hot path.

Build a single Map<id, prevLine> and fold the insert/replace checks into
one O(n) pass. Behavior is identical: a line missing from the previous
render is "inserted", a line present with changed content is "replaced"
(the two were already mutually exclusive).

Closes THU-MAIC#705
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.

perf(renderer): O(n²) line diffing in code-block typing animation

1 participant