Files
dsh_shenxian/scripts/relay-mem-calibrate.mjs
T
admin 146c3d25ef feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库
覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)

验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
2026-09-17 17:03:27 +08:00

179 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 覆盖网络 序⑥ · S6 `MEM_PER_HOST_MB` 校准(⛔ 一次性脚本、不进产品路径)。
*
* ## 为什么必须放大测
* 现网 `used = 2` ⇒ relay 的 RSS(72888 KB)**只反映 Node 基座**,参数表 §5.1 的
* "2 MB/台" 是**推导值不是实测**。本脚本把 N 拉到 2/10/25/50/100,量 RSS 斜率。
*
* ## 干净方案(⛔ 不污染生产)
* - **relay 起在本机回环**(独立进程、独立高层口号,与生产的 20080 无关);
* - **N 个合成 client 跑在"本脚本进程内"** —— 这是刻意的:只要被测量的对象是 **relay 子进程**
* 的 RSS,client 的内存长在本进程里 ⇒ **不进被测量**。(若让 client 也各自起进程,
* 机器上会多出 100 个 Node 基座 ≈ 4 GB,纯浪费。)
* - 全程 **127.0.0.1**,零公网面 ⇒ ⛔ 不动 47 的任何配置(S6 权限附注)。
*
* ## 判据
* 线性回归 `RSS(N) = a + b·N`,`R² ≥ 0.9` 方为有效;否则**如实报告跳点**,⛔ 不许硬套斜率。
*
* 用法:node scripts/relay-mem-calibrate.mjs [--base 19000] [--port 23456]
*
* @module scripts/relay-mem-calibrate
*/
import { spawn } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = join(HERE, '..')
function arg(name, dflt) {
const i = process.argv.indexOf(`--${name}`)
return i >= 0 ? process.argv[i + 1] : dflt
}
const PORT = Number(arg('port', 23456))
const BASE = Number(arg('base', 19000))
const SPAN = Number(arg('span', 3000))
const POINTS = (arg('points', '2,10,25,50,100')).split(',').map(Number)
const logs = []
const { RelayClient } = await import(pathToFileURL(join(REPO, 'lib/net/relay/client.js')).href)
const tmp = mkdtempSync(join(tmpdir(), 'relay-mem-'))
const keysFile = join(tmp, 'keys.json')
const keys = {}
const secrets = {}
for (let i = 1; i <= Math.max(...POINTS); i++) {
const sec = randomBytes(32).toString('hex')
secrets[i] = sec
keys[`ops/w-${i}`] = sec
}
writeFileSync(keysFile, JSON.stringify(keys, null, 2))
const relay = spawn(
process.execPath,
[
join(REPO, 'lib/net/relay/main.js'),
'--port',
String(PORT),
'--keys-file',
keysFile,
'--base',
String(BASE),
'--span',
String(SPAN),
'--max-hosts',
'0',
],
{ cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] },
)
relay.stdout.on('data', (d) => logs.push(String(d)))
relay.stderr.on('data', (d) => logs.push(String(d)))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function status() {
const r = await fetch(`http://127.0.0.1:${PORT}/status`)
return r.json()
}
async function used() {
try {
return (await status()).capacity.used
} catch {
return -1
}
}
async function rssKbMedian(pid, times = 7, gapMs = 500) {
const arr = []
for (let i = 0; i < times; i++) {
arr.push(await rssKbOnce(pid))
await sleep(gapMs)
}
const s = arr.filter((x) => Number.isFinite(x)).sort((a, b) => a - b)
return { median: s[Math.floor(s.length / 2)], min: s[0], max: s[s.length - 1], n: s.length }
}
async function rssKbOnce(pid) {
if (process.platform === 'win32') {
const r = await new Promise((res) => {
const p = spawn('powershell', ['-NoProfile', '-Command', `(Get-Process -Id ${pid}).WorkingSet64`])
let o = ''
p.stdout.on('data', (d) => (o += d))
p.on('close', () => res(o.trim()))
})
return Math.round(Number(r) / 1024)
}
const r = await new Promise((res) => {
const p = spawn('sh', ['-c', `ps -o rss= -p ${pid}`])
let o = ''
p.stdout.on('data', (d) => (o += d))
p.on('close', () => res(o.trim()))
})
return Number(r)
}
const clients = []
const samples = []
const results = { platform: process.platform, node: process.version, points: [], raw: [] }
for (const target of POINTS) {
while (clients.length < target) {
const i = clients.length + 1
const c = new RelayClient({
url: `ws://127.0.0.1:${PORT}/dshs-relay`,
hostId: `w-${i}`,
networkId: 'ops',
secret: secrets[i],
ports: [BASE + i],
})
c.start()
clients.push(c)
}
let u = -1
for (let i = 0; i < 60; i++) {
u = await used()
if (u >= target) break
await sleep(500)
}
await sleep(1000)
const rss = await rssKbMedian(relay.pid)
samples.push({ n: clients.length, used: u, rssKb: rss.median, rssMin: rss.min, rssMax: rss.max, nSamples: rss.n })
process.stderr.write(`[mem] N=${clients.length} used=${u} RSS=${rss.median} KB (min ${rss.min} / max ${rss.max})\n`)
}
// 线性回归
const n = samples.length
const sx = samples.reduce((a, s) => a + s.n, 0)
const sy = samples.reduce((a, s) => a + s.rssKb, 0)
const sxx = samples.reduce((a, s) => a + s.n * s.n, 0)
const sxy = samples.reduce((a, s) => a + s.n * s.rssKb, 0)
const b = (n * sxy - sx * sy) / (n * sxx - sx * sx)
const a = (sy - b * sx) / n
const my = sy / n
const ssTot = samples.reduce((acc, s) => acc + (s.rssKb - my) ** 2, 0)
const ssRes = samples.reduce((acc, s) => acc + (s.rssKb - (a + b * s.n)) ** 2, 0)
results.points = samples
results.fit = {
interceptKb: Math.round(a),
slopeKbPerHost: Math.round(b * 10) / 10,
r2: Math.round((1 - ssRes / ssTot) * 10000) / 10000,
}
results.projected = {
memPerHostMbCeil20: Math.ceil((b / 1024) * 1.2 * 10) / 10,
at225HostsMb: Math.round((a + b * 225) / 1024),
}
const last = samples[samples.length - 1]
process.stdout.write(`### RESULT ### ${JSON.stringify({ ...results, lastUsed: last.used })}\n`)
for (const c of clients) c.stop()
relay.kill()
await sleep(500)
rmSync(tmp, { recursive: true, force: true })
process.exit(0)