第 0 阶段:教材项目 2 任务 1 在网页中原样运行

- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、
  GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、
  教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式)
- server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发
- web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏
- 端到端测试与 100 个程序并发的压力测试
- 教材逐任务摘要(docs/教材摘要)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 10:15:14 +08:00
commit 98be8331bc
71 files changed
+7919

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
// 并发压力测试:同时运行 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 { ROOT, RUNNER_DLL, 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', RUNNER_DLL, '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 })