e2b-dev / e2b-dev/code-interpreter
O(n²) string concatenation in readLines causes OOM on large stdout (>1 MB)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2.4k
- Forks
- 228
- Avg merge
- 48m
- Merged PRs (30d)
- 5
Description
Bug: O(n²) string concatenation in readLines causes OOM on large stdout
Summary
The readLines() function in @e2b/code-interpreter uses buffer += chunk to accumulate the HTTP response body. This is O(n²) string concatenation in JavaScript — each += copies the entire existing buffer plus the new chunk into a fresh string. For large stdout outputs (>1 MB), this causes massive memory amplification and multi-second event loop stalls, leading to OOM kills on the host process.
Environment
@e2b/code-interpreter: 2.3.3 (also confirmed on 2.4.0 — same code)- Node.js: v22
- OS: Linux (Kubernetes pods, 4 GB memory limit)
Reproduction
- Create a sandbox and run code that produces ~20 MB of stdout:
import { Sandbox } from '@e2b/code-interpreter';
const sandbox = await Sandbox.create();
// Generate ~22 MB of stdout
const execution = await sandbox.runCode(`print("x" * 22_000_000)`);
- Monitor the host Node.js process memory. The heap will spike to 1–1.5 GB and the event loop will stall for 10–20 seconds.
Root Cause
In js/src/utils.ts, readLines():
buffer += new TextDecoder().decode(value); // line 14
Each iteration creates a new string of size len(buffer) + len(chunk) while the old buffer is still referenced. For a 22 MB response arriving in ~1,400 chunks of ~16 KB:
- Total bytes copied:
Σ(i × 16KB)for i = 1..1400 ≈ 15.7 GB of string allocations - Peak heap: 1.5 GB+ (V8 can't GC fast enough under allocation pressure)
- Event loop stalls: 10–20 seconds (GC pauses)
Evidence
We captured a V8 heap snapshot on a production worker after processing 22 MB of stdout. A single retained string — {"type":"stdout","text":"..."} — consumed 119,920 kB (117 MB, 37% of the heap). The retainer chain traces directly to the readLines async generator's parameters_and_registers (the buffer local variable), held through the ReadableStream reader → Promise chain → fetch Request body.
Measured Impact
| Metric | Current (buffer +=) |
Fixed (array + join) |
|---|---|---|
| Peak heap | 211 MB | 20 MB |
| Peak RSS | 329 MB | 95 MB |
| Elapsed time | 29.8s | 4.1s |
| Memory amplification | 9x | 0.9x |
(Standalone benchmark with 22 MB stdout. Production workers with existing heap pressure show 80x amplification.)
Suggested Fix
Replace quadratic string concatenation with array-based buffering:
// js/src/utils.ts – readLines()
export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
const reader = stream.getReader()
const decoder = new TextDecoder()
const chunks: string[] = [] // ← array instead of string
let searchStart = 0
try {
while (true) {
const { done, value } = await reader.read()
if (value !== undefined) {
chunks.push(decoder.decode(value, { stream: true }))
}
if (done) {
const remaining = chunks.join('')
if (remaining.length > 0) {
yield remaining
}
break
}
// Check for newlines in accumulated data
const buffer = chunks.join('')
let newlineIdx: number
let start = 0
while ((newlineIdx = buffer.indexOf('\n', start)) !== -1) {
yield buffer.slice(start, newlineIdx)
start = newlineIdx + 1
}
// Keep only the remainder after the last newline
chunks.length = 0
if (start < buffer.length) {
chunks.push(buffer.slice(start))
}
}
} finally {
reader.releaseLock()
}
}
This changes the complexity from O(n²) to O(n) and eliminates the OOM risk for large outputs.
Happy to open a PR if this approach looks good.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in js/src/utils.ts at the readLines() implementation and inspect how chunks, newline boundaries, and decoder state are handled. Verify the change with the existing stream-reading behavior and a large stdout reproduction; done means line parsing remains correct without the memory spike and event-loop stalls described for roughly 22 MB of output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- performance
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100