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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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