Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: lazy OSX frameworks with lzld #23716

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ rustflags = [
]

[target.aarch64-apple-darwin]
# The purpose of lzld is to reduce startup time on Mac
# by lazy loading frameworks. See tools/lzld/README.md
linker = "tools/lzld/lzld"
rustflags = [
"-C",
"link-args=-fuse-ld=lld -weak_framework Metal -weak_framework MetalPerformanceShaders -weak_framework QuartzCore -weak_framework CoreGraphics",
"linker-flavor=ld64.lld",
]

[target.'cfg(all())']
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,16 @@ const ci = {
run:
"deno run --allow-write --allow-read --allow-run=git ./tests/node_compat/runner/setup.ts --check",
},
// For remote debugging only.
// {
// name: "Setup tmate session",
// if: [
// "(matrix.job == 'test' || matrix.job == 'bench') &&",
// "matrix.profile == 'debug' && (matrix.use_sysroot ||",
// "github.repository == 'denoland/deno')",
// ].join("\n"),
// uses: "mxschmitt/action-tmate@v3",
// },
{
name: "Build debug",
if: "matrix.job == 'test' && matrix.profile == 'debug'",
Expand Down
11 changes: 0 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ log = { version = "0.4.20", features = ["kv"] }
lsp-types = "=0.97.0" # used by tower-lsp and "proposed" feature is unstable in patch releases
memmem = "0.1.1"
monch = "=0.5.0"
notify = "=6.1.1"
notify = { version = "=6.1.1", default-features = false, features = ["macos_kqueue"] }
num-bigint = { version = "0.4", features = ["rand"] }
once_cell = "1.17.1"
os_pipe = { version = "=1.1.5", features = ["io_safety"] }
Expand Down
13 changes: 7 additions & 6 deletions tests/integration/shared_library_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,12 @@ fn macos_shared_libraries() {
use test_util as util;

// target/release/deno:
// /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1953.1.0)
// /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 1228.0.0)
// /System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore (compatibility version 1.2.0, current version 1.11.0, weak)
// /System/Library/Frameworks/Metal.framework/Versions/A/Metal (compatibility version 1.0.0, current version 341.16.0, weak)
// /System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics (compatibility version 64.0.0, current version 1774.0.4, weak)
// /System/Library/Frameworks/MetalPerformanceShaders.framework/Versions/A/MetalPerformanceShaders (compatibility version 1.0.0, current version 127.0.19, weak)
// /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
// /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)
// /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

// path and whether its weak or not
#[cfg(target_arch = "x86_64")]
const EXPECTED: [(&str, bool); 9] = [
("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", false),
("/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", false),
Expand All @@ -65,6 +60,12 @@ fn macos_shared_libraries() {
("/usr/lib/libSystem.B.dylib", false),
("/usr/lib/libobjc.A.dylib", false),
];
#[cfg(target_arch = "aarch64")]
const EXPECTED: [(&str, bool); 3] = [
("/usr/lib/libiconv.2.dylib", false),
("/usr/lib/libSystem.B.dylib", false),
("/usr/lib/libobjc.A.dylib", false),
];

let otool = std::process::Command::new("otool")
.arg("-L")
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/fs_events_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Deno.test({ permissions: { read: true } }, function watchFsInvalidPath() {
} else {
assertThrows(() => {
Deno.watchFs("non-existent.file");
}, Deno.errors.NotFound);
});
}
});

Expand All @@ -32,7 +32,7 @@ async function getTwoEvents(
const events = [];
for await (const event of iter) {
events.push(event);
if (events.length > 2) break;
if (events.length >= 2) break;
}
return events;
}
Expand Down
3 changes: 3 additions & 0 deletions tools/lzld/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target

*.o
12 changes: 12 additions & 0 deletions tools/lzld/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
ARCH = $(shell uname -m)

liblzld_${ARCH}.a: lzld.m
cc -c lzld.m -o lzld.o
ar rcs liblzld_${ARCH}.a lzld.o

clean:
rm -f liblzld_${ARCH}.a lzld.o

all: liblzld_${ARCH}.a

.PHONY: clean all
66 changes: 66 additions & 0 deletions tools/lzld/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# `lzld`

This tools implements an alternative of the (deprecated) `ld -lazy_framework` to
lazy load frameworks as needed. Symbols used are manually added to `lzld.m`.

The purpose of this ld wrapper is to improve startup time on Mac. Because Deno
includes WebGPU, it needs to link to Metal and QuartzCore. We've observed that
loading frameworks during startup can cost as much as 8ms of startup time.

## Adding a new symbol binding

Add a binding for the used symbol in `lzld.m`, eg:

```diff
void *(*MTLCopyAllDevices_)(void) = 0;
+void *(*MTLSomethingSomething_)(void) = 0;

void loadMetalFramework() {
void *handle = dlopen("/System/Library/Frameworks/Metal.framework/Metal", RTLD_LAZY);
if (handle) {
MTLCopyAllDevices_ = dlsym(handle, "MTLCopyAllDevices");
+ MTLSomethingSomething_ = dlsym(handle, "MTLSomethingSomething");
}
}


+extern void *MTLSomethingSomething(void) {
+ if (MTLSomethingSomething_ == 0) {
+ loadMetalFramework();
+ }
+
+ return MTLSomethingSomething_();
+}

extern void *MTLCopyAllDevices(void) {
```

then build the static library with `make liblzld_arm64.a`.

## Usage

```toml
[target.aarch64-apple-darwin]
rustflags = [
"-C",
"linker=/path/to/lzld/lzld",
"-C",
"link-args=-L/path/to/lzld -llzld",
]
```

### Usage without `lzld` wrapper

1. `rustc -Z link-native-libraries=no -L/path/to/lzld -llzld`: Requires nightly
but doesn't need a wrapper linker.

2. Manaully source modification: Remove `#[link]` attributes from all
dependencies and link to `liblzld.a`.

## Design

It's pretty simple. Drop in `lzld` as the linker. It strips out `-framework`
arguments and links a static library (`liblzld.a`) that will lazy load the
framework via `dlopen` when needed.

Rest of the arguments are passed as-is to `lld`.
Binary file added tools/lzld/liblzld_arm64.a
Binary file not shown.
56 changes: 56 additions & 0 deletions tools/lzld/lzld
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
#
# This script is a wrapper around the `lld` linker that adds the path to the
# `liblzld` static library to the linker search path and links against it.
#
# Additionally, this script strips out `-framework` arguments

arch=$(uname -m)

# Find the path to the `lld` binary.
lld=$(which lld)
if [ -z "$lld" ]; then
lld=$(which ld64.lld)
fi

if [ ! -x "$lld" ]; then
echo "Error: unable to find 'lld' binary" >&2
exit 23
fi

# Filter out `-framework` arguments.
args=()

while [ $# -gt 0 ]; do
case "$1" in
-framework)
shift 2
;;
-Wl)
# Process `-Wl`
shift
IFS=',' read -r -a wl_args <<< "$1"
for arg in "${wl_args[@]}"; do
args+=("$arg")
done
shift
;;
*)
if [[ $1 == -Wl,* ]]; then
IFS=',' read -r -a wl_args <<< "${1:4}"
for arg in "${wl_args[@]}"; do
args+=("$arg")
done
else
args+=("$1")
fi
shift
;;
esac
done

args+=("-L$(dirname "$(realpath "$0")")")
args+=("-llzld_${arch}")

exec "$lld" "${args[@]}"

37 changes: 37 additions & 0 deletions tools/lzld/lzld.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
lzld - lazy loading of OSX frmeworks

This is compiled as a static library and linked against the binary via `lzld` script.
This code dynamically load frameworks when symbol is called using dlopen and dlsym.

Dependencies:
- dlfcn.h: Header file providing functions for dynamic linking.
- QuartzCore.framework: Provides graphics rendering support. (WebGPU)
- Metal.framework: Framework for high-performance GPU-accelerated graphics and computing. (WebGPU)
*/

#import <dlfcn.h>

// -- QuartzCore.framework

void *kCAGravityTopLeft = 0;

// -- Metal.framework

void *(*MTLCopyAllDevices_)(void) = 0;

void loadMetalFramework() {
void *handle = dlopen("/System/Library/Frameworks/Metal.framework/Metal", RTLD_LAZY);
if (handle) {
MTLCopyAllDevices_ = dlsym(handle, "MTLCopyAllDevices");
}
}

extern void *MTLCopyAllDevices(void) {
if (MTLCopyAllDevices_ == 0) {
loadMetalFramework();
}

return MTLCopyAllDevices_();
}

Loading