Playwright MCP 4 场景剧本(.playwright-mcp/task16/): - README.md:总览 + 账号/环境/资源断言指南 - scenarios/01-create-and-join-by-code.md:创建→会议号加入→媒体打通→结束,含 Redis/MySQL/media-server 三维资源断言 - scenarios/02-invite-link-with-password.md:邀请链接带密码 + P0-4 "密码不走 URL query" 回归 - scenarios/03-notify-quickjoin.md:通知中心「立即加入」跳转 + 过期卡片降级 - scenarios/04-host-toolkit-and-transfer.md:主持人四件套 + 宽限期重连/到期自动转让(P1-1 原子性 + P1-7 处理锁验证) - scenarios/05-reopen-5times-resource-cleanup.md:反复开关 5 次 + 前端 heap 快照/ Redis 累积对比/ media-server 观测端点对比,验证 Step A(f5ae095)资源清理修复 手工回归点(manual-regression.md):17 项 MCP 难以稳定覆盖的交互场景 (HTTPS 自签 / 跨网段 iOS / 网络抖动 / 物理返回 / 跨 tab 并发 / 摄像头抢占 等) .gitignore 调整: - 放行 .playwright-mcp/task16/ 下的 Markdown 剧本文档进入版本控制 - 仍忽略运行时产物(*.log/*.png/*.yml/截图) - task15/ 保持未跟踪状态,避免历史截图噪声入库 Made-with: Cursor
6.2 KiB
6.2 KiB
场景 05:反复开关 5 次会议 —— 资源清理专项回归
目标
- 端到端验证 Step A 资源清理修复(提交
f5ae095):- 前端
_engine/AudioContext/setInterval/_pendingBroadcastTimers在每次结束会议后完全释放 - 后端 Redis
resource:*/member_state:*/host_grace:*/empty_ttl:*零累积 - mediasoup Router / Transport / Producer / Consumer 不残留
- 前端
- 回归 B1 / B2 / B3 三条资源清理路径
- 资源断言曲线:5 轮循环后 Redis
dbsize与 mediasoup/internal/info恢复到 baseline
前置条件
依 ../README.md 通用前置;额外确认:
- 单浏览器 context 双 tab(Alice + Bob)
- 开启 Chromium DevTools → Memory → 勾选「Record allocation stacks」,在脚本执行前 + 第 5 轮结束后各拍一次 heap snapshot
MCP 执行脚本
步骤 0:baseline 采集
# baseline_redis.txt
redis-cli DBSIZE | tee /tmp/baseline_redis.txt
redis-cli KEYS 'echo:meeting:*' | wc -l >> /tmp/baseline_redis.txt
# baseline_media.json
curl -s -H "X-Internal-Token: $INTERNAL_TOKEN" http://localhost:3300/internal/info \
| jq .stats > /tmp/baseline_media.json
在 Alice tab 的 DevTools Performance 面板录制整段脚本;脚本起止时刻各打一个 performance.mark。
步骤 1-5:循环 5 次创建→打通→结束
对 i in {1..5}:
- 创建:Alice tab 会议首页 → 创建即时会议 → 设备预览 → 立即进入(记录
$ROOM_CODE_i) - 加入:Bob tab 通过会议号加入
$ROOM_CODE_i - 打通断言(轻量):两端
browser_evaluate检查localAudioEnabled && localVideoEnabled - 结束:Alice 点「结束会议」;Bob tab 等待
room.endedtoast - 结束后清理断言(每轮都要做):
echo "===== Round $i =====" redis-cli KEYS "echo:meeting:resource:$ROOM_CODE_i:*" # 必须为空 redis-cli KEYS "echo:meeting:member_state:$ROOM_CODE_i:*" # 必须为空 redis-cli KEYS "echo:meeting:host_grace:$ROOM_CODE_i" # 必须 nil redis-cli KEYS "echo:meeting:empty_ttl:$ROOM_CODE_i" # 必须 nil - 前端资源断言(每轮):
// 在 Alice tab 的结束页执行 const store = window.__pinia?.state?.value?.meeting return { round: $i, localState: store.localState, // 'ended' pendingBroadcastTimers: window.__broadcastTimersSize ?? 'N/A', } - 回首页:Alice tab 物理返回(
browser_press_keyBack / 点击结束页「返回」按钮)触发onUnload→exitEndedRoom browser_evaluate再次确认 pinia state 全 reset:localState='idle' && participantCount=0 && !engineExists- Bob tab 同样回首页
- 延迟 2 秒模拟用户操作间隙,进入下一轮
步骤 6:5 轮结束后的累积断言(核心)
# 对比 baseline,必须零增量
echo "===== Redis 累积对比 ====="
redis-cli DBSIZE
redis-cli KEYS 'echo:meeting:*' | wc -l
diff /tmp/baseline_redis.txt <(redis-cli DBSIZE && redis-cli KEYS 'echo:meeting:*' | wc -l)
echo "===== media-server 累积对比 ====="
curl -s -H "X-Internal-Token: $INTERNAL_TOKEN" http://localhost:3300/internal/info | jq .stats > /tmp/after5_media.json
diff /tmp/baseline_media.json /tmp/after5_media.json
echo "===== MySQL 快照 ====="
mysql -e "SELECT COUNT(*) AS active FROM meeting_rooms WHERE status='active'" echochat
# 期望:active=0
mysql -e "SELECT COUNT(*) AS alive FROM meeting_participants WHERE left_at IS NULL" echochat
# 期望:alive=0
步骤 7:前端内存快照对比
- 在 Alice tab 打开 DevTools → Memory → 拍 heap snapshot #2(5 轮结束后)
- 对比 #1(baseline)与 #2:
- 关键类:
MediaStream/RTCPeerConnection/AudioContext/Consumer/Producer的对象数量 - 允许少量增长(mediasoup-client
Device缓存),但必须远小于 5×
- 关键类:
- 记录 Performance tab 的「JS Heap」曲线:应呈锯齿下降(每次 GC 回收),不得持续单调上升
预期结果汇总
| 断言维度 | 期望 |
|---|---|
| 每轮 Redis 清理 | resource/member_state/host_grace/empty_ttl 全空 |
5 轮累积 Redis DBSIZE |
与 baseline 一致(允许 ±2 容错,针对 session/heartbeat key) |
| 5 轮累积 media-server | Router / Transport / Producer / Consumer 回到 baseline |
| MySQL | active 会议数 = 0,left_at IS NULL 参与者 = 0 |
| 前端 heap snapshot | 关键对象类数量增长 ≤ 2(近似常数,非线性) |
| 前端 Performance | JS Heap 曲线呈锯齿,无持续上升趋势 |
| 控制台 | 无 unhandled promise rejection / NotReadableError / leaked listener |
资源清理验证矩阵(对应 Step A 修复)
| 代码路径 | 验证手段 | 通过条件 |
|---|---|---|
F1 _onRoomEnded 结束页数据保留 |
localState='ended' 时 currentRoom 非 null |
结束页正确渲染 |
F1 exitEndedRoom 动作 |
onUnload 后 currentRoom=null |
pinia state reset |
F2 endMeeting 本地兜底 |
断开 WS 再结束(额外场景) | 本地仍能触发 _onRoomEnded |
F3 _pendingBroadcastTimers |
结束后 clearTimeout |
window.__broadcastTimersSize=0 |
B1 cleanupRoomRedisResidual |
每轮 Redis 查 | resource:* / member_state:* = 0 |
B3 OnRoomEnded timer 取消 |
pprof / sync.Map 大小 | graceTimers / emptyTTLTimers 稳定无增长 |
失败排查
| 现象 | 定位 |
|---|---|
某轮 resource:* 非空 |
EndRoom 未走 cleanup → 检查 commit f5ae095 是否正确落地 |
Redis DBSIZE 持续增长 |
检查 session/heartbeat key 是否泄漏;排除 meeting 模块 |
| 前端 heap 持续上升 | 抓 heap diff,找保留路径(常见:Producer._events、Vue reactive 持有 Consumer) |
localState 卡 ended 不回 idle |
onUnload 未调 exitEndedRoom 或 room.vue 组件未卸载(检查 H5 uni.reLaunch 行为) |
延伸测试(可选)
- 10 轮:循环次数改 10,观察是否线性无增长
- 跨 tab 并发:第 3 轮时 Bob 主动 leave(而非等 Alice end),验证双路径清理一致
- 主动断网:第 4 轮打通后 Alice 关网卡 2 分钟 → 触发 host grace → 进入场景 04 路径