Files
IOT_SIM/server/test/load.ts
T
xuyueandClaude Opus 5.5 697f31e9ce 部署:bubblewrap 沙箱、编译检查、访问口令、Docker Compose
- 学生程序每次运行放进 bubblewrap:断网,只读挂载系统文件、运行时和自己的工程目录,
  看不到其他进程和服务器环境变量;限制堆内存 128 MB、CPU 时间 120 秒、运行 60 分钟,
  nice 10,全站同时运行上限 150;启动时自检沙箱,不可用时拒绝运行
- 编译检查(SIM001):网络、反射、启动进程、调用本机代码、加载程序集、dynamic 给出中文报错
- 访问口令:cookie 保存口令哈希,按真实 IP 限制输错次数;前端口令页
- 服务端配置集中到 config.ts,可用环境变量覆盖;Bun 直接提供打包好的网页
- Dockerfile(多阶段,服务端打包成单文件)、compose.yaml(Caddy 只收 HTTP,接入 NPM 的
  npm_proxy 网络)、deploy/deploy.sh(本机构建后经 ssh 传到服务器)和部署说明
- 测试:沙箱隔离、内存上限、CPU 时间上限、编译检查

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:40:09 +08:00

104 lines
4.3 KiB
TypeScript
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.
// 并发压力测试:同时运行 N 个学生程序(教材项目 2 任务 1),测量内存、启动时间和操作往返延迟。
// 用法:bun test/load.ts [N],默认 100。运行前先编译运行时:bun run build:runtime
import { readFileSync } from 'node:fs'
import { cp, mkdir, rm } from 'node:fs/promises'
import { resolve } from 'node:path'
import type { ControlNode, RunnerMessage } from '@iot/shared'
import { compileService } from '../src/compile-service'
import { config, ROOT } from '../src/config'
import { readLines } from '../src/runner-process'
const COUNT = Number(process.argv[2] ?? 100)
const dir = resolve(ROOT, '.runs/load-test')
await rm(dir, { recursive: true, force: true })
await mkdir(dir, { recursive: true })
await cp(resolve(ROOT, 'samples/p2-t1-LED显示屏'), dir, { recursive: true })
const compiled = await compileService.compile(dir)
compileService.stop()
if (!compiled.ok) throw new Error('示例编译失败')
interface Instance {
proc: Bun.Subprocess<'pipe', 'pipe', 'inherit'>
form?: ControlNode
onMessage?: (m: RunnerMessage) => void
}
function findId(node: ControlNode, name: string): string | undefined {
if (node.name === name) return node.id
for (const child of node.children) {
const id = findId(child, name)
if (id) return id
}
}
function send(instance: Instance, message: object) {
instance.proc.stdin.write(`${JSON.stringify(message)}\n`)
instance.proc.stdin.flush()
}
/** 读取 /proc 里的内存数据(MB)。Rss 会把多个进程共享的 .NET 运行库重复计算,Pss 按进程数平摊,合计值更接近真实占用 */
function memoryMb(pid: number, field: 'Rss' | 'Pss') {
const rollup = readFileSync(`/proc/${pid}/smaps_rollup`, 'utf8')
return Number(new RegExp(`^${field}:\\s+(\\d+)`, 'm').exec(rollup)![1]) / 1024
}
console.log(`启动 ${COUNT} 个学生程序…`)
const startAt = performance.now()
const instances: Instance[] = []
const shown: Promise<void>[] = []
for (let i = 0; i < COUNT; i++) {
const proc = Bun.spawn(['dotnet', config.runnerDll, 'run', dir], { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' })
const instance: Instance = { proc }
instances.push(instance)
shown.push(
new Promise((resolveShown) => {
void readLines(proc.stdout, (line) => {
const message = JSON.parse(line) as RunnerMessage
if (message.type === 'formShown') {
instance.form = message.form
resolveShown()
}
instance.onMessage?.(message)
})
}),
)
}
await Promise.all(shown)
console.log(`全部窗体显示完成:${Math.round(performance.now() - startAt)} ms`)
await Bun.sleep(3000)
for (const field of ['Rss', 'Pss'] as const) {
const values = instances.map((i) => memoryMb(i.proc.pid, field))
const total = values.reduce((a, b) => a + b, 0)
console.log(`内存 ${field}:合计 ${Math.round(total)} MB,平均 ${Math.round(total / COUNT)} MB`)
}
// 每个程序:选 COM5 → 打开 → 输入文字 → 推送,测量从发出"推送"到收到 LED 变化的往返时间
const latencies = await Promise.all(
instances.map(async (instance) => {
const form = instance.form!
const id = (name: string) => findId(form, name)!
send(instance, { type: 'action', id: id('com_Port'), action: 'dropDown' })
send(instance, { type: 'action', id: id('com_Port'), action: 'select', index: 4 })
send(instance, { type: 'action', id: id('btn_Open'), action: 'click' })
send(instance, { type: 'action', id: id('txt_SendText'), action: 'text', value: '压力测试' })
await Bun.sleep(200)
const led = id('led')
const sentAt = performance.now()
const done = new Promise<number>((resolveDone) => {
instance.onMessage = (m) => {
if (m.type === 'patch' && m.changes.some((c) => c.id === led && c.prop === 'LedText')) resolveDone(performance.now() - sentAt)
}
})
send(instance, { type: 'action', id: id('btn_Send'), action: 'click' })
return done
}),
)
latencies.sort((a, b) => a - b)
const pct = (p: number) => Math.round(latencies[Math.min(latencies.length - 1, Math.floor((latencies.length * p) / 100))])
console.log(`操作往返(${COUNT} 个程序同时点击):中位数 ${pct(50)} ms,P95 ${pct(95)} ms,最大 ${pct(100)} ms`)
for (const instance of instances) instance.proc.kill()
await rm(dir, { recursive: true, force: true })