perf(evidence): reuse sealed bundle reads in compare and export (#2715)
* perf(evidence): reuse sealed bundle reads in compare and export `rea compare`, the compare_bundles MCP tool and `rea evidence-export` read bundles through the mutable reader, so each consumer re-authenticated every record again: compareBundles parsed both bundles a second time and bundleDigest's serializeEvidenceBundle a third time; export's writer parsed the source once more before serializing. These consumers only read the bundle, so read it through the existing sealed reader (readImmutableEvidenceBundle). Later parses hit the immutable bundle cache. Ordinary mutable reads, kept deliberately by #1721, are unchanged. Output is byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(process): report no remaining time after a provider deadline fires Node timers can fire while performance.now() is still a fraction of a millisecond short of the monotonic deadline. ProviderStartupDeadline then reported interruption "timeout" and an aborted signal while remainingMs() returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0 (Hopper and Ghidra startup) could run past an elapsed deadline, and the real-timer test "aborts a bounded wait when its real monotonic deadline expires" failed intermittently in CI (expected 1 to be 0). Once the deadline's own timer has classified a timeout, report zero remaining time. A deterministic regression test fires the timer with the monotonic clock 0.4 ms short of the deadline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
46f51ee6cc
commit
9680da76c8
|
|
@ -11,6 +11,7 @@ import { compareBundles } from "../domain/bundleComparison.js";
|
|||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
readEvidenceBundle,
|
||||
readImmutableEvidenceBundle,
|
||||
writeEvidenceBundle,
|
||||
} from "./EvidenceBundleFiles.js";
|
||||
|
||||
|
|
@ -33,7 +34,8 @@ export const exportEvidenceBundleCommand = async (
|
|||
outputPath: string,
|
||||
overwrite: boolean,
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const loaded = await readEvidenceBundle(sourcePath);
|
||||
// The sealed snapshot lets the writer reuse this authentication.
|
||||
const loaded = await readImmutableEvidenceBundle(sourcePath);
|
||||
if (!loaded.ok) return loaded;
|
||||
const resources = new ArtifactResourceScope();
|
||||
const written = await writeEvidenceBundle(
|
||||
|
|
@ -69,9 +71,11 @@ export const compareEvidenceBundlesCommand = async (input: {
|
|||
readonly leftPath: string;
|
||||
readonly rightPath: string;
|
||||
}): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
// Sealed snapshots let comparison and its bundle digests reuse this
|
||||
// authentication instead of re-validating every record twice more.
|
||||
const [left, right] = await Promise.all([
|
||||
readEvidenceBundle(input.leftPath),
|
||||
readEvidenceBundle(input.rightPath),
|
||||
readImmutableEvidenceBundle(input.leftPath),
|
||||
readImmutableEvidenceBundle(input.rightPath),
|
||||
]);
|
||||
if (!left.ok) return left;
|
||||
if (!right.ok) return right;
|
||||
|
|
|
|||
|
|
@ -33,3 +33,20 @@ it("aborts a bounded wait when its real monotonic deadline expires", async () =>
|
|||
deadline.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports no remaining time once its timer fires before the monotonic deadline", () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
const now = vi.spyOn(performance, "now").mockReturnValue(1_000);
|
||||
const deadline = new ProviderStartupDeadline(40);
|
||||
try {
|
||||
// Timers may fire while the monotonic clock is still just short of the deadline.
|
||||
now.mockReturnValue(1_039.6);
|
||||
vi.advanceTimersByTime(40);
|
||||
expect(deadline.signal.aborted).toBe(true);
|
||||
expect(deadline.interruption).toBe("timeout");
|
||||
expect(deadline.remainingMs()).toBe(0);
|
||||
} finally {
|
||||
deadline.dispose();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ export class ProviderStartupDeadline {
|
|||
|
||||
/** Milliseconds still available to the next startup phase. */
|
||||
remainingMs(): number {
|
||||
// Node timers can fire a fraction of a millisecond before the monotonic
|
||||
// deadline, so an elapsed timer must not report time left to spend.
|
||||
if (this.#interruption === "timeout") return 0;
|
||||
return Math.max(0, Math.ceil(this.#deadlineAt - performance.now()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
|
||||
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
|
||||
import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js";
|
||||
import { readImmutableEvidenceBundle } from "../application/EvidenceBundleFiles.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import { compareBundles } from "../domain/bundleComparison.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
|
|
@ -33,12 +33,12 @@ export const registerBundleComparisonTool = (
|
|||
context.mcpReq.signal,
|
||||
async () => {
|
||||
const [left, right] = await Promise.all([
|
||||
readEvidenceBundle(
|
||||
readImmutableEvidenceBundle(
|
||||
input.left_bundle_path,
|
||||
context.mcpReq.signal,
|
||||
resources,
|
||||
),
|
||||
readEvidenceBundle(
|
||||
readImmutableEvidenceBundle(
|
||||
input.right_bundle_path,
|
||||
context.mcpReq.signal,
|
||||
resources,
|
||||
|
|
|
|||
Loading…
Reference in New Issue