Repository navigation
fix: preserve database state when exporting - #644
Conversation
Measured comparison: serialize (#643) vs backup (#644)Recommendation for the stated priority of large databases: prefer the backup implementation in #644. It removes the need for a whole-database allocation in WASM, at a measured ~5× latency cost on the largest case. This is a WASM-headroom improvement, not a low-total-memory download implementation: the current destination is still MEMFS. A genuinely low-memory save/download should use a separate file-backed backup API (OPFS/real filesystem), not return a full Actual CI validation
Same-runner timing and memoryThe table below is from run 40303987097: Node v24.21.0 on Linux / AMD EPYC 7763. Both optimized WASM bundles ran on the same runner, in separate child processes, with alternating strategy order. Each size/strategy uses 3 fresh processes and 7 repeated exports per process, with GC between exports. First-export medians have 3 samples; repeated-export medians have 21 samples. Exported data was reopened and verified with
Peak RSS is a whole-process high-water mark measured before round-trip validation; it includes setup/runtime memory and is not an isolated SQLite allocation measurement. These are synthetic 4 KiB BLOB rows with an expression index, not guarantees for every schema or browser. The independent repeat measured 150 / 730 ms repeated-export medians for the largest case, reinforcing the ~5× difference. The decisive memory-limit testBoth production WASM modules declare a 2048 MiB maximum. For a controlled limit test, the script lowered only the WASM memory section's maximum to 64 MiB, leaving the initial memory and application code unchanged. The WebAssembly engine enforced the limit. With a 108.28 MiB database:
This establishes that backup can export databases larger than the permitted WASM heap. It does not establish a tested multi-gigabyte browser file-size ceiling. Available JS/process memory, filesystem buffering, browser buffer limits and storage quotas still matter. Serialization additionally uses one Bundle sizesCombined optimized JS + WASM: serialize 732,429 bytes raw / 353,854 bytes gzip; backup 732,552 raw / 353,688 gzip. The difference is negligible. What a low-memory download needsThe current byte-array APIs both create two full-size additional buffers: serialize uses WASM snapshot + JS output; backup uses a MEMFS destination + JS output. For file size D, both can approach roughly 3D total residency including the source, plus overhead. The next architectural step would be backing the backup destination with a seekable disk-backed file, e.g. an OPFS SyncAccessHandle through a filesystem/VFS adapter in a dedicated worker. Once complete, a browser Incremental backup alone is not a download stream: SQLite may rewrite earlier pages and finalize the header at completion, so a forward-only sink cannot directly consume the writes. The one-off artifact-dependent CI step has been removed after recording these results; the standalone comparison script remains reusable with two dist directories: node test/compare_exports.cjs /path/to/serialize-dist /path/to/backup-dist |
Optimized backup results — supersedes the earlier ~895 ms measurementThe preallocated implementation at Same-runner comparisonGreen CI run 40311200414: Node v24.21.0, Linux, AMD EPYC 7763. Compared current backup, previous backup at
For the largest case:
Independent green repeat 40311191909, on an AMD EPYC 9V45: largest-case repeated medians were 700.57 ms previous backup / 152.89 ms optimized backup / 139.48 ms serialize. Peak RSS was 1828.0 / 1276.5 / 1834.2 MiB, respectively. Exact timing and memory numbers are machine-dependent, but both runs show the same improvement. Correctness and memory limits
Complexity clarificationThe original MEMFS 12.5%-growth strategy is amortized O(n), not O(n²); earlier discussion overstated the asymptotic issue. The substantial constant overhead came from repeated growth copies, the extra final output copy and the backup write path. The optimization preallocates once and fills the result in a linear page pass. The combined optimization was measured; the individual contributors were not separately profiled. Recommendation: use the optimized backup implementation in #644. It now approaches serialize's throughput on large files without requiring a database-sized WASM allocation, and materially reduces total peak process memory. The one-off artifact-download benchmark CI step has been removed after recording these results. No builds, tests or benchmarks ran on the limited development machine. |
Summary
Export the main database using SQLite's Online Backup API instead of closing and reopening its connection. Preserve prepared statements/cursor positions, custom functions and aggregates, update hooks, PRAGMAs, TEMP objects, attached databases and change counters.
The backup writes pages directly into one preallocated JS-owned result buffer. Close and unlink only the private destination, then return that buffer without an extra full-file copy or a database-sized WASM allocation. Normalize the exported header so WAL sources produce independently reopenable files. Clean up all temporary resources on success and failure.
Transaction behavior: export permits read transactions but rejects active write transactions without committing, rolling back or changing savepoints. This replaces the previous implicit rollback on export and is documented in
.export()'s doc comment. Callers must commit or roll back writes before exporting.Includes the 1.15.2 package/lockfile version bump for release. Supersedes #642 and #643.
Validation
The experimental comparison script and temporary CI steps have been removed from the final diff. Recorded CI measurements and methodology: a 577.53 MiB export improved from 892 ms to 233 ms, with peak process RSS reduced from 1867 to 1248 MiB and WASM memory remaining at 21 MiB. A 108 MiB database also exported successfully under a hard 64 MiB WASM cap.