From 225d664e49cb3b4e99ec18b6fb09ec9b3cd0d996 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:01:00 +0000 Subject: [PATCH] fix(kuna): run the inventory with the configured decode lanes REA_KUNA_JOBS reached only `decompile-project`, so the export's first step, the `kuna functions` inventory that every cold request waits for, always ran serially. Kuna's `functions --jobs N` returns the serial inventory byte for byte, so both inventory runs now pass the configured jobs. On a 254 MB Mach-O the inventory takes 23 s with 8 lanes instead of 66 s; the default of one job is unchanged. Refs #1957 Co-Authored-By: Claude Opus 5.5 --- docs/kuna-provider.md | 16 +++++++------- src/kuna/KunaCommand.ts | 10 +++++++++ src/kuna/KunaProjectExport.ts | 3 ++- src/kuna/KunaSessionClient.ts | 6 +++++- .../providers/kuna/kunaProjectCache.test.ts | 8 ++++++- .../providers/kuna/kunaProvider.test.ts | 21 +++++++++++++++++++ 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/docs/kuna-provider.md b/docs/kuna-provider.md index b860b4c251..61522b3d2e 100644 --- a/docs/kuna-provider.md +++ b/docs/kuna-provider.md @@ -13,14 +13,14 @@ rea doctor --provider kuna Select it explicitly with `--provider kuna`, MCP's `open_binary.provider_id: "kuna"`, or `REA_ANALYSIS_PROVIDER=kuna`. With Kuna and another provider configured for the same target, `auto` reports `ambiguous` instead of choosing one. -| Setting | Default | Meaning | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `REA_KUNA_COMMAND` | none (required) | Absolute path of the `kuna` executable. | -| `REA_KUNA_CACHE_DIR` | `~/Library/Caches/rea/kuna` (macOS), `$XDG_CACHE_HOME/rea/kuna` (Linux; `~/.cache/rea/kuna` when `XDG_CACHE_HOME` is unset or relative) | Absolute directory REA owns for project exports. Kuna's own spec cache also lives here, not in your Kuna cache. | -| `REA_KUNA_CACHE_MAX_BYTES` | 4 GiB | Size cap; least recently used exports are evicted above it. | -| `REA_KUNA_PROJECT_TIMEOUT_MS` | 30 minutes | Deadline for one project export. | -| `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile. | -| `REA_KUNA_JOBS` | 1 | `decompile-project --jobs`. 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. | +| Setting | Default | Meaning | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REA_KUNA_COMMAND` | none (required) | Absolute path of the `kuna` executable. | +| `REA_KUNA_CACHE_DIR` | `~/Library/Caches/rea/kuna` (macOS), `$XDG_CACHE_HOME/rea/kuna` (Linux; `~/.cache/rea/kuna` when `XDG_CACHE_HOME` is unset or relative) | Absolute directory REA owns for project exports. Kuna's own spec cache also lives here, not in your Kuna cache. | +| `REA_KUNA_CACHE_MAX_BYTES` | 4 GiB | Size cap; least recently used exports are evicted above it. | +| `REA_KUNA_PROJECT_TIMEOUT_MS` | 30 minutes | Deadline for one project export. | +| `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile. | +| `REA_KUNA_JOBS` | 1 | `--jobs` for `kuna functions` and `decompile-project`. The inventory is identical for any value. For the export, 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. | Windows is not supported yet. Supported targets are ELF and thin Mach-O executables for x86-64, AArch64 and ARM; PE and universal Mach-O are rejected with a typed reason. diff --git a/src/kuna/KunaCommand.ts b/src/kuna/KunaCommand.ts index c8e4b7e76b..763c689ae8 100644 --- a/src/kuna/KunaCommand.ts +++ b/src/kuna/KunaCommand.ts @@ -35,6 +35,16 @@ import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; */ export const KUNA_INVENTORY_OUTPUT_BYTES = 128 * 1024 * 1024; +/** + * The `kuna functions` inventory of one image. Its `--jobs` decode lanes + * return the serial inventory byte for byte, so the configured jobs change only + * how fast discovery runs (a 254 MB Mach-O: 66 s serially, 23 s with 8 lanes). + */ +export const kunaInventoryArguments = ( + input: string, + jobs: number, +): readonly string[] => ["functions", input, "--json", "--jobs", String(jobs)]; + /** Test seam for the owned process launcher; production uses the default. */ export type KunaProcessLauncher = ( options: OwnedProviderProcessSpawnOptions, diff --git a/src/kuna/KunaProjectExport.ts b/src/kuna/KunaProjectExport.ts index a782cdeccb..cdd4fada87 100644 --- a/src/kuna/KunaProjectExport.ts +++ b/src/kuna/KunaProjectExport.ts @@ -16,6 +16,7 @@ import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js"; import { copyAndHash, KUNA_INVENTORY_OUTPUT_BYTES, + kunaInventoryArguments, runKunaCommand, type KunaProcessLauncher, } from "./KunaCommand.js"; @@ -209,7 +210,7 @@ export const exportKunaProject = async ( request.onStatus({ phase: "inventory", inventoryFunctions: null }); const listed = await run( - ["functions", input, "--json"], + kunaInventoryArguments(input, configuration.jobs), KUNA_INVENTORY_OUTPUT_BYTES, ); if (!listed.ok) return listed.outcome; diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 6faaecfd1a..6534d6ed50 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -35,6 +35,7 @@ import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js"; import { copyAndHash, KUNA_INVENTORY_OUTPUT_BYTES, + kunaInventoryArguments, runKunaCommand, type KunaProcessLauncher, } from "./KunaCommand.js"; @@ -1167,7 +1168,10 @@ export class KunaSessionClient implements AnalysisClient { if (!workspace.ok) return workspace; const run = await runKunaCommand({ installation, - arguments: ["functions", workspace.value.input, "--json"], + arguments: kunaInventoryArguments( + workspace.value.input, + installation.configuration.jobs, + ), workspace: workspace.value.root.path, operation: "list_procedures", timeoutMs: installation.configuration.functionTimeoutMs, diff --git a/tests/boundary/providers/kuna/kunaProjectCache.test.ts b/tests/boundary/providers/kuna/kunaProjectCache.test.ts index 0885029ea4..1eb07b8948 100644 --- a/tests/boundary/providers/kuna/kunaProjectCache.test.ts +++ b/tests/boundary/providers/kuna/kunaProjectCache.test.ts @@ -168,7 +168,13 @@ it("publishes a verified export and serves later requests from the cache", async // Kuna listed and decompiled a staged copy from REA's own HOME, in reference mode. const [listing] = await callsOf(kuna, "functions"); const [exported] = await callsOf(kuna, "decompile-project"); - expect(listing?.argv).toEqual(["functions", exported?.argv[1], "--json"]); + expect(listing?.argv).toEqual([ + "functions", + exported?.argv[1], + "--json", + "--jobs", + "1", + ]); expect(exported?.argv.slice(2)).toEqual([ "-o", expect.stringContaining(join("cache", "staging")), diff --git a/tests/boundary/providers/kuna/kunaProvider.test.ts b/tests/boundary/providers/kuna/kunaProvider.test.ts index 1613929cf0..8907c3fe38 100644 --- a/tests/boundary/providers/kuna/kunaProvider.test.ts +++ b/tests/boundary/providers/kuna/kunaProvider.test.ts @@ -223,6 +223,27 @@ it("verifies a warm export once for concurrent first requests on a fresh server" } }); +it("runs every inventory with the configured decode lanes", async () => { + const { kuna, open } = await session( + "rea-kuna-inventory-jobs-", + { project: "hang" }, + { REA_KUNA_JOBS: "4" }, + ); + const client = open(); + try { + await run(client, "list_procedures"); + await expect.poll(() => exportsOf(kuna)).toHaveLength(1); + const calls = await fakeKunaCalls(kuna); + const inventories = calls.filter(({ argv }) => argv[0] === "functions"); + expect(inventories.length).toBeGreaterThan(0); + for (const { argv } of inventories) + expect(argv.slice(2)).toEqual(["--json", "--jobs", "4"]); + expect((await exportsOf(kuna))[0]?.argv.slice(-2)).toEqual(["--jobs", "4"]); + } finally { + expect(await client.close()).toEqual({ ok: true, value: null }); + } +}); + it("lists, searches and resolves procedures from Kuna's inventory", async () => { const { open } = await session("rea-kuna-inventory-"); const client = open();