From 7c6368f932eed9324296c1cbf799871f7f61e6e8 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:06:28 +0000 Subject: [PATCH] perf(managed): validate a sealed member parent once across pages Each managed-members page reparsed the whole parent report, about 35 ms for an 18 MB assembly result regardless of the page size. The analysis view service now reports whether the parent Evidence is a sealed snapshot, and the member view reuses the validated report for that sealed result through a weak reference. Unsealed input is still validated on every call. The round-trip test fixture now carries an exported type, an ldstr literal, a field access and a call edge, and requires every collection to be nonempty so a row schema that drops facts cannot pass on empty arrays. Co-Authored-By: Claude Opus 5.5 --- .../analysisView/AnalysisViewService.ts | 3 + src/domain/analysisView/analysisView.ts | 2 + src/domain/analysisView/managedMemberView.ts | 57 +++++++++++++------ .../mcp/managedMemberSummaryMcp.test.ts | 42 +++++++++++++- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index f12033ee0..c4cc79f81 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -21,6 +21,7 @@ import { type InspectAnalysisViewInput, } from "../../domain/analysisView/analysisView.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; +import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js"; import { err, ok, type Result } from "../../domain/result.js"; import { WebNetworkApiSurfaceAccumulator } from "../../domain/analysisView/WebNetworkApiSurfaceAccumulator.js"; @@ -66,6 +67,8 @@ export const inspectAnalysisViewValidated = ( evidenceId: parent.evidence_id, operation: parent.operation, normalizedResult: parent.normalized_result, + // Sealing an Evidence root deep-freezes it; only the root is registered. + sealedResult: isImmutableJsonSnapshot(parent), subject: parent.subject, limitations: parent.limitations, parameters: parent.parameters, diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index d676d491c..2c124bd14 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -599,6 +599,8 @@ export interface AnalysisViewParent { readonly evidenceId: string; readonly operation: string; readonly normalizedResult: unknown; + /** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */ + readonly sealedResult?: boolean; /** Preserve the authenticated source identity, including explicit absence. */ readonly subject?: Evidence["subject"]; readonly limitations: readonly string[]; diff --git a/src/domain/analysisView/managedMemberView.ts b/src/domain/analysisView/managedMemberView.ts index 399dd59a1..c2c54ccdf 100644 --- a/src/domain/analysisView/managedMemberView.ts +++ b/src/domain/analysisView/managedMemberView.ts @@ -139,6 +139,44 @@ const collectionPage = ( } }; +// A sealed parent result cannot change, so its validated report stays exact for +// every later page. The weak reference lets memory pressure reclaim the copy; +// unsealed input is validated on each call. +const validatedReports = new WeakMap< + object, + WeakRef +>(); + +/** Validate the parent once per sealed result instead of once per page. */ +const parseMemberReport = ({ + normalizedResult, + sealedResult, +}: AnalysisViewParent): Result => { + const sealed = + sealedResult === true && + typeof normalizedResult === "object" && + normalizedResult !== null; + const cached = sealed + ? validatedReports.get(normalizedResult)?.deref() + : undefined; + if (cached !== undefined) return ok(cached); + const parsed = managedMemberInspectionSchema.safeParse(normalizedResult); + if (!parsed.success) + return err( + analysisInputErrorFromIssues( + "inspect_analysis_view", + parsed.error.issues.map((issue) => ({ + ...issue, + path: ["source", "normalized_result", ...issue.path], + })), + { source: { normalized_result: normalizedResult } }, + { cause: parsed.error }, + ), + ); + if (sealed) validatedReports.set(normalizedResult, new WeakRef(parsed.data)); + return ok(parsed.data); +}; + /** Everything but the member collections, which are replaced by their counts. */ const summarizeMembers = ( report: ManagedMemberInspection, @@ -189,22 +227,9 @@ export const projectManagedMemberView = ( }, ]), ); - const parsed = managedMemberInspectionSchema.safeParse( - parent.normalizedResult, - ); - if (!parsed.success) - return err( - analysisInputErrorFromIssues( - "inspect_analysis_view", - parsed.error.issues.map((issue) => ({ - ...issue, - path: ["source", "normalized_result", ...issue.path], - })), - { source: { normalized_result: parent.normalizedResult } }, - { cause: parsed.error }, - ), - ); - const report = parsed.data; + const parsed = parseMemberReport(parent); + if (!parsed.ok) return parsed; + const report = parsed.value; const shared = { parent_evidence_id: parent.evidenceId, parent_operation: "inspect_managed_members" as const, diff --git a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts index 83afc5d0c..d3ddf0d69 100644 --- a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts @@ -39,7 +39,43 @@ const createHarness = async () => { it("summarizes selected members and pages every original collection after the file disappears", async () => { const directory = await createTestTempDirectory("rea-member-summary-"); const path = join(directory, "fixture.dll"); - await writeFile(path, buildManagedPeFixture({ methodRowCount: 2 })); + await writeFile( + path, + buildManagedPeFixture({ + methodRowCount: 2, + // ldarg.0; ldfld field; call member ref; ldstr "ok"; pop; ret + ilBody: Buffer.from([ + (18 << 2) | 2, + 0x02, + 0x7b, + 1, + 0, + 0, + 0x04, + 0x28, + 1, + 0, + 0, + 0x0a, + 0x72, + 1, + 0, + 0, + 0x70, + 0x26, + 0x2a, + ]), + userStringHeap: Buffer.from([0, 5, 0x6f, 0, 0x6b, 0, 0]), + exportedTypes: [ + { + namespace: "System", + name: "Object", + flags: 0x0020_0001, + implementationRaw: (1 << 2) | 1, + }, + ], + }), + ); const { client, session } = await createHarness(); const definition = (await client.listTools()).tools.find( (x) => x.name === "inspect_managed_members", @@ -84,8 +120,6 @@ it("summarizes selected members and pages every original collection after the fi expect(view.summary.coverage).toEqual(report.coverage); expect(view.summary.identity_scope).toEqual(report.identity_scope); expect(view.summary).not.toHaveProperty("methods"); - expect(report.methods.length).toBeGreaterThan(0); - expect(report.member_refs.length).toBeGreaterThan(0); for (const detail of [undefined, "complete"]) { const complete = parseEvidence( ( @@ -106,6 +140,8 @@ it("summarizes selected members and pages every original collection after the fi ? report.user_strings?.literals : report[collection]; if (rows == null) throw new Error(`${collection} was not examined`); + // Empty collections would compare equal even if a row schema dropped facts. + expect(rows.length, collection).toBeGreaterThan(0); expect(view.summary.reported_counts[collection]).toBe(rows.length); const paged: unknown[] = []; for (let offset: number | null = 0; offset !== null;) {