Skip to content

Commit 7dd5782

Browse files
daniel-nolandclaude
andcommitted
build(nix): link a test archive's binaries in a rustc of their own
With links bounded, the memory left at the peak is mostly rustc: a link waiting for its slot is a rustc process that has finished compiling and is waiting on its linker, with its whole compilation session -- a gigabyte or two -- still resident. Cargo's `-j` keeps that to one per job slot, but the slots are spent holding memory nobody uses. rustc can stop after writing its objects (`-Zno-link`, which leaves an `.rlink` describing the link) and do the link in a separate invocation (`-Zlink-only`). nix/scripts/rustc-split-link.sh, a `RUSTC_WRAPPER`, does that for the crates that link (binaries and test harnesses), so the process waiting on a slot is a small one. Everything else passes straight through. It is set where links are bounded, the `-Clinker-plugin-lto` test archives; `RUSTC_WRAPPER` is not part of cargo's fingerprint, so the dependency artifacts built without it are reused (the archive compiled the same 69 crates either way). Building `tests.all` at `profile=release`, two link slots, 8 cores: peak 13.2-14.1 -> 11.3 GiB, mean 6.5-6.8 -> 4.2 GiB, 611-616 -> 620 s. The split archive's suite passed (3074 of 3075; the one failure, a Unix socket path longer than SUN_LEN under a long worktree path, fails the same way without the split). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
1 parent f127bb3 commit 7dd5782

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

‎default.nix‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,13 @@ let
906906
runtimeEnv.LINK_CC = "${pkgs.pkgsBuildHost.llvmPackages'.clang}/bin/${cc}";
907907
text = builtins.readFile ./nix/scripts/link-slots.sh;
908908
};
909+
# A `RUSTC_WRAPPER` that compiles the crates that link and links them in separate rustc processes,
910+
# so that one waiting for a link slot is not holding a compilation session (see the script).
911+
rustc-split-link = pkgs.pkgsBuildHost.writeShellApplication {
912+
name = "rustc-split-link";
913+
runtimeInputs = [ pkgs.pkgsBuildHost.coreutils ];
914+
text = builtins.readFile ./nix/scripts/rustc-split-link.sh;
915+
};
909916
objcopy = if is-cross-compile then "${ctarget}-objcopy" else "objcopy";
910917
package-list = builtins.fromJSON (
911918
builtins.readFile (
@@ -1419,11 +1426,18 @@ let
14191426
#
14201427
# One slot makes the links the critical path for little memory; two keep the peak bounded
14211428
# without that.
1429+
#
1430+
# A link waiting for its slot still has its rustc resident, compilation session and all, so
1431+
# link in a separate rustc (nix/scripts/rustc-split-link.sh). With two slots on 8 cores that
1432+
# took the peak from 13.2-14.1 to 11.3 GiB and the mean from 6.5-6.8 to 4.2 GiB, at the same
1433+
# 620 s. `RUSTC_WRAPPER` stays out of cargo's fingerprint, so the dependency artifacts built
1434+
# without it are reused.
14221435
lto-link-slots =
14231436
pkgs.lib.optionalAttrs (builtins.elem "-Clinker-plugin-lto" profile-tests'.RUSTFLAGS)
14241437
{
14251438
LINK_SLOTS = "2";
14261439
LINK_THINLTO_JOBS = "4";
1440+
RUSTC_WRAPPER = "${rustc-split-link}/bin/rustc-split-link";
14271441
};
14281442
in
14291443
pkgs.callPackage invoke {

‎nix/scripts/rustc-split-link.sh‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright Open Network Fabric Authors
3+
4+
# The `RUSTC_WRAPPER` of a test archive whose links are bounded (see link-slots.sh).
5+
#
6+
# A link waiting for its slot is a rustc process that has finished compiling and is waiting on its
7+
# linker, with its whole compilation session -- a gigabyte or two -- still resident. So for the
8+
# crates that link (binaries and test harnesses), compile with `-Zno-link`, which writes the objects
9+
# and an `.rlink` describing the link and then exits, freeing all of that; then link in a second,
10+
# small rustc with `-Zlink-only`, given the same arguments with the `.rlink` in place of the source.
11+
# Libraries, build scripts and cargo's own probes (`rustc -vV`) pass straight through. The `-Z`
12+
# flags need `RUSTC_BOOTSTRAP=1`, which these builds set.
13+
14+
rustc="$1"
15+
shift
16+
17+
links=false
18+
name=
19+
out_dir=
20+
extra=
21+
src=
22+
prev=
23+
for arg in "$@"; do
24+
case "${prev}" in
25+
--crate-name) name="${arg}" ;;
26+
--out-dir) out_dir="${arg}" ;;
27+
--crate-type) if [ "${arg}" = bin ]; then links=true; fi ;;
28+
-C) case "${arg}" in extra-filename=*) extra="${arg#extra-filename=}" ;; esac ;;
29+
esac
30+
case "${arg}" in
31+
--test | --crate-type=bin) links=true ;;
32+
-Cextra-filename=*) extra="${arg#-Cextra-filename=}" ;;
33+
--out-dir=*) out_dir="${arg#--out-dir=}" ;;
34+
*.rs) if [ -z "${src}" ] && [ -f "${arg}" ]; then src="${arg}"; fi ;;
35+
esac
36+
prev="${arg}"
37+
done
38+
# Build scripts link too, but natively, without LTO, and cargo runs them as soon as they are built.
39+
case "${name}" in build_script_*) links=false ;; esac
40+
41+
if ! "${links}" || [ -z "${src}" ] || [ -z "${out_dir}" ] || [ -z "${name}" ]; then
42+
exec "${rustc}" "$@"
43+
fi
44+
45+
"${rustc}" "$@" -Zno-link
46+
47+
rlink="${out_dir}/${name}${extra}.rlink"
48+
if [ ! -f "${rlink}" ]; then
49+
printf 'rustc-split-link: -Zno-link left no %s\n' "${rlink}" >&2
50+
exit 1
51+
fi
52+
53+
args=()
54+
for arg in "$@"; do
55+
if [ "${arg}" = "${src}" ]; then
56+
args+=("${rlink}")
57+
else
58+
args+=("${arg}")
59+
fi
60+
done
61+
status=0
62+
"${rustc}" "${args[@]}" -Zlink-only || status=$?
63+
rm -f "${rlink}"
64+
exit "${status}"

0 commit comments

Comments
 (0)