Files
deno/.gitmodules
Divy Srivastava 80bc12f9e1 perf: lazy-load OSX frameworks with lzld (#35341)
## Why

On `aarch64-apple-darwin`, the deno binary links 8 system frameworks —
CoreFoundation, Foundation, Security, CoreServices, Metal, QuartzCore,
CoreGraphics, MetalPerformanceShaders — but only touches them in
rarely-hit paths (`fs.watch`/FSEvents, keychain, WebGPU). Even with the
existing `-weak_framework` linkage, **dyld loads + initializes them at
process launch** (weak only makes missing symbols non-fatal; the dylib
is still loaded). That's pure startup cost paid on every `deno run`.

`dyld_info -imports` shows deno imports only ~47 symbols from these
frameworks; QuartzCore/CoreGraphics/MPS import **zero**.

## What

[lzld](https://github.com/littledivy/lzld) is a thin `ld64.lld` wrapper
that strips `-framework`/`-weak_framework` and links `liblzld` instead —
a static shim that provides exactly the symbols deno imports, each
`dlopen`-ing its framework on **first use**. Frameworks a given run
never touches are never loaded.

- Added as a git submodule at `tools/lzld`.
- **CI builds the macOS aarch64 release binary through lzld by default**
(`.github/workflows/ci.ts`): the release step inits the submodule,
builds `liblzld`, and patches the linker before `cargo build --release`.
Guarded to Darwin/arm64; other matrix entries skip it.
- Opt-in build helper `tools/build_lzld.sh` for local use; the default
`cargo build` is unchanged.
- `.cargo/config.toml` is left untouched: `-fuse-ld` requires an
*absolute* linker path (Apple clang rejects relative ones), computed at
build time by both CI and the helper.

## `otool -L` — frameworks gone from the load commands

**Before (shipping main):** 11 load commands, 8 frameworks loaded at
launch
```
$ otool -L $(which deno)
	/System/Library/Frameworks/Security.framework/.../Security
	/System/Library/Frameworks/CoreFoundation.framework/.../CoreFoundation
	/usr/lib/libobjc.A.dylib
	/System/Library/Frameworks/CoreServices.framework/.../CoreServices
	/usr/lib/libSystem.B.dylib
	/System/Library/Frameworks/QuartzCore.framework/.../QuartzCore (weak)
	/System/Library/Frameworks/Metal.framework/.../Metal (weak)
	/System/Library/Frameworks/Foundation.framework/.../Foundation
	/usr/lib/libiconv.2.dylib
	/System/Library/Frameworks/MetalPerformanceShaders.framework/.../MetalPerformanceShaders (weak)
	/System/Library/Frameworks/CoreGraphics.framework/.../CoreGraphics (weak)
```

**After (lzld build):** 3 load commands, 0 frameworks
```
$ otool -L target/release/deno
	/usr/lib/libSystem.B.dylib
	/usr/lib/libobjc.A.dylib
	/usr/lib/libiconv.2.dylib
$ otool -l target/release/deno | grep -iE 'Metal|CoreFoundation|Foundation|Security|QuartzCore|CoreServices|CoreGraphics'
  (none)
```

## Results

aarch64-macOS (Apple M4 Max, macOS 15.6), warm, lzld vs. stock main.
Interleaved A/B, 80 rounds each (means).

| invocation | main | lzld | Δ |
|---|---|---|---|
| `deno run empty.js` | 11.8 ms | 11.6 ms | −0.2 ms |
| `deno task echo` | 7.1 ms | 6.7 ms | −0.4 ms (−6%) |
| `deno --help` | 6.2 ms | 5.7 ms | −0.5 ms (−8%) |
| `deno --version` | 5.2 ms | 4.8 ms | −0.4 ms (−7%) |
| `LC_LOAD` dylibs | 11 | **3** | −8 |
| frameworks loaded at launch | 8 | **0** | −8 |

The saving is the framework load+init cost avoided at launch (~0.2–0.5
ms), consistent in direction; it's a larger fraction of the cheaper
commands. (deno startup is much faster than when
[#23716](https://github.com/denoland/deno/pull/23716) first proposed
this, so the absolute headroom is smaller now.)

## Correctness

The shim handles data constants that have no call to intercept
(`kCFAllocatorDefault` = NULL sentinel;
`kCFTypeArrayCallBacks`/`kCFRunLoopDefaultMode` substituted at the use
site once their framework loads). Validated on a standalone test
(CoreFoundation not loaded at startup, loads on first call,
`CFArray`/`kCFTypeArrayCallBacks` paths correct) and `deno run`/`eval`
work normally.

## Scope / caveats

- aarch64-macOS only; other targets unaffected.
- Symbol set is generated from `dyld_info -imports`; regenerate if
deno's framework imports change (see `tools/lzld/README.md`).
2026-06-20 10:21:36 +05:30

21 lines
656 B
Plaintext

[submodule "tests/util/std"]
path = tests/util/std
url = https://github.com/denoland/deno_std
shallow = true
[submodule "tests/wpt/suite"]
path = tests/wpt/suite
url = https://github.com/web-platform-tests/wpt.git
shallow = true
[submodule "tests/node_compat/runner/suite"]
path = tests/node_compat/runner/suite
url = https://github.com/denoland/node_test.git
shallow = true
ignore = untracked
[submodule "tests/bench/testdata/lsp_benchdata"]
path = tests/bench/testdata/lsp_benchdata
url = https://github.com/denoland/deno_lsp_benchdata.git
shallow = true
[submodule "tools/lzld"]
path = tools/lzld
url = https://github.com/littledivy/lzld