Files
dsh_shenxian/scripts/verify-mem-model.mjs
T
admin f6d4ad9b15 fix(mem): 统一实例内存配额口径 + status 暴露真值(消灭「388 MiB」误报)
用户问「实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多」。
实测:guest 真实 MemoryMax = 672 MiB、admin = 384 MiB(都真调整过);
而「功能管理」显示的是插件**自建的第二套模型**(基线 285 / univer 64 / mcn 39,
且把 clamp 下限 384 当硬上限)⇒ 全勾显示 388 并报「⚠ 将超出上限」,纯误报。
根因:档案 81 R1 把「写死 384」改为「按插件集合推导」时**只改了编排器**,
插件那份副本原地漂移,此后 univer 384→512、mcn-suite 128 都没同步。

平台侧(把已算好的真值透出来,零新增计算):
- spawner.ts 新增可选 quotaInfo?(userId)(照 breakerInfo 模式)
- orchestrator.ts 实现 quotaInfo(),走**同一个** instanceMemMb()/heapMbFor()
- /api/dsh/status 返回 quota: {memMb, heapMb}

防漂(根因是「同一事实两处副本」,必须让它不可能再漂):
- 新增 scripts/verify-mem-model.mjs,逐项交叉断言常量/成本表/clamp 算式/接线点,
  接进 npm run verify —— 任一处漂移即构建失败(已做反向验证:改回 285 即 exit 1)

验证:build 零报错;verify 全绿(含新增 15 项断言);服务端 md5 与本机编译产物一致;
GET /api/dsh/status → quota {memMb:384,heapMb:256} 与实测 402653184 吻合。

注:poc/business-plugins/**(0.3.6)与其 package.json 未在此提交 —— 该文件被并行会话
连续改过两轮(0.3.4/0.3.5,0.3.5 已上线但未 commit),刻意留给他们一起提交。
2026-09-13 21:38:04 +08:00

99 lines
5.2 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
/**
* verify-mem-model.mjs —— 内存模型「单一事实源」交叉校验(档案 84)
*
* 为什么需要:2026-09-13 实测发现「功能管理」那条内存条显示 388 MiB 并报「将超出上限」,
* 而平台真实配额是 672 / 384 —— 根因是**同一个事实被复制成了两份**:
* · 编排器 `src/supervisor/orchestrator.ts` 的 PLUGIN_MEM_MB / BASE / MIN / MAX(决定 MemoryMax)
* · 客户端 `poc/business-plugins/lib/client.js` 的 MEM_TABLE / MEM_BASE_MIB / …(决定界面显示)
* R1 把「写死 384」改成「按插件集合推导」时只改了前者 ⇒ 后者原地漂了。
*
* 本脚本把两处**钉死在一起**:任一常量或表项对不上,`npm run verify` 直接失败。
* 用法:node scripts/verify-mem-model.mjs 退出码 0=全绿 / 1=有失败
*/
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const read = (p) => readFileSync(join(ROOT, p), 'utf8')
let failed = 0
const ok = (name, cond, extra = '') => {
console.log((cond ? ' ✓ ' : ' ✗ ') + name + (extra ? ' ' + extra : ''))
if (!cond) failed++
}
// ── ① 编排器(权威)────────────────────────────────────────
const orch = read('src/supervisor/orchestrator.ts')
const spawner = read('src/supervisor/spawner.ts')
const dshRoute = read('src/web/routes/dsh.ts')
const num = (src, name) => {
const m = new RegExp('const ' + name + ' = (\\d+)').exec(src)
return m ? Number(m[1]) : null
}
const ORCH = {
base: num(orch, 'BASE_MEM_MB'),
min: num(orch, 'MIN_MEM_MB'),
max: num(orch, 'MAX_MEM_MB'),
headroom: num(orch, 'HEAP_HEADROOM_MB'),
}
const orchTable = {}
{
const block = /const PLUGIN_MEM_MB: Record<string, number> = \{([\s\S]*?)\n\}/.exec(orch)
if (block) for (const m of block[1].matchAll(/'([^']+)':\s*(\d+)/g)) orchTable[m[1]] = Number(m[2])
}
ok('编排器常量齐备(BASE/MIN/MAX/HEADROOM)', ORCH.base !== null && ORCH.min !== null && ORCH.max !== null && ORCH.headroom !== null,
`-> ${JSON.stringify(ORCH)}`)
ok('编排器插件成本表非空', Object.keys(orchTable).length > 0, '-> ' + JSON.stringify(orchTable))
// ── ② 客户端(副本)────────────────────────────────────────
const client = read('poc/business-plugins/lib/client.js')
const cnum = (src, name) => {
const m = new RegExp('var ' + name + ' = (\\d+)').exec(src)
return m ? Number(m[1]) : null
}
const CLIENT = {
base: cnum(client, 'MEM_BASE_MIB'),
min: cnum(client, 'MEM_MIN_MIB'),
max: cnum(client, 'MEM_MAX_MIB'),
headroom: cnum(client, 'MEM_HEAP_HEADROOM_MIB'),
}
const clientTable = {}
{
const block = /var MEM_TABLE = \{([\s\S]*?)\}/.exec(client)
if (block) for (const m of block[1].matchAll(/"([^"]+)":\s*(\d+)/g)) clientTable[m[1]] = Number(m[2])
}
// ── ③ 断言:两处必须逐项一致 ────────────────────────────────
const pairs = [
['基线 BASE', ORCH.base, CLIENT.base],
['下限 MIN', ORCH.min, CLIENT.min],
['硬顶 MAX', ORCH.max, CLIENT.max],
['堆预留 HEADROOM', ORCH.headroom, CLIENT.headroom],
]
for (const [label, a, b] of pairs) ok(`常量一致 · ${label}`, a === b, `-> 编排器 ${a} / 客户端 ${b}`)
const allKeys = [...new Set([...Object.keys(orchTable), ...Object.keys(clientTable)])].sort()
const bad = allKeys.filter((k) => orchTable[k] !== clientTable[k])
ok('插件成本表逐键一致', bad.length === 0,
bad.length === 0 ? `-> ${allKeys.length} 项:${allKeys.map((k) => k + '=' + orchTable[k]).join(', ')}`
: '-> 不一致:' + bad.map((k) => `${k}(编排器 ${orchTable[k]} / 客户端 ${clientTable[k]})`).join('; '))
// ── ④ 断言:clamp 算式同款(防「常量对了、规则又各写一套」)────
ok('编排器 clamp 规则在位', /Math\.min\(Math\.max\(mb, MIN_MEM_MB\), MAX_MEM_MB\)/.test(orch))
ok('客户端 clamp 规则与编排器同构', /Math\.min\(Math\.max\(raw, MEM_MIN_MIB\), MEM_MAX_MIB\)/.test(client))
ok('客户端 heap 推导与编排器同构', /Math\.max\(128, Math\.min\(256, memMb - MEM_HEAP_HEADROOM_MIB\)\)/.test(client))
ok('编排器 heap 推导算法未变', /Math\.max\(128, Math\.min\(256, memMb - HEAP_HEADROOM_MB\)\)/.test(orch))
// ── ⑤ 断言:status 真的把真值透出来了(否则前端的「读真值」是空接线)──
ok('Spawner 接口声明 quotaInfo?', /quotaInfo\?\(userId: string\): \{ memMb: number; heapMb: number \} \| null/.test(spawner))
ok('编排器实现 quotaInfo', /quotaInfo\(userId: string\): \{ memMb: number; heapMb: number \} \| null \{/.test(orch))
ok('/api/dsh/status 返回 quota', /quota: app\.supervisor\.quotaInfo\?\.\(request\.user!\.id\) \?\? null/.test(dshRoute))
ok('客户端会去读 /api/dsh/status', /\/api\/dsh\/status/.test(client) && /setQuotaInfo\(/.test(client))
console.log(failed === 0 ? '\n结论:全绿 ✅' : '\n结论:有 ' + failed + ' 项失败 ❌')
process.exit(failed === 0 ? 0 : 1)