Repository navigation
Keep opened files' sizes once DuckDB's heap passes 2 GiB - #171
Merged
Merged
Conversation
Loading the 50K × 1,000 Parquet file (388 MB) into one table worked twice. Every load after that failed with "too small to be a Parquet file" or "Prefetch registered for bytes outside file … file size: 0". Five loads of a 3 MB file all worked. The size comes from duckdb-wasm's browser runtime. When DuckDB opens a file, `openFile` allocates 24 bytes and writes the file's size, a buffer pointer and a modification time into them through `HEAPF64[(ptr >> 3) + i]`. `>>` is a signed shift. After two such loads the heap is past 2 GiB, and malloc starts returning addresses above that. For those addresses the index is negative, so the writes are dropped, and DuckDB reads freshly grown, zeroed memory: a file of 0 bytes. A patched runtime logged the right size on every open while the failing loads' 24-byte blocks sat at 2.6 GB. Holding a 2.2 GB table makes every load fail. duckdb-wasm's main branch still uses this shift. The fix wraps the runtime's `openFile` in DuckDB's worker. During the call, the module's `HEAPF64` is a view that moves a write at a negative index back to where it belongs: the index plus 2^29. If the call grows the heap, the view follows the new one. duckdb-wasm publishes the runtime when it instantiates its WebAssembly module, so the fix hooks that assignment. The fix is added to the worker's bootstrap as source text, since that worker runs duckdb-wasm's own script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review of this PR: the fix did not fail safe, and what shipped was not what the tests ran. - A fix that threw while installing threw at the top of DuckDB's worker, so every createDataTable rejected with WORKER_CRASHED. That happens with a duckdb-wasm build whose DUCKDB_RUNTIME cannot be redefined, or a bundler helper injected into the stringified function. The bootstrap now runs the fix in try/catch and warns, and DuckDB runs as before without it. - Every file open redefined HEAPF64 on the module. One that cannot be redefined made every open throw, extension downloads included. The call now goes to duckdb-wasm's openFile unchanged. - The fix was a TypeScript function's toString(), so the bundler decided what shipped. It is now openFileFixScript.js, imported with ?raw. DuckDB's worker runs its text as written, and the unit tests run the same text in VM contexts. - Every read of DUCKDB_RUNTIME now fixes a runtime not fixed yet, for one published empty and filled in afterwards. - Docs: troubleshooting FAQ §31 covers the two errors. The CSP guide's coi caveats say coi's pthread workers go without the fix. The fix's comment names duckdb-wasm's other signed-shift writes, which the library never reaches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The delta review of this PR found two small things. - If the module's HEAPF64 could not be redefined, the fix left the call to duckdb-wasm without a word, although troubleshooting FAQ §31 tells users to look for "[data-table] DuckDB runs without the fix…". It now logs that warning in the worker's console, once. - After each call, HEAPF64 became a writable data property, even if it had been an accessor. The module now gets back its own descriptor, or none if the heap was inherited. A heap the call grew is then assigned as Emscripten would have assigned it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolved as in the build of this round's fixes merged together. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Loading several large Parquet files into one page stopped working after the second one. Every load after that failed with "too small to be a Parquet file" or "Prefetch registered for bytes outside file … file size: 0". The cause is a signed shift in duckdb-wasm's browser runtime, which drops an opened file's size once DuckDB's heap passes 2 GiB. The library now corrects those writes inside DuckDB's worker.
This matters for the manual pass, which loads several large files in one page.
Why: root cause
When DuckDB opens a file, duckdb-wasm's runtime (
openFile) allocates 24 bytes and writes three values into them throughmod.HEAPF64[(ptr >> 3) + i]: the file's size, a buffer pointer and a modification time.>>is a signed shift. Once the heap is past 2 GiB,_malloccan return an address above 2^31. For such an address(ptr >> 3)is negative, so aFloat64Arraysilently drops the writes. DuckDB then reads the freshly grown memory, which is zeroed, and sees a file of 0 bytes.Evidence:
openFilesaw the File, with the right size (387,603,633 bytes).dropSourceFileare not involved: every open found its handle.mainbranch (packages/duckdb-wasm/src/bindings/runtime_browser.ts) still writesmod.HEAPF64[(result >> 3) + …]. The glue code around it already passes pointers unsigned (t>>>=0), so reads into buffers above 2 GiB work. Only these writes are affected.What changed
src/worker/openFileFixScript.js(new): the fix, as plain JavaScript. It wraps the runtime'sopenFile.HEAPF64is a view that moves a write at a negative index to that index plus 2^29, which is where(ptr >> 3) + icame from.HEAPF64descriptor, accessor or data property, or none if the heap was inherited. A heap the call grew is then assigned as Emscripten would have assigned it.HEAPF64cannot be redefined gets duckdb-wasm's ownopenFile, unchanged, and the worker's console says so once.openFile(File handles, OPFS, and HTTP, as used for extension downloads), not only File handles.globalThis.DUCKDB_RUNTIMEwhen its WebAssembly module is instantiated, so the fix hooks that assignment. It checks again whenever the runtime is read, for a runtime published empty and filled in afterwards.src/worker/openFileFix.ts(new):duckdbWorkerSource. It builds DuckDB's worker bootstrap:importScripts(mainWorker), then the fix's text, imported with?rawso no bundler rewrites it.try/catch. A fix that cannot install itself leaves DuckDB as it was and logs a warning in the worker's console.udf_runtime.ts) anddropFiles' pointer array (bindings_base.ts).src/worker/duckdb.tsbuilds its worker fromduckdbWorkerSource, for both the CDN and self-hosted bundles.coihosts see.coicaveats say thecoipthread workers go without the fix.docs/dev/memory-envelope.mdrecords the finding.Tests
Browser tests:
tests/browser/parquet-file-load.spec.ts, "loads a Parquet File again and again once DuckDB's heap is past 2 GiB":duckdb.ts): all three loads fail withInvalid Input Error: File '__dt_source_N.parquet' too small to be a Parquet file.tests/browser/duckdb-worker-bootstrap.spec.ts(new), "DuckDB still starts and loads when the openFile fix cannot install itself":DUCKDB_RUNTIMEa property that cannot be redefined;openFileFix.ts):createDataTablerejects withWorkerInitError: Worker error: Uncaught TypeError: Cannot redefine property: DUCKDB_RUNTIME.Unit tests,
tests/worker/openFileFix.test.ts, 16 tests.openFileFixScript.js?raw) in freshvmcontexts, with a fake runtime that writes as duckdb-wasm does and a heap that drops negative-index writes as aFloat64Arraydoes.openFilefor a heap that cannot be redefined, and for a module that takes no new properties, with one warning;HEAPF64put back as it was, with a grown heap handed to its setter, and an inherited heap left inherited;Mutations: 15 run on the reworked fix, 13 caught.
fixcall before the hook was installed, and a check for a non-configurableHEAPF64in front of thetry/catch. Each only repeated what the code after it does, so I removed both.Big files, headless Chromium, on the fixed branch, all in one page:
wide-50k.parquet(388 MB), a fresh File each timewide-50k↔deep-1m(1M × 41), 5 loadsThese were run on cd45424. The fix's text has not changed in substance since.
Gates
All eight pass on 83dff48:
Review
The review confirmed the diagnosis on 1.33.1-dev57.0 and found one Medium and four Lows, all fixed in b0a2021:
createDataTablereject withWORKER_CRASHED. The fix now runs in atry/catch, warns, and leaves DuckDB as it was.HEAPF64cannot be redefined made every file open throw. The call now goes to duckdb-wasm'sopenFileunchanged.toString(), whatever the bundler made of it. The fix is nowopenFileFixScript.js, shipped as its text with?raw, and the unit tests run that same text.coi.DUCKDB_RUNTIME, for a runtime published empty and filled in afterwards. That is thecoipthread worker's pattern, but those workers load their own script, so the fix does not reach them.openFileFix.ts's comment names the scalar UDF result writes (udf_runtime.ts) anddropFiles' pointer array (bindings_base.ts). The library registers no UDFs and calls onlydropFile.The delta review (of b0a2021) found it mergeable, with two small things, fixed in 83dff48:
HEAPF64that cannot be redefined was silent, although FAQ §31 tells users to look for[data-table] DuckDB runs without the fix…. It now logs that warning once.HEAPF64became a writable data property after each call. The original descriptor is now restored exactly. Unit tests:HEAPF64keeps its getter, setter and attributes, and its setter gets the heap the call grew;Mutations for the delta: 5 of 5 caught:
Not changed
LoadPayload. The loader-options PR changes those, so the two don't overlap.coi(pthread) bundle's pthread workers. They load their own script and are not patched. The library's default bundles aremvpandeh, so this only matters for a host that self-hostscoion a cross-origin-isolated page.mainWorkerpaths (/assets/duckdb/…).importScriptsin DuckDB's blob-URL worker cannot resolve those. Resolving them against the library worker'sself.location.hrefwould fix it. It is left for its own PR, with a test that self-hosts the bundles.🤖 Generated with Claude Code