awg-ctrl: keep CPU mode order in /api/metrics; diffbench compare and host runner
The differential bench (GEN 2 and 3.1, with I1, S4=0, upgrade with a 3.x module) now shows the Go awg-ctrl identical to the TS one. The only real difference it found: host.cpu was a Go map, so its keys came out sorted instead of in /proc/stat order. - tools/diffbench/compare.mjs: decodes vpn:// (Node and Go zlib differ), masks live host numbers, Express dev-mode stack traces, temp file names and the order of calls from concurrent background work - tools/diffbench/local.sh: the same run without Docker, stubs only on the run's own PATH (for disposable machines) - run.sh: wait for the background shaping apply before stopping the service, otherwise the stub log is cut short
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/maeneko/forgetting/internal/jsjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Метрики для Prometheus. awg-ctrl отдаёт JSON, а текстовый формат собирает awg-ui: на ноде
|
// Метрики для Prometheus. awg-ctrl отдаёт JSON, а текстовый формат собирает awg-ui: на ноде
|
||||||
@@ -32,7 +34,7 @@ type metricsPeer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type metricsHost struct {
|
type metricsHost struct {
|
||||||
CPU map[string]float64 `json:"cpu"`
|
CPU jsjson.Object `json:"cpu"` // порядок режимов как в /proc/stat (и в Node-версии)
|
||||||
Cores int `json:"cores"`
|
Cores int `json:"cores"`
|
||||||
Load []float64 `json:"load"`
|
Load []float64 `json:"load"`
|
||||||
Mem struct {
|
Mem struct {
|
||||||
@@ -89,13 +91,13 @@ func readHost() *metricsHost {
|
|||||||
if len(first) < 2 {
|
if len(first) < 2 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
h := &metricsHost{CPU: map[string]float64{}, Cores: runtime.NumCPU()}
|
h := &metricsHost{Cores: runtime.NumCPU()}
|
||||||
for i, m := range cpuModes {
|
for i, m := range cpuModes {
|
||||||
v := 0.0
|
v := 0.0
|
||||||
if i+1 < len(first) {
|
if i+1 < len(first) {
|
||||||
v = orZero(jsNumber(first[i+1]))
|
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")
|
mem, err := os.ReadFile("/proc/meminfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// compare.mjs <ts.json> <go.json>: расхождения двух прогонов стенда. 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<undecodable " + e.message + ">"; } };
|
||||||
|
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, "<ts>").replace(/"(created_at|lastHandshake|ts|time|uptime|generated_at)":\d+/g, '"$1":<n>');
|
||||||
|
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" ? "<n>" : 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(/<pre>([A-Za-z]+Error)[^<]*(<br>[\s\S]*)?<\/pre>/, "<pre>$1</pre>");
|
||||||
|
}
|
||||||
|
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");
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# local.sh <ts|go> <имя-прогона> — тот же прогон, что 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"
|
||||||
@@ -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 шейпинга асинхронный — даём ему отработать до сценария
|
sleep 1 # стартовый apply шейпинга асинхронный — даём ему отработать до сценария
|
||||||
cd /bench
|
cd /bench
|
||||||
node scenario-ctrl.mjs "/out/$NAME.json"
|
node scenario-ctrl.mjs "/out/$NAME.json"
|
||||||
|
# фоновый apply шейпинга после restart/upgrade должен доработать, иначе журнал заглушек обрежется
|
||||||
|
sleep 3
|
||||||
kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true
|
kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true
|
||||||
node collect.mjs "/out/$NAME.json"
|
node collect.mjs "/out/$NAME.json"
|
||||||
cp /run/bench/ctrl.log "/out/$NAME.log"
|
cp /run/bench/ctrl.log "/out/$NAME.log"
|
||||||
|
|||||||
Reference in New Issue
Block a user