diff --git a/internal/ctrl/metrics.go b/internal/ctrl/metrics.go index b9ac5f5..74d4817 100644 --- a/internal/ctrl/metrics.go +++ b/internal/ctrl/metrics.go @@ -8,6 +8,8 @@ import ( "strings" "sync" "time" + + "github.com/maeneko/forgetting/internal/jsjson" ) // Метрики для Prometheus. awg-ctrl отдаёт JSON, а текстовый формат собирает awg-ui: на ноде @@ -32,9 +34,9 @@ type metricsPeer struct { } type metricsHost struct { - CPU map[string]float64 `json:"cpu"` - Cores int `json:"cores"` - Load []float64 `json:"load"` + CPU jsjson.Object `json:"cpu"` // порядок режимов как в /proc/stat (и в Node-версии) + Cores int `json:"cores"` + Load []float64 `json:"load"` Mem struct { Total float64 `json:"total"` Available float64 `json:"available"` @@ -89,13 +91,13 @@ func readHost() *metricsHost { if len(first) < 2 { return nil } - h := &metricsHost{CPU: map[string]float64{}, Cores: runtime.NumCPU()} + h := &metricsHost{Cores: runtime.NumCPU()} for i, m := range cpuModes { v := 0.0 if i+1 < len(first) { v = orZero(jsNumber(first[i+1])) } - h.CPU[m] = v / 100 // USER_HZ = 100 на всех обычных ядрах + h.CPU = append(h.CPU, jsjson.KV{Key: m, Val: v / 100}) // USER_HZ = 100 на всех обычных ядрах } mem, err := os.ReadFile("/proc/meminfo") if err != nil { diff --git a/tools/diffbench/compare.mjs b/tools/diffbench/compare.mjs new file mode 100644 index 0000000..d452024 --- /dev/null +++ b/tools/diffbench/compare.mjs @@ -0,0 +1,31 @@ +// compare.mjs : расхождения двух прогонов стенда. vpn:// раскрываются +// (сжатие у Node и Go разное), время маскируется. +import fs from "fs"; import zlib from "zlib"; +const [a, b] = process.argv.slice(2).map(f => JSON.parse(fs.readFileSync(f, "utf8"))); +const dec = s => { try { const buf = Buffer.from(s.slice(6), "base64url"); return "vpn<" + zlib.inflateSync(buf.subarray(4)).toString() + ">"; } catch (e) { return "vpn"; } }; +const norm = v => { + if (typeof v === "string") { + let s = v.replace(/vpn:\/\/[A-Za-z0-9_-]+/g, dec); + s = s.replace(/\d{4}-\d\d-\d\dT[\d:.-]+Z/g, "").replace(/"(created_at|lastHandshake|ts|time|uptime|generated_at)":\d+/g, '"$1":'); + return s; + } + if (Array.isArray(v)) return v.map(norm); + if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, /^(created_at|lastHandshake|ts|uptime)$/.test(k) && typeof x === "number" ? "" : norm(x)])); + return v; +}; +// Известный шум: живые цифры хоста (порядок ключей сохраняем), стек Express в dev-режиме, +// порядок вызовов из параллельных фоновых задач, отступы в CREATE TABLE. +const host = h => h && { cpu: Object.keys(h.cpu), cores: h.cores, load: h.load?.length, mem: Object.keys(h.mem) }; +for (const r of [a, b]) { + for (const s of r.steps) { + if (s.body?.host) { s.body.host = host(s.body.host); s.text = JSON.stringify(s.body); } + if (s.ctype === "text/html") s.body = s.body.replace(/
([A-Za-z]+Error)[^<]*(
[\s\S]*)?<\/pre>/, "
$1
"); + } + r.state.stubLog = r.state.stubLog.map(e => JSON.stringify(e).replace(/awg_(sync|psk)_\d+/g, "awg_$1_N")).sort(); + r.state.schema = r.state.schema.replace(/\s+/g, " "); +} +let diffs = 0; +const show = (label, x, y) => { const X = JSON.stringify(norm(x)), Y = JSON.stringify(norm(y)); if (X !== Y) { diffs++; let i = 0; while (X[i] === Y[i]) i++; const from = Math.max(0, i - 150); console.log(`### ${label} @${i}\n TS: …${X.slice(from, i + 350)}\n GO: …${Y.slice(from, i + 350)}`); } }; +a.steps.forEach((s, i) => { const t = b.steps[i]; show(`${i} ${s.name}`, { st: s.status, ct: s.ctype, body: s.body, text: s.text }, { st: t.status, ct: t.ctype, body: t.body, text: t.text }); }); +for (const k of Object.keys(a.state)) show("state." + k, a.state[k], b.state[k]); +console.log(diffs ? `${diffs} differences` : "IDENTICAL"); diff --git a/tools/diffbench/local.sh b/tools/diffbench/local.sh new file mode 100755 index 0000000..b4d1c61 --- /dev/null +++ b/tools/diffbench/local.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# local.sh <имя-прогона> — тот же прогон, что run.sh, но без Docker: на одноразовой +# машине (CI-раннер, облачная песочница). Заглушки кладутся в свой каталог и попадают только +# в PATH этого прогона; ВНИМАНИЕ: setup.sh пересоздаёт /etc/amnezia — не запускать на сервере. +# Нужны node 20+, sqlite3, openssl, go; npm install в awg-ctrl/ делается при первом запуске. +# Окружение то же: GEN, WITH_I1, S4, STUB_MODULE. Результат — tools/diffbench/out/<имя>.json. +set -e +IMPL="$1"; NAME="$2" +B=$(cd "$(dirname "$0")" && pwd); ROOT=$(cd "$B/../.." && pwd) +mkdir -p "$B/bin/stubs" "$B/out" +for c in awg awg-quick ip tc nft modinfo; do + printf '#!/bin/sh\nexec node %s %s "$@"\n' "$B/stubs/stub.mjs" "$c" > "$B/bin/stubs/$c"; chmod +x "$B/bin/stubs/$c" +done +export PATH="$B/bin/stubs:$PATH" +if [ "$IMPL" = "go" ]; then (cd "$ROOT" && go build -o "$B/bin/awg-control" ./cmd/awg-control) +elif [ ! -d "$ROOT/awg-ctrl/node_modules" ]; then (cd "$ROOT/awg-ctrl" && npm install --no-audit --no-fund --loglevel=error --no-package-lock); fi +sh "$B/setup.sh" +export PORT=3005 SERVER_IP=203.0.113.7 SERVER_PORT=47619 SERVER_NAME=VPN \ + INTERNAL_AUTH_PUB_FILE=/etc/amnezia/amneziawg/internal_auth_public.key \ + SHAPE_SAMPLE_S=3600 AWG_CLI=/nonexistent/awg-ctrl +cd "$ROOT/awg-ctrl" +if [ "$IMPL" = "ts" ]; then node node_modules/.bin/tsx index.ts > /run/bench/ctrl.log 2>&1 & +else "$B/bin/awg-control" ctrl > /run/bench/ctrl.log 2>&1 & +fi +PID=$! +i=0; until curl -s -o /dev/null http://127.0.0.1:3005/health; do i=$((i+1)); [ $i -gt 150 ] && { cat /run/bench/ctrl.log; kill $PID; exit 1; }; sleep 0.1; done +sleep 1 +cd "$B" +node scenario-ctrl.mjs "$B/out/$NAME.json" +sleep 3 +kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true +node collect.mjs "$B/out/$NAME.json" +cp /run/bench/ctrl.log "$B/out/$NAME.log" diff --git a/tools/diffbench/run.sh b/tools/diffbench/run.sh index 1894187..640566a 100755 --- a/tools/diffbench/run.sh +++ b/tools/diffbench/run.sh @@ -17,6 +17,8 @@ i=0; until curl -s -o /dev/null http://127.0.0.1:3005/health; do i=$((i+1)); [ $ sleep 1 # стартовый apply шейпинга асинхронный — даём ему отработать до сценария cd /bench node scenario-ctrl.mjs "/out/$NAME.json" +# фоновый apply шейпинга после restart/upgrade должен доработать, иначе журнал заглушек обрежется +sleep 3 kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true node collect.mjs "/out/$NAME.json" cp /run/bench/ctrl.log "/out/$NAME.log"