From c7345512eae285158ea035be619ef6c8deb09adf Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 06:06:38 +0800 Subject: [PATCH] perf(native): index admitted value-trace nodes Replace a graph-wide node scan for each parameter use with membership in the set of admitted node IDs. Preserve node-budget omission, pagination, and existing ID-collision behavior. Flameox profiles identify the eliminated scan. Six-run medians on a bounded synthetic four-procedure workload improved from 2538.17 ms to 220.32 ms (11.52x); full JSON responses have identical SHA-256 4071fc7ec17be01e6a23d53814a47b3b1bb0ebd80099d21ef8431da487801725. This measures production trace composition with a synthetic provider, not real Ghidra target speed. External design research through DeepWiki was checked against Ghidra 12.1.4 DataGraph: https://github.com/NationalSecurityAgency/ghidra/blob/8b6bbb857accdfa20dc5b2f5dea471178c2e9fbc/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/graphanalysis/DataGraph.java --- .../native/NativeValueTrace.test.ts | 127 ++++++++++++++++++ src/application/native/NativeValueTrace.ts | 8 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/application/native/NativeValueTrace.test.ts b/src/application/native/NativeValueTrace.test.ts index 0d2b19c77..0f7828f06 100644 --- a/src/application/native/NativeValueTrace.test.ts +++ b/src/application/native/NativeValueTrace.test.ts @@ -7,6 +7,7 @@ import { import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js"; import { functionDossierSchema } from "../../domain/hopperValues.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; +import { nativeValueTraceSchema } from "../../domain/native/nativeValueTrace.js"; import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; import { ok, err } from "../../domain/result.js"; @@ -102,6 +103,132 @@ const analysis: AnalysisOperationPort = { ); }, }; +describe("native value parameter-use node membership", () => { + it("retains only parameter uses whose parameter nodes fit the node budget", async () => { + const operationCount = 3_000; + const parameterUseCount = 12_000; + const base = functionDossierSchema.parse(ghidraFunctionDossier()); + const procedureAddress = base.procedure.address; + const largeFlow = { + available: true as const, + provenance: "ghidra-high-pcode" as const, + operations: Array.from({ length: operationCount }, (_, index) => ({ + ...node(`operation-${String(index)}`, procedureAddress, "MULTIEQUAL"), + inputs: [scalar, scalar, scalar, scalar], + })), + def_use: [], + effects: [], + parameters: [{ ordinal: 0, name: "value", data_type: "/int" }], + parameter_uses: Array.from({ length: parameterUseCount }, (_, index) => ({ + ordinal: 0, + use: `operation-${String(index % operationCount)}`, + input_index: Math.floor(index / operationCount), + })), + truncated: false, + omitted_operations_lower_bound: 0, + known_omitted_inputs: 0, + known_omitted_edges: 0, + limitations: [], + }; + const largeDossier = { + ...base, + native_value_flow: largeFlow, + }; + const largeAnalysis: AnalysisOperationPort = { + execute: async () => + ok(createAnalysisExecution(largeDossier, provider, { subject })), + }; + + const includedParameter = await traceNativeValues(largeAnalysis, { + procedure: procedureAddress, + max_nodes: 20_000, + max_edges: 40_000, + offset: operationCount, + limit: 1, + }); + if (!includedParameter.ok) throw includedParameter.error; + const includedResult = nativeValueTraceSchema.parse( + includedParameter.value, + ); + expect(includedResult).toMatchObject({ + total_nodes: operationCount + 1, + total_edges: parameterUseCount, + nodes: [ + expect.objectContaining({ + id: `${procedureAddress}/parameter:0`, + kind: "parameter", + }), + ], + truncated: true, + }); + expect(includedResult.edges).toHaveLength(parameterUseCount); + + const omittedParameter = await traceNativeValues(largeAnalysis, { + procedure: procedureAddress, + max_nodes: operationCount, + max_edges: 40_000, + offset: operationCount - 1, + limit: 1, + }); + if (!omittedParameter.ok) throw omittedParameter.error; + const omittedResult = nativeValueTraceSchema.parse(omittedParameter.value); + expect(omittedResult).toMatchObject({ + total_nodes: operationCount, + total_edges: 0, + nodes: [expect.objectContaining({ kind: "operation" })], + edges: [], + truncated: true, + }); + }); + + it("preserves operation IDs that collide with generated parameter IDs", async () => { + const base = functionDossierSchema.parse(ghidraFunctionDossier()); + const address = base.procedure.address; + const execution = createAnalysisExecution( + { + ...base, + native_value_flow: { + available: true, + provenance: "ghidra-high-pcode", + operations: [ + node("parameter:0", address, "COPY"), + node("use", address, "COPY"), + ], + def_use: [], + effects: [], + parameters: [{ ordinal: 0, name: "value", data_type: "/int" }], + parameter_uses: [{ ordinal: 0, use: "use", input_index: 1 }], + truncated: false, + omitted_operations_lower_bound: 0, + known_omitted_inputs: 0, + known_omitted_edges: 0, + limitations: [], + }, + }, + provider, + { subject }, + ); + const result = await traceNativeValues( + { execute: async () => ok(execution) }, + { procedure: address, max_nodes: 2 }, + ); + + if (!result.ok) throw result.error; + const parsedResult = nativeValueTraceSchema.parse(result.value); + expect(parsedResult).toMatchObject({ + total_nodes: 2, + total_edges: 1, + edges: [ + { + source: `${address}/parameter:0`, + target: `${address}/use`, + kind: "parameter-use", + }, + ], + }); + }); +}); + describe("bounded native value dependency composition", () => { it("reports work truncation and stable pagination without traversing ambiguous calls", async () => { const bounded = await traceNativeValues(analysis, { diff --git a/src/application/native/NativeValueTrace.ts b/src/application/native/NativeValueTrace.ts index b14925570..34403fdf7 100644 --- a/src/application/native/NativeValueTrace.ts +++ b/src/application/native/NativeValueTrace.ts @@ -21,6 +21,7 @@ export const traceNativeValues = async ( ): EnhancedResult => { const input = nativeValueTraceInputSchema.parse(parameters); const nodes: typeof nativeValueTraceSchema._output.nodes = []; + const nodeIds = new Set(); const edges: typeof nativeValueTraceSchema._output.edges = []; const procedures: typeof nativeValueTraceSchema._output.procedures = []; const unknowns: typeof nativeValueTraceSchema._output.unknowns = []; @@ -154,25 +155,28 @@ export const traceNativeValues = async ( bytes += size; ids.add(operation.id); nodes.push(node); + nodeIds.add(node.id); } for (const parameter of flow.parameters) { if (nodes.length >= input.max_nodes) { truncated = true; break; } + const id = `${address}/parameter:${parameter.ordinal}`; nodes.push({ - id: `${address}/parameter:${parameter.ordinal}`, + id, procedure: address, evidence_id: evidenceId, kind: "parameter", operation: null, parameter, }); + nodeIds.add(id); } for (const use of flow.parameter_uses) { if ( !ids.has(use.use) || - !nodes.some((node) => node.id === `${address}/parameter:${use.ordinal}`) + !nodeIds.has(`${address}/parameter:${use.ordinal}`) ) continue; if (edges.length >= input.max_edges) {