Skip to content

[Bug]: code-interpreter-js readLines is O(n²) on long output lines and blocks the event loop #1990

Open

Description

@jcalem-rogo

Sandbox ID or Build ID

N/A (client-side SDK bug, not specific to one sandbox)

Environment

@e2b/code-interpreter 2.6.0 in production; also reproduced against packages/code-interpreter-js/src/utils.ts on main (2.9.0). Node 22.

Timestamp of the issue

2026-10-09 22:25 UTC (first seen in production; reproducible at any time)

Frequency

Happens every time

Expected behavior

runCode takes time proportional to output size, so a long single-line output (base64 image, wide DataFrame, large JSON) doesn't block the caller's event loop.

Actual behavior

readLines appends each chunk to the buffer (buffer += decoder.decode(...)) and then searches the whole buffer with buffer.indexOf('\n'). When a line has no newline, V8 has to rebuild the entire buffer into one string on every chunk, so the total cost grows with the square of the line length. All of that runs synchronously on the caller's main thread. In production, a single cell printing one ~40 MB line stalled our Node process's event loop for several seconds and added about 2 GB of memory.

Issue reproduction

Copy readLines from utils.ts and feed it one newline-free line in 16 KB chunks:

const CHUNK = 16 * 1024
for (const mb of [1, 2, 4, 8, 16, 32]) {
  const chunk = new Uint8Array(CHUNK).fill(97); const n = (mb * 1024 * 1024) / CHUNK; let k = 0
  const stream = new ReadableStream({ pull(c) { k < n ? (c.enqueue(chunk), k++) : c.close() } })
  const t = performance.now(); for await (const _ of readLines(stream)) {}
  console.log(`${mb} MB: ${(performance.now() - t).toFixed(0)} ms`)
}
Line size Time
1 MB 8 ms
2 MB 20 ms
4 MB 76 ms
8 MB 263 ms
16 MB 1,153 ms
32 MB 5,152 ms

Additional context

This is e2b-dev/code-interpreter#251 (ENG-3808). That issue got stranded when the SDK moved into this monorepo in #1768, and the fix PR there (e2b-dev/code-interpreter#308) now conflicts with a deleted file. #1946 fixed UTF-8 decoding across chunks but kept the quadratic buffering. The fix proposed in #251 should work: search only the new chunk for \n, keep the partial pieces in an array, and join once per complete line. It also needs to keep #1946's streaming TextDecoder. We're running a patch like this on 2.6.0, and it handles 40 MB single-line output without stalling.

Activity

  1. devin-ai-integration commented on Oct 15, 2026

    @devin-ai-integration
    Contributor

    Triage

    Written by Devin

  2. Kylinny commented on Oct 15, 2026

    @Kylinny

    I'd like to take this one. The O(n²) in readLines looks like a bounded fix (decode only the new chunk, search for \n in the new chunk, accumulate line fragments and join once per line). I'll reproduce with a benchmark first, fix JS + the equivalent Python sync/async paths, and open a PR with before/after numbers. Thanks for the detailed report!

  3. Kylinny commented on Oct 15, 2026

    @Kylinny

    Fix PR opened: #1991 — now scans only the newly arrived chunk instead of re-scanning the whole buffer, making it linear on long single-line outputs (40MB line: ~9s → ~0.1s). 15 new regression tests pass.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Code InterpreterbugSomething isn't workingsdkImprovements or additions to SDKs

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions