mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Keep the generator tag reader and the glibc math unit out of hello
The static hello binary grew past the 96 KiB ceiling of the runtime tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and hello links to about 65 KB there. The Sandbox gate installs Ubuntu's clang-22 without lld, so it links with GNU ld.dd418b067crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c defines floor, fmod, frexp and other glibc names as hidden functions so a program can start without libm. With GNU ld that cannot work: ld decides --as-needed before section GC, so the runtime's unreachable code keeps libm needed, and it keeps every regular definition of a name a needed shared library defines, hidden or not. Every executable carried the whole unit (about 2.5 KB of code and unwind data) and still loaded libm. Links without lld now leave the unit out; its functions answer what glibc's do.192ea3247installed scr_gen_tag_reader from a load-time constructor in scr_async.c. A constructor is a link root on every format, so each executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string literals (about 1.1 KB). The generator runtime now installs the reader when it creates a generator, the only time a reader can be needed. Hello is now 93,928 B with GNU ld and 65,472 B with lld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2c5b3b48d4
commit
019fc51e31
@@ -75,7 +75,7 @@ export function executableLinkInputs(options: {
|
||||
* links as needed: the runtime defines the libm functions most programs
|
||||
* reach (scr_glibc_math.c) and Math's transcendentals (scr_ieee754.c), so a
|
||||
* program that calls no other libm function, such as pow, starts without
|
||||
* loading libm.so. */
|
||||
* loading libm.so when it links with lld (see inputsWithoutLld). */
|
||||
export function systemLibraryLinkerArgs(
|
||||
target: Pick<NativeTargetSpec, "platform">,
|
||||
names: readonly string[],
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
@@ -244,15 +244,19 @@ export async function linkNativeExecutable(
|
||||
stagedRoot = staged.root;
|
||||
// Linux links with lld when the default driver finds it.
|
||||
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
|
||||
const args = [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
const linkArgs = (withLld: boolean): string[] => {
|
||||
const inputs = plan.inputs.map((input) => staged.replacements.get(input) ?? input);
|
||||
return [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...(withLld ? lld : []),
|
||||
...plan.driverFlags,
|
||||
...(withLld ? inputs : inputsWithoutLld(plan.target.platform, inputs)),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
};
|
||||
const args = linkArgs(lld.length !== 0);
|
||||
// The pack snapshots bracket both verification passes and private staging;
|
||||
// the program-object snapshot begins before helper emission. Snapshot the
|
||||
// remaining cache-bearing inputs before the linker consumes them, then
|
||||
@@ -285,10 +289,7 @@ export async function linkNativeExecutable(
|
||||
// A failed link with lld runs again with the driver's default linker,
|
||||
// whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
await execFileAsync(
|
||||
linker,
|
||||
args.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
await execFileAsync(linker, linkArgs(false));
|
||||
});
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync }
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
|
||||
import { driverFindsLld, inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
@@ -59,3 +59,10 @@ test.skipIf(process.platform === "win32")(
|
||||
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
test("Linux links without lld leave out the glibc math unit", () => {
|
||||
const inputs = ["/pack/scr_lib.o", "/pack/scr_glibc_math.o", "/tmp/program.o"];
|
||||
expect(inputsWithoutLld("linux", inputs)).toEqual(["/pack/scr_lib.o", "/tmp/program.o"]);
|
||||
expect(inputsWithoutLld("darwin", inputs)).toEqual(inputs);
|
||||
expect(inputsWithoutLld("windows", inputs)).toEqual(inputs);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
try {
|
||||
@@ -47,3 +47,21 @@ export function lldLinkerArgs(
|
||||
? ["-fuse-ld=lld"]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The link inputs for a Linux link without lld. The runtime's
|
||||
* scr_glibc_math.c defines glibc's floor, fmod, frexp and a few others as
|
||||
* hidden functions so that a program calling no other libm function starts
|
||||
* without libm.so. That works with lld, which garbage-collects the unused
|
||||
* definitions and decides `--as-needed` from the live code. GNU ld decides
|
||||
* `--as-needed` before section GC, so the runtime's unreachable code keeps
|
||||
* libm needed, and it keeps every regular definition of a name that a needed
|
||||
* shared library also defines, hidden or not: the unit would add all of its
|
||||
* code to every executable and save nothing. Its functions answer what
|
||||
* glibc's do, so linking glibc's own instead is equivalent.
|
||||
*/
|
||||
export function inputsWithoutLld(platform: string, inputs: readonly string[]): string[] {
|
||||
return platform === "linux"
|
||||
? inputs.filter((input) => basename(input) !== "scr_glibc_math.o")
|
||||
: [...inputs];
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
import { splitLlvmLibraryProgram } from "../backend/llvm/split.js";
|
||||
import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js";
|
||||
import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js";
|
||||
import { lldLinkerArgs } from "../backend/lld.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "../backend/lld.js";
|
||||
import { openNativeExecutableCache } from "./executable-cache.js";
|
||||
import { LinkerJobs } from "./linker-job.js";
|
||||
import { nativeFileIdentity } from "./file-identity.js";
|
||||
@@ -569,15 +569,16 @@ export class NativeCompiler {
|
||||
});
|
||||
// Linux links with lld when the driver finds it.
|
||||
const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker);
|
||||
const linkArgs = [
|
||||
const linkArgsWith = (withLld: boolean): string[] => [
|
||||
...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs),
|
||||
...lld,
|
||||
...(withLld ? lld : []),
|
||||
...plan.driverFlags,
|
||||
...plan.inputs,
|
||||
...(withLld ? plan.inputs : inputsWithoutLld(target.platform, plan.inputs)),
|
||||
...systemLibraryLinkerArgs(target, plan.systemLibraries),
|
||||
"-o",
|
||||
stagedOutput,
|
||||
];
|
||||
const linkArgs = linkArgsWith(lld.length !== 0);
|
||||
// Host Linux links with lld run the linker job the driver would.
|
||||
const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false);
|
||||
const job = jobs?.link([...plan.inputs, stagedOutput], stage);
|
||||
@@ -593,10 +594,7 @@ export class NativeCompiler {
|
||||
// A failed link with lld runs again with the driver's default
|
||||
// linker, whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
runNativeTool(
|
||||
driver,
|
||||
linkArgs.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
runNativeTool(driver, linkArgsWith(false));
|
||||
}
|
||||
}
|
||||
timing("native-link");
|
||||
|
||||
@@ -5385,6 +5385,11 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa
|
||||
bool stackless) {
|
||||
ScrGen *g = scr_mem_calloc(sizeof *g);
|
||||
if (!g) scr_oom();
|
||||
/* Install the tag reader with each generator rather than in a constructor:
|
||||
* a constructor is a link root, so it would keep scr_gen_tag and this
|
||||
* unit's string literals in every executable, generators or not. Threads
|
||||
* store the same value, relaxed. */
|
||||
__atomic_store_n(&scr_gen_tag_reader, scr_gen_tag, __ATOMIC_RELAXED);
|
||||
g->rc = 1;
|
||||
g->receiver = scr_dyn_this_get();
|
||||
g->state = SCR_GEN_UNSTARTED;
|
||||
@@ -5959,10 +5964,6 @@ const char *scr_gen_tag(const ScrGen *g) {
|
||||
return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator";
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void scr_gen_tag_reader_init(void) {
|
||||
scr_gen_tag_reader = scr_gen_tag;
|
||||
}
|
||||
|
||||
void scr_gen_close(ScrGen *g) {
|
||||
scr_gen_slot_reset(&g->ret);
|
||||
scr_gen_resume_return(g);
|
||||
|
||||
@@ -82,8 +82,9 @@ void scr_gen_set_tag(ScrGen *g, const char *tag);
|
||||
void scr_gen_copy_tag(ScrGen *g, const ScrGen *from);
|
||||
const char *scr_gen_tag(const ScrGen *g);
|
||||
/* scr_gen_tag for units that also link without the generator runtime (a
|
||||
* library links none): the generator runtime sets it at startup, and only
|
||||
* then can a generator capsule exist. */
|
||||
* library links none): the generator runtime sets it when it creates a
|
||||
* generator, so it is set whenever a generator capsule exists. Read it with
|
||||
* a relaxed atomic load. */
|
||||
extern const char *(*scr_gen_tag_reader)(const ScrGen *g);
|
||||
ScrGen *scr_gen_retain(ScrGen *g);
|
||||
void scr_gen_release(ScrGen *g); /* NULL-tolerant */
|
||||
|
||||
@@ -1613,7 +1613,7 @@ static bool scr_insp_is_generator(const ScrDyn *d) {
|
||||
}
|
||||
|
||||
static ScrStr *scr_insp_generator(const ScrDyn *d) {
|
||||
const char *tag = scr_gen_tag_reader(d->v.typed_ref.ptr);
|
||||
const char *tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr);
|
||||
if (tag[0] == '!') {
|
||||
static const char message[] = "util.inspect of this iterator object is not supported yet";
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
|
||||
|
||||
@@ -9633,7 +9633,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14);
|
||||
if (scr_dyn_generator(d)) {
|
||||
const char *gen_tag = scr_gen_tag_reader(d->v.typed_ref.ptr);
|
||||
const char *gen_tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr);
|
||||
if (gen_tag[0] == '!') {
|
||||
static const char message[] = "Object.prototype.toString of this iterator is not supported yet";
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
|
||||
|
||||
@@ -142,6 +142,9 @@ nativeToolchainTest(
|
||||
"scr_url_",
|
||||
"scr_json_parse",
|
||||
"scr_date_",
|
||||
// The generator runtime installs its tag reader with its first
|
||||
// generator; a load-time constructor would retain it everywhere.
|
||||
"scr_gen_tag",
|
||||
]) {
|
||||
expect(helloSymbols, `hello retains ${family}`).not.toContain(family);
|
||||
}
|
||||
@@ -159,8 +162,10 @@ nativeToolchainTest(
|
||||
|
||||
// Symbol absence is the primary reachability contract. Keep a deliberately
|
||||
// roomy, platform-specific hello-world ceiling too: it catches losing
|
||||
// section GC without pinning an exact linker/SDK byte count. The canonical
|
||||
// Linux build is about 84KB and current Mach-O builds are about 70KB. Most
|
||||
// section GC without pinning an exact linker/SDK byte count. The Linux
|
||||
// build is about 65KB with lld (CI's pinned LLVM provides it) and about
|
||||
// 94KB with GNU ld, which keeps a unit's string literals whole once any of
|
||||
// them is reachable; current Mach-O builds are about 70KB. Most
|
||||
// of Linux hello's runtime code is the cycle collector, which its exit
|
||||
// path reaches (scr_lib_cleanup releases an array). ELF segments are page
|
||||
// aligned, so the Linux size moves in 4 KiB steps whenever .text crosses
|
||||
|
||||
Reference in New Issue
Block a user