mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-22 17:13:20 +00:00
Merge main for current Customisation Sync regression coverage
This commit is contained in:
commit
a790d88b97
125 files changed
+7310
-544
No files matched your search
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { P2PConnectionInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { CLOUDFLARE_TURN_TYPE } from "@/integrations/cloudflare/settings";
|
||||
import { validateManagedTurnSettings } from "@/integrations/turnSettings";
|
||||
import { translateLiveSyncMessage as translate, translateIfAvailable } from "@/common/translation";
|
||||
|
||||
type TurnSettings = Pick<P2PConnectionInfo, "P2P_turnServers" | "P2P_turnUsername" | "P2P_turnCredential" | "P2P_managedType" | "P2P_managedId" | "P2P_managedToken">;
|
||||
let { settings = $bindable() }: { settings: TurnSettings } = $props();
|
||||
const managedType = $derived(settings.P2P_managedType ?? "");
|
||||
const error = $derived(validateManagedTurnSettings(settings));
|
||||
|
||||
function selectProvider(type: string) {
|
||||
settings.P2P_managedType = type || undefined;
|
||||
settings.P2P_managedId = type ? "" : undefined;
|
||||
settings.P2P_managedToken = type ? "" : undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="turn-configuration">
|
||||
<label>
|
||||
<span>{translate("TURN configuration")}</span>
|
||||
<select aria-label={translate("TURN configuration")} name="p2p-turn-source" value={managedType} onchange={(event) => selectProvider(event.currentTarget.value)}>
|
||||
<option value="">{translate("Manual")}</option>
|
||||
<option value={CLOUDFLARE_TURN_TYPE}>{translate("Managed (Cloudflare)")}</option>
|
||||
{#if managedType !== "" && managedType !== CLOUDFLARE_TURN_TYPE}
|
||||
<option value={managedType} disabled>{translate("Unsupported TURN configuration")}</option>
|
||||
{/if}
|
||||
</select>
|
||||
</label>
|
||||
{#if managedType === ""}
|
||||
<label>
|
||||
<span>{translate("TURN Server URLs (comma-separated)")}</span>
|
||||
<textarea name="p2p-turn-servers" rows="3" placeholder="turn:turn.example.com:3478"
|
||||
bind:value={settings.P2P_turnServers} autocapitalize="off" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>{translate("TURN Username")}</span>
|
||||
<input type="text" name="p2p-turn-username" placeholder={translate("Enter TURN username")} bind:value={settings.P2P_turnUsername}
|
||||
autocomplete="off" autocapitalize="off" spellcheck="false" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{translate("TURN Credential")}</span>
|
||||
<input type="password" name="p2p-turn-credential" placeholder={translate("Enter TURN credential")} bind:value={settings.P2P_turnCredential}
|
||||
autocomplete="new-password" />
|
||||
</label>
|
||||
{:else if managedType === CLOUDFLARE_TURN_TYPE}
|
||||
<label>
|
||||
<span>{translate("TURN Key ID")}</span>
|
||||
<input type="text" name="p2p-turn-turnKeyId" bind:value={settings.P2P_managedId}
|
||||
autocomplete="off" autocapitalize="off" spellcheck="false" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{translate("TURN Key API Token")}</span>
|
||||
<input type="password" name="p2p-turn-apiToken" bind:value={settings.P2P_managedToken}
|
||||
autocomplete="new-password" autocapitalize="off" spellcheck="false" />
|
||||
</label>
|
||||
<p>{translate("The API token is saved with this profile and included in Setup URI and QR code sharing. Temporary TURN credentials are kept in memory only.")}</p>
|
||||
{/if}
|
||||
{#if error}
|
||||
<p role="status" class="turn-error">{translateIfAvailable(error)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
label { display: grid; gap: 0.25rem; margin: 0.75rem 0; }
|
||||
input, textarea, select { box-sizing: border-box; width: 100%; }
|
||||
p { font-size: var(--font-ui-small, 0.9rem); }
|
||||
.turn-error { color: var(--text-error, #b33); }
|
||||
</style>
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { ObsidianLiveSyncSettings, SettingsMigrationState } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
|
||||
import type {
|
||||
ReviewHarnessScenarioResult,
|
||||
ReviewHarnessScenarioStatus,
|
||||
} from "./reviewHarnessTypes";
|
||||
import type { ReviewHarnessScenarioResult, ReviewHarnessScenarioStatus } from "./reviewHarnessTypes";
|
||||
|
||||
export type { ReviewHarnessScenarioResult, ReviewHarnessScenarioStatus } from "./reviewHarnessTypes";
|
||||
|
||||
@@ -32,6 +29,14 @@ export const REVIEW_HARNESS_SCENARIOS = [
|
||||
mode: "automatic",
|
||||
access: "dedicated-vault-fixtures",
|
||||
},
|
||||
{
|
||||
id: "id-generation-performance",
|
||||
title: "ID generation performance",
|
||||
description:
|
||||
"Measures legacy and independent IDs with fixed in-memory inputs. Reports time per 1,000 IDs and per ID, key derivation time, and JavaScript heap samples where available. Keep Obsidian in the foreground.",
|
||||
mode: "automatic",
|
||||
access: "read-only",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const REVIEW_HARNESS_SCENARIO_IDS = REVIEW_HARNESS_SCENARIOS.map(({ id }) => id);
|
||||
@@ -114,7 +119,9 @@ const NEW_VAULT_RECOMMENDATION_KEYS = [
|
||||
"E2EEAlgorithm",
|
||||
] as const;
|
||||
|
||||
type LifecycleSettingKey = (typeof PRESERVED_SYNC_SETTING_KEYS)[number] | (typeof NEW_VAULT_RECOMMENDATION_KEYS)[number];
|
||||
type LifecycleSettingKey =
|
||||
| (typeof PRESERVED_SYNC_SETTING_KEYS)[number]
|
||||
| (typeof NEW_VAULT_RECOMMENDATION_KEYS)[number];
|
||||
type SettingsForLifecycleInspection = Partial<Pick<ObsidianLiveSyncSettings, LifecycleSettingKey>>;
|
||||
|
||||
export function inspectSettingsLifecycle(input: {
|
||||
@@ -130,9 +137,7 @@ export function inspectSettingsLifecycle(input: {
|
||||
};
|
||||
}
|
||||
|
||||
const invalidSyncSettings = PRESERVED_SYNC_SETTING_KEYS.filter(
|
||||
(key) => typeof input.settings[key] !== "boolean"
|
||||
);
|
||||
const invalidSyncSettings = PRESERVED_SYNC_SETTING_KEYS.filter((key) => typeof input.settings[key] !== "boolean");
|
||||
if (invalidSyncSettings.length > 0) {
|
||||
return {
|
||||
status: "failed",
|
||||
@@ -205,6 +210,7 @@ export interface ReviewHarnessReportScenario {
|
||||
readonly mode: ReviewHarnessScenarioMode;
|
||||
readonly status: ReviewHarnessScenarioStatus;
|
||||
readonly detail: string;
|
||||
readonly observations?: readonly string[];
|
||||
}
|
||||
|
||||
export interface ReviewHarnessReportInput {
|
||||
@@ -248,13 +254,15 @@ export function formatReviewHarnessReport(input: ReviewHarnessReportInput): stri
|
||||
);
|
||||
const scenarios = table(
|
||||
["Scenario", "Mode", "Status", "Detail"],
|
||||
input.scenarios.map(({ id, title, mode, status, detail }) => [
|
||||
`${title} (${id})`,
|
||||
mode,
|
||||
status,
|
||||
detail,
|
||||
])
|
||||
input.scenarios.map(({ id, title, mode, status, detail }) => [`${title} (${id})`, mode, status, detail])
|
||||
);
|
||||
const observations = input.scenarios
|
||||
.filter((scenario) => scenario.observations?.length)
|
||||
.map(
|
||||
({ title, observations }) =>
|
||||
`### ${title}\n\n${observations!.map((value) => `- ${tableCell(value)}`).join("\n")}`
|
||||
)
|
||||
.join("\n\n");
|
||||
return `## Self-hosted LiveSync Review Harness report
|
||||
|
||||
Generated at \`${tableCell(input.generatedAt)}\`.
|
||||
@@ -267,6 +275,8 @@ ${environment}
|
||||
|
||||
${scenarios}
|
||||
|
||||
${observations}
|
||||
|
||||
<details>
|
||||
<summary>Event transcript</summary>
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ describe("Review Harness contract", () => {
|
||||
"settings-lifecycle",
|
||||
"compatibility-review",
|
||||
"vault-round-trip",
|
||||
"id-generation-performance",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ReviewHarnessRuntime {
|
||||
getCompatibilityPause(): CompatibilityPause | undefined;
|
||||
openCompatibilityReview(): Promise<void>;
|
||||
runVaultRoundTrip(): Promise<ReviewHarnessScenarioResult>;
|
||||
runIdBenchmark(): Promise<ReviewHarnessScenarioResult>;
|
||||
readContinuation(): string | null;
|
||||
writeContinuation(value: string): void;
|
||||
deleteContinuation(): void;
|
||||
@@ -159,6 +160,8 @@ export class ReviewHarnessController {
|
||||
});
|
||||
} else if (id === "vault-round-trip") {
|
||||
result = await this.runtime.runVaultRoundTrip();
|
||||
} else if (id === "id-generation-performance") {
|
||||
result = await this.runtime.runIdBenchmark();
|
||||
} else {
|
||||
const inspection = this.inspectCompatibilityReview();
|
||||
result =
|
||||
@@ -206,10 +209,7 @@ export class ReviewHarnessController {
|
||||
detail: "The device-local compatibility review remains pending.",
|
||||
observations: inspection.observations,
|
||||
};
|
||||
this.record(
|
||||
"compatibility-review-updated",
|
||||
this.results["compatibility-review"].status
|
||||
);
|
||||
this.record("compatibility-review-updated", this.results["compatibility-review"].status);
|
||||
} catch (error) {
|
||||
this.setUnexpectedFailure("compatibility-review", error);
|
||||
} finally {
|
||||
@@ -259,6 +259,7 @@ export class ReviewHarnessController {
|
||||
mode,
|
||||
status: this.results[id].status,
|
||||
detail: this.results[id].detail,
|
||||
observations: this.results[id].observations,
|
||||
})),
|
||||
transcript: this.transcript,
|
||||
});
|
||||
|
||||
@@ -80,6 +80,11 @@ function createRuntime(): ReviewHarnessRuntime & {
|
||||
detail: "The owned fixture tree was exercised and removed.",
|
||||
observations: [],
|
||||
})),
|
||||
runIdBenchmark: vi.fn(async () => ({
|
||||
status: "passed" as const,
|
||||
detail: "ID generation measurements completed.",
|
||||
observations: ["Chunk 256 B: 1000 IDs total=43.00 ms; per ID=0.0430 ms"],
|
||||
})),
|
||||
readContinuation() {
|
||||
return this.continuation;
|
||||
},
|
||||
@@ -150,6 +155,60 @@ describe("ReviewHarnessController", () => {
|
||||
expect(runtime.reportError).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs ID measurements on request and includes their units in the copied report", async () => {
|
||||
const runtime = createRuntime();
|
||||
const controller = new ReviewHarnessController(runtime);
|
||||
|
||||
await controller.runAutomaticScenarios();
|
||||
expect(runtime.runIdBenchmark).not.toHaveBeenCalled();
|
||||
|
||||
await controller.runScenario("id-generation-performance");
|
||||
await controller.copyReport();
|
||||
|
||||
expect(runtime.runIdBenchmark).toHaveBeenCalledOnce();
|
||||
expect(controller.snapshot().results["id-generation-performance"].status).toBe("passed");
|
||||
expect(vi.mocked(runtime.copyText).mock.calls[0][0]).toContain("1000 IDs total=43.00 ms; per ID=0.0430 ms");
|
||||
expect(runtime.runVaultRoundTrip).not.toHaveBeenCalled();
|
||||
expect(runtime.events).toEqual([]);
|
||||
expect(runtime.continuation).toBeNull();
|
||||
});
|
||||
|
||||
it("excludes an unexpected measurement error from the copied report", async () => {
|
||||
const runtime = createRuntime();
|
||||
runtime.runIdBenchmark = vi.fn().mockRejectedValue(new Error("private measurement error"));
|
||||
const controller = new ReviewHarnessController(runtime);
|
||||
|
||||
await controller.runScenario("id-generation-performance");
|
||||
|
||||
expect(controller.snapshot().results["id-generation-performance"].status).toBe("failed");
|
||||
expect(controller.createReport()).not.toContain("private measurement error");
|
||||
expect(runtime.reportError).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not overlap an ID measurement with another scenario", async () => {
|
||||
const runtime = createRuntime();
|
||||
let finish!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
runtime.runIdBenchmark = vi.fn(async () => {
|
||||
await pending;
|
||||
return { status: "passed" as const, detail: "Measured", observations: [] };
|
||||
});
|
||||
const controller = new ReviewHarnessController(runtime);
|
||||
|
||||
const running = controller.runScenario("id-generation-performance");
|
||||
await controller.runScenario("id-generation-performance");
|
||||
await controller.runScenario("vault-round-trip");
|
||||
|
||||
expect(runtime.runIdBenchmark).toHaveBeenCalledOnce();
|
||||
expect(runtime.runVaultRoundTrip).not.toHaveBeenCalled();
|
||||
expect(controller.snapshot().running).toBe(true);
|
||||
finish();
|
||||
await running;
|
||||
expect(controller.snapshot().running).toBe(false);
|
||||
});
|
||||
|
||||
it("deletes a one-shot continuation before exposing the resumed guided step", () => {
|
||||
const runtime = createRuntime();
|
||||
runtime.continuation = JSON.stringify({
|
||||
@@ -167,9 +226,7 @@ describe("ReviewHarnessController", () => {
|
||||
expect(controller.snapshot().results["compatibility-review"]).toMatchObject({
|
||||
status: "waiting-for-user",
|
||||
});
|
||||
expect(controller.snapshot().resumedRequestId).toBe(
|
||||
"compatibility-review-2026-07-18T11:59:00.000Z"
|
||||
);
|
||||
expect(controller.snapshot().resumedRequestId).toBe("compatibility-review-2026-07-18T11:59:00.000Z");
|
||||
});
|
||||
|
||||
it("does not copy rejected continuation values into the report", () => {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ReviewHarnessScenarioResult } from "./reviewHarnessTypes";
|
||||
|
||||
export interface IdBenchmarkOperations {
|
||||
deriveKey(): Promise<unknown>;
|
||||
chunkId(piece: string, independent: boolean): Promise<string>;
|
||||
documentId(path: string, independent: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
type BenchmarkPerformance = Pick<Performance, "now"> & {
|
||||
readonly memory?: { readonly usedJSHeapSize: number };
|
||||
};
|
||||
|
||||
const ID_COUNT = 1000;
|
||||
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`;
|
||||
}
|
||||
|
||||
export async function runReviewHarnessIdBenchmark(
|
||||
operations: IdBenchmarkOperations,
|
||||
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 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.",
|
||||
];
|
||||
const derivationSamples: number[] = [];
|
||||
for (let sample = 0; sample < SAMPLES; sample++) {
|
||||
await yieldControl();
|
||||
const started = clock.now();
|
||||
await operations.deriveKey();
|
||||
derivationSamples.push(clock.now() - started);
|
||||
sampleHeap();
|
||||
}
|
||||
observations.push(`ID key derivation at save time: ${summary(derivationSamples)} per derivation.`);
|
||||
|
||||
const cases = [
|
||||
...[256, 4096, 32768].map((bytes) => {
|
||||
const prefix = "r".repeat(bytes - 8);
|
||||
return {
|
||||
label: `Chunk IDs, ${bytes} B`,
|
||||
run: (i: number, independent: boolean) =>
|
||||
operations.chunkId(prefix + i.toString(36).padStart(8, "0"), independent),
|
||||
};
|
||||
}),
|
||||
{
|
||||
label: "Obfuscated document IDs",
|
||||
run: (i: number, independent: boolean) => operations.documentId(`benchmark/path-${i}.md`, independent),
|
||||
},
|
||||
];
|
||||
for (const scenario of cases) {
|
||||
const samples: [number[], number[]] = [[], []];
|
||||
for (let sample = 0; sample < SAMPLES; sample++) {
|
||||
for (const independent of sample % 2 === 0 ? [false, true] : [true, false]) {
|
||||
for (let i = 0; i < WARMUP_COUNT; i++) await scenario.run(i, independent);
|
||||
let elapsed = 0;
|
||||
for (let batch = 0; batch < ID_COUNT; batch += BATCH_SIZE) {
|
||||
await yieldControl();
|
||||
const started = clock.now();
|
||||
for (let i = batch; i < batch + BATCH_SIZE; i++) await scenario.run(i, independent);
|
||||
elapsed += clock.now() - started;
|
||||
sampleHeap();
|
||||
}
|
||||
samples[independent ? 1 : 0].push(elapsed);
|
||||
}
|
||||
}
|
||||
for (const [index, values] of samples.entries()) {
|
||||
const median = [...values].sort((a, b) => a - b)[1];
|
||||
observations.push(
|
||||
`${scenario.label}, ${index === 0 ? "legacy" : "independent"}: ${ID_COUNT} IDs total ${summary(values)}; per ID=${(median / ID_COUNT).toFixed(4)} ms.`
|
||||
);
|
||||
}
|
||||
}
|
||||
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 samples are approximate, may include other Obsidian work, and are affected by garbage collection. They are neither total app RAM nor a true peak."
|
||||
);
|
||||
return { status: "passed", detail: "ID generation measurements completed.", observations };
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runReviewHarnessIdBenchmark, type IdBenchmarkOperations } from "./reviewHarnessIdBenchmark";
|
||||
|
||||
function fixture() {
|
||||
let elapsed = 0;
|
||||
let derivations = 0;
|
||||
const chunkCounts = [0, 0];
|
||||
const documentCounts = [0, 0];
|
||||
const chunkSizes = new Set<number>();
|
||||
const operations: IdBenchmarkOperations = {
|
||||
deriveKey: () => {
|
||||
derivations++;
|
||||
elapsed += 42;
|
||||
return Promise.resolve("private-derived-key");
|
||||
},
|
||||
chunkId: (piece, independent) => {
|
||||
chunkCounts[independent ? 1 : 0]++;
|
||||
chunkSizes.add(piece.length);
|
||||
elapsed += independent ? 2 : 1;
|
||||
return Promise.resolve("private-chunk-id");
|
||||
},
|
||||
documentId: (_path, independent) => {
|
||||
documentCounts[independent ? 1 : 0]++;
|
||||
elapsed += independent ? 4 : 3;
|
||||
return Promise.resolve("private-document-id");
|
||||
},
|
||||
};
|
||||
return {
|
||||
operations,
|
||||
now: () => elapsed,
|
||||
yieldControl: () => {
|
||||
elapsed += 100;
|
||||
return Promise.resolve();
|
||||
},
|
||||
counts: () => ({ derivations, chunkCounts, documentCounts, chunkSizes: [...chunkSizes] }),
|
||||
};
|
||||
}
|
||||
|
||||
describe("Review Harness ID measurements", () => {
|
||||
it("reports totals and per-ID timings separately, excluding warm-up and cooperative pauses", async () => {
|
||||
const f = fixture();
|
||||
const result = await runReviewHarnessIdBenchmark(f.operations, { now: f.now }, f.yieldControl);
|
||||
const report = result.observations.join("\n");
|
||||
|
||||
expect(result.status).toBe("passed");
|
||||
expect(report).toContain("1000 IDs total median=1000.00 ms; range=1000.00–1000.00 ms; per ID=1.0000 ms");
|
||||
expect(report).toContain("1000 IDs total median=2000.00 ms; range=2000.00–2000.00 ms; per ID=2.0000 ms");
|
||||
expect(report).toContain("Obfuscated document IDs, legacy: 1000 IDs total median=3000.00 ms");
|
||||
expect(report).toContain("Obfuscated document IDs, independent: 1000 IDs total median=4000.00 ms");
|
||||
expect(report).toContain("ID key derivation at save time: median=42.00 ms");
|
||||
expect(report).toContain("JavaScript heap: unavailable on this device.");
|
||||
expect(report).not.toContain("private-");
|
||||
expect(f.counts()).toEqual({
|
||||
derivations: 3,
|
||||
chunkCounts: [9288, 9288],
|
||||
documentCounts: [3096, 3096],
|
||||
chunkSizes: [256, 4096, 32768],
|
||||
});
|
||||
});
|
||||
|
||||
it("labels the highest sampled heap separately from total app RAM and allows a lower final sample", async () => {
|
||||
const f = fixture();
|
||||
let reads = 0;
|
||||
const clock = {
|
||||
now: f.now,
|
||||
get memory() {
|
||||
return { usedJSHeapSize: (reads++ === 0 ? 2 : reads === 2 ? 5 : 1) * 1048576 };
|
||||
},
|
||||
};
|
||||
const result = await runReviewHarnessIdBenchmark(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");
|
||||
});
|
||||
|
||||
it.each([Number.NaN, Number.POSITIVE_INFINITY, -1, "throws"])(
|
||||
"keeps timings usable when the heap API returns %s",
|
||||
async (value) => {
|
||||
const f = fixture();
|
||||
const result = await runReviewHarnessIdBenchmark(
|
||||
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|private-/u);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DEFAULT_SETTINGS, deriveIdKey } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { path2id_base } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { HashManager } from "@vrtmrz/livesync-commonlib/hashing";
|
||||
import type { IdBenchmarkOperations } from "./reviewHarnessIdBenchmark";
|
||||
|
||||
const FIXTURE_PASSPHRASE = "Self-hosted LiveSync ID benchmark passphrase";
|
||||
const FIXTURE_SOURCE = "Self-hosted LiveSync ID benchmark source";
|
||||
const FIXTURE_KEY = "ab".repeat(32);
|
||||
|
||||
export async function createIdBenchmarkOperations(): Promise<IdBenchmarkOperations> {
|
||||
const managers: HashManager[] = [];
|
||||
for (const independent of [false, true]) {
|
||||
const settings = Object.freeze({
|
||||
...DEFAULT_SETTINGS,
|
||||
encrypt: true,
|
||||
passphrase: FIXTURE_PASSPHRASE,
|
||||
hashAlg: "xxhash64" as const,
|
||||
idDerivationVersion: independent ? (1 as const) : (0 as const),
|
||||
idDerivationKey: independent ? FIXTURE_KEY : "",
|
||||
});
|
||||
// HashManager only reads currentSettings; this fixture has no storage or live service access.
|
||||
const settingService = { currentSettings: () => settings } as HashManager["options"]["settingService"];
|
||||
const manager = new HashManager({ settingService });
|
||||
if (!(await manager.initialise())) throw new Error("The benchmark hash manager could not initialise.");
|
||||
managers.push(manager);
|
||||
}
|
||||
return {
|
||||
deriveKey: () => deriveIdKey(FIXTURE_SOURCE),
|
||||
chunkId: (piece, independent) => managers[independent ? 1 : 0].computeHash(piece),
|
||||
// Fixture paths are already normalised; use the same ID calculation as PathService.
|
||||
documentId: (path, independent) =>
|
||||
path2id_base(path as FilePath, FIXTURE_PASSPHRASE, false, independent ? FIXTURE_KEY : undefined),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { createIdBenchmarkOperations } from "./reviewHarnessIdBenchmarkRuntime";
|
||||
|
||||
describe("Review Harness benchmark implementation", () => {
|
||||
it("uses the packaged legacy and independent algorithms with isolated fixed settings", async () => {
|
||||
const originalDefaults = structuredClone(DEFAULT_SETTINGS);
|
||||
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network access is forbidden"));
|
||||
try {
|
||||
const operations = await createIdBenchmarkOperations();
|
||||
const chunk = "r".repeat(256);
|
||||
const legacy = await operations.chunkId(chunk, false);
|
||||
const independent = await operations.chunkId(chunk, true);
|
||||
|
||||
expect(legacy).toMatch(/^\+[0-9a-z]{1,13}$/u);
|
||||
expect(independent).toMatch(/^\+[0-9a-f]{64}$/u);
|
||||
expect(independent).toBe("+9223e53d99e80c29effee9e95e38ed168d13c14f717054f9e996a1cd0a597000");
|
||||
expect(await operations.chunkId(chunk, false)).toBe(legacy);
|
||||
expect(await operations.chunkId(chunk, true)).toBe(independent);
|
||||
expect(await operations.chunkId("s".repeat(256), true)).not.toBe(independent);
|
||||
|
||||
const legacyPath = await operations.documentId("benchmark/path-1.md", false);
|
||||
const independentPath = await operations.documentId("benchmark/path-1.md", true);
|
||||
expect(legacyPath).toMatch(/^f:[0-9a-f]{64}$/u);
|
||||
expect(independentPath).toMatch(/^f:[0-9a-f]{64}$/u);
|
||||
expect(legacyPath).not.toBe(independentPath);
|
||||
expect(await operations.documentId("benchmark/path-1.md", true)).toBe(independentPath);
|
||||
|
||||
const second = await createIdBenchmarkOperations();
|
||||
expect(await second.chunkId(chunk, true)).toBe(independent);
|
||||
expect(await operations.deriveKey()).toMatch(/^[0-9a-f]{64}$/u);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(DEFAULT_SETTINGS).toEqual(originalDefaults);
|
||||
} finally {
|
||||
fetch.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user