From 708bd187d83cd23b3c39890adbcb8d6a68eb301b Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 8 Jul 2026 10:03:35 +0000 Subject: [PATCH] Add P2P split-container netem stress fixture --- src/apps/cli/testdeno/bench-p2p-split-node.ts | 435 ++++++++++++++++++ src/apps/cli/testdeno/deno.json | 1 + test/bench-network/Dockerfile.runner | 3 +- test/bench-network/README.md | 61 ++- test/bench-network/compose.yml | 88 ++++ test/bench-network/run-bench.sh | 5 +- 6 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 src/apps/cli/testdeno/bench-p2p-split-node.ts diff --git a/src/apps/cli/testdeno/bench-p2p-split-node.ts b/src/apps/cli/testdeno/bench-p2p-split-node.ts new file mode 100644 index 0000000..14bc945 --- /dev/null +++ b/src/apps/cli/testdeno/bench-p2p-split-node.ts @@ -0,0 +1,435 @@ +import { join } from "@std/path"; +import { startCliInBackground } from "./helpers/backgroundCli.ts"; +import { assertFilesEqual, runCliOrFail } from "./helpers/cli.ts"; +import { createDeterministicDataset, type DatasetEntry } from "./helpers/dataset.ts"; +import { discoverPeer } from "./helpers/p2p.ts"; +import { applyP2pSettings, applyP2pTestTweaks, initSettingsFile } from "./helpers/settings.ts"; + +type Role = "host" | "client"; + +type NetemSummary = { + enabled: boolean; + profile: string; + interface: string; + delayMs: number; + jitterMs: number; + lossPercent: number; + bandwidthMbit: number; + mtu: number; + tcQdisc?: string; + ipAddr?: string; + ipRoute?: string; +}; + +type HostReady = { + generatedAt: string; + totalFiles: number; + totalBytes: number; + mdFileCount: number; + binFileCount: number; + mirrorElapsedMs: number; + netem: NetemSummary; +}; + +type P2PConnectionStats = { + candidatePathCollected: boolean; + selectedPath: string; + localCandidate?: { candidateType: string; protocol: string; relayProtocol: string }; + remoteCandidate?: { candidateType: string; protocol: string; relayProtocol: string }; +}; + +function errorToRecord(error: unknown): Record { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + }; + } + return { + name: "UnknownError", + message: String(error), + }; +} + +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 non-negative number, got '${raw}'`); + } + return parsed; +} + +function nowMs(): number { + return performance.now(); +} + +async function commandOutput(command: string, args: string[]): Promise { + const output = await new Deno.Command(command, { + args, + stdin: "null", + stdout: "piped", + stderr: "piped", + }).output(); + const stdout = new TextDecoder().decode(output.stdout); + const stderr = new TextDecoder().decode(output.stderr); + if (!output.success) { + throw new Error(`${command} ${args.join(" ")} failed\nstdout: ${stdout}\nstderr: ${stderr}`); + } + return stdout.trim(); +} + +async function commandOk(command: string, args: string[]): Promise { + await commandOutput(command, args); +} + +async function applyNetemIfRequested(): Promise { + const enabled = readEnvString("BENCH_NETEM_ENABLED", "0") === "1"; + const profile = readEnvString("NETEM_PROFILE", "home-wifi"); + const iface = readEnvString("NETEM_INTERFACE", "eth0"); + const delayMs = readEnvNumber("NETEM_DELAY_MS", 20); + const jitterMs = readEnvNumber("NETEM_JITTER_MS", 5); + const lossPercent = readEnvNumber("NETEM_LOSS_PERCENT", 0.1); + const bandwidthMbit = readEnvNumber("NETEM_BANDWIDTH_MBIT", 100); + const mtu = readEnvNumber("NETEM_MTU", 1500); + + const summary: NetemSummary = { + enabled, + profile, + interface: iface, + delayMs, + jitterMs, + lossPercent, + bandwidthMbit, + mtu, + }; + + if (!enabled) { + return summary; + } + + await commandOk("ip", ["link", "set", "dev", iface, "mtu", String(mtu)]); + await new Deno.Command("tc", { args: ["qdisc", "del", "dev", iface, "root"] }).output(); + await commandOk("tc", [ + "qdisc", + "add", + "dev", + iface, + "root", + "netem", + "delay", + `${delayMs}ms`, + `${jitterMs}ms`, + "loss", + `${lossPercent}%`, + "rate", + `${bandwidthMbit}mbit`, + ]); + summary.tcQdisc = await commandOutput("tc", ["qdisc", "show", "dev", iface]); + summary.ipAddr = await commandOutput("ip", ["addr", "show", iface]); + summary.ipRoute = await commandOutput("ip", ["route"]); + return summary; +} + +async function waitForFile(path: string, timeoutMs: number): Promise { + const started = Date.now(); + while (Date.now() - started < timeoutMs) { + try { + const stat = await Deno.stat(path); + if (stat.isFile) { + return; + } + } catch { + // wait + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}`); +} + +async function readJsonFile(path: string): Promise { + return JSON.parse(await Deno.readTextFile(path)) as T; +} + +function pickSampleFiles(entries: DatasetEntry[]): DatasetEntry[] { + const unique = new Map(); + for (const entry of [entries.find((e) => e.kind === "md"), entries.find((e) => e.kind === "bin"), entries.at(-1)]) { + if (entry) { + unique.set(entry.relativePath, entry); + } + } + return [...unique.values()]; +} + +async function readLatestP2PConnectionStats(path: string): Promise { + try { + const lines = (await Deno.readTextFile(path)) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + return lines.length === 0 ? undefined : (JSON.parse(lines.at(-1)!) as P2PConnectionStats); + } catch { + return undefined; + } +} + +function buildCommonConfig() { + const runId = readEnvString("BENCH_SPLIT_RUN_ID", readEnvString("BENCH_ROOM_ID", "bench-split-run")); + const baseWorkRoot = readEnvString("BENCH_SPLIT_WORK_ROOT", "/p2p-work"); + return { + runId, + workRoot: join(baseWorkRoot, runId), + resultRoot: readEnvString("BENCH_SPLIT_RESULT_ROOT", "/workspace/src/apps/cli/testdeno/bench-results"), + relay: readEnvString("BENCH_RELAY", "ws://nostr-relay:7777/"), + appId: readEnvString("BENCH_APP_ID", "self-hosted-livesync-cli-benchmark"), + roomId: readEnvString("BENCH_ROOM_ID", "bench-split-room"), + passphrase: readEnvString("BENCH_PASSPHRASE", "bench-split-passphrase"), + turnServers: readEnvString("BENCH_TURN_SERVERS", ""), + datasetDirName: readEnvString("BENCH_DATASET_DIR", "bench-dataset"), + datasetSeed: readEnvString("BENCH_SEED", "livesync-benchmark-seed"), + mdFileCount: Math.floor(readEnvNumber("BENCH_MD_FILE_COUNT", 20)), + mdMinSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MIN_SIZE_BYTES", 512)), + mdMaxSizeBytes: Math.floor(readEnvNumber("BENCH_MD_MAX_SIZE_BYTES", 2048)), + binFileCount: Math.floor(readEnvNumber("BENCH_BIN_FILE_COUNT", 5)), + binSizeBytes: Math.floor(readEnvNumber("BENCH_BIN_SIZE_BYTES", 8192)), + peersTimeoutSeconds: readEnvNumber("BENCH_PEERS_TIMEOUT", 60), + syncTimeoutSeconds: readEnvNumber("BENCH_SYNC_TIMEOUT", 300), + nodeTimeoutMs: readEnvNumber("BENCH_SPLIT_NODE_TIMEOUT_MS", 360_000), + profile: readEnvString("BENCH_NETWORK_PROFILE", readEnvString("NETEM_PROFILE", "split-compose")), + }; +} + +async function prepareP2PSettings( + settingsPath: string, + peerName: string, + config: ReturnType +) { + await initSettingsFile(settingsPath); + await applyP2pSettings( + settingsPath, + config.roomId, + config.passphrase, + config.appId, + config.relay, + "~.*", + config.turnServers + ); + await applyP2pTestTweaks(settingsPath, peerName, config.passphrase); +} + +async function runHost(): Promise { + const config = buildCommonConfig(); + const netem = await applyNetemIfRequested(); + await Deno.mkdir(config.workRoot, { recursive: true }); + await Deno.mkdir(config.resultRoot, { recursive: true }); + + const hostVault = join(config.workRoot, "vault-host"); + const hostSettings = join(config.workRoot, "settings-host.json"); + await Deno.mkdir(hostVault, { recursive: true }); + await prepareP2PSettings(hostSettings, "p2p-split-host", config); + + 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, + }); + await Deno.writeTextFile( + join(config.workRoot, "sample-files.json"), + JSON.stringify(pickSampleFiles(seedFiles.entries), null, 2) + ); + + const mirrorStart = nowMs(); + await runCliOrFail(hostVault, "--settings", hostSettings, "mirror"); + const mirrorElapsedMs = Number((nowMs() - mirrorStart).toFixed(1)); + const hostReady: HostReady = { + generatedAt: new Date().toISOString(), + totalFiles: seedFiles.totalFiles, + totalBytes: seedFiles.totalBytes, + mdFileCount: seedFiles.mdCount, + binFileCount: seedFiles.binCount, + mirrorElapsedMs, + netem, + }; + await Deno.writeTextFile(join(config.workRoot, "host-ready.json"), JSON.stringify(hostReady, null, 2)); + + const host = startCliInBackground(hostVault, "--settings", hostSettings, "p2p-host"); + try { + await host.waitUntilContains("P2P host is running", 20_000); + await Deno.writeTextFile( + join(config.workRoot, "p2p-host-ready.json"), + JSON.stringify({ generatedAt: new Date().toISOString() }) + ); + await waitForFile(join(config.workRoot, "client-done.json"), config.nodeTimeoutMs); + } finally { + await host.stop(); + } +} + +async function runClient(): Promise { + const config = buildCommonConfig(); + const netem = await applyNetemIfRequested(); + await Deno.mkdir(config.resultRoot, { recursive: true }); + await waitForFile(join(config.workRoot, "host-ready.json"), config.nodeTimeoutMs); + await waitForFile(join(config.workRoot, "p2p-host-ready.json"), config.nodeTimeoutMs); + + const clientVault = join(config.workRoot, "vault-client"); + const clientSettings = join(config.workRoot, "settings-client.json"); + const statsPath = join(config.workRoot, "p2p-connection-stats.jsonl"); + await Deno.mkdir(clientVault, { recursive: true }); + await prepareP2PSettings(clientSettings, "p2p-split-client", config); + + const hostReady = await readJsonFile(join(config.workRoot, "host-ready.json")); + const timestamp = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15); + const outputDir = join(config.resultRoot, `p2p-split-${config.profile}-${timestamp}`); + await Deno.mkdir(outputDir, { recursive: true }); + + const previousStatsPath = Deno.env.get("LIVESYNC_P2P_STATS_JSONL"); + Deno.env.set("LIVESYNC_P2P_STATS_JSONL", statsPath); + let stage = "peer-discovery"; + let peerDiscoveryCommandElapsedMs: number | undefined; + let syncElapsedMs: number | undefined; + try { + const peerDiscoveryCommandStart = nowMs(); + const peer = await discoverPeer(clientVault, clientSettings, config.peersTimeoutSeconds); + peerDiscoveryCommandElapsedMs = Number((nowMs() - peerDiscoveryCommandStart).toFixed(1)); + + stage = "p2p-sync"; + const syncStart = nowMs(); + await runCliOrFail( + clientVault, + "--settings", + clientSettings, + "p2p-sync", + peer.id, + String(config.syncTimeoutSeconds) + ); + syncElapsedMs = Number((nowMs() - syncStart).toFixed(1)); + + stage = "sample-verification"; + const samples = await readJsonFile(join(config.workRoot, "sample-files.json")); + for (const sample of samples) { + const pulledPath = join(config.workRoot, `pulled-${sample.relativePath.replaceAll("/", "_")}`); + await runCliOrFail(clientVault, "--settings", clientSettings, "pull", sample.relativePath, pulledPath); + await assertFilesEqual( + sample.absolutePath, + pulledPath, + `sample file mismatch after split sync: ${sample.relativePath}` + ); + } + + const p2pConnectionStats = await readLatestP2PConnectionStats(statsPath); + const result = { + ok: true, + generatedAt: new Date().toISOString(), + caseName: "p2p-split-compose", + mode: "p2p-split-compose-benchmark", + runId: config.runId, + simulationTier: Deno.env.get("BENCH_NETEM_ENABLED") === "1" ? "2" : "1", + networkProfile: config.profile, + networkModel: + Deno.env.get("BENCH_NETEM_ENABLED") === "1" ? "split-compose-egress-netem" : "split-compose-no-netem", + relay: config.relay, + turnServers: config.turnServers, + turnEnabled: config.turnServers.trim().length > 0, + p2pCandidatePathVerified: p2pConnectionStats?.candidatePathCollected === true, + p2pConnectionStats, + hostNetem: hostReady.netem, + clientNetem: netem, + totalFiles: hostReady.totalFiles, + totalBytes: hostReady.totalBytes, + mdFileCount: hostReady.mdFileCount, + binFileCount: hostReady.binFileCount, + mirrorElapsedMs: hostReady.mirrorElapsedMs, + peerDiscoveryTimeoutSeconds: config.peersTimeoutSeconds, + peerDiscoveryCommandElapsedMs, + syncElapsedMs, + throughputBytesPerSec: Number((hostReady.totalBytes / (syncElapsedMs / 1000)).toFixed(2)), + throughputMiBPerSec: Number((hostReady.totalBytes / (syncElapsedMs / 1000) / 1024 / 1024).toFixed(4)), + }; + await Deno.writeTextFile(join(outputDir, "summary.json"), JSON.stringify(result, null, 2)); + await Deno.writeTextFile( + join(config.workRoot, "client-done.json"), + JSON.stringify({ generatedAt: new Date().toISOString(), outputDir, ok: true }) + ); + console.log(JSON.stringify(result, null, 2)); + } catch (error) { + const p2pConnectionStats = await readLatestP2PConnectionStats(statsPath); + const result = { + ok: false, + generatedAt: new Date().toISOString(), + caseName: "p2p-split-compose", + mode: "p2p-split-compose-benchmark", + runId: config.runId, + simulationTier: Deno.env.get("BENCH_NETEM_ENABLED") === "1" ? "2" : "1", + networkProfile: config.profile, + networkModel: + Deno.env.get("BENCH_NETEM_ENABLED") === "1" ? "split-compose-egress-netem" : "split-compose-no-netem", + relay: config.relay, + turnServers: config.turnServers, + turnEnabled: config.turnServers.trim().length > 0, + p2pCandidatePathVerified: p2pConnectionStats?.candidatePathCollected === true, + p2pConnectionStats, + hostNetem: hostReady.netem, + clientNetem: netem, + totalFiles: hostReady.totalFiles, + totalBytes: hostReady.totalBytes, + mdFileCount: hostReady.mdFileCount, + binFileCount: hostReady.binFileCount, + mirrorElapsedMs: hostReady.mirrorElapsedMs, + peerDiscoveryTimeoutSeconds: config.peersTimeoutSeconds, + peerDiscoveryCommandElapsedMs, + syncElapsedMs, + failure: { + stage, + ...errorToRecord(error), + }, + }; + await Deno.writeTextFile(join(outputDir, "summary.json"), JSON.stringify(result, null, 2)); + await Deno.writeTextFile( + join(config.workRoot, "client-done.json"), + JSON.stringify({ generatedAt: new Date().toISOString(), outputDir, ok: false }) + ); + console.log(JSON.stringify(result, null, 2)); + throw error; + } finally { + if (previousStatsPath === undefined) { + Deno.env.delete("LIVESYNC_P2P_STATS_JSONL"); + } else { + Deno.env.set("LIVESYNC_P2P_STATS_JSONL", previousStatsPath); + } + } +} + +async function main(): Promise { + const role = readEnvString("BENCH_P2P_SPLIT_ROLE", "") as Role; + if (role === "host") { + await runHost(); + return; + } + if (role === "client") { + await runClient(); + return; + } + throw new Error("BENCH_P2P_SPLIT_ROLE must be 'host' or 'client'"); +} + +if (import.meta.main) { + main().catch((error) => { + console.error("[Fatal Error]", error); + Deno.exit(1); + }); +} diff --git a/src/apps/cli/testdeno/deno.json b/src/apps/cli/testdeno/deno.json index 095ab2a..3a369e3 100644 --- a/src/apps/cli/testdeno/deno.json +++ b/src/apps/cli/testdeno/deno.json @@ -19,6 +19,7 @@ "bench:couchdb": "deno run --env-file=.test.env -A --no-check bench-couchdb.ts", "bench:cases": "deno run --env-file=.test.env -A --no-check bench-network-cases.ts", "bench:latency-sweep": "deno run --env-file=.test.env -A --no-check bench-latency-sweep.ts", + "bench:p2p-split-node": "deno run --env-file=.test.env -A --no-check bench-p2p-split-node.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", diff --git a/test/bench-network/Dockerfile.runner b/test/bench-network/Dockerfile.runner index 759be55..25afbd6 100644 --- a/test/bench-network/Dockerfile.runner +++ b/test/bench-network/Dockerfile.runner @@ -3,7 +3,7 @@ FROM node:24-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl unzip python3 make g++ \ + && apt-get install -y --no-install-recommends ca-certificates curl unzip python3 make g++ iproute2 \ && rm -rf /var/lib/apt/lists/* ENV DENO_INSTALL=/usr/local @@ -25,6 +25,7 @@ WORKDIR /workspace/src/apps/cli/testdeno RUN deno cache --lock=deno.lock \ bench-network-cases.ts \ bench-latency-sweep.ts \ + bench-p2p-split-node.ts \ bench-p2p.ts \ bench-couchdb.ts diff --git a/test/bench-network/README.md b/test/bench-network/README.md index a2545d7..61e172c 100644 --- a/test/bench-network/README.md +++ b/test/bench-network/README.md @@ -57,9 +57,9 @@ is executed in an actual tethered/VPN environment. The primary local comparison is between a remote-database path and a direct P2P path: -| Case | Data path | What is measured | What is not measured | -| --- | --- | --- | --- | -| `couchdb-baseline` | Device A -> CouchDB -> Device B | Two one-shot CLI synchronisation commands through a local HTTP latency proxy | Real WAN jitter, packet loss, bandwidth limits, VPN encapsulation, and server contention | +| Case | Data path | What is measured | What is not measured | +| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | +| `couchdb-baseline` | Device A -> CouchDB -> Device B | Two one-shot CLI synchronisation commands through a local HTTP latency proxy | Real WAN jitter, packet loss, bandwidth limits, VPN encapsulation, and server contention | | `p2p-direct-local` | Device A -> Device B after Nostr signalling | One CLI P2P synchronisation command over WebRTC DataChannel with TURN disabled | Public relay operation, mobile carrier behaviour, TURN relay throughput, and first-peer discovery latency | Use the CouchDB result as the remote-store baseline and the P2P result as the @@ -138,6 +138,61 @@ NETEM_MTU=1380 \ docker compose -f test/bench-network/compose.yml --profile netem run --rm netem-smoke ``` +## Split-container P2P emulation + +The optional `p2p-split` profile runs the P2P host and client in separate +Compose services. Each service can apply `tc netem` to its own egress interface +and the client result records the selected WebRTC ICE candidate pair. + +```bash +BENCH_MD_FILE_COUNT=2 \ +BENCH_BIN_FILE_COUNT=1 \ +BENCH_PEERS_TIMEOUT=10 \ +BENCH_SPLIT_RUN_ID="$(date -u +%Y%m%d%H%M%S)" \ +docker compose -f test/bench-network/compose.yml --profile p2p-split up \ + --abort-on-container-exit --exit-code-from p2p-split-client \ + p2p-split-host p2p-split-client +``` + +By default this uses the `home-wifi` profile (`20 ms` delay, `5 ms` jitter, +`0.1%` loss, `100 Mbit`, and `1500` MTU) on both P2P containers. Override the +same `NETEM_*` variables used by the TCP shim to model a stricter profile. + +```bash +BENCH_MD_FILE_COUNT=100 \ +BENCH_MD_MIN_SIZE_BYTES=512 \ +BENCH_MD_MAX_SIZE_BYTES=2048 \ +BENCH_BIN_FILE_COUNT=25 \ +BENCH_BIN_SIZE_BYTES=8192 \ +BENCH_PEERS_TIMEOUT=60 \ +BENCH_SYNC_TIMEOUT=420 \ +BENCH_SPLIT_RUN_ID="$(date -u +%Y%m%d%H%M%S)" \ +BENCH_NETWORK_PROFILE=tethering-vpn \ +NETEM_PROFILE=tethering-vpn \ +NETEM_DELAY_MS=140 \ +NETEM_JITTER_MS=50 \ +NETEM_LOSS_PERCENT=1.0 \ +NETEM_BANDWIDTH_MBIT=10 \ +NETEM_MTU=1380 \ +docker compose -f test/bench-network/compose.yml --profile p2p-split up \ + --abort-on-container-exit --exit-code-from p2p-split-client \ + p2p-split-host p2p-split-client +``` + +This is a Linux-only manual benchmark fixture, not a required pull-request CI +job. It shapes each P2P container's egress path, including signalling traffic, +and should be reported separately from the CouchDB TCP-shim measurements. The +result JSON includes `ok: true` for completed runs; failed runs still write a +summary with `ok: false` and a `failure` object before returning a non-zero +exit code. + +Remove the shared work volume between repeated manual runs when you do not use +a unique `BENCH_SPLIT_RUN_ID`: + +```bash +docker compose -f test/bench-network/compose.yml --profile p2p-split down --volumes +``` + ## Shimmed CouchDB benchmark The optional `shim` profile runs a CouchDB benchmark through a TCP forwarding diff --git a/test/bench-network/compose.yml b/test/bench-network/compose.yml index 29572c6..e444ab5 100644 --- a/test/bench-network/compose.yml +++ b/test/bench-network/compose.yml @@ -160,6 +160,91 @@ services: volumes: - ./bench-results:/workspace/src/apps/cli/testdeno/bench-results + p2p-split-host: + build: + context: ../.. + dockerfile: test/bench-network/Dockerfile.runner + profiles: + - p2p-split + depends_on: + nostr-relay: + condition: service_healthy + cap_add: + - NET_ADMIN + environment: + BENCH_COMMAND: p2p-split-node + BENCH_P2P_SPLIT_ROLE: host + BENCH_SPLIT_RUN_ID: ${BENCH_SPLIT_RUN_ID:-bench-split-run} + BENCH_SPLIT_WORK_ROOT: /p2p-work + BENCH_SPLIT_RESULT_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_RELAY: ws://nostr-relay:7777/ + BENCH_APP_ID: ${BENCH_APP_ID:-self-hosted-livesync-cli-benchmark} + BENCH_ROOM_ID: ${BENCH_ROOM_ID:-bench-split-room} + BENCH_PASSPHRASE: ${BENCH_PASSPHRASE:-bench-split-passphrase} + BENCH_TURN_SERVERS: ${BENCH_TURN_SERVERS:-} + BENCH_MD_FILE_COUNT: ${BENCH_MD_FILE_COUNT:-20} + BENCH_MD_MIN_SIZE_BYTES: ${BENCH_MD_MIN_SIZE_BYTES:-512} + BENCH_MD_MAX_SIZE_BYTES: ${BENCH_MD_MAX_SIZE_BYTES:-2048} + BENCH_BIN_FILE_COUNT: ${BENCH_BIN_FILE_COUNT:-5} + BENCH_BIN_SIZE_BYTES: ${BENCH_BIN_SIZE_BYTES:-8192} + BENCH_SYNC_TIMEOUT: ${BENCH_SYNC_TIMEOUT:-300} + BENCH_PEERS_TIMEOUT: ${BENCH_PEERS_TIMEOUT:-60} + BENCH_NETEM_ENABLED: ${BENCH_NETEM_ENABLED:-1} + BENCH_NETWORK_PROFILE: ${BENCH_NETWORK_PROFILE:-home-wifi} + NETEM_PROFILE: ${NETEM_PROFILE:-home-wifi} + NETEM_INTERFACE: ${NETEM_INTERFACE:-eth0} + NETEM_DELAY_MS: ${NETEM_DELAY_MS:-20} + NETEM_JITTER_MS: ${NETEM_JITTER_MS:-5} + NETEM_LOSS_PERCENT: ${NETEM_LOSS_PERCENT:-0.1} + NETEM_BANDWIDTH_MBIT: ${NETEM_BANDWIDTH_MBIT:-100} + NETEM_MTU: ${NETEM_MTU:-1500} + LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: ${LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS:-60000} + LIVESYNC_TEST_TEE: ${BENCH_LIVESYNC_TEST_TEE:-0} + volumes: + - p2p-split-work:/p2p-work + - ./bench-results:/workspace/src/apps/cli/testdeno/bench-results + + p2p-split-client: + build: + context: ../.. + dockerfile: test/bench-network/Dockerfile.runner + profiles: + - p2p-split + depends_on: + nostr-relay: + condition: service_healthy + p2p-split-host: + condition: service_started + cap_add: + - NET_ADMIN + environment: + BENCH_COMMAND: p2p-split-node + BENCH_P2P_SPLIT_ROLE: client + BENCH_SPLIT_RUN_ID: ${BENCH_SPLIT_RUN_ID:-bench-split-run} + BENCH_SPLIT_WORK_ROOT: /p2p-work + BENCH_SPLIT_RESULT_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_RELAY: ws://nostr-relay:7777/ + BENCH_APP_ID: ${BENCH_APP_ID:-self-hosted-livesync-cli-benchmark} + BENCH_ROOM_ID: ${BENCH_ROOM_ID:-bench-split-room} + BENCH_PASSPHRASE: ${BENCH_PASSPHRASE:-bench-split-passphrase} + BENCH_TURN_SERVERS: ${BENCH_TURN_SERVERS:-} + BENCH_SYNC_TIMEOUT: ${BENCH_SYNC_TIMEOUT:-300} + BENCH_PEERS_TIMEOUT: ${BENCH_PEERS_TIMEOUT:-60} + BENCH_NETEM_ENABLED: ${BENCH_NETEM_ENABLED:-1} + BENCH_NETWORK_PROFILE: ${BENCH_NETWORK_PROFILE:-home-wifi} + NETEM_PROFILE: ${NETEM_PROFILE:-home-wifi} + NETEM_INTERFACE: ${NETEM_INTERFACE:-eth0} + NETEM_DELAY_MS: ${NETEM_DELAY_MS:-20} + NETEM_JITTER_MS: ${NETEM_JITTER_MS:-5} + NETEM_LOSS_PERCENT: ${NETEM_LOSS_PERCENT:-0.1} + NETEM_BANDWIDTH_MBIT: ${NETEM_BANDWIDTH_MBIT:-100} + NETEM_MTU: ${NETEM_MTU:-1500} + LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: ${LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS:-60000} + LIVESYNC_TEST_TEE: ${BENCH_LIVESYNC_TEST_TEE:-0} + volumes: + - p2p-split-work:/p2p-work + - ./bench-results:/workspace/src/apps/cli/testdeno/bench-results + netem-smoke: build: context: ../.. @@ -179,3 +264,6 @@ services: NETEM_RESULT_ROOT: /bench-results volumes: - ./bench-results:/bench-results + +volumes: + p2p-split-work: diff --git a/test/bench-network/run-bench.sh b/test/bench-network/run-bench.sh index 4ca9eee..3227205 100644 --- a/test/bench-network/run-bench.sh +++ b/test/bench-network/run-bench.sh @@ -8,9 +8,12 @@ case "${BENCH_COMMAND:-cases}" in latency-sweep) exec deno task bench:latency-sweep ;; + p2p-split-node) + exec deno task bench:p2p-split-node + ;; *) echo "Unknown BENCH_COMMAND: ${BENCH_COMMAND}" >&2 - echo "Expected one of: cases, latency-sweep" >&2 + echo "Expected one of: cases, latency-sweep, p2p-split-node" >&2 exit 2 ;; esac