fix(kuna): verify a warm export once for concurrent first requests (#4049)

Every request a session admitted checked `#project` before awaiting the
cache lookup, so concurrent first requests, such as batch_decompile's
parallel resolutions on a fresh server, each re-hashed every export file
and re-parsed the export. Concurrent first requests now share one lookup.

Refs #1957

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-15 03:41:27 -07:00
1 parent 1b8a9c831b
commit b6529bf1af
3 files changed
+46 -4

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ Cross-references, callers, callees and basic blocks are not available from this
The first analysis request in a session looks for this target's export in the cache. `list_documents` reads the selected path without initializing analysis. The export is keyed by the target's SHA-256, the Kuna executable's SHA-256, its version and `--jobs`.
- **Warm cache:** analysis reads reuse the verified export. A fresh server verifies its file digests; that cost scales with export size.
- **Warm cache:** analysis reads reuse the verified export. A fresh server verifies its file digests once, even for concurrent first requests; that cost scales with export size.
- **Cold cache:**
- REA starts `kuna decompile-project` in the background on a private copy of the target.
- Meanwhile, pseudocode comes from one `kuna decompile --addr` run, and listings from `kuna functions --json`.
+13 -2
View File
@@ -193,6 +193,8 @@ export class KunaSessionClient implements AnalysisClient {
| Promise<Result<readonly KunaInventoryFunction[], AnalysisError>>
| undefined;
#project: Promise<Result<KunaProject, AnalysisError>> | undefined;
/** The cache lookup that decides #project, shared by concurrent first requests. */
#starting: Promise<void> | undefined;
#reader: KunaProjectReader | undefined;
#inventoryIndex: InventoryIndex | undefined;
#status: KunaExportStatus | undefined;
@@ -998,8 +1000,17 @@ export class KunaSessionClient implements AnalysisClient {
}
/** Reuse a verified export, or start building one in the background. */
async #startProject(installation: KunaInstallation): Promise<void> {
if (this.#project !== undefined) return;
#startProject(installation: KunaInstallation): Promise<void> {
if (this.#project !== undefined) return Promise.resolve();
// Concurrent first requests share one lookup, which verifies every
// export file's digest on a fresh server.
this.#starting ??= this.#lookupOrBuild(installation).finally(() => {
this.#starting = undefined;
});
return this.#starting;
}
async #lookupOrBuild(installation: KunaInstallation): Promise<void> {
const key = kunaProjectKey({
targetSha256: this.#target.sha256,
kunaSha256: installation.sha256,
@@ -10,13 +10,14 @@ import {
} from "node:fs/promises";
import { join } from "node:path";
import { expect, it as test } from "vitest";
import { expect, it as test, vi } from "vitest";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
import type { JsonValue } from "../../../../src/domain/jsonValue.js";
import type { KunaProcessLauncher } from "../../../../src/kuna/KunaCommand.js";
import { KunaProvider } from "../../../../src/kuna/KunaProvider.js";
import { KUNA_FUNCTION_TEXT_BYTES } from "../../../../src/kuna/KunaLimits.js";
import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js";
import { cleanupOwnedProcessGroup } from "../../../../src/process/ProcessOwnership.js";
import { spawnOwnedProviderProcess } from "../../../../src/process/ProviderProcess.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
@@ -186,6 +187,36 @@ it("answers a cold request from one function, then serves the finished export",
await again.close();
});
it("verifies a warm export once for concurrent first requests on a fresh server", async () => {
const { kuna, target, cacheRoot, profile, open } = await session(
"rea-kuna-warm-concurrent-",
);
const builder = open();
await run(builder, "procedure_assembly", { procedure: "main" });
expect(await builder.close()).toEqual({ ok: true, value: null });
const lookup = vi.spyOn(KunaProjectCache.prototype, "lookup");
const fresh = new KunaProvider(
{ REA_KUNA_COMMAND: kuna, REA_KUNA_CACHE_DIR: cacheRoot },
{ platform: process.platform },
).createClient(target, profile);
try {
const answers = await Promise.all(
["main", "parse", "0x1000", "main"].map((procedure) =>
run(fresh, "procedure_pseudo_code", { procedure }),
),
);
for (const answer of answers)
expect(answer.rawResult).toMatchObject({
source: { kind: "project-export" },
});
expect(lookup).toHaveBeenCalledTimes(1);
} finally {
lookup.mockRestore();
expect(await fresh.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();