Skip to content

perf: onedir backend takes warm desktop launches from 20s to 2s - #6

Merged
ycli0536 merged 1 commit into
mainfrom
onedir-backend-layout
Aug 30, 2026
Merged

perf: onedir backend takes warm desktop launches from 20s to 2s#6
ycli0536 merged 1 commit into
mainfrom
onedir-backend-layout

Conversation

@ycli0536

Copy link
Copy Markdown
Owner

Step 2 of the startup plan — the other half of the 37s, after #4 removed the matplotlib half.

Why onefile was slow forever

macOS caches code-signature validation by path. Onefile extracts its 39MB archive to a fresh _MEI temp path on every launch, so every native library in the scientific stack was re-validated every time — ~20s, on every launch, with no way to ever get faster.

Onedir ships the tree at a stable path inside the bundle, so validation is paid once per install/update.

How it ships

externalBin wants one file per target triple, so a directory can't go through it. Instead:

  • build_sidecar.py freezes with --onedir and stages the tree to frontend/src-tauri/resources/backend/ (tests rewritten: staging, exec bit, stale-tree replacement, missing-executable error).
  • tauri.conf.json declares it in the resources map.
  • lib.rs resolves the executable via resource_dir() and spawns it with the same tauri-plugin-shell Command machinery — the port/parent-pid/log-forwarding code is untouched.

Both desktop workflows already run build_sidecar.py before tauri build, so CI needs zero changes. (tauri-build validates the resource path at compile time, so a bare cargo check now needs the backend staged first — same de-facto requirement externalBin had.)

Bonus: the orphan-process root cause is gone

Onedir has no bootloader middle layer — the spawned process is the Python server, so CommandChild::kill() reaches it directly. The orphan class #3 worked around can no longer occur on the normal quit path; the parent-pid watchdog stays for crash/force-quit, where no exit handler runs.

Measured on the packaged .app (aarch64-darwin)

scenario before this PR now
warm launch → backend ready 20–22s (37–39s before #4) 2.2s
backend process alone, warm 0.6–0.8s
first-ever launch (cold caches) 20–22s ~23s, once per install/update
after an OS reboot 20–22s 3.3s — the validation cache survives reboots
two instances ✓ 3354 + OS-assigned, both serving
clean quit ✓ (via watchdog) ✓ direct kill, no orphan, port released
SIGKILL on the shell ✓ watchdog ✓ watchdog, seconds

An unplanned bonus test: the machine rebooted mid-verification with two instances running — on login, macOS auto-relaunched the app and the whole port chain came up correctly.

Disk: staged tree is 115M vs the 39M onefile archive; the uncompressed layout is exactly what makes launches fast, and installers compress it back down.

Backend suite: 382 passed.

Remaining from the original plan: a frontend "backend starting" indicator to cover the one-time cold launch; that's small and separate.

🤖 Generated with Claude Code

Onefile extracted a 39MB archive to a fresh temp path on every launch, so
macOS re-validated every native library's code signature every time - the
validation cache is keyed by path, and the path never repeated. That was
the ~20s that remained after dropping matplotlib, and it was paid on
every single launch, forever.

The backend now freezes with PyInstaller's onedir layout and ships at a
stable path inside the bundle, so signature validation is paid once per
install or update.

A directory cannot go through tauri.conf.json's externalBin - that
contract wants one file per target triple - so the tree is staged under
frontend/src-tauri/resources/backend/ and declared in the resources map;
the shell resolves the executable through resource_dir() and spawns it
with the same tauri-plugin-shell Command machinery as before. Both
desktop workflows already run build_sidecar.py before tauri build, so CI
needs no changes.

Onedir also removes PyInstaller's bootloader middle layer: the process
the shell spawns IS the Python server, so CommandChild::kill() reaches it
directly. The orphan class fixed by the parent-pid watchdog cannot occur
on the normal quit path anymore; the watchdog stays for what no exit
handler can cover (crash, force quit).

Measured on the packaged .app (aarch64-darwin):

- warm launch, open to backend ready:  2.2s   (was 37-39s originally,
  20-22s after dropping matplotlib)
- backend process alone, warm:         0.6-0.8s
- first-ever launch (cold caches):     ~23s, once per install/update
- after an OS reboot:                  3.3s - the validation cache
  survives reboots, so only installs and updates pay the cold price
- two instances: ports 3354 + OS-assigned, both serving
- clean quit: all processes exit, no orphan, port released
- SIGKILL on the shell: watchdog removes the backend within seconds

Disk cost: the staged tree is 115M against the 39M onefile archive - the
uncompressed layout is exactly what makes launches fast, and installers
compress it back down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ycli0536
ycli0536 merged commit 57ac481 into main Aug 30, 2026
10 checks passed
@ycli0536
ycli0536 deleted the onedir-backend-layout branch August 30, 2026 23:43
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.

1 participant