mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
perf(native): skip llvm-nm's dyld-info pass when the symbol table names every export (#3200)
* perf(native): skip llvm-nm's dyld-info pass when the symbol table names every export inspect_macho ran `nm -gjU`, whose default dyld-info pass takes seconds on large apps (Keynote arm64 5.8 s, x86_64 10.2 s) yet under -gU can only add export-trie names that dyld_info -exports already reports. Run `nm -gjU --no-dyldinfo` and rerun the default pass only when dyld_info reports an export the symbol table lacks (e.g. strip -T), so its exact names still disambiguate dyld_info rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): fall back to nm's dyld-info pass for unreadable export rows Account for every dyld_info export line instead of only the rows the parser returns, so an unfamiliar row format reruns the default nm pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7484401601
commit
e5791229f3
5 files changed
+164
-1
No files matched your search
@@ -41,9 +41,15 @@ interface NativeMachoInspectionContext {
|
||||
|
||||
const DISCOVERY_COMMANDS = [["lipo", ["-detailed_info"]]] as const;
|
||||
|
||||
// llvm-nm's default dyld-info pass fakes up symbols the symbol table lacks
|
||||
// from dyld metadata. It takes seconds on large apps (Keynote arm64: 5.8 s
|
||||
// versus 0.01 s without it), and under -gU it can only add export-trie names.
|
||||
const SYMBOL_TABLE_EXPORTS = ["nm", ["-gjU", "--no-dyldinfo"]] as const;
|
||||
const DYLD_INFO_EXPORTS = ["nm", ["-gjU"]] as const;
|
||||
|
||||
const REQUIRED_SLICE_COMMANDS = [
|
||||
["otool", ["-h", "-l"]],
|
||||
["nm", ["-gjU"]],
|
||||
SYMBOL_TABLE_EXPORTS,
|
||||
["dyld_info", ["-imports"]],
|
||||
["dyld_info", ["-exports"]],
|
||||
["dwarfdump", ["--uuid"]],
|
||||
@@ -111,6 +117,18 @@ export const inspectNativeMacho = async (
|
||||
else if (optional.error._tag === "AnalysisCapabilityUnavailableError")
|
||||
limitations.push(VTOOL_UNAVAILABLE_LIMITATION);
|
||||
else return err(optional.error);
|
||||
const nmIndex = captures.findIndex(({ tool }) => tool === "nm");
|
||||
const dyldExports = captures.filter(({ tool }) => tool === "dyld_info")[1];
|
||||
if (nmIndex < 0 || dyldExports === undefined)
|
||||
throw new TypeError("Missing nm or dyld_info export capture");
|
||||
// A stripped symbol table (strip -T) can omit names the export trie keeps.
|
||||
// Only then does llvm-nm's dyld-info pass add names, and its exact names
|
||||
// disambiguate dyld_info rows, so rerun it rather than lose them.
|
||||
if (!namesEveryExport(captures[nmIndex]!.stdout, dyldExports)) {
|
||||
const withDyldInfo = await runSliceCommand(DYLD_INFO_EXPORTS);
|
||||
if (!withDyldInfo.ok) return err(withDyldInfo.error);
|
||||
captures[nmIndex] = withDyldInfo.value;
|
||||
}
|
||||
const otool = captures.find(({ tool }) => tool === "otool");
|
||||
if (otool === undefined) throw new TypeError("Missing otool capture");
|
||||
let load: ReturnType<typeof parseOtoolLoadCommands>;
|
||||
@@ -293,6 +311,34 @@ const captureLookup =
|
||||
return capture;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether every dyld_info export row names a symbol-table export. A row this
|
||||
* parser cannot read counts as uncovered, so an unfamiliar format falls back.
|
||||
*/
|
||||
const namesEveryExport = (
|
||||
nmOutput: string,
|
||||
dyldExports: NativeCommandCapture,
|
||||
): boolean => {
|
||||
const names = new Set(parseNmExports(nmOutput).map(({ name }) => name));
|
||||
return sliceOutput(dyldExports)
|
||||
.split(/\r?\n/u)
|
||||
.every((rawLine, index) => {
|
||||
const line = rawLine.trim();
|
||||
if (
|
||||
line.length === 0 ||
|
||||
(index === 0 && line.endsWith(":")) ||
|
||||
/^-?exports:$/u.test(line) ||
|
||||
/^offset\s+symbol$/u.test(line)
|
||||
)
|
||||
return true;
|
||||
const rows = parseDyldSymbols(line, "exports", null, names);
|
||||
// The parser withholds a row only when nm names both of its readings.
|
||||
if (rows.length === 0)
|
||||
return names.has(/^0x[\da-f]+\s+(.+)$/iu.exec(line)?.[1] ?? "");
|
||||
return rows.length === 1 && names.has(rows[0]!.name);
|
||||
});
|
||||
};
|
||||
|
||||
const parseNmExports = (output: string) =>
|
||||
output
|
||||
.split(/\r?\n/u)
|
||||
|
||||
@@ -552,3 +552,91 @@ it("retains annotation parsing without nm name evidence", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
/** Replay fixtures, answering nm by whether its dyld-info pass is disabled. */
|
||||
const symbolTableRunner = (
|
||||
outputs: Readonly<Record<string, string>>,
|
||||
nm: { readonly symbolTable: string; readonly withDyldInfo: string },
|
||||
) => {
|
||||
const nmArguments: (readonly string[])[] = [];
|
||||
const replay = (output: string) =>
|
||||
new FixtureRunner({ ...outputs, nm: output });
|
||||
const runner: NativeCommandRunner = {
|
||||
run: (tool, arguments_) => {
|
||||
if (tool !== "nm") return replay("").run(tool, arguments_);
|
||||
nmArguments.push(arguments_);
|
||||
return replay(
|
||||
arguments_.includes("--no-dyldinfo") ? nm.symbolTable : nm.withDyldInfo,
|
||||
).run(tool, arguments_);
|
||||
},
|
||||
};
|
||||
return { runner, nmArguments };
|
||||
};
|
||||
|
||||
/** nm arguments in provenance, which records the artifact operand as a placeholder. */
|
||||
const nmCommands = (result: unknown) =>
|
||||
(result as { provenance: { tool: string; command: string[] }[] }).provenance
|
||||
.filter(({ tool }) => tool === "nm")
|
||||
.map(({ command }) => [...command.slice(1, -1), "/private/fixture"]);
|
||||
|
||||
it("reads exports from the symbol table without llvm-nm's dyld-info pass", async () => {
|
||||
const names = await fixture("dyld-inventory/nm.txt");
|
||||
const { runner, nmArguments } = symbolTableRunner(
|
||||
{ "dyld_info:-exports": await fixture("dyld-inventory/exports.txt") },
|
||||
{ symbolTable: names, withDyldInfo: names },
|
||||
);
|
||||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||||
.createClient(machoTarget("/private/fixture"))
|
||||
.execute("inspect_macho", {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(nmArguments).toEqual([
|
||||
["-gjU", "--no-dyldinfo", "-arch", "arm64", "/private/fixture"],
|
||||
]);
|
||||
expect(nmCommands(execution.value.result)).toEqual(nmArguments);
|
||||
});
|
||||
|
||||
it("reruns nm with its dyld-info pass when a stripped symbol table omits trie exports", async () => {
|
||||
const trieNames = await fixture("dyld-inventory/stripped-swift-nm.txt");
|
||||
const { runner, nmArguments } = symbolTableRunner(
|
||||
{
|
||||
"dyld_info:-exports": await fixture(
|
||||
"dyld-inventory/stripped-swift-exports.txt",
|
||||
),
|
||||
},
|
||||
{ symbolTable: "", withDyldInfo: trieNames },
|
||||
);
|
||||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||||
.createClient(machoTarget("/private/fixture"))
|
||||
.execute("inspect_macho", {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
const rerun = ["-gjU", "-arch", "arm64", "/private/fixture"];
|
||||
expect(nmArguments).toEqual([
|
||||
["-gjU", "--no-dyldinfo", "-arch", "arm64", "/private/fixture"],
|
||||
rerun,
|
||||
]);
|
||||
// Provenance names the nm run whose output the exports came from.
|
||||
expect(nmCommands(execution.value.result)).toEqual([rerun]);
|
||||
const exported = (
|
||||
execution.value.result as { exports: { items: { name: string }[] } }
|
||||
).exports.items.map(({ name }) => name);
|
||||
expect(exported).toEqual(trieNames.trim().split("\n").sort());
|
||||
});
|
||||
|
||||
it("reruns nm with its dyld-info pass when a dyld_info export row is unreadable", async () => {
|
||||
const names = await fixture("dyld-inventory/nm.txt");
|
||||
const exportsOutput = await fixture("dyld-inventory/exports.txt");
|
||||
const { runner, nmArguments } = symbolTableRunner(
|
||||
{
|
||||
"dyld_info:-exports": `${exportsOutput.trimEnd()}\n <unrecognized export row>\n`,
|
||||
},
|
||||
{ symbolTable: names, withDyldInfo: names },
|
||||
);
|
||||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||||
.createClient(machoTarget("/private/fixture"))
|
||||
.execute("inspect_macho", {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(nmArguments.map((arguments_) => arguments_.slice(0, 2))).toEqual([
|
||||
["-gjU", "--no-dyldinfo"],
|
||||
["-gjU", "-arch"],
|
||||
]);
|
||||
});
|
||||
@@ -50,6 +50,20 @@ xcrun dyld_info -exports /tmp/libre.dylib
|
||||
xcrun nm -gjU /tmp/libre.dylib
|
||||
```
|
||||
|
||||
`stripped-swift-exports.txt` and `stripped-swift-nm.txt` are `xcrun dyld_info
|
||||
-exports` and the default `xcrun nm -gjU` for a Swift dylib whose symbol table
|
||||
was stripped with `strip -T`. Its export trie keeps all six names, while
|
||||
`xcrun nm -gjU --no-dyldinfo` prints none:
|
||||
|
||||
```sh
|
||||
printf 'public func hello() -> Int { 1 }\npublic struct S { public init() {} ; public func f() -> Int { 2 } }\n' > /tmp/m.swift
|
||||
xcrun swiftc -emit-library -module-name M /tmp/m.swift -o /tmp/libMT.dylib
|
||||
xcrun strip -T /tmp/libMT.dylib
|
||||
xcrun dyld_info -exports /tmp/libMT.dylib
|
||||
xcrun nm -gjU /tmp/libMT.dylib
|
||||
xcrun nm -gjU --no-dyldinfo /tmp/libMT.dylib
|
||||
```
|
||||
|
||||
`codesign-missing.txt` is stderr from `codesign -d --verbose=4` on a nonexistent
|
||||
path (exit 1). `codesign-unsigned.txt` is stderr from the same command on a copy
|
||||
of the fixture after `codesign --remove-signature` (exit 1). Only the latter is
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
fixture [arm64]:
|
||||
-exports:
|
||||
offset symbol
|
||||
0x00000598 _$s1M1SV1fSiyF
|
||||
0x00000594 _$s1M1SVACycfC
|
||||
0x000005A4 _$s1M1SVMa
|
||||
0x000005CC _$s1M1SVMn
|
||||
0x00004010 _$s1M1SVN
|
||||
0x00000588 _$s1M5helloSiyF
|
||||
@@ -0,0 +1,6 @@
|
||||
_$s1M1SV1fSiyF
|
||||
_$s1M1SVACycfC
|
||||
_$s1M1SVMa
|
||||
_$s1M1SVMn
|
||||
_$s1M1SVN
|
||||
_$s1M5helloSiyF
|
||||
Reference in new issue
Block a user