You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ESP32-S3 first-time install misses 4 min at 10 Mbit (5.81 min, 399 MB) #1944
PR #1943 makes the build benchmark measure real first-time installs. On a 10 Mbit link, fbuild takes 5.81 min on ESP32-S3 and receives 399 MB (PlatformIO: 10.89 min, 675 MB).
Where the download goes
Toolchain: 168 MB (xtensa-esp-elf-14.2.0)
esptool bundle: 82 MB (esptool-linux-amd64.zip)
Libs slice: 86 MB (esp32s3; the combined bundle is about 347 MB)
Core: 41 MB (esp32-core-3.3.12)
Network overhead and small items make up the rest.
Target
Under 4 min needs about 115 MB less on the wire. Dropping the esptool bundle alone gives about 312 MB, or about 4.4 min, so the toolchain has to shrink too.
Options to evaluate
Replace the esptool bundle with something smaller. PlatformIO's esptoolpy package is about 3.4 MB installed but needs Python, or implement elf2image natively.
Publish a single-chip, zstd-compressed toolchain on the mirror.
Measured the first option on real hardware/host, and PR #1945 takes it:
pioarduino publishes the same esptool release as the 617 KiB source archive that fbuild already installs for Windows (esptoolpy-v5.3.0.zip → v5.3.0/esptool.zip). The standalone bundle is 82,508,446 B; the archive is 617,641 B.
Images are byte-identical (same elf2image argv, sha256=3ef75dd1…), and the source path is faster warm: 0.10 s vs 0.29–0.31 s per image, because the PyInstaller one-file bundle unpacks ~217 MB on every invocation.
Cold install of the source path end-to-end: 26.8 s, then 4.2 s warm. It needs uv (present for every uv tool install fbuild) and a Python 3.10 interpreter, so the fallback to the standalone binary stays and engages only when that environment cannot be built.
That removes ~70 MiB: ~331 MB / ~4.4 min at 10 Mbit, which matches your estimate for dropping the bundle. The remaining gap to under 4 min is the 168 MB toolchain, i.e. the mirror-side "single-chip, zstd-compressed toolchain" option — this PR does not touch it, so the issue stays open for that.
Measured the remaining option on the real toolchain fbuild installs, so it stops being a guess.
Source: the extracted xtensa-esp-elf 13.2.0 unified toolchain already in the local fbuild
prod cache (660 MB extracted, linux x86_64), re-packed with tar + zstd. "Single-chip" excludes xtensa-esp-elf/lib/esp32 and lib/esp32s2, keeping esp32s3.
archive
size
vs the 168 MB vendor download
whole, zstd -3
173.6 MB
+3%
whole, zstd -19
125.7 MB
−42 MB (25%)
single-chip esp32s3, zstd -3
100.6 MB
−67 MB (40%)
single-chip esp32s3, zstd -19
73.1 MB
−95 MB (56%)
Why slicing pays: the per-chip SDK libs are lib/esp32 204 MB + lib/esp32s2 103 MB + lib/esp32s3 102 MB of 660 MB. Compiler/cc1/ld binaries are chip-shared (bin 80 MB, libexec 80 MB) and the top-level multilib archives (~66 MB) stay, so the chip halves are
exactly the removable bulk.
Compression alone is not enough — that is the load-bearing detail. zstd -19 on the whole
tree saves only 42 MB (125.7 MB); the slice is what buys the other 53 MB. Doing "compress the
toolchain" without slicing would leave you at 275 MB / 4.0 min, i.e. still on the line.
Completeness check, not just a smaller tarball: the zstd -19 slice was extracted to a clean dir
and bin/xtensa-esp32s3-elf-gcc reports 13.2.0, -print-libgcc-file-name resolves inside the
slice (<slice>/lib/gcc/xtensa-esp-elf/13.2.0/esp32s3/libgcc.a), and compiling a C++ TU with -std=gnu++17 -Os -c exits 0.
Projection at this issue's own measured rate (399 MB / 5.81 min = 68.7 MB/min ≈ 9.2 Mbit/s):
plus the single-chip zstd toolchain: 222 MB → 3.24 min — clears the under-4-min target
Caveat: I measured 13.2.0 because 14.2.0 (xtensa-esp-elf-14.2.0_20260121, the one in your
399 MB trace) is not in this host's cache. SDK lib content differs, so expect ~73–85 MB rather
than a byte-exact 73.1; re-measure on 14.2.0 at publish time.
Not implemented here: the server half is a slicer + publisher modelled on the existing ci/slice_esp_libs.py → ci/publish_esp_slices.py → @esp-libs shard path (the mirror
credentials are present on this host), and the client half needs a manifest contract mapping
toolchain URL + MCU → chip slice plus install-layout/caching keys, with the whole archive kept
as the fallback. That is a multi-run change; this comment is the sizing, with real bytes.
Server half of the last option is now implemented rather than sized: #1947 adds ci/slice_toolchain.py, which emits one deterministic per-chip tar.zst plus a slices.json index carrying the same manifest_slices shape the ESP libs slicer
already produces.
Measured on the real cached xtensa-esp-elf 13.2.0 tree (--chips esp32s3 --verify, 4m50s including verification):
value
slice
75,299,480 B (75.3 MB)
payload
423,807,794 B, 2,030 entries
vendor unified download
168 MB
sha256
00a6ca50…c66c
The slice is verified by using it: extracting it and running its own xtensa-esp32s3-elf-gcc (13.2.0), -print-libgcc-file-name resolving to lib/gcc/xtensa-esp-elf/13.2.0/esp32s3/libgcc.ainside the extraction, and a -std=gnu++17 -Os -c probe compile. Selection is by path component, so bin/libexec/include and the top-level multilib archives are never dropped; bin+libexec are chip-shared and are the bulk of what remains.
Two things the implementation had to get right that the sizing did not show:
the real tree ships an in-tree .. symlink (lib/bfd-plugins/liblto_plugin.so -> ../../libexec/...), so the guard tests containment rather than rejecting ..;
and .install_complete is never archived, because fbuild's install check is
directory-existence based and a stale marker would let a partial extraction pass.
On the issue's own measured rate (68.7 MB/min) this puts the install at
~222 MB / ~3.24 min after #1945, inside the under-4-min target.
Still open on this issue: publishing the slices to the mirror (key layout +
shard registration, modelled on publish_esp_slices.py) and the client half
(resolve toolchain URL + MCU → slice with the whole archive as fallback). I
deliberately did not upload archives that nothing resolves yet; the client PR is
where the end-to-end ESP32-S3 build proves this path.
Update after #1945, which replaced the 82 MB esptool bundle with a 617 KiB source archive. The published nightly (run 42359356709, ab98de1c) shows ESP32-S3 fbuild cold at 4.51 min / 310.8 MB (PlatformIO 10.83 min / 676 MB, Arduino CLI 26.64 min / 1.92 GB).
Getting under 4 min needs about 30 MB more off the wire. The 168 MB toolchain is the remaining big item.
Client half is merged: #1949 (53277a80). fbuild now resolves per-chip toolchain slices; it no longer needs the whole 168 MB unified archive when a slice is published.
What landed:
@esp-toolchain shard for toolchain-xtensa-esp-elf, alongside the existing @esp-libs one. The RISC-V toolchain is deliberately not slice-capable (its multilibs are ISA variants, not chips).
A slice installs into its own chip-scoped cache dir (<version>+<chip>), because a slice holds ONE chip's SDK libs; a whole archive keeps the shared dir and is preferred when already installed, so existing hosts never re-download and two chips still get two correct trees.
A listed slice is authoritative: a corrupt slice fails the install instead of quietly fetching the full archive.
Esp32Toolchain::with_slice_variant + the orchestrator pass build.mcu on both the build and install --check paths.
Verified: 265 lib tests (5 new drive the real staged_install against a local server and assert the vendor archive is never requested), 222 fbuild-build-esp tests, workspace clippy -D warnings, dylint for both touched crates, and 3 mutations RED.
Still open on this issue: publishing the slices (publish_esp_slices.py-shaped uploader + the @esp-toolchain shard entry, keyed so slices.json from ci/slice_toolchain.py maps onto the manifest's asset.slices), then the end-to-end ESP32-S3 install measurement — the ~222 MB / ~3.24 min projection is unproven until a slice is actually served. Until then every client takes the whole-archive fallback, i.e. today's behaviour.
Publishing is done and the path is now measured end-to-end on this host.
Server (#1951, e729c666): @esp-toolchain is a live shard. linux-amd64 / 14.2.0_20260121 slices, each --verifyd and re-downloaded + hashed against its
pin after upload: esp32s3 106,462,349 B, esp32s2 106,655,161 B, esp32
141,361,956 B, against the 176,559,292 B vendor archive (whole bundle mirrored
bucket-first too, pinned sha da31f36d…). ci/publish_toolchain_slices.py is
the reproducible uploader; --check keeps the Index descriptor honest.
Client (#1953, 6e5be796) — both published slices were inert before it, and
that is why the earlier numbers never moved:
slice_shard matched only toolchain-xtensa-esp-elf, while Esp32Toolchain
names its package esp32-xtensa-gcc: the shard was never even fetched.
The slice was then re-verified against the WHOLE archive's checksum
(expected da31f36d…, got 36a9f0e7…), so every slice install aborted.
Measured after both fixes (rebuilt CLI + daemon, EMPTY isolated cache,
minimal esp32-s3-devkitc-1 project): toolchain 106,462,349 B slice instead of
the 168 MB archive; install dir …-linux-gnu+esp32s3 = 603 MB instead of 1.2 GB;
two shards fetched (esp-libs + @esp-toolchain); no whole archive on disk; build succeeded in 65.1s with flash 355899 / ram 74919 — identical firmware
sizes to the whole-archive build, so the slice is not a degraded toolchain.
Still open on this issue:
Other host packages (linux-arm64, macos*, win32) need their extracted
tree and are one publish_toolchain_slices.py invocation each; those hosts keep
the whole archive until then (supported fallback, not a failure).
The benchmark's first-install measurement should now show the ~222 MB /
under-4-min projection; that needs a nightly run with the released tool.
Note for whoever reads Pages: fastled.github.io snapshots main on github-pages deployments that do not fire on every push, so until the next
one the primary manifest answers NotListed for @esp-toolchain and clients
fall back to whole archives (raw.githubusercontent is already current).
Activity
zackees commentedon Nov 7, 2026
Measured the first option on real hardware/host, and PR #1945 takes it:
esptoolpy-v5.3.0.zip→v5.3.0/esptool.zip). The standalone bundle is 82,508,446 B; the archive is 617,641 B.elf2imageargv,sha256=3ef75dd1…), and the source path is faster warm: 0.10 s vs 0.29–0.31 s per image, because the PyInstaller one-file bundle unpacks ~217 MB on every invocation.uv(present for everyuv tool install fbuild) and a Python 3.10 interpreter, so the fallback to the standalone binary stays and engages only when that environment cannot be built.That removes ~70 MiB: ~331 MB / ~4.4 min at 10 Mbit, which matches your estimate for dropping the bundle. The remaining gap to under 4 min is the 168 MB toolchain, i.e. the mirror-side "single-chip, zstd-compressed toolchain" option — this PR does not touch it, so the issue stays open for that.
zackees commentedon Nov 7, 2026
Measured the remaining option on the real toolchain fbuild installs, so it stops being a guess.
Source: the extracted
xtensa-esp-elf13.2.0 unified toolchain already in the local fbuildprod cache (660 MB extracted, linux x86_64), re-packed with tar + zstd. "Single-chip" excludes
xtensa-esp-elf/lib/esp32andlib/esp32s2, keeping esp32s3.Why slicing pays: the per-chip SDK libs are
lib/esp32204 MB +lib/esp32s2103 MB +lib/esp32s3102 MB of 660 MB. Compiler/cc1/ld binaries are chip-shared (bin80 MB,libexec80 MB) and the top-level multilib archives (~66 MB) stay, so the chip halves areexactly the removable bulk.
Compression alone is not enough — that is the load-bearing detail. zstd -19 on the whole
tree saves only 42 MB (125.7 MB); the slice is what buys the other 53 MB. Doing "compress the
toolchain" without slicing would leave you at 275 MB / 4.0 min, i.e. still on the line.
Completeness check, not just a smaller tarball: the zstd -19 slice was extracted to a clean dir
and
bin/xtensa-esp32s3-elf-gccreports 13.2.0,-print-libgcc-file-nameresolves inside theslice (
<slice>/lib/gcc/xtensa-esp-elf/13.2.0/esp32s3/libgcc.a), and compiling a C++ TU with-std=gnu++17 -Os -cexits 0.Projection at this issue's own measured rate (399 MB / 5.81 min = 68.7 MB/min ≈ 9.2 Mbit/s):
Caveat: I measured 13.2.0 because 14.2.0 (
xtensa-esp-elf-14.2.0_20260121, the one in your399 MB trace) is not in this host's cache. SDK lib content differs, so expect ~73–85 MB rather
than a byte-exact 73.1; re-measure on 14.2.0 at publish time.
Not implemented here: the server half is a slicer + publisher modelled on the existing
ci/slice_esp_libs.py→ci/publish_esp_slices.py→@esp-libsshard path (the mirrorcredentials are present on this host), and the client half needs a manifest contract mapping
toolchain URL + MCU → chip slice plus install-layout/caching keys, with the whole archive kept
as the fallback. That is a multi-run change; this comment is the sizing, with real bytes.
zackees commentedon Nov 7, 2026
Server half of the last option is now implemented rather than sized: #1947 adds
ci/slice_toolchain.py, which emits one deterministic per-chiptar.zstplus aslices.jsonindex carrying the samemanifest_slicesshape the ESP libs sliceralready produces.
Measured on the real cached
xtensa-esp-elf13.2.0 tree (--chips esp32s3 --verify, 4m50s including verification):00a6ca50…c66cThe slice is verified by using it: extracting it and running its own
xtensa-esp32s3-elf-gcc(13.2.0),-print-libgcc-file-nameresolving tolib/gcc/xtensa-esp-elf/13.2.0/esp32s3/libgcc.ainside the extraction, and a-std=gnu++17 -Os -cprobe compile. Selection is by path component, sobin/libexec/includeand the top-level multilib archives are never dropped;bin+libexecare chip-shared and are the bulk of what remains.Two things the implementation had to get right that the sizing did not show:
the real tree ships an in-tree
..symlink (lib/bfd-plugins/liblto_plugin.so -> ../../libexec/...), so the guard tests containment rather than rejecting..;and
.install_completeis never archived, because fbuild's install check isdirectory-existence based and a stale marker would let a partial extraction pass.
On the issue's own measured rate (68.7 MB/min) this puts the install at
~222 MB / ~3.24 min after #1945, inside the under-4-min target.
Still open on this issue: publishing the slices to the mirror (key layout +
shard registration, modelled on
publish_esp_slices.py) and the client half(resolve toolchain URL + MCU → slice with the whole archive as fallback). I
deliberately did not upload archives that nothing resolves yet; the client PR is
where the end-to-end ESP32-S3 build proves this path.
zackees commentedon Nov 7, 2026
Update after #1945, which replaced the 82 MB esptool bundle with a 617 KiB source archive. The published nightly (run 42359356709,
ab98de1c) shows ESP32-S3 fbuild cold at 4.51 min / 310.8 MB (PlatformIO 10.83 min / 676 MB, Arduino CLI 26.64 min / 1.92 GB).Getting under 4 min needs about 30 MB more off the wire. The 168 MB toolchain is the remaining big item.
zackees commentedon Nov 7, 2026
Client half is merged: #1949 (
53277a80). fbuild now resolves per-chip toolchain slices; it no longer needs the whole 168 MB unified archive when a slice is published.What landed:
@esp-toolchainshard fortoolchain-xtensa-esp-elf, alongside the existing@esp-libsone. The RISC-V toolchain is deliberately not slice-capable (its multilibs are ISA variants, not chips).<version>+<chip>), because a slice holds ONE chip's SDK libs; a whole archive keeps the shared dir and is preferred when already installed, so existing hosts never re-download and two chips still get two correct trees.Esp32Toolchain::with_slice_variant+ the orchestrator passbuild.mcuon both the build andinstall --checkpaths.Verified: 265 lib tests (5 new drive the real
staged_installagainst a local server and assert the vendor archive is never requested), 222fbuild-build-esptests, workspace clippy-D warnings, dylint for both touched crates, and 3 mutations RED.Still open on this issue: publishing the slices (
publish_esp_slices.py-shaped uploader + the@esp-toolchainshard entry, keyed soslices.jsonfromci/slice_toolchain.pymaps onto the manifest'sasset.slices), then the end-to-end ESP32-S3 install measurement — the ~222 MB / ~3.24 min projection is unproven until a slice is actually served. Until then every client takes the whole-archive fallback, i.e. today's behaviour.zackees commentedon Nov 8, 2026
Publishing is done and the path is now measured end-to-end on this host.
Server (#1951,
e729c666):@esp-toolchainis a live shard. linux-amd64 /14.2.0_20260121slices, each--verifyd and re-downloaded + hashed against itspin after upload:
esp32s3106,462,349 B,esp32s2106,655,161 B,esp32141,361,956 B, against the 176,559,292 B vendor archive (whole bundle mirrored
bucket-first too, pinned sha
da31f36d…).ci/publish_toolchain_slices.pyisthe reproducible uploader;
--checkkeeps the Index descriptor honest.Client (#1953,
6e5be796) — both published slices were inert before it, andthat is why the earlier numbers never moved:
slice_shardmatched onlytoolchain-xtensa-esp-elf, whileEsp32Toolchainnames its package
esp32-xtensa-gcc: the shard was never even fetched.(
expected da31f36d…, got 36a9f0e7…), so every slice install aborted.Measured after both fixes (rebuilt CLI + daemon, EMPTY isolated cache,
minimal
esp32-s3-devkitc-1project): toolchain 106,462,349 B slice instead ofthe 168 MB archive; install dir
…-linux-gnu+esp32s3= 603 MB instead of 1.2 GB;two shards fetched (
esp-libs+@esp-toolchain); no whole archive on disk;build succeeded in 65.1swithflash 355899 / ram 74919— identical firmwaresizes to the whole-archive build, so the slice is not a degraded toolchain.
Still open on this issue:
linux-arm64,macos*,win32) need their extractedtree and are one
publish_toolchain_slices.pyinvocation each; those hosts keepthe whole archive until then (supported fallback, not a failure).
under-4-min projection; that needs a nightly run with the released tool.
fastled.github.iosnapshotsmainongithub-pagesdeployments that do not fire on every push, so until the nextone the primary manifest answers
NotListedfor@esp-toolchainand clientsfall back to whole archives (raw.githubusercontent is already current).