From e5aa8d5c88aa3ca9b258e2e13d95c72db607190d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 8 Jul 2026 10:02:41 +0000 Subject: [PATCH] Add CouchDB netem shim benchmark --- src/apps/cli/testdeno/bench-couchdb.ts | 9 +++ src/apps/cli/testdeno/bench-network-cases.ts | 35 +++++++++ test/bench-network/Dockerfile.shim | 10 +++ test/bench-network/README.md | 30 ++++++++ test/bench-network/compose.yml | 62 +++++++++++++++ test/bench-network/netem-tcp-shim.sh | 80 ++++++++++++++++++++ 6 files changed, 226 insertions(+) create mode 100644 test/bench-network/Dockerfile.shim create mode 100644 test/bench-network/netem-tcp-shim.sh diff --git a/src/apps/cli/testdeno/bench-couchdb.ts b/src/apps/cli/testdeno/bench-couchdb.ts index 255aaaf..d337efa 100644 --- a/src/apps/cli/testdeno/bench-couchdb.ts +++ b/src/apps/cli/testdeno/bench-couchdb.ts @@ -23,6 +23,9 @@ type BenchmarkConfig = { passphrase: string; encrypt: boolean; managedCouchdb: boolean; + simulationTier: string; + networkProfile: string; + networkModel: string; }; function readEnvString(name: string, fallback: string): string { @@ -90,6 +93,9 @@ function buildConfig(): BenchmarkConfig { passphrase: readEnvString("BENCH_PASSPHRASE", `bench-${Date.now()}`), encrypt: readEnvBool("BENCH_ENCRYPT", true), managedCouchdb: readEnvBool("BENCH_COUCHDB_MANAGED", true), + simulationTier: readEnvString("BENCH_SIMULATION_TIER", "1"), + networkProfile: readEnvString("BENCH_NETWORK_PROFILE", "http-latency-proxy"), + networkModel: readEnvString("BENCH_NETWORK_MODEL", "local-http-proxy"), }; } @@ -285,6 +291,9 @@ async function main(): Promise { couchdbProxyUri: config.couchdbProxyUri, couchdbDbname: config.couchdbDbname, managedCouchdb: config.managedCouchdb, + simulationTier: config.simulationTier, + networkProfile: config.networkProfile, + networkModel: config.networkModel, rttRequestedMs: config.requestedRttMs, proxyApplied: proxy.applied, proxyNote: proxy.note, diff --git a/src/apps/cli/testdeno/bench-network-cases.ts b/src/apps/cli/testdeno/bench-network-cases.ts index 7997845..c4d5072 100644 --- a/src/apps/cli/testdeno/bench-network-cases.ts +++ b/src/apps/cli/testdeno/bench-network-cases.ts @@ -40,6 +40,7 @@ function buildCases(): BenchmarkCase[] { const couchdbRtt = readEnvString("BENCH_COUCHDB_RTT_MS", "20"); const tetheringVpnRtt = readEnvString("BENCH_TETHERING_VPN_RTT_MS", "120"); const localTurnServers = readEnvString("BENCH_LOCAL_TURN_SERVERS", "turn:127.0.0.1:3478"); + const shimCouchdbUri = readEnvString("BENCH_SHIM_COUCHDB_URI", "http://couchdb-shim:5984"); return [ { @@ -79,6 +80,40 @@ function buildCases(): BenchmarkCase[] { BENCH_COUCHDB_RTT_MS: tetheringVpnRtt, }, }, + { + name: "couchdb-netem-home-wifi", + runner: "couchdb", + description: + "Tier 2 CouchDB path through the Compose netem TCP shim using the home-wifi profile.", + dataPath: "Device A -> netem TCP shim -> CouchDB -> netem TCP shim -> Device B", + trustBoundary: "CouchDB operator and constrained network shim", + env: { + ...base, + BENCH_CASE: "couchdb-netem-home-wifi", + BENCH_COUCHDB_BACKEND_URI: shimCouchdbUri, + BENCH_COUCHDB_RTT_MS: "1", + BENCH_SIMULATION_TIER: "2", + BENCH_NETWORK_PROFILE: "home-wifi", + BENCH_NETWORK_MODEL: "compose-netem-tcp-shim", + }, + }, + { + name: "couchdb-netem-tethering-vpn", + runner: "couchdb", + description: + "Tier 2 CouchDB path through the Compose netem TCP shim using a tethering-vpn profile.", + dataPath: "Device A -> netem TCP shim -> CouchDB -> netem TCP shim -> Device B", + trustBoundary: "CouchDB operator and constrained smartphone/VPN-like network shim", + env: { + ...base, + BENCH_CASE: "couchdb-netem-tethering-vpn", + BENCH_COUCHDB_BACKEND_URI: shimCouchdbUri, + BENCH_COUCHDB_RTT_MS: "1", + BENCH_SIMULATION_TIER: "2", + BENCH_NETWORK_PROFILE: "tethering-vpn", + BENCH_NETWORK_MODEL: "compose-netem-tcp-shim", + }, + }, { name: "p2p-smartphone-vpn-direct", runner: "p2p", diff --git a/test/bench-network/Dockerfile.shim b/test/bench-network/Dockerfile.shim new file mode 100644 index 0000000..3abc08c --- /dev/null +++ b/test/bench-network/Dockerfile.shim @@ -0,0 +1,10 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:3.22 + +RUN apk add --no-cache iproute2 socat + +COPY test/bench-network/netem-tcp-shim.sh /usr/local/bin/livesync-netem-tcp-shim +RUN chmod +x /usr/local/bin/livesync-netem-tcp-shim + +CMD ["livesync-netem-tcp-shim"] diff --git a/test/bench-network/README.md b/test/bench-network/README.md index e0fe6b7..afc6f51 100644 --- a/test/bench-network/README.md +++ b/test/bench-network/README.md @@ -42,6 +42,8 @@ Available local cases: - `couchdb-baseline` - `p2p-direct-local` - `couchdb-tethering-vpn-proxy` +- `couchdb-netem-home-wifi` +- `couchdb-netem-tethering-vpn` - `p2p-smartphone-vpn-direct` - `p2p-user-turn` @@ -130,3 +132,31 @@ NETEM_BANDWIDTH_MBIT=10 \ NETEM_MTU=1380 \ docker compose -f test/bench-network/compose.yml --profile netem run --rm netem-smoke ``` + +## Shimmed CouchDB benchmark + +The optional `shim` profile runs a CouchDB benchmark through a TCP forwarding +container that applies `tc netem`. This is a manual Tier 2 synchronisation +measurement path; it is intentionally separate from required pull-request CI. + +```bash +docker compose -f test/bench-network/compose.yml --profile shim run --rm bench-runner-shim +``` + +The default profile is `home-wifi`. A smartphone/VPN-like profile can be +requested by overriding both the shim parameters and the benchmark case: + +```bash +NETEM_PROFILE=tethering-vpn \ +NETEM_DELAY_MS=140 \ +NETEM_JITTER_MS=50 \ +NETEM_LOSS_PERCENT=1.0 \ +NETEM_BANDWIDTH_MBIT=10 \ +NETEM_MTU=1380 \ +BENCH_CASES=couchdb-netem-tethering-vpn \ +docker compose -f test/bench-network/compose.yml --profile shim run --rm bench-runner-shim +``` + +The benchmark result records `simulationTier`, `networkProfile`, and +`networkModel`. The shim also writes its applied `tc qdisc`, route, and +interface state under `test/bench-network/bench-results/`. diff --git a/test/bench-network/compose.yml b/test/bench-network/compose.yml index 23c91eb..29572c6 100644 --- a/test/bench-network/compose.yml +++ b/test/bench-network/compose.yml @@ -98,6 +98,68 @@ services: volumes: - ./bench-results:/workspace/src/apps/cli/testdeno/bench-results + couchdb-shim: + build: + context: ../.. + dockerfile: test/bench-network/Dockerfile.shim + profiles: + - shim + depends_on: + couchdb: + condition: service_healthy + cap_add: + - NET_ADMIN + environment: + 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} + NETEM_RESULT_ROOT: /bench-results + SHIM_LISTEN_PORT: 5984 + SHIM_TARGET_HOST: couchdb + SHIM_TARGET_PORT: 5984 + volumes: + - ./bench-results:/bench-results + healthcheck: + test: ["CMD-SHELL", "nc -z 127.0.0.1 5984"] + interval: 2s + timeout: 5s + retries: 30 + + bench-runner-shim: + build: + context: ../.. + dockerfile: test/bench-network/Dockerfile.runner + profiles: + - shim + depends_on: + couchdb-shim: + condition: service_healthy + environment: + BENCH_COMMAND: ${BENCH_COMMAND:-cases} + BENCH_CASES: ${BENCH_CASES:-couchdb-netem-home-wifi} + BENCH_CASES_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_SWEEP_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_COUCHDB_MANAGED: "false" + BENCH_COUCHDB_BACKEND_URI: http://couchdb-shim:5984 + BENCH_SHIM_COUCHDB_URI: http://couchdb-shim:5984 + BENCH_COUCHDB_URI: http://127.0.0.1:15989 + BENCH_COUCHDB_USER: ${BENCH_COUCHDB_USER:-admin} + BENCH_COUCHDB_PASSWORD: ${BENCH_COUCHDB_PASSWORD:-testpassword} + 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_COUCHDB_RTT_MS: ${BENCH_COUCHDB_RTT_MS:-1} + BENCH_SYNC_TIMEOUT: ${BENCH_SYNC_TIMEOUT:-300} + BENCH_LIVESYNC_TEST_TEE: ${BENCH_LIVESYNC_TEST_TEE:-0} + volumes: + - ./bench-results:/workspace/src/apps/cli/testdeno/bench-results + netem-smoke: build: context: ../.. diff --git a/test/bench-network/netem-tcp-shim.sh b/test/bench-network/netem-tcp-shim.sh new file mode 100644 index 0000000..35c0b0b --- /dev/null +++ b/test/bench-network/netem-tcp-shim.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env sh +set -eu + +profile="${NETEM_PROFILE:-home-wifi}" +iface="${NETEM_INTERFACE:-eth0}" +delay_ms="${NETEM_DELAY_MS:-20}" +jitter_ms="${NETEM_JITTER_MS:-5}" +loss_percent="${NETEM_LOSS_PERCENT:-0.1}" +bandwidth_mbit="${NETEM_BANDWIDTH_MBIT:-100}" +mtu="${NETEM_MTU:-1500}" +listen_port="${SHIM_LISTEN_PORT:-5984}" +target_host="${SHIM_TARGET_HOST:-couchdb}" +target_port="${SHIM_TARGET_PORT:-5984}" +out_root="${NETEM_RESULT_ROOT:-/bench-results}" +timestamp="$(date -u +%Y%m%d-%H%M%S)" +out_dir="${out_root}/netem-shim-${profile}-${timestamp}" +out_file="${out_dir}/summary.json" + +json_lines() { + awk ' + { + gsub(/\\/, "\\\\"); + gsub(/"/, "\\\""); + printf "%s \"%s\"", (NR == 1 ? "" : ",\n"), $0; + } + ' +} + +mkdir -p "$out_dir" + +if ! ip link show "$iface" >/dev/null 2>&1; then + echo "Network interface '$iface' was not found" >&2 + ip addr >&2 + exit 2 +fi + +ip link set dev "$iface" mtu "$mtu" +tc qdisc del dev "$iface" root >/dev/null 2>&1 || true +tc qdisc add dev "$iface" root netem \ + delay "${delay_ms}ms" "${jitter_ms}ms" \ + loss "${loss_percent}%" \ + rate "${bandwidth_mbit}mbit" + +ip_addr="$(ip addr show "$iface" | json_lines)" +ip_route="$(ip route | json_lines)" +tc_qdisc="$(tc qdisc show dev "$iface" | json_lines)" + +cat > "$out_file" <