From 2c7240cf9d8231e16ad01de7678af9dbd01bab46 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:12:05 -0700 Subject: [PATCH] perf(native): run independent Mach-O slice commands concurrently (#2466) inspect_macho launched otool, nm, two dyld_info reads, dwarfdump and vtool one at a time. Each launch also resolves the tool through `xcrun --find`, so a warm call spent about 350 ms in eight serial supervised launches. The slice commands only read the selected artifact, so launch them together. Every launch still settles before the result, and declared order still fixes provenance and which failure is reported. Co-authored-by: Claude Opus 5.5 --- src/native/NativeMachoInspection.test.ts | 66 +++++++++++++++++++++++- src/native/NativeMachoInspection.ts | 25 ++++----- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/native/NativeMachoInspection.test.ts b/src/native/NativeMachoInspection.test.ts index 4ed533c2e8..6fe24eb903 100644 --- a/src/native/NativeMachoInspection.test.ts +++ b/src/native/NativeMachoInspection.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; -import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCancelledError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; import { err, ok } from "../domain/result.js"; import type { NativeCommandCapture } from "./CommandRunner.js"; import { inspectNativeMacho } from "./NativeMachoInspection.js"; @@ -186,3 +189,64 @@ describe("Mach-O UUID reconciliation", () => { ); }); }); + +describe("Mach-O slice command scheduling", () => { + const SLICE_TOOLS = ["otool", "nm", "dyld_info", "dyld_info", "dwarfdump"]; + + /** Finish later-launched slice commands first and record overlap. */ + const reorderingRunner = (failing: ReadonlySet = new Set()) => { + const fixtures = new NativeFixtureRunner(); + let inFlight = 0; + let maxInFlight = 0; + let launched = 0; + const run = async (tool: string, args: readonly string[]) => { + if (tool === "file" || tool === "lipo") return fixtures.run(tool, args); + const order = launched++; + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + for (let turn = 0; turn < 12 - order * 2; turn += 1) + await new Promise((resolve) => setImmediate(resolve)); + inFlight -= 1; + if (failing.has(tool)) + return err(new AnalysisOutputError("inspect_macho", `${tool} failed`)); + return fixtures.run(tool, args); + }; + return { run, maxInFlight: () => maxInFlight }; + }; + + const inspectWith = (run: ReturnType["run"]) => + inspectNativeMacho({ + target: nativeMachoTarget("/fixture/app"), + run, + invocation: (capture) => ({ + tool: capture.tool, + command: [capture.executable, ...capture.arguments], + tool_version: capture.toolVersion, + version_reason: capture.versionReason, + executable_sha256: capture.executableSha256, + exit: { code: capture.exitCode, signal: capture.signal }, + stdout_bytes: capture.stdoutBytes, + stderr_bytes: capture.stderrBytes, + }), + }); + + it("runs independent slice commands together and keeps provenance in declared order", async () => { + const runner = reorderingRunner(); + const result = await inspectWith(runner.run); + if (!result.ok) throw result.error; + expect(runner.maxInFlight()).toBe(SLICE_TOOLS.length + 1); + expect(result.value.result).toMatchObject({ + provenance: ["file", "lipo", ...SLICE_TOOLS, "vtool"].map((tool) => ({ + tool, + })), + }); + }); + + it("reports the first failing slice command in declared order", async () => { + const runner = reorderingRunner(new Set(["nm", "dwarfdump"])); + const result = await inspectWith(runner.run); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toMatchObject({ reason: "nm failed" }); + }); +}); diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts index 2ec6cd4a93..24ccb2514d 100644 --- a/src/native/NativeMachoInspection.ts +++ b/src/native/NativeMachoInspection.ts @@ -79,8 +79,11 @@ export const inspectNativeMacho = async ( ); const selectedArchitecture = selection.status === "selected" ? selection.architecture : null; - for (const [tool, prefix] of REQUIRED_SLICE_COMMANDS) { - const captured = await context.run( + const runSliceCommand = ([tool, prefix]: readonly [ + string, + readonly string[], + ]) => + context.run( tool, [ ...prefix, @@ -89,20 +92,18 @@ export const inspectNativeMacho = async ( ], context.signal, ); + // Slice commands only read the selected artifact, so launch them together. + // Every launch settles before return; declared order fixes provenance and + // which failure is reported. + const [required, optional] = await Promise.all([ + Promise.all(REQUIRED_SLICE_COMMANDS.map(runSliceCommand)), + runSliceCommand(OPTIONAL_VTOOL_COMMAND), + ]); + for (const captured of required) { if (!captured.ok) return err(captured.error); captures.push(captured.value); } const limitations: string[] = []; - const [tool, prefix] = OPTIONAL_VTOOL_COMMAND; - const optional = await context.run( - tool, - [ - ...prefix, - ...selectedArchitectureArguments(tool, selectedArchitecture), - context.target.path, - ], - context.signal, - ); if (optional.ok) captures.push(optional.value); else if (optional.error._tag === "AnalysisCapabilityUnavailableError") limitations.push(VTOOL_UNAVAILABLE_LIMITATION);