mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-20 16:22:31 +00:00
cli: add large-file-test and benchmark between couchdb and p2p
This commit is contained in:
@@ -6,3 +6,4 @@ node_modules
|
|||||||
.*.json
|
.*.json
|
||||||
*.env
|
*.env
|
||||||
!.test.env
|
!.test.env
|
||||||
|
bench-results
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.ts";
|
||||||
|
import { assertFilesEqual, runCliOrFail } from "./helpers/cli.ts";
|
||||||
|
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
|
||||||
|
import { createDeterministicDataset, type DatasetEntry } from "./helpers/dataset.ts";
|
||||||
|
|
||||||
|
type BenchmarkConfig = {
|
||||||
|
couchdbBackendUri: string;
|
||||||
|
couchdbProxyUri: string;
|
||||||
|
couchdbUser: string;
|
||||||
|
couchdbPassword: string;
|
||||||
|
couchdbDbname: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
datasetSeed: string;
|
||||||
|
mdFileCount: number;
|
||||||
|
mdMinSizeBytes: number;
|
||||||
|
mdMaxSizeBytes: number;
|
||||||
|
binFileCount: number;
|
||||||
|
binSizeBytes: number;
|
||||||
|
syncTimeoutSeconds: number;
|
||||||
|
requestedRttMs: number;
|
||||||
|
passphrase: string;
|
||||||
|
encrypt: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function readEnvString(name: string, fallback: string): string {
|
||||||
|
const value = Deno.env.get(name)?.trim();
|
||||||
|
return value && value.length > 0 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvNumber(name: string, fallback: number): number {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`${name} must be a positive number, got '${raw}'`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvBool(name: string, fallback: boolean): boolean {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return /^(1|true|yes|on)$/i.test(raw.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowMs(): number {
|
||||||
|
return performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMs(value: number): string {
|
||||||
|
return `${value.toFixed(1)} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number): string {
|
||||||
|
if (value < 1024) {
|
||||||
|
return `${value} B`;
|
||||||
|
}
|
||||||
|
const kib = value / 1024;
|
||||||
|
if (kib < 1024) {
|
||||||
|
return `${kib.toFixed(1)} KiB`;
|
||||||
|
}
|
||||||
|
return `${(kib / 1024).toFixed(1)} MiB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfig(): BenchmarkConfig {
|
||||||
|
return {
|
||||||
|
couchdbBackendUri: readEnvString("BENCH_COUCHDB_BACKEND_URI", "http://127.0.0.1:5989"),
|
||||||
|
couchdbProxyUri: readEnvString("BENCH_COUCHDB_URI", "http://127.0.0.1:15989"),
|
||||||
|
couchdbUser: readEnvString("BENCH_COUCHDB_USER", readEnvString("username", "admin")),
|
||||||
|
couchdbPassword: readEnvString("BENCH_COUCHDB_PASSWORD", readEnvString("password", "password")),
|
||||||
|
couchdbDbname: readEnvString("BENCH_COUCHDB_DBNAME", `bench-couchdb-${Date.now()}`),
|
||||||
|
datasetDirName: readEnvString("BENCH_DATASET_DIR", "bench-dataset"),
|
||||||
|
datasetSeed: readEnvString("BENCH_SEED", "livesync-benchmark-seed"),
|
||||||
|
mdFileCount: Math.floor(readEnvNumber("BENCH_MD_FILE_COUNT", 1500)),
|
||||||
|
mdMinSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MIN_SIZE_BYTES", 1024)),
|
||||||
|
mdMaxSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MAX_SIZE_BYTES", 20 * 1024)),
|
||||||
|
binFileCount: Math.floor(readEnvNumber("BENCH_BIN_FILE_COUNT", 500)),
|
||||||
|
binSizeBytes: Math.floor(readEnvNumber("BENCH_BIN_SIZE_BYTES", 100 * 1024)),
|
||||||
|
syncTimeoutSeconds: readEnvNumber("BENCH_SYNC_TIMEOUT", 240),
|
||||||
|
requestedRttMs: Math.floor(readEnvNumber("BENCH_COUCHDB_RTT_MS", 50)),
|
||||||
|
passphrase: readEnvString("BENCH_PASSPHRASE", `bench-${Date.now()}`),
|
||||||
|
encrypt: readEnvBool("BENCH_ENCRYPT", true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalResultPath(): string | undefined {
|
||||||
|
const raw = Deno.env.get("BENCH_RESULT_JSON")?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSampleFiles(entries: DatasetEntry[]): DatasetEntry[] {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const md = entries.find((e) => e.kind === "md");
|
||||||
|
const bin = entries.find((e) => e.kind === "bin");
|
||||||
|
const middle = entries[Math.floor(entries.length / 2)];
|
||||||
|
const last = entries[entries.length - 1];
|
||||||
|
const unique = new Map<string, DatasetEntry>();
|
||||||
|
for (const entry of [md, bin, middle, last]) {
|
||||||
|
if (entry) {
|
||||||
|
unique.set(entry.relativePath, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...unique.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyHandle = {
|
||||||
|
stop: () => Promise<void>;
|
||||||
|
applied: boolean;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function startCouchdbProxy(options: {
|
||||||
|
backendUri: string;
|
||||||
|
proxyUri: string;
|
||||||
|
requestedRttMs: number;
|
||||||
|
}): ProxyHandle {
|
||||||
|
const backend = new URL(options.backendUri);
|
||||||
|
const proxy = new URL(options.proxyUri);
|
||||||
|
const halfDelayMs = Math.max(1, Math.floor(options.requestedRttMs / 2));
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const listener = Deno.serve(
|
||||||
|
{
|
||||||
|
hostname: proxy.hostname,
|
||||||
|
port: Number(proxy.port),
|
||||||
|
signal: controller.signal,
|
||||||
|
onError(error) {
|
||||||
|
console.error(`[Proxy] ${String(error)}`);
|
||||||
|
return new Response("proxy error", { status: 502 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, halfDelayMs));
|
||||||
|
|
||||||
|
const targetUrl = new URL(request.url);
|
||||||
|
targetUrl.protocol = backend.protocol;
|
||||||
|
targetUrl.host = backend.host;
|
||||||
|
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
headers.delete("content-length");
|
||||||
|
|
||||||
|
let requestBody: ArrayBuffer | undefined;
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
try {
|
||||||
|
requestBody = await request.arrayBuffer();
|
||||||
|
} catch {
|
||||||
|
requestBody = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstream = await fetch(targetUrl, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: requestBody,
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseHeaders = new Headers(upstream.headers);
|
||||||
|
responseHeaders.delete("content-length");
|
||||||
|
const responseBody = await upstream.arrayBuffer();
|
||||||
|
|
||||||
|
return new Response(responseBody, {
|
||||||
|
status: upstream.status,
|
||||||
|
statusText: upstream.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applied: true,
|
||||||
|
note: `local reverse proxy on ${proxy.origin} with ${halfDelayMs}ms pre-forward delay`,
|
||||||
|
stop: async () => {
|
||||||
|
controller.abort();
|
||||||
|
await listener.finished.catch(() => {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = buildConfig();
|
||||||
|
const resultPath = readOptionalResultPath();
|
||||||
|
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-couchdb-bench");
|
||||||
|
const vaultA = workDir.join("vault-a");
|
||||||
|
const vaultB = workDir.join("vault-b");
|
||||||
|
const settingsA = workDir.join("settings-a.json");
|
||||||
|
const settingsB = workDir.join("settings-b.json");
|
||||||
|
await Deno.mkdir(vaultA, { recursive: true });
|
||||||
|
await Deno.mkdir(vaultB, { recursive: true });
|
||||||
|
|
||||||
|
await initSettingsFile(settingsA);
|
||||||
|
await initSettingsFile(settingsB);
|
||||||
|
|
||||||
|
await startCouchdb(config.couchdbBackendUri, config.couchdbUser, config.couchdbPassword, config.couchdbDbname);
|
||||||
|
|
||||||
|
const proxy = startCouchdbProxy({
|
||||||
|
backendUri: config.couchdbBackendUri,
|
||||||
|
proxyUri: config.couchdbProxyUri,
|
||||||
|
requestedRttMs: config.requestedRttMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
applyRemoteSyncSettings(settingsA, {
|
||||||
|
remoteType: "COUCHDB",
|
||||||
|
couchdbUri: config.couchdbProxyUri,
|
||||||
|
couchdbUser: config.couchdbUser,
|
||||||
|
couchdbPassword: config.couchdbPassword,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
encrypt: config.encrypt,
|
||||||
|
passphrase: config.passphrase,
|
||||||
|
}),
|
||||||
|
applyRemoteSyncSettings(settingsB, {
|
||||||
|
remoteType: "COUCHDB",
|
||||||
|
couchdbUri: config.couchdbProxyUri,
|
||||||
|
couchdbUser: config.couchdbUser,
|
||||||
|
couchdbPassword: config.couchdbPassword,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
encrypt: config.encrypt,
|
||||||
|
passphrase: config.passphrase,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seedFiles = await createDeterministicDataset({
|
||||||
|
rootDir: vaultA,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
seed: config.datasetSeed,
|
||||||
|
mdCount: config.mdFileCount,
|
||||||
|
mdMinSizeBytes: config.mdMinSizeBytes,
|
||||||
|
mdMaxSizeBytes: config.mdMaxSizeBytes,
|
||||||
|
binCount: config.binFileCount,
|
||||||
|
binSizeBytes: config.binSizeBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mirrorStart = nowMs();
|
||||||
|
await runCliOrFail(vaultA, "--settings", settingsA, "mirror");
|
||||||
|
const mirrorElapsed = nowMs() - mirrorStart;
|
||||||
|
|
||||||
|
const syncAStart = nowMs();
|
||||||
|
await runCliOrFail(vaultA, "--settings", settingsA, "sync");
|
||||||
|
const syncAElapsed = nowMs() - syncAStart;
|
||||||
|
|
||||||
|
const syncBStart = nowMs();
|
||||||
|
await runCliOrFail(vaultB, "--settings", settingsB, "sync");
|
||||||
|
const syncBElapsed = nowMs() - syncBStart;
|
||||||
|
|
||||||
|
const sampleFiles = pickSampleFiles(seedFiles.entries);
|
||||||
|
for (const sample of sampleFiles) {
|
||||||
|
const pulledPath = workDir.join(`pulled-${sample.relativePath.split("/").join("_")}`);
|
||||||
|
await runCliOrFail(vaultB, "--settings", settingsB, "pull", sample.relativePath, pulledPath);
|
||||||
|
await assertFilesEqual(sample.absolutePath, pulledPath, `sample file mismatch after CouchDB sync: ${sample.relativePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
mode: "couchdb-cli-benchmark",
|
||||||
|
couchdbBackendUri: config.couchdbBackendUri,
|
||||||
|
couchdbProxyUri: config.couchdbProxyUri,
|
||||||
|
couchdbDbname: config.couchdbDbname,
|
||||||
|
rttRequestedMs: config.requestedRttMs,
|
||||||
|
proxyApplied: proxy.applied,
|
||||||
|
proxyNote: proxy.note,
|
||||||
|
datasetSeed: config.datasetSeed,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
totalFiles: seedFiles.totalFiles,
|
||||||
|
totalBytes: seedFiles.totalBytes,
|
||||||
|
mdFileCount: seedFiles.mdCount,
|
||||||
|
binFileCount: seedFiles.binCount,
|
||||||
|
mirrorElapsedMs: Number(mirrorElapsed.toFixed(1)),
|
||||||
|
syncAElapsedMs: Number(syncAElapsed.toFixed(1)),
|
||||||
|
syncBElapsedMs: Number(syncBElapsed.toFixed(1)),
|
||||||
|
totalSyncElapsedMs: Number((syncAElapsed + syncBElapsed).toFixed(1)),
|
||||||
|
throughputBytesPerSec: Number((seedFiles.totalBytes / ((syncAElapsed + syncBElapsed) / 1000)).toFixed(2)),
|
||||||
|
throughputMiBPerSec: Number((seedFiles.totalBytes / ((syncAElapsed + syncBElapsed) / 1000) / 1024 / 1024).toFixed(4)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resultPath) {
|
||||||
|
await Deno.writeTextFile(resultPath, JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
console.error(
|
||||||
|
`[Benchmark] couchdb mirrored ${seedFiles.totalFiles} files (${formatBytes(seedFiles.totalBytes)}) in ${formatMs(
|
||||||
|
mirrorElapsed
|
||||||
|
)}, synced in ${formatMs(syncAElapsed + syncBElapsed)} (${result.throughputBytesPerSec} B/s, ${result.throughputMiBPerSec} MiB/s)`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await proxy.stop();
|
||||||
|
await stopCouchdb().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`[Fatal Error]`, error);
|
||||||
|
Deno.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { TempDir } from "./helpers/temp.ts";
|
||||||
|
import { applyP2pSettings, applyP2pTestTweaks, initSettingsFile } from "./helpers/settings.ts";
|
||||||
|
import { startCliInBackground } from "./helpers/backgroundCli.ts";
|
||||||
|
import { discoverPeer, maybeStartLocalRelay, stopLocalRelayIfStarted } from "./helpers/p2p.ts";
|
||||||
|
import { assertFilesEqual, runCliOrFail } from "./helpers/cli.ts";
|
||||||
|
import { createDeterministicDataset, type DatasetEntry } from "./helpers/dataset.ts";
|
||||||
|
|
||||||
|
type BenchmarkConfig = {
|
||||||
|
relay: string;
|
||||||
|
appId: string;
|
||||||
|
roomId: string;
|
||||||
|
passphrase: string;
|
||||||
|
datasetDirName: string;
|
||||||
|
datasetSeed: string;
|
||||||
|
mdFileCount: number;
|
||||||
|
mdMinSizeBytes: number;
|
||||||
|
mdMaxSizeBytes: number;
|
||||||
|
binFileCount: number;
|
||||||
|
binSizeBytes: number;
|
||||||
|
peersTimeoutSeconds: number;
|
||||||
|
syncTimeoutSeconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function readEnvString(name: string, fallback: string): string {
|
||||||
|
const value = Deno.env.get(name)?.trim();
|
||||||
|
return value && value.length > 0 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvNumber(name: string, fallback: number): number {
|
||||||
|
const raw = Deno.env.get(name);
|
||||||
|
if (raw === undefined || raw.trim() === "") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
throw new Error(`${name} must be a positive number, got '${raw}'`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowMs(): number {
|
||||||
|
return performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMs(value: number): string {
|
||||||
|
return `${value.toFixed(1)} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number): string {
|
||||||
|
if (value < 1024) {
|
||||||
|
return `${value} B`;
|
||||||
|
}
|
||||||
|
const kib = value / 1024;
|
||||||
|
if (kib < 1024) {
|
||||||
|
return `${kib.toFixed(1)} KiB`;
|
||||||
|
}
|
||||||
|
const mib = kib / 1024;
|
||||||
|
return `${mib.toFixed(1)} MiB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfig(): BenchmarkConfig {
|
||||||
|
return {
|
||||||
|
relay: readEnvString("BENCH_RELAY", "ws://localhost:4000/"),
|
||||||
|
appId: readEnvString("BENCH_APP_ID", "self-hosted-livesync-cli-benchmark"),
|
||||||
|
roomId: readEnvString("BENCH_ROOM_ID", `bench-room-${Date.now()}`),
|
||||||
|
passphrase: readEnvString("BENCH_PASSPHRASE", `bench-${Date.now()}`),
|
||||||
|
datasetDirName: readEnvString("BENCH_DATASET_DIR", "bench-dataset"),
|
||||||
|
datasetSeed: readEnvString("BENCH_SEED", "livesync-benchmark-seed"),
|
||||||
|
mdFileCount: Math.floor(readEnvNumber("BENCH_MD_FILE_COUNT", 1500)),
|
||||||
|
mdMinSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MIN_SIZE_BYTES", 1024)),
|
||||||
|
mdMaxSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MAX_SIZE_BYTES", 20 * 1024)),
|
||||||
|
binFileCount: Math.floor(readEnvNumber("BENCH_BIN_FILE_COUNT", 500)),
|
||||||
|
binSizeBytes: Math.floor(readEnvNumber("BENCH_BIN_SIZE_BYTES", 100 * 1024)),
|
||||||
|
peersTimeoutSeconds: readEnvNumber("BENCH_PEERS_TIMEOUT", 20),
|
||||||
|
syncTimeoutSeconds: readEnvNumber("BENCH_SYNC_TIMEOUT", 240),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalResultPath(): string | undefined {
|
||||||
|
const raw = Deno.env.get("BENCH_RESULT_JSON")?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSampleFiles(entries: DatasetEntry[]): DatasetEntry[] {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const md = entries.find((e) => e.kind === "md");
|
||||||
|
const bin = entries.find((e) => e.kind === "bin");
|
||||||
|
const middle = entries[Math.floor(entries.length / 2)];
|
||||||
|
const last = entries[entries.length - 1];
|
||||||
|
const unique = new Map<string, DatasetEntry>();
|
||||||
|
for (const entry of [md, bin, middle, last]) {
|
||||||
|
if (entry) {
|
||||||
|
unique.set(entry.relativePath, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...unique.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const config = buildConfig();
|
||||||
|
const resultPath = readOptionalResultPath();
|
||||||
|
|
||||||
|
const relayStarted = await maybeStartLocalRelay(config.relay);
|
||||||
|
await using workDir = await TempDir.create("livesync-cli-p2p-bench");
|
||||||
|
|
||||||
|
const hostVault = workDir.join("vault-host");
|
||||||
|
const clientVault = workDir.join("vault-client");
|
||||||
|
const hostSettings = workDir.join("settings-host.json");
|
||||||
|
const clientSettings = workDir.join("settings-client.json");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
Deno.mkdir(hostVault, { recursive: true }),
|
||||||
|
Deno.mkdir(clientVault, { recursive: true }),
|
||||||
|
initSettingsFile(hostSettings),
|
||||||
|
initSettingsFile(clientSettings),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
applyP2pSettings(hostSettings, config.roomId, config.passphrase, config.appId, config.relay, "~.*"),
|
||||||
|
applyP2pSettings(clientSettings, config.roomId, config.passphrase, config.appId, config.relay, "~.*"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
applyP2pTestTweaks(hostSettings, "p2p-bench-host", config.passphrase),
|
||||||
|
applyP2pTestTweaks(clientSettings, "p2p-bench-client", config.passphrase),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seedFiles = await createDeterministicDataset({
|
||||||
|
rootDir: hostVault,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
seed: config.datasetSeed,
|
||||||
|
mdCount: config.mdFileCount,
|
||||||
|
mdMinSizeBytes: config.mdMinSizeBytes,
|
||||||
|
mdMaxSizeBytes: config.mdMaxSizeBytes,
|
||||||
|
binCount: config.binFileCount,
|
||||||
|
binSizeBytes: config.binSizeBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mirrorStart = nowMs();
|
||||||
|
await runCliOrFail(hostVault, "--settings", hostSettings, "mirror");
|
||||||
|
const mirrorElapsed = nowMs() - mirrorStart;
|
||||||
|
|
||||||
|
const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host");
|
||||||
|
try {
|
||||||
|
const hostReadyStart = nowMs();
|
||||||
|
await host.waitUntilContains("P2P host is running", 20000);
|
||||||
|
const hostReadyElapsed = nowMs() - hostReadyStart;
|
||||||
|
|
||||||
|
const peerDiscoveryStart = nowMs();
|
||||||
|
const peer = await discoverPeer(clientVault, clientSettings, config.peersTimeoutSeconds);
|
||||||
|
const peerDiscoveryElapsed = nowMs() - peerDiscoveryStart;
|
||||||
|
|
||||||
|
const syncStart = nowMs();
|
||||||
|
await runCliOrFail(
|
||||||
|
clientVault,
|
||||||
|
"--settings",
|
||||||
|
clientSettings,
|
||||||
|
"p2p-sync",
|
||||||
|
peer.id,
|
||||||
|
String(config.syncTimeoutSeconds)
|
||||||
|
);
|
||||||
|
const syncElapsed = nowMs() - syncStart;
|
||||||
|
|
||||||
|
const sampleFiles = pickSampleFiles(seedFiles.entries);
|
||||||
|
for (const sample of sampleFiles) {
|
||||||
|
const pulledPath = workDir.join(`pulled-${sample.relativePath.replaceAll("/", "_")}`);
|
||||||
|
await runCliOrFail(clientVault, "--settings", clientSettings, "pull", sample.relativePath, pulledPath);
|
||||||
|
await assertFilesEqual(sample.absolutePath, pulledPath, `sample file mismatch after sync: ${sample.relativePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
mode: "p2p-cli-benchmark",
|
||||||
|
relay: config.relay,
|
||||||
|
appId: config.appId,
|
||||||
|
roomId: config.roomId,
|
||||||
|
datasetSeed: config.datasetSeed,
|
||||||
|
datasetDirName: config.datasetDirName,
|
||||||
|
peerId: peer.id,
|
||||||
|
peerName: peer.name,
|
||||||
|
totalFiles: seedFiles.totalFiles,
|
||||||
|
totalBytes: seedFiles.totalBytes,
|
||||||
|
mdFileCount: seedFiles.mdCount,
|
||||||
|
binFileCount: seedFiles.binCount,
|
||||||
|
mirrorElapsedMs: Number(mirrorElapsed.toFixed(1)),
|
||||||
|
hostReadyElapsedMs: Number(hostReadyElapsed.toFixed(1)),
|
||||||
|
peerDiscoveryElapsedMs: Number(peerDiscoveryElapsed.toFixed(1)),
|
||||||
|
syncElapsedMs: Number(syncElapsed.toFixed(1)),
|
||||||
|
throughputBytesPerSec: Number((seedFiles.totalBytes / (syncElapsed / 1000)).toFixed(2)),
|
||||||
|
throughputMiBPerSec: Number((seedFiles.totalBytes / (syncElapsed / 1000) / 1024 / 1024).toFixed(4)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resultPath) {
|
||||||
|
await Deno.writeTextFile(resultPath, JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
console.error(
|
||||||
|
`[Benchmark] mirrored ${seedFiles.totalFiles} files (${formatBytes(seedFiles.totalBytes)}) in ${formatMs(mirrorElapsed)}, ` +
|
||||||
|
`synced in ${formatMs(syncElapsed)} ` +
|
||||||
|
`(${result.throughputBytesPerSec} B/s, ${result.throughputMiBPerSec} MiB/s)`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await host.stop();
|
||||||
|
await stopLocalRelayIfStarted(relayStarted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`[Fatal Error]`, error);
|
||||||
|
Deno.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RESULTS_ROOT="${SCRIPT_DIR}/bench-results"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUT_DIR="${RESULTS_ROOT}/${TIMESTAMP}"
|
||||||
|
|
||||||
|
mkdir -p "${OUT_DIR}"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] output directory: ${OUT_DIR}"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] running p2p benchmark"
|
||||||
|
(
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
|
BENCH_RESULT_JSON="${OUT_DIR}/p2p.json" deno task bench:p2p
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "[bench-wrapper] running couchdb benchmark with RTT ${BENCH_COUCHDB_RTT_MS:-default} ms (emulating HTTP network latency)"
|
||||||
|
(
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
|
BENCH_RESULT_JSON="${OUT_DIR}/couchdb.json" deno task bench:couchdb
|
||||||
|
)
|
||||||
|
|
||||||
|
cat > "${OUT_DIR}/README.txt" <<EOF
|
||||||
|
Bench wrapper result set
|
||||||
|
|
||||||
|
Generated at: ${TIMESTAMP}
|
||||||
|
Directory: ${OUT_DIR}
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- p2p.json
|
||||||
|
- couchdb.json
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[bench-wrapper] verify outputs by cat"
|
||||||
|
echo "========== ${OUT_DIR}/README.txt =========="
|
||||||
|
cat "${OUT_DIR}/README.txt"
|
||||||
|
echo "========== ${OUT_DIR}/p2p.json =========="
|
||||||
|
cat "${OUT_DIR}/p2p.json"
|
||||||
|
echo "========== ${OUT_DIR}/couchdb.json =========="
|
||||||
|
cat "${OUT_DIR}/couchdb.json"
|
||||||
|
|
||||||
|
echo "[bench-wrapper] done"
|
||||||
|
echo "[bench-wrapper] result directory: ${OUT_DIR}"
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
|
"test:p2p-sync": "deno test --env-file=.test.env -A --no-check test-p2p-sync.ts",
|
||||||
"test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
|
"test:p2p-three-nodes": "deno test --env-file=.test.env -A --no-check test-p2p-three-nodes-conflict.ts",
|
||||||
"test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
|
"test:p2p-upload-download": "deno test --env-file=.test.env -A --no-check test-p2p-upload-download-repro.ts",
|
||||||
|
"bench:p2p": "deno run --env-file=.test.env -A --no-check bench-p2p.ts",
|
||||||
|
"bench:couchdb": "deno run --env-file=.test.env -A --no-check bench-couchdb.ts",
|
||||||
|
"bench:item1": "bash ./bench-run-item1.sh",
|
||||||
|
"bench:item1:full": "BENCH_MD_FILE_COUNT=1500 BENCH_MD_MIN_SIZE_BYTES=1024 BENCH_MD_MAX_SIZE_BYTES=20480 BENCH_BIN_FILE_COUNT=500 BENCH_BIN_SIZE_BYTES=102400 BENCH_COUCHDB_RTT_MS=50 bash ./bench-run-item1.sh",
|
||||||
"test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
|
"test:e2e-couchdb": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-couchdb.ts",
|
||||||
"test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts"
|
"test:e2e-matrix": "deno test --env-file=.test.env -A --no-check test-e2e-two-vaults-matrix.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user