Skip to content

fix(js): linear buffering and stateful UTF-8 decoding in readLines#308

Open
ondraulehla wants to merge 1 commit into
e2b-dev:mainfrom
ondraulehla:fix/readlines-linear-buffering
Open

fix(js): linear buffering and stateful UTF-8 decoding in readLines#308
ondraulehla wants to merge 1 commit into
e2b-dev:mainfrom
ondraulehla:fix/readlines-linear-buffering

Conversation

@ondraulehla

@ondraulehla ondraulehla commented Jul 14, 2026

Copy link
Copy Markdown

Fixes #251

Problem

readLines() in js/src/utils.ts accumulates the response body with buffer += chunk and re-scans the whole buffer on every chunk. String append plus full re-scan is O(n^2): a large single-line stdout (the exact shape produced by print("x" * 22_000_000)) causes multi-second event-loop stalls and OOM kills on memory-constrained hosts, as reported in #251 (Linear: ENG-3808).

There is a second, related bug: a fresh new TextDecoder() is created for every chunk, so a multi-byte UTF-8 sequence split across a chunk boundary (any emoji, most non-Latin scripts) decodes as U+FFFD replacement characters.

Fix

  • Accumulate decoded fragments in an array (pending). Appending is O(1), and join('') runs only when a line completes, so total work is linear.
  • Scan for newlines only within the newly decoded chunk instead of the whole buffer.
  • Reuse a single TextDecoder with { stream: true } and flush it at stream end. This fixes the split-sequence decoding and preserves trailing bytes.

Semantics are unchanged: the same lines are yielded for embedded, trailing, and consecutive newlines, and for the final unterminated remainder.

Benchmark

Node v24, 64 KiB chunks, single-line payload terminated by \n (the #251 scenario):

stdout size old (buffer +=) new (array + stream decoder)
22 MB 0.82 s 0.05 s
44 MB 3.67 s 0.11 s
88 MB 13.89 s 0.18 s

The old implementation scales roughly 4x per size doubling (quadratic); the new one scales linearly and is 77x faster at 88 MB.

Tests

Added js/tests/utils.test.ts with 9 unit tests covering line splitting across chunk boundaries, empty lines, trailing text without a newline, empty streams, multi-byte UTF-8 split across chunks, incomplete sequences at stream end, and a ~8 MB many-chunk case. These are pure unit tests over a synthetic ReadableStream, so no sandbox or API key is needed. pnpm test tests/utils.test.ts, pnpm build, and pnpm lint all pass.

🤖 Generated with Claude Code

@cla-bot

cla-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @ondraulehla on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

readLines() grew a single string with `buffer += chunk` and re-scanned it
on every chunk, making large outputs O(n^2): a 22 MB single-line stdout
took ~30s and OOM-killed memory-constrained hosts (e2b-dev#251). It also created
a fresh TextDecoder per chunk, so multi-byte UTF-8 sequences split across
chunk boundaries decoded as U+FFFD.

Accumulate decoded fragments in an array (O(1) append, joined only when a
line completes), scan for newlines only within the new chunk, and reuse
one TextDecoder with { stream: true }, flushing it at stream end.

Fixes e2b-dev#251

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ondraulehla ondraulehla force-pushed the fix/readlines-linear-buffering branch from af8e59b to 01dcfc2 Compare July 14, 2026 19:21
@cla-bot

cla-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @ondraulehla on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@ondraulehla

Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Jul 14, 2026
@cla-bot

cla-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

O(n²) string concatenation in readLines causes OOM on large stdout (>1 MB)

1 participant