perf: schedule batch decompilation reads on demand

This commit is contained in:
morluto committed 2026-10-24 02:29:08 +08:00
1 parent 23ab2c4069
commit 06fe7eecbc
3 files changed
+100 -105

No files matched your search

+74 -94
View File
@@ -98,14 +98,6 @@ const isAnalysisCancellation = (
error: AnalysisError,
): error is AnalysisCancelledError => error._tag === "AnalysisCancelledError";
const firstAnalysisCancellation = <Value>(
results: readonly Result<Value, AnalysisError>[],
): AnalysisCancelledError | undefined => {
for (const result of results)
if (!result.ok && isAnalysisCancellation(result.error)) return result.error;
return undefined;
};
/**
* Composes direct provider operations into bounded reverse-engineering tools.
* Direct callers use `execute`; adapters that own validation use
@@ -423,34 +415,24 @@ export class EnhancedTools {
addresses: readonly string[],
signal?: AbortSignal,
): EnhancedResult {
const resolutions = await Promise.all(
addresses.map(async (address) => {
const result = await resolveProcedureAddress(
this.#call.bind(this),
address,
signal,
);
return { address, result };
}),
);
const resolutionCancellation = firstAnalysisCancellation(
resolutions.map(({ result }) => result),
);
if (resolutionCancellation !== undefined)
return err(resolutionCancellation);
const resolutions: BatchResolution[] = [];
for (const address of addresses) {
const result = await resolveProcedureAddress(
this.#call.bind(this),
address,
signal,
);
if (!result.ok && isAnalysisCancellation(result.error)) return result;
resolutions.push({ address, result });
}
const identities = await this.#batchIdentities(resolutions, signal);
const ready: BatchReadyItem[] = [];
for (const identity of identities) {
if (!identity.ok) return err(identity.error);
ready.push(identity.value);
}
const decompilations = await this.#batchPseudocode(ready, signal);
const items: BatchDecompileItem[] = [];
for (const decompilation of decompilations) {
if (!decompilation.ok) return err(decompilation.error);
items.push(decompilation.value);
}
if (!identities.ok) return identities;
const decompilations = await this.#batchPseudocode(
identities.value,
signal,
);
if (!decompilations.ok) return decompilations;
const items = decompilations.value;
const succeeded = items.filter(({ status }) => status === "ok").length;
return ok({
items,
@@ -463,75 +445,73 @@ export class EnhancedTools {
async #batchIdentities(
resolutions: readonly BatchResolution[],
signal?: AbortSignal,
): Promise<Result<BatchReadyItem, AnalysisCancelledError>[]> {
return Promise.all(
resolutions.map(async ({ address, result }) => {
if (!result.ok)
return ok({
address,
providerProcedure: address,
procedure: {
status: "unknown" as const,
error: projectAnalysisError(result.error),
},
});
const identity = await this.#batchProcedureIdentity(
result.value,
signal,
);
return identity.ok
? ok({
address,
providerProcedure: result.value,
procedure: identity.value,
})
: err(identity.error);
}),
);
): Promise<Result<BatchReadyItem[], AnalysisCancelledError>> {
const ready: BatchReadyItem[] = [];
for (const { address, result } of resolutions) {
if (!result.ok) {
ready.push({
address,
providerProcedure: address,
procedure: {
status: "unknown",
error: projectAnalysisError(result.error),
},
});
continue;
}
const identity = await this.#batchProcedureIdentity(result.value, signal);
if (!identity.ok) return identity;
ready.push({
address,
providerProcedure: result.value,
procedure: identity.value,
});
}
return ok(ready);
}
async #batchPseudocode(
ready: readonly BatchReadyItem[],
signal?: AbortSignal,
): Promise<Result<BatchDecompileItem, AnalysisCancelledError>[]> {
return Promise.all(
ready.map(async ({ address, procedure, providerProcedure }) => {
const result = await this.#call(
"procedure_pseudo_code",
{ procedure: providerProcedure },
signal,
procedure.status === "resolved" ? "provider" : undefined,
);
if (!result.ok && isAnalysisCancellation(result.error))
return err(result.error);
const item: BatchDecompileItem = !result.ok
): Promise<Result<BatchDecompileItem[], AnalysisCancelledError>> {
const items: BatchDecompileItem[] = [];
for (const { address, procedure, providerProcedure } of ready) {
const result = await this.#call(
"procedure_pseudo_code",
{ procedure: providerProcedure },
signal,
procedure.status === "resolved" ? "provider" : undefined,
);
if (!result.ok && isAnalysisCancellation(result.error))
return err(result.error);
const item: BatchDecompileItem = !result.ok
? {
address,
procedure,
status: "error",
error: projectAnalysisError(result.error),
}
: typeof result.value !== "string" || result.value.length === 0
? {
address,
procedure,
status: "error",
error: projectAnalysisError(result.error),
}
: typeof result.value !== "string" || result.value.length === 0
? {
address,
procedure,
status: "error",
error: projectAnalysisError(
new AnalysisOutputError(
"procedure_pseudo_code",
"provider returned empty pseudocode",
),
error: projectAnalysisError(
new AnalysisOutputError(
"procedure_pseudo_code",
"provider returned empty pseudocode",
),
}
: {
address,
procedure,
status: "ok",
pseudocode: result.value,
};
return ok(item);
}),
);
),
}
: {
address,
procedure,
status: "ok",
pseudocode: result.value,
};
items.push(item);
}
return ok(items);
}
async #batchProcedureIdentity(
+1 -1
View File
@@ -42,7 +42,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
),
enhanced(
"batch_decompile",
"Decompile each explicit procedure symbol or address concurrently. Preserves each input selector in address and reports the observed canonical entry and entry label in procedure; identity or label failures remain explicit without discarding usable pseudocode. Returns ordered per-item success or error results and aggregate counts.",
"Decompile each explicit procedure symbol or address, with one provider read active per batch. Preserves each input selector in address and reports the observed canonical entry and entry label in procedure; identity or label failures remain explicit without discarding usable pseudocode. Returns ordered per-item success or error results and aggregate counts. The complete reply retains every pseudocode body; use selected small batches to bound retained output.",
enhancedInputSchemas.batch_decompile,
),
enhanced(
@@ -396,7 +396,11 @@ describe("enhanced MCP batch cancellation", () => {
{
stage: "entry-label lookup",
cancelAt: "address_name",
expectedCalls: ["procedure_address:0x1", "address_name:0x1"],
expectedCalls: [
"procedure_address:0x1",
"procedure_address:0x2",
"address_name:0x1",
],
operation: "address_name",
},
{
@@ -404,7 +408,9 @@ describe("enhanced MCP batch cancellation", () => {
cancelAt: "procedure_pseudo_code",
expectedCalls: [
"procedure_address:0x1",
"procedure_address:0x2",
"address_name:0x1",
"address_name:0x2",
"procedure_pseudo_code:0x1",
],
operation: "procedure_pseudo_code",
@@ -430,7 +436,7 @@ describe("enhanced MCP batch cancellation", () => {
const response = await client.callTool({
name: "batch_decompile",
arguments: { addresses: ["0x1"] },
arguments: { addresses: ["0x1", "0x2"] },
});
expect(response.isError).toBe(true);
@@ -472,7 +478,6 @@ describe("enhanced MCP batch cancellation", () => {
"procedure_address:0x1",
"procedure_address:0x2",
"address_name:0x1",
"address_name:0x2",
]);
});
});
@@ -483,16 +488,24 @@ describe("enhanced MCP batches", () => {
{ length: 37 },
(_, index) => `0x${index.toString(16)}`,
);
let active = 0;
let peakActive = 0;
const analysis: AnalysisOperationPort = {
execute: async (name, arguments_) => {
if (name === "procedure_address")
return ok(String(arguments_.procedure));
if (name === "address_name")
return ok(`name for ${String(arguments_.address)}`);
if (name !== "procedure_pseudo_code")
throw new Error(`Unexpected operation: ${name}`);
active++;
peakActive = Math.max(peakActive, active);
await new Promise((resolve) => setImmediate(resolve));
return ok(`pseudo for ${String(arguments_.procedure)}`);
try {
if (name === "procedure_address")
return ok(String(arguments_.procedure));
if (name === "address_name")
return ok(`name for ${String(arguments_.address)}`);
if (name !== "procedure_pseudo_code")
throw new Error(`Unexpected operation: ${name}`);
return ok(`pseudo for ${String(arguments_.procedure)}`);
} finally {
active--;
}
},
};
const client = await connect(analysis);
@@ -503,6 +516,8 @@ describe("enhanced MCP batches", () => {
},
});
expect(result.isError).not.toBe(true);
expect(peakActive).toBe(1);
expect(active).toBe(0);
expect(result.structuredContent).toMatchObject({
normalized_result: {
items: addresses.map((address) => ({