Skip to content

fix: a million-character line cut by the capture edge no longer freezes the tab - #9

Merged
bmmmm merged 1 commit into
mainfrom
fix/long-line-budget
Oct 7, 2026
Merged

bmmmm merged 1 commit into
mainfrom
fix/long-line-budget

Conversation

@bmmmm

@bmmmm bmmmm commented Oct 7, 2026

Copy link
Copy Markdown
Owner

What and why

trimToClip in collect.ts reads one glyph rect per character of a line that the capture edge cuts, and read on to the line's end. A raw .js or .json file shown by the browser is one line of a million characters: measured, the content script took longer than 120 s in both engines — the tab froze. The scan now stops where it cannot find anything more. No time budget: a review measurement showed one would throw away the text layer a many-line 1 MB file still yields after its per-line binary search (main: 13 runs after 8.5 s; with a 5 s budget: 0 runs).

Claims

  • A single-fragment line cut by the capture edge is scanned only up to the first glyph outside the clip after one inside; a 1 MB <pre> line in a 400 px clipped container yields its first words in well under a second instead of minutes (trimToClip, monotonic from line.parts.length === 1)
  • A bidi line (several fragments in splitIntoLines) is still scanned to its end, so the right-to-left part that re-enters the clip is kept
  • A collapsed space, which has no glyph rect, does not end the scan

Verification

$ pnpm check
Checked 148 files in 101ms. No fixes applied.

$ pnpm test
ℹ pass 465
ℹ fail 0
  552 passed (2.3m)

$ pnpm test:glue:chromium
ℹ pass 25
ℹ fail 0

$ pnpm test:glue
ℹ pass 58
ℹ fail 3     # the three known 1265 px viewport cases (classic scrollbars on this machine), same on main

Tests

  • Added / changed: tests/layout/long-line.spec.ts — the clipped 1 MB line (first words, stats.ms < 3000), collapsed triple spaces inside the clip, a bidi line whose Hebrew part re-enters the clip
  • Fails on main without this change: yes — 30 s test timeout in Firefox and Chromium for the 1 MB line (the same scenario measured at >120 s); dropping the glyph operand yields "word", dropping the monotonic operand yields "abc" instead of the full run; the first >= 0 operand is pinned by the existing clip-partial.html fixture (6 specs go red without it)

Public contracts

  • Saved SVG structure unchanged: raster <image> tiles under an invisible text layer — one <text> with textLength per run, fill-opacity="0", OCR words in <g id="ocr">, linked runs wrapped in <a href>
  • Capture record unchanged: the JSON in <snapii:capture> keeps schema: 1 and every field of captureRecord() in src/shared/svg/metadata.ts (no field removed, renamed or retyped); the Dublin Core fields stay as they are
  • Manifest permissions unchanged: activeTab, scripting, downloads, clipboardWrite, storage — no host_permissions, no content_scripts (decision D1, docs/development.md)
  • Settings in storage.sync keep their keys, types and meaning (Settings in src/shared/types.ts); a value stored by an older version still loads
  • Add-on id stays snapii@qmmq.de
  • Privacy holds: no network request of any kind, data_collection_permissions stays none, the saved file reveals nothing new about the user
  • No new dependency shipped in dist/ — or NOTICE and the license texts are updated with it

Saved-file impact

  • Golden file tests/unit/golden/simple.svg changed: no. Ten edge inputs (Devanagari, Arabic, RTL with digits, unicode-bidi: plaintext, bidi-override, ligatures, a ZWJ emoji, ::first-letter, tabs, negative letter-spacing), each cut left and right, give byte-identical runs in both engines (review measurement)
  • Opened a new file in Firefox, selected the text and followed a link: yes (roundtrip and vector specs)

Risk and rollback

  • What breaks if this is wrong: a single-fragment line whose glyph x is not monotonic would lose its tail after the clip's first exit — no such input was found
  • How to tell from the outside: copied text from a captured line ends early where the picture shows more
  • Revert is clean: yes, one commit

Out of scope

  • A capture edge a million px into such a line still reads every character before the clip (about 14 s measured); a binary search for the first glyph inside the clip would fix that — separate change
  • The per-line binary search in splitIntoLines on a many-line 1 MB file (8 s on Chromium) is untouched

Assistance

  • I wrote or reviewed every line and can explain why each change is there
  • I ran the commands under Verification myself and pasted their real output

…es the tab

trimToClip read one rect per character of the line, to its end: a raw .js
or .json file shown as one line took minutes in both engines (measured:
over 120 s for 1 MB). For a line that is one bidi fragment the scan now
stops at the first glyph outside the clip after one inside; a bidi line is
still scanned to its end, and a collapsed space (no glyph rect) does not
end the scan. No time budget: one would have thrown away the text a
many-line file still yields after the per-line binary search.

Red before the fix: long-line.spec.ts, 30 s timeout in Firefox and
Chromium for the clipped 1 MB line; the glyph and monotonic operands
each go red when dropped ("word" / "abc" instead of the full run).
@bmmmm
bmmmm merged commit 6f56e58 into main Oct 7, 2026
2 checks passed
@bmmmm
bmmmm deleted the fix/long-line-budget branch October 7, 2026 22:09
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