navidrome/db/pglite/build
Deluan d66aa23f88 build(pglite): cap max_wal_size at 128MB in the wasm module
PGlite splices a fixed list of -c settings into every start and never
reads postgresql.conf or postgresql.auto.conf on restart (ALTER SYSTEM
writes to the wrong directory and pg_reload_conf is a no-op without a
postmaster), so the only way to lower max_wal_size is that list. With
128MB, a CHECKPOINT trims pg_wal from 405 MB to 134 MB locally instead
of letting it grow to the 1 GB default.
2026-09-05 01:32:06 -04:00
..
build.sh.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
clear_error_portals.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
Dockerfile-arm64.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
max_wal_size.diff build(pglite): cap max_wal_size at 128MB in the wasm module 2026-09-05 01:32:06 -04:00
message_context_reset.diff fix(pglite): stop leaking every wire message's memory in the wasm module 2026-09-04 21:38:21 -04:00
README.md build(pglite): cap max_wal_size at 128MB in the wasm module 2026-09-05 01:32:06 -04:00
upstream-build.sh.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
wasi-c.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00
wasm-build.sh.diff feat(pglite): run Navidrome on the embedded PostgreSQL end to end 2026-09-04 20:41:52 -04:00

Rebuilding PGlite for WASI with optimization (2026-09-04)

Source of the build: https://github.com/roastedroot/pglite4j wasm-build/ (self-contained Docker build of electric-sql/postgres-pglite, branch REL_17_5_WASM-pglite). Clone it to /tmp/pglite4j and apply the diffs here:

  • Dockerfile-arm64.diff — the image downloads x86_64 toolchains; on Apple Silicon Docker (arm64) clang cannot run. Swaps wasi-sdk, binaryen, wasmtime, wizer and wasi-vfs for their arm64/aarch64 assets.
  • wasm-build.sh.diff — upstream overwrites CC_PGLITE, and PostgreSQL's configure does not add its default -O2 when CFLAGS is given, so the stock build is -O0. Injects ${PGLITE_OPT:--O2} at the front of CC_PGLITE.
  • build.sh.diff — passes PGLITE_OPT (and PGLITE_PIC, unused so far) into the container.
  • wasi-c.diff — makes the forced -fPIC in the compiler wrapper overridable (PGLITE_PIC). NOTE: the matching -fpic in postgresql-src/wasm-build/build-pgcore.sh (cloned at build time) is not covered by a diff here; the -fno-pic experiment did not take effect and is unmeasured.
  • max_wal_size.diff — goes in patches/postgresql-pglite/ as src-include-port-wasm_common.h.zz-max-wal-size.diff (it must sort after pglite4j's own wasm_common.h patch). PGlite splices a fixed list of -c settings into every start (WASM_PGOPTS), never reads postgresql.conf/postgresql.auto.conf on restart, and pg_reload_conf() is a no-op without a postmaster, so ALTER SYSTEM cannot change anything. Adding max_wal_size=128MB to that list caps pg_wal at ~130 MB instead of 1 GB once a CHECKPOINT runs.
  • message_context_reset.diff — goes in patches/pglite-wasm/. The fork stubs MemoryContextResetAndDeleteChildren (removed in PostgreSQL 17) as an empty macro, so MessageContext is never reset and every wire message leaks its parse/plan state (~10 KB per statement; a library scan reached 15 GB). The patch maps it to MemoryContextReset.
  • clear_error_portals.diff — goes in patches/pglite-wasm/. A PostgreSQL ERROR traps the wasm instance, which skips every PG_CATCH, so the active portal is never marked failed and the next statement fails with "cannot drop active portal". Wrapping AbortCurrentTransaction() in clear_error() with shmem_exit_inprogress = true makes AtAbort_Portals mark it failed.

Build (about 5 minutes on 16 cores):

cd /tmp/pglite4j/wasm-build
CLEAN=true DEBUG=false PGLITE_OPT=-O2 ./build.sh
xz -dc output/sdk-dist/pglite-wasi.tar.xz | gzip -1 > pglite-wasi-O2-fix.tar.gz

DEBUG=false matters: DEBUG=true (the default) builds single-threaded and keeps debug output.

Output layout ("v2" in db/pglite): tmp/pglite/bin/pglite.wasi with the share files embedded through wasi-vfs and a pre-initialized cluster in pgdata/ produced by wizer, so initdb never runs at startup (1.8 s instead of 13 s). Preopens must be /tmp, /pgdata, /dev in that order; PGDATA=/pgdata; do not call _start/pgl_initdb/ pgl_backend; call interactive_write(0) once; on a trap check pgl_check_error() then clear_error(), interactive_write(-1), one more interactive_one().

Result archive used by the tests and benchmark: tmp/pglite-wasi-O2-fix.tar.gz (6.7 MB; wasm 17.3 MB).

-fpic: measured, no effect (do not redo)

PGLITE_PIC must be patched into three places, and the one that matters is easy to miss: wasm-build/build-pgcore.sh at the workspace root is the copy that runs — postgresql-src/wasm-build/build-pgcore.sh is not. (Same for wasm-build.sh.) With all three patched, -fno-pic reaches configure and every compile.

Result: no measurable difference on any query, and the final wasm code section changes by 96 bytes out of 5.35 MB. -fPIC does change object files, but wasm-ld relaxes the relocations back to direct addressing when producing a static executable. Keep the upstream default.

Without the wizer snapshot (the durable build; 2026-09-04)

The snapshot makes reopening a data dir unsafe: it carries the pristine cluster's transaction counter and WAL position, and pgl_backend traps if asked to restart on a live snapshot. Build with PGLITE_WIZER=0 instead:

cd /tmp/pglite4j/wasm-build
DEBUG=false PGLITE_OPT=-O2 PGLITE_WIZER=0 ./build.sh
# package with a marker the bridge recognizes:
tar xJf output/sdk-dist/pglite-wasi.tar.xz -C /some/dir && touch /some/dir/tmp/pglite/NOSNAPSHOT
(cd /some/dir && tar czf pglite-wasi.tar.gz tmp pgdata)

Two quirks. The initdb pre-population step (wasmtime run --env INITDB_ONLY=1) failed on a clean output/pgdata with "cannot make new WAL entries during recovery"; restoring a pgdata/ from an earlier good archive into output/pgdata before building sidesteps it (the build then skips pre-init). And the cluster only has template1; the bridge connects to it explicitly. build.sh.diff now wraps only the wizer invocation in PGLITE_WIZER.