Keep the boot image under Chez's LZ4 fasl ceiling, and add --boot fast|small|plain - #889
Merged
Conversation
added 2 commits
September 7, 2026 19:53
…line vfasl
A binary whose vfasl boot image reaches 256MiB died inside Sbuild_heap before a
line of its own code ran, with "uncompressed size -N ... is smaller than
expected size M". Chez's c/new-io.c returns an LZ4 entry's decompressed length
as Sfixnum(r) with `int r`, and Sfixnum is ((ptr)(uptr)((x)*8)) — the multiply
happens in the argument's own type, so 2^28 and up overflow to a negative fixnum
and c/fasl.c's length check can never match. The gzip arm of the same function
hands zlib a uLong and has no ceiling.
0.8.5 is what made it reachable. A plain boot is one compressed entry per
top-level form and its entries are kilobytes; vfasl-convert-file combines each
input boot file into ONE entry, so a program's whole compiled half became a
single image. jolt links against whatever Chez the machine has and cannot fix
the kernel, so it keeps the image off the ceiling instead: build.ss reads back
the converted boot's entry headers and re-encodes with gzip when an LZ4 entry
declares 2^28 or more. Only a previously-broken build changes; every image under
the ceiling is byte-for-byte what it was. Covers the self-contained, cc,
--library and build-jolt paths.
The codec choice sits behind sa-vfasl-convert-file's new optional argument,
since compress-format is a Chez name and the portability gate allows those only
in the adapter.
Also adds --no-vfasl, :jolt/build {:no-vfasl true} and JOLT_NO_VFASL=1, which
keep the plain boot for an app whose download size matters more than its startup
— an iOS --target tpb64l build grew 7.6MB in the binary and ~5MB in the IPA with
no way to decline. Measured on the build smoke's app, one image, three boots:
vfasl+LZ4 27.6MB/0.26s, vfasl+gzip 16.8MB/0.44s, plain 26.0MB/0.50s. The gzip
boot beats the opt-out on both axes, so the tradeoff is documented rather than
assumed; exposing the codec as a build choice is tracked separately.
make vfaslceiling pins the kernel fact, the entry scanner and the fallback, the
last by lowering the ceiling under a boot it can build in a second. Its check
that LZ4 *fails* at 2^28 is deliberately a check on Chez: when a future release
fixes the overflow it turns red, which is the signal to delete the workaround.
Closes #886
The opt-out added for jolt#886 turned out to be the wrong end of the curve to expose. Measuring the three boot encodings jolt can produce, over two apps and two machine types (binary size and warm start, against the plain boot): hello, host ta6le plain 25,919,203/495ms lz4 +5.5%/249ms gzip -35.7%/429ms build-app, host ta6le plain 26,062,746/502ms lz4 +5.8%/250ms gzip -35.5%/434ms hello, target tpb64l plain 24,873,035 lz4 +5.8% gzip -38.1% A gzip-compressed vfasl boot beats the plain boot on BOTH axes for every jolt app measured: about a third smaller and still faster to start, because the size cost is mostly the codec's rather than vfasl's. On tpb64l — the target in #886 — it lands 9.5MB below the plain boot the report asked for, where the complaint was the default costing 7.6MB. So an app that wants a small download wants the codec, and --no-vfasl alone would have pointed everyone at a floor nobody should want. --boot is ordered along that curve: fast (vfasl+LZ4, the default, unchanged), small (vfasl+gzip), plain (no vfasl). :jolt/build {:boot :small} and JOLT_BOOT follow; --no-vfasl, :no-vfasl and JOLT_NO_VFASL stay as aliases for `--boot plain`, which is the spelling #886 asked for. Precedence is resolved once, in jolt.main — flag, then deps.edn, then environment — rather than half there and half in build.ss. plain stays available because a target that cannot vfasl at all still needs it. The ratios are a property of what is in the image, not of the machine, so nothing user-facing quotes one: the same three encodings over Chez's own boots, which carry no jolt runtime, cost fast +37% and gain small only 3-4%, with small there slower than plain. That is also why the #886 reporter saw +24% where a jolt app sees +6%. The build smoke covers all three modes, asserting the artifact (a build that quietly converted anyway is what plain exists to prevent), the size ordering small < fast and small < plain (which is what catches `small` silently falling back to the default), that a bad value is rejected, and that each binary runs.
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.
Two things about the boot image 0.8.5 introduced: one that stops a large binary from starting at all, and the opt-out asked for in #886.
A large binary's boot image loads again
A program big enough for its vfasl boot image to reach 256MiB built fine and then died on every run, inside
Sbuild_heap, before a line of its own code had executed:The negative number is the tell. Chez's kernel decompresses a fasl entry and compares the result against the size the entry declares; on the LZ4 arm (
c/new-io.c,S_bytevector_uncompress) it returns that result asSfixnum(r)withint r, andSfixnumis((ptr)(uptr)((x)*8))— the multiply happens in the argument's own type. At 2^28 bytes it overflowsint, the length comes back negative, and the comparison can never succeed. The gzip arm of the same function hands zlib auLongand has no ceiling. Measured against Chez 10.4.1: LZ4 round-trips at 2^28-1, fails at 2^28, and gzip round-trips at both.Nothing before 0.8.5 could reach it. A plain boot is one compressed entry per top-level form and its entries are kilobytes; the vfasl boot 0.8.5 introduced combines each input boot file into ONE entry, so a program's whole compiled half became a single image — 83MB for the build smoke's app, 43MB for jolt itself, and past 256MiB an executable that cannot start. The failure scaled with the program, which is the worst shape for it: every app built with 0.8.5 worked, right up to the one that didn't.
jolt links against whatever Chez the machine has, so it cannot fix the kernel; it keeps the image off the ceiling instead.
jolt buildnow reads back the entry headers of the boot it just converted, and when an LZ4 entry declares an uncompressed size at or over 2^28 it re-encodes the image with gzip and says so. Only a build that was previously broken changes — every image under the ceiling is byte-for-byte what it was — and it keeps the vfasl format, so what it gives up is decompression speed, not the load. The same check covers--libraryand jolt's own boot.The codec choice sits behind
sa-vfasl-convert-file's new optional argument, sincecompress-formatis a Chez name and the portability gate allows those only in the adapter.jolt build --boot fast|small|plainpicks how the boot is encodedCloses #886. A vfasl boot is an image of the loaded heap — it starts fast and takes room — and there was no way to decline it: the reported iOS
--target tpb64lbuild grew 7.6MB in the binary and about 5MB in the compressed IPA.The flag is ordered along the one curve those numbers sit on:
--bootfast(default)smallplain:jolt/build {:boot :small}andJOLT_BOOT=smallfollow;--no-vfasl,:no-vfasl trueandJOLT_NO_VFASL=1— the spelling #886 asked for — stay as aliases for--boot plain. Precedence is resolved in one place (flag, thendeps.edn, then environment). Covers the self-contained, cc-linked and--librarypaths; jolt's own boot is not ajolt buildand is unaffected.Why it is a three-way flag and not just an opt-out. The size cost turns out to be mostly the codec's rather than vfasl's, so for a jolt app
smallbeatsplainon both axes at once. Measured over two apps and two machine types — binary size and warm start (min of 12 runs), against the plain boot as the baseline, with only the appended boot payload differing between binaries:plainfastsmallta6leta6letpb64lOn
tpb64l— the target in #886 —smalllands 9.5MB below the plain boot the report asked for, where the complaint was the default costing 7.6MB.plainstays because a target that cannot vfasl at all still needs it, not because it is the size answer.The ratios are a property of what is in the image, not of the machine, so nothing user-facing quotes one: the same three encodings over Chez's own boots, which carry no jolt runtime, cost
fast+37% and gainsmallonly 3–4%, withsmallthere slower thanplain. That is also why the reporter saw +24% where a jolt app sees +6%.tpb64lnumbers were taken by cross-building against a real target pack (Chez 10.4.1,make bootquick XM=tpb64lplus a cross kernel) — no emulation needed, since portable bytecode runs over a native host kernel. Two notes found on the way: plainpbcannot vfasl at all ("cannot vfasl with unknown endianness"), so an endianness-pinned machine is required; and atpb64lpack whose kernel lacks libffi produces a binary that aborts at startup, because jolt's runtime usesforeign-procedure.Gates
make vfaslceiling(new, inCI-GATES) pins all three legs the fix rests on — that the ceiling is real and is exactly 2^28, that gzip has none, and that the entry scanner and the fallback do what they claim, the last by lowering the ceiling under a boot small enough to build in a second. The check for LZ4 failing at 2^28 is deliberately a check on the kernel: when a future Chez fixes the overflow it turns red, and that is the signal to delete the workaround rather than a regression. It runs withJOLT_MAX_HEAP=offbecause two of its checks have to allocate 256MiB to ask the question at all.build-smoke.shgains a--bootcase covering all three modes: asserted by the artifact (a build that quietly converted anyway is whatplainexists to prevent), by the size orderingsmall < fastandsmall < plain(which is what catchessmallsilently falling back to the default), by a bad value being rejected, and by running each binary.Verified locally:
vfaslceiling15/15,buildsmoke(including the new case, through the self-contained path),portcheck,adaptercheck,manifestcheck,readmecheck,completionssmoke, and two fullmake jolt-releasebuilds. With the ceiling forced to 1024, a realjolt buildre-encoded and the resulting binary ran — so a Chez kernel does boot a gzip vfasl image.buildlibsmokeskips on this machine (its Chez is not-fPIC); CI runs it underJOLT_REQUIRE_BUILDLIB=1.