Skip to content

feat(lgbgen): compress the embedded core bundle (default off) - #502

Open
mparrett wants to merge 3 commits into
feat/lgb-compressionfrom
feat/lgb-compression-core
Open

feat(lgbgen): compress the embedded core bundle (default off)#502
mparrett wants to merge 3 commits into
feat/lgb-compressionfrom
feat/lgb-compression-core

Conversation

@mparrett

@mparrett mparrett commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #501 (base branch feat/lgb-compression).

What it does

Adds lgbgen --compress for emitting a compressed embedded-core bundle. The flag is off by default: the committed core remains uncompressed unless the maintainer explicitly chooses the size/startup tradeoff under #503.

The bundle is written atomically through a same-directory temporary file. Encoding, sync, chmod, and close must all succeed before rename, so a failed generation leaves the existing checked-in core_compiled.lgb intact.

Fresh measurements

Measured on an Apple M2 after rebasing onto the reviewed #501 format:

uncompressed compressed delta
core .lgb 297,359 B 89,776 B -207,583 B (3.31x)
lg binary 13,844,450 B 13,629,794 B -214,656 B
decode core 2.23 ms 3.40 ms +1.17 ms, +91,824 B/op, +303 allocs
warm process boot 7.9 ± 0.8 ms 9.8 ± 1.1 ms about +1.9 ms

Compression primarily benefits larger bundles; for smaller programs, the framing/codec overhead can consume or exceed the byte savings. The committed core is large enough to show a clear size win, but the startup regression makes default-on a policy decision rather than a mechanical one.

Encode cost for the core rose from about 1.35 ms to 24.37 ms and from 344,159 B/op to 2,220,688 B/op. That cost is paid during generation, not process startup.

Validation

  • Atomic-write tests cover preserving an existing destination after an encode failure, successful replacement, mode preservation, and temporary-file cleanup.
  • Decode and encode benchmarks are checked in under pkg/bytecode.
  • go test ./... passes locally.

Policy

Keep compression opt-in for now. The default-on decision remains under #503, including the stripping nuance and any target-specific defaults.

@mparrett

Copy link
Copy Markdown
Collaborator Author

Tracked under Epic #503 (Footprint & constrained targets).

@nnunley

nnunley commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

One note from a multi-agent pass (which found nothing wrong with this PR itself — the flag plumbing and the generated.sums change both verified correct): the bundle write in cmd/lgbgen is os.Create + direct write, so an encode error exits leaving a half-written core_compiled.lgb on disk for the next make build/make generate to trip over. That's pre-existing rather than introduced here — but since compression adds a new way for the encode to fail mid-stream and nothing else is queued to touch this path, this PR is a reasonable place to either fold in a write-to-temp + rename (a few lines), or spin it out as a fresh issue under Epic #503 so it doesn't get lost.

@nooga nooga left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR's own diff looks good — small, correct, and I independently reproduced the numbers in the description exactly (244,131 → 77,039 bytes, ~2x decode cost). Not requesting changes here since there's nothing wrong in these ~65 lines; the actual blocker is upstream in #501 (which this stacks directly on) — its rebase needs to land first, since #501's boot-path code has drifted from what main now has in boot.go/run.go.

One pre-existing item worth picking up while you're in this area, since it's specifically about the write path this PR adds compression to: writeBundle (main.go:591) does os.Create + direct-write-through-Encode, so an encode failure partway leaves a truncated core_compiled.lgb on disk — and that's the file checked into the repo, so a failed go generate would corrupt the committed artifact for the next build. nnunley flagged this back on 07-15 under Epic #503; write-to-temp + rename would close it. Not blocking this PR specifically, just flagging since it's adjacent to what you're touching.

Whether to default this on for the embedded core is the open policy question under #503 — no action needed from me there, that's yours to decide whenever you're ready.

@mparrett
mparrett force-pushed the feat/lgb-compression-core branch from d59e5ea to 7e61f8d Compare August 5, 2026 13:22
@mparrett
mparrett marked this pull request as ready for review August 6, 2026 03:39
@mparrett

mparrett commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Followed up on the review comments in 7e61f8da and refreshed the PR description with the post-rebase measurements.

  • Rebased the stack onto the updated feat(bytecode): opt-in DEFLATE compression for .lgb bundles #501 head.
  • Replaced the direct os.Create write with a same-directory temporary write followed by sync, close, mode preservation, and atomic rename. A failed encode leaves the existing checked-in bundle untouched; tests cover both failure preservation and successful replacement/cleanup.
  • Kept --compress default-off. The current M2 measurement is 297,359 → 89,776 bytes for the core and 13,844,450 → 13,629,794 bytes for the binary, with warm boot moving from 7.9 ± 0.8 ms to 9.8 ± 1.1 ms. That keeps default-on as the explicit policy question under Epic: Footprint & constrained targets #503.
  • Added checked-in decode and encode benchmarks; go test ./... passes locally.

@nooga / @nnunley, this should close the write-path concern. The only remaining question is the intentionally deferred default-on policy, including the stripping nuance noted for later discussion.

@mparrett

mparrett commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Note: First time trying this in the repo.

Stack #695 was created successfully
image

@mparrett
mparrett force-pushed the feat/lgb-compression-core branch from 7e61f8d to e016854 Compare August 9, 2026 01:17
@nnunley

nnunley commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Reads clean on top of #501. The atomic write nooga asked for is here — CreateTemp in the
destination dir, write → SyncChmodRename, mode preserved, with tests covering
failure-preserves-destination and no leftover temps. They carry //go:build bootstrap and CI
runs that tag (go.yml:90), so they actually execute.

Confirmed the committed core is untouched: --compress defaults off, core_compiled.lgb is
not in the diff, and generated.sums moves only because it digests lgbgen's own sources. I
deflated the branch's core at level 9 and got 301,252 → 89,806 bytes against your 297,359 →
89,776; the difference is core regeneration since you measured.

Two nits: no directory fsync after the rename (durability, not atomicity), and the size is
reported from the temp file's Stat rather than the destination — same bytes, just reads odd.

mparrett and others added 3 commits August 10, 2026 19:05
Program bundles are raw serialized bytecode today. A `FlagCompressed`
header bit deflates the module body (string table, chunks, consts, NS
table) while leaving the magic, version, flags, and capability payload —
including the opcode-set signature — in plaintext, so a version/opcode
mismatch is still rejected before any inflate. A one-byte codec tag after
the header (currently flate) reserves room for zstd without spending a
second flag bit.

Opt-in at compile time: `lg -c -z` and `lg -b -z`. A bundle without the
bit is byte-identical to before; decode auto-detects and inflates
transparently, so the full `lg`, `lg-runtime`, and the appended `-b`
payload all read compressed bundles with no caller change. The byte-backed
decode path (embedded-core / zero-copy source maps) inflates into a
resident buffer and re-wraps it, preserving deferred source-map slicing.

xsofy main.lg: 1,151,139 -> 394,052 bytes (2.9x). Stacks on #424 (the
runtime-only / self-contained-artifact work); compressing the embedded
core_compiled.lgb — the universal per-binary win — is the follow-up, kept
separate because it touches the boot hot path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Positional iota flag bits with per-version admitted sets close the
silent bit-collision failure mode with #624. Also normalize encode
version from flags, clarify lg -version, and reject bare -z.

Co-authored-by: Cursor <cursoragent@cursor.com>
Teach the core generator to emit a v3 DEFLATE-compressed bundle while keeping the committed core uncompressed by default. The generator now writes through a synced same-directory temporary file and renames only after encode and close succeed, so a failed generation cannot truncate the checked-in core artifact.

Fresh Apple M2 measurements on current main: core bundle 297,359 -> 89,776 bytes (3.31x); stripped native binary 13,844,450 -> 13,629,794 bytes (-214,656); warm end-to-end boot 7.9 +/- 0.8 -> 9.8 +/- 1.1 ms. In-process decode averaged 2.23 -> 3.40 ms, +91,824 B/op and +303 allocs/op.

Compressed generation is intentionally off the boot path but is more expensive: core encode averaged 1.35 -> 24.37 ms and 344,159 -> 2,220,688 B/op. Paired encode/decode benchmarks keep this tradeoff reproducible without committing a compressed core.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants