Skip to content

Improve startup time on macOS - #124

Merged
danlamanna merged 2 commits into
masterfrom
faster-startup
Oct 30, 2026
Merged

danlamanna merged 2 commits into
masterfrom
faster-startup

Conversation

@danlamanna

Copy link
Copy Markdown
Member

No description provided.

The macOS and Windows packaging jobs resolve "3.12" to the python.org build that
ships on the GitHub runners (/usr/local/bin/python3.12) rather than downloading a
python-build-standalone interpreter. python.org builds keep the stdlib's C
extensions as ~50 separate lib-dynload/*.so files plus libssl and libcrypto,
while python-build-standalone compiles them into libpython.

That matters on macOS because the onefile binary extracts every shared library
into a fresh temp directory on each launch, and the first load of a freshly
written Mach-O file costs ~120ms (dyld blocks in fcntl while the kernel runs its
first-execution checks). The released v12.7.0 macOS binary loads 49 such files
before printing --version.

Timing of `isic --version` (hyperfine, 10 runs after 2 warmups, Apple M2 Max,
macOS 15.8). Homebrew's Python 3.14 has the same split-stdlib layout as the
python.org build and stands in for it:

| build                                           | libs loaded | median | mean ± sd     | min   | max   |
|-------------------------------------------------|-------------|--------|---------------|-------|-------|
| master, Homebrew Python 3.14 (split stdlib)     | 51          | 6.51s  | 6.50s ± 0.02  | 6.47s | 6.55s |
| master, uv-managed Python 3.14 (this change)    | 6           | 1.48s  | 1.49s ± 0.02  | 1.45s | 1.51s |

For reference the released v12.7.0 macOS binary measures 6.3s on the same machine.
charset_normalizer wheels ship two mypyc-compiled extension modules (cd and md)
alongside their pure python fallbacks. On macOS, every shared library the onefile
binary extracts costs ~120ms on each launch (the first load of a freshly written
Mach-O file blocks in dyld while the kernel runs its first-execution checks), and
the size of the library barely matters: these two total under 1MB but cost ~240ms
in `-X importtime` inside the frozen binary versus ~4ms unfrozen.

Installing charset_normalizer from its sdist in the package env gives the pure
python modules (the sdist only builds the mypyc extensions when
CHARSET_NORMALIZER_USE_MYPYC=1). Only requests uses it, for guessing the encoding
of responses without a charset, which isn't on any hot path here.

Timing of `isic --version` (hyperfine, 10 runs after 2 warmups, two rounds each,
Apple M2 Max, macOS 15.8, uv-managed Python 3.14):

| build                          | libs loaded | median | mean ± sd    | min   | max   |
|--------------------------------|-------------|--------|--------------|-------|-------|
| previous commit                | 6           | 1.53s  | 1.55s ± 0.07 | 1.47s | 1.70s |
| previous commit, second round  | 6           | 1.51s  | 1.52s ± 0.04 | 1.46s | 1.61s |
| this commit                    | 4           | 1.23s  | 1.23s ± 0.02 | 1.19s | 1.27s |
| this commit, second round      | 4           | 1.23s  | 1.23s ± 0.02 | 1.18s | 1.26s |
@danlamanna
danlamanna merged commit c6f1fab into master Oct 30, 2026
9 checks passed
@danlamanna
danlamanna deleted the faster-startup branch October 30, 2026 14:58
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