Skip to content

perf: quadratic time and memory complexity when large string token spans multiple chunks #14

Description

@lcweden

Summary

JSONTextDecoder exhibits quadratic time and memory complexity when a large JSON string token spans multiple input chunks.

On a 270 MB input with a single ~48 MB string field, core-passthrough takes ~409 seconds at the default 64 KB chunk size, compared to ~0.7 seconds for raw I/O on the same file.

Problem

Three source-level behaviors combine to produce this:

  1. consumeString() (src/utils/wire.ts) — rescans from the opening quote on every call and does not preserve scan position across invocations.
  2. readToken() (src/modules/decoder.ts) — returns undefined when consumeString() returns 0, leaving the incomplete string in the unread region.
  3. appendBytes() (src/modules/cursor.ts) — allocates a new Uint8Array of size unread.length + bytes.length and copies all unread bytes on every append.

Together, these behaviors cause repeated rescanning and repeated copying of the same growing unread buffer whenever a single string token crosses chunk boundaries.

Impact

  • Severe performance degradation on large streamed inputs
  • Memory usage grows unnecessarily due to repeated buffer reallocation and copying
  • Default chunk sizes amplify the problem for large string values

Reproduction

Use a large JSON document where one string field is much larger than the stream chunk size, for example a ~270 MB file containing a single ~48 MB string field.

Expected behavior:

  • Decoding time should scale approximately linearly with input size.
  • Memory growth should remain bounded to what is necessary for incremental decoding.

Actual behavior:

  • Runtime becomes effectively quadratic.
  • Repeated unread-buffer copying and rescanning dominate execution time.

Suggested direction

Potential remediation areas:

  • Preserve string scan progress across consumeString() invocations so already-validated bytes are not rescanned.
  • Adjust readToken() / buffering behavior so incomplete strings do not force repeated whole-buffer work.
  • Rework appendBytes() to avoid allocating and copying the full unread region on every append.

Acceptance criteria

  • Large string tokens spanning many chunks are processed with near-linear time complexity.
  • Buffer growth avoids repeated full-copy behavior on each append.
  • The provided large-input scenario no longer exhibits orders-of-magnitude slowdown relative to raw I/O.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions