mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-03 06:48:18 +00:00
Share application adapter and diagnostic helpers
This commit is contained in:
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"])(
|
||||
|
||||
Reference in new issue
Block a user