Skip to content

Fix/scanner quadratic scanning - #1

Merged
maxrice merged 2 commits into
mainfrom
fix/scanner-quadratic-scanning
Sep 16, 2026
Merged

maxrice merged 2 commits into
mainfrom
fix/scanner-quadratic-scanning

Conversation

@maxrice

@maxrice maxrice commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

No description provided.

The scanner addresses text by character offset. Resolving a character
offset against a UTF-8 string means walking it from the start, so calling
mb_substr() once per character made scanning cost time proportional to the
square of the document length. Both the per-character reads in
StringHelper::charCodeAt() and the per-token reads in
StringHelper::substring() paid it.

Split the document into characters once in the constructor and index that
array instead. charCodeAt() takes an ord() fast path for single-byte
characters, which covers essentially all of a JSON document's structure.
Memory cost is roughly 46x the source size, paid once per scanner.

The text property is no longer read now that every access goes through the
character array, so it is gone.

Measured over a document shaped like a Shopify theme locale file, running
parse + modify + applyEdits:

     8 KB      55 ms  ->    9.3 ms
    33 KB     720 ms  ->   22.4 ms
    90 KB   5,307 ms  ->   61.4 ms
   183 KB  21,384 ms  ->  128.0 ms

Scaling is linear after the change: twice the input costs 2.1x the time.

The new complexity tests assert a ratio between two input sizes rather than
a wall-clock budget, so they carry the same meaning on any machine.
Quadratic scanning takes ~16x as long for 4x the input and linear scanning
takes ~4x, which leaves a wide margin around the threshold of 8. The second
test guards against a fix that speeds up ASCII by falling back to byte
offsets while leaving multibyte documents quadratic.
The scanner reports offsets and lengths in characters rather than bytes,
and the editor depends on that agreement: when the two disagreed, an edit
after a multibyte character landed at the wrong position and mangled the
output. The editor tests cover that end to end, but the scanner itself had
no multibyte tests at all, so a change to how it addresses characters
failed a long way from its cause.

These assert offsets, token values, lengths, line and column numbers, and
the token stream itself across 2-, 3- and 4-byte characters, Greek, Thai,
Cyrillic, CJK and emoji.

They pass against the scanner both before and after the switch to indexing
a character array, which is the point: they describe behavior that was
already correct rather than behavior the change introduced.
@maxrice
maxrice merged commit 2e9fa9f into main Sep 16, 2026
5 checks passed
@maxrice maxrice mentioned this pull request Sep 16, 2026
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