Skip to content

Fix quadratic string decode, improve performance - #4

Merged
alecgeatches merged 1 commit into
mainfrom
fix/quadratic-strings
Aug 18, 2026
Merged

alecgeatches merged 1 commit into
mainfrom
fix/quadratic-strings

Conversation

@alecgeatches

Copy link
Copy Markdown
Owner

What

See WordPress/gutenberg-sync-engines#29 where this fix was proposed.

Lib0\StringDecoder::read() located each string's UTF-16 start offset by calling Str::sliceUtf16(), which re-splits the shared string buffer with preg_match_all and walks it from offset 0 on every read. The V2 update format concatenates all item strings into that one buffer, so decoding n strings cost O(n²). The JS original (lib0/decoding.js) is str.slice(), where native UTF-16 indexing makes the same line cheap, so the quadratic was a porting artifact.

The fix

Reads are strictly sequential (read() is the only method and only moves forward, and lengths from UintOptRleDecoder are never negative), so the buffer is split into chars once and a forward cursor replaces the per-read rescan. Per-char semantics match Str::sliceUtf16() exactly:

  • Same /./us split and same str_split() fallback for invalid UTF-8.
  • Same UTF-16 unit accounting: a 4-byte UTF-8 sequence from the /u split is exactly a code point above 0xFFFF, i.e. two UTF-16 units.
  • A char straddling a read boundary is kept for re-inclusion in the next read, mirroring sliceUtf16()'s global-position behavior. The zero-length read guard is also preserved.

Benchmarks

Decoding n small strings from one shared buffer, plus an end-to-end applyUpdateV2 of a document with 2,000 non-mergeable items:

Benchmark Before After
Decode 1,000 strings 525 ms 1.4 ms
Decode 3,000 strings 4,691 ms 2.8 ms
Decode 6,000 strings 18,710 ms 6.0 ms
applyUpdateV2, 2,000-item doc 1,663 ms 16.1 ms

@alecgeatches alecgeatches self-assigned this Aug 18, 2026
@alecgeatches
alecgeatches merged commit 4c7de7e into main Aug 18, 2026
3 checks passed
@alecgeatches
alecgeatches deleted the fix/quadratic-strings branch August 18, 2026 15:32
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