Share application adapter and diagnostic helpers

This commit is contained in:
vorotamoroz committed 2026-10-29 15:04:09 +00:00
1 parent 4027417bf5
commit b9dcb0d4c7
26 files changed
+937 -305

No files matched your search

@@ -1,4 +1,5 @@
import type { ReviewHarnessScenarioResult } from "./reviewHarnessTypes";
import { createReviewHarnessHeapSampler } from "./reviewHarnessHeapSampling";
export const REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS = [
{ label: "4 KiB", bytes: 4 * 1024 },
@@ -23,15 +24,6 @@ type BenchmarkPerformance = Pick<Performance, "now"> & {
type HashAlgorithm = "SHA-256" | "xxHash64";
function readHeap(clock: BenchmarkPerformance): number | undefined {
try {
const bytes = clock.memory?.usedJSHeapSize;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;
} catch {
return undefined;
}
}
function summary(samples: readonly number[]): string {
const sorted = [...samples].sort((a, b) => a - b);
const middle = sorted[Math.floor(sorted.length / 2)];
@@ -63,19 +55,13 @@ export async function runReviewHarnessFileContentHashBenchmark(
throw new Error("The xxHash64 initialisation time is invalid.");
}
const before = readHeap(clock);
let highest = before;
const sampleHeap = () => {
const value = readHeap(clock);
if (value !== undefined) highest = Math.max(highest ?? value, value);
return value;
};
const heap = createReviewHarnessHeapSampler(clock);
const inputs: { readonly label: string; readonly bytes: number; readonly content: Blob }[] = [];
for (const { label, bytes } of REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS) {
await yieldControl();
inputs.push({ label, bytes, content: operations.createInput(bytes) });
sampleHeap();
heap.sample();
}
const hash = (algorithm: HashAlgorithm, content: Blob) =>
@@ -99,7 +85,7 @@ export async function runReviewHarnessFileContentHashBenchmark(
const elapsed = clock.now() - started;
checkHash(algorithm, inputs[0].label, result);
warmupTimes.set(algorithm, elapsed);
sampleHeap();
heap.sample();
}
const observations = [
@@ -124,7 +110,7 @@ export async function runReviewHarnessFileContentHashBenchmark(
throw new Error(`The ${algorithm} measurement returned an invalid duration.`);
}
samples[algorithm].push(elapsed);
sampleHeap();
heap.sample();
}
}
for (const algorithm of ["SHA-256", "xxHash64"] as const) {
@@ -132,16 +118,7 @@ export async function runReviewHarnessFileContentHashBenchmark(
}
}
const after = sampleHeap();
if (highest === undefined) {
observations.push("JavaScript heap: unavailable on this device.");
} else {
const mib = (bytes: number | undefined) =>
bytes === undefined ? "unavailable" : `${(bytes / 1048576).toFixed(2)} MiB`;
observations.push(
`JavaScript heap: before=${mib(before)}; highest sampled=${mib(highest)}; after=${mib(after)}.`
);
}
observations.push(heap.finish());
observations.push(
"Heap samples are approximate, may include other Obsidian work, and are affected by garbage collection. They are neither total app RAM nor a true peak."
);
@@ -74,6 +74,7 @@ describe("Review Harness file content hash measurements", () => {
expect(result.status).toBe("passed");
expect(result.detail).toBe("File content hash measurements completed.");
expect(report).toContain("JavaScript heap: unavailable on this device.");
expect([...f.inputs().contentBySize.keys()]).toEqual(
REVIEW_HARNESS_FILE_CONTENT_HASH_INPUTS.map(({ bytes }) => bytes)
);
@@ -107,6 +108,49 @@ describe("Review Harness file content hash measurements", () => {
expect(report).not.toContain("4096n");
});
it("tracks the sampled maximum and reports a lower final heap sample", async () => {
const f = fixture();
let reads = 0;
const clock = {
now: f.now,
get memory() {
const sample = reads++;
const mebibytes =
sample === 0 ? 2 : sample === 1 ? 5 : sample === 31 ? 1 : 3;
return { usedJSHeapSize: mebibytes * 1048576 };
},
};
const result = await runReviewHarnessFileContentHashBenchmark(f.operations, clock, f.yieldControl);
expect(result.observations).toContain(
"JavaScript heap: before=2.00 MiB; highest sampled=5.00 MiB; after=1.00 MiB."
);
expect(result.observations.join("\n")).toContain("neither total app RAM nor a true peak");
expect(reads).toBe(32);
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, -1, "throws"] as const)(
"keeps timings usable when the heap API returns %s",
async (value) => {
const f = fixture();
const result = await runReviewHarnessFileContentHashBenchmark(
f.operations,
{
now: f.now,
get memory() {
if (value === "throws") throw new Error("Heap API unavailable");
return { usedJSHeapSize: value as number };
},
},
f.yieldControl
);
expect(result.status).toBe("passed");
expect(result.observations).toContain("JavaScript heap: unavailable on this device.");
expect(result.observations.join("\n")).not.toMatch(/NaN|Infinity/u);
}
);
it("uses the same deterministic byte sequence for short and long inputs", async () => {
const shortInput = createFixedFileContentHashInput(4 * 1024);
const longInput = createFixedFileContentHashInput(64 * 1024);
@@ -0,0 +1,34 @@
type HeapPerformance = {
readonly memory?: { readonly usedJSHeapSize: number };
};
function readHeap(clock: HeapPerformance): number | undefined {
try {
const bytes = clock.memory?.usedJSHeapSize;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;
} catch {
return undefined;
}
}
export function createReviewHarnessHeapSampler(clock: HeapPerformance): {
sample(): void;
finish(): string;
} {
const before = readHeap(clock);
let highest = before;
let after = before;
const sample = () => {
after = readHeap(clock);
if (after !== undefined) highest = Math.max(highest ?? after, after);
};
const finish = () => {
sample();
if (highest === undefined) return "JavaScript heap: unavailable on this device.";
const mib = (bytes: number | undefined) =>
bytes === undefined ? "unavailable" : `${(bytes / 1048576).toFixed(2)} MiB`;
return `JavaScript heap: before=${mib(before)}; highest sampled=${mib(highest)}; after=${mib(after)}.`;
};
return { sample, finish };
}
@@ -1,4 +1,5 @@
import type { ReviewHarnessScenarioResult } from "./reviewHarnessTypes";
import { createReviewHarnessHeapSampler } from "./reviewHarnessHeapSampling";
export interface IdBenchmarkOperations {
deriveKey(): Promise<unknown>;
@@ -15,15 +16,6 @@ const SAMPLES = 3;
const BATCH_SIZE = 100;
const WARMUP_COUNT = 32;
function readHeap(clock: BenchmarkPerformance): number | undefined {
try {
const bytes = clock.memory?.usedJSHeapSize;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;
} catch {
return undefined;
}
}
function summary(samples: readonly number[]): string {
const sorted = [...samples].sort((a, b) => a - b);
return `median=${sorted[1].toFixed(2)} ms; range=${sorted[0].toFixed(2)}–${sorted[2].toFixed(2)} ms`;
@@ -34,13 +26,7 @@ export async function runReviewHarnessIdBenchmark(
clock: BenchmarkPerformance = performance,
yieldControl: () => Promise<void> = () => new Promise((resolve) => window.setTimeout(resolve, 0))
): Promise<ReviewHarnessScenarioResult> {
const before = readHeap(clock);
let highest = before;
const sampleHeap = () => {
const value = readHeap(clock);
if (value !== undefined) highest = Math.max(highest ?? value, value);
return value;
};
const heap = createReviewHarnessHeapSampler(clock);
const observations = [
"Fixed synthetic inputs; 3 samples, alternating legacy/independent order; 32 warm-up IDs per sample. Legacy Chunk algorithm: xxhash64.",
"Compute timings include input construction and awaited ID generation. Initialisation, warm-up, and pauses between batches are excluded. This does not measure a Rebuild or remote transfer.",
@@ -51,7 +37,7 @@ export async function runReviewHarnessIdBenchmark(
const started = clock.now();
await operations.deriveKey();
derivationSamples.push(clock.now() - started);
sampleHeap();
heap.sample();
}
observations.push(`ID key derivation at save time: ${summary(derivationSamples)} per derivation.`);
@@ -80,7 +66,7 @@ export async function runReviewHarnessIdBenchmark(
const started = clock.now();
for (let i = batch; i < batch + BATCH_SIZE; i++) await scenario.run(i, independent);
elapsed += clock.now() - started;
sampleHeap();
heap.sample();
}
samples[independent ? 1 : 0].push(elapsed);
}
@@ -92,16 +78,7 @@ export async function runReviewHarnessIdBenchmark(
);
}
}
const after = sampleHeap();
if (highest === undefined) {
observations.push("JavaScript heap: unavailable on this device.");
} else {
const mib = (bytes: number | undefined) =>
bytes === undefined ? "unavailable" : `${(bytes / 1048576).toFixed(2)} MiB`;
observations.push(
`JavaScript heap: before=${mib(before)}; highest sampled=${mib(highest)}; after=${mib(after)}.`
);
}
observations.push(heap.finish());
observations.push(
"Heap samples are approximate, may include other Obsidian work, and are affected by garbage collection. They are neither total app RAM nor a true peak."
);
@@ -73,6 +73,7 @@ describe("Review Harness ID measurements", () => {
"JavaScript heap: before=2.00 MiB; highest sampled=5.00 MiB; after=1.00 MiB."
);
expect(result.observations.join("\n")).toContain("neither total app RAM nor a true peak");
expect(reads).toBe(245);
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, -1, "throws"])(