#!/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)