mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): walk the completion value in O(depth), not O(width)
`_check_done_value` and `_encode_json_plain` pushed one stack entry per child (plus a separator marker, and `dict.items()` materialized as a list), so the bookkeeping scaled with the value's WIDTH rather than its depth. A value the byte meter admits could then die on the walk's own frames: a flat `[0] * 2_000_000` serializes to 4.0 MB, but measured peaks were 145.2 MB in the meter and 114.7 MB in the encoder — 28.7x the serialized size, far past the 12x the load-time address-space gate reserves. Each container now pushes ONE cursor frame that pulls its children one at a time and writes into a shared `io.StringIO`, so the output string is the only width-proportional allocation and the caller already metered its size. Measured on the same value: 0.0 MB in the meter and 9.0 MB in the encoder (2.3x), with identical verdicts.
This commit is contained in:
@@ -3685,6 +3685,29 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
expect(result.error?.kind).toBe('output-limit')
|
||||
}, 30_000)
|
||||
|
||||
it('checks and encodes a wide completion value in O(depth), not O(width)', async () => {
|
||||
// A wide flat list serializes to ~2 bytes per element but the pre-fix walk
|
||||
// enqueued one traversal tuple per element (_check_done_value) and one stack
|
||||
// entry plus a separator marker per element (_encode_json_plain) — ~56 bytes
|
||||
// per element, ~28x the serialized size. A value the byte meter admits could
|
||||
// therefore OOM on the checker's or encoder's own bookkeeping, the inversion
|
||||
// the load gate exists to prevent (the gate reserves 12x, not 28x). Both now
|
||||
// walk with an O(depth) cursor that pulls one child at a time, so the only
|
||||
// width-proportional allocation is the output string the meter bounded.
|
||||
//
|
||||
// Config: maxValueBytes 20 MiB against 384 MiB (20*12 = 240 MiB < 320 MiB
|
||||
// budgetable, so it loads). `[0] * 6_000_000` is ~12 MB of JSON, under the
|
||||
// 20 MiB budget, so it must round-trip. Pre-fix the ~400 MB of per-element
|
||||
// frames plus the interpreter exceeded 384 MiB and returned MemoryError as an
|
||||
// exception. Linux-only RLIMIT_AS repro; on macOS the value round-trips
|
||||
// either way, but the fixture stays within the address space so it is honest.
|
||||
const { runtime } = await setup({ maxValueBytes: 20 * 1024 * 1024, addressSpaceMb: 384, maxWallMs: 20_000 })
|
||||
const result = await runtime.run({ program: 'return [0] * 6_000_000', bindings: [] })
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(Array.isArray(result.value)).toBe(true)
|
||||
expect((result.value as number[]).length).toBe(6_000_000)
|
||||
}, 30_000)
|
||||
|
||||
it('bounds a flood of zero-byte log lines through the per-entry separator charge', async () => {
|
||||
// Blank print() lines carry zero content bytes; without the +1 separator
|
||||
// charge they would bypass maxLogBytes entirely and grow the retained
|
||||
|
||||
Reference in New Issue
Block a user