Files
EchoChat/.playwright-mcp/task16/scenarios/05-reopen-5times-resource-cleanup.md
bujinyuan 5ed14c2aac docs(meeting): Task 16 E2E 剧本落盘 + .gitignore 精细化
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
2026-04-23 17:21:42 +08:00

6.2 KiB
Raw Blame History

场景 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 双 tabAlice + Bob
  • 开启 Chromium DevTools → Memory → 勾选「Record allocation stacks」,在脚本执行前 + 第 5 轮结束后各拍一次 heap snapshot

MCP 执行脚本

步骤 0baseline 采集

# 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}

  1. 创建Alice tab 会议首页 → 创建即时会议 → 设备预览 → 立即进入(记录 $ROOM_CODE_i
  2. 加入Bob tab 通过会议号加入 $ROOM_CODE_i
  3. 打通断言(轻量):两端 browser_evaluate 检查 localAudioEnabled && localVideoEnabled
  4. 结束Alice 点「结束会议」Bob tab 等待 room.ended toast
  5. 结束后清理断言(每轮都要做):
    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
    
  6. 前端资源断言(每轮):
    // 在 Alice tab 的结束页执行
    const store = window.__pinia?.state?.value?.meeting
    return {
      round: $i,
      localState: store.localState,                // 'ended'
      pendingBroadcastTimers: window.__broadcastTimersSize ?? 'N/A',
    }
    
  7. 回首页Alice tab 物理返回(browser_press_key Back / 点击结束页「返回」按钮)触发 onUnloadexitEndedRoom
  8. browser_evaluate 再次确认 pinia state 全 resetlocalState='idle' && participantCount=0 && !engineExists
  9. Bob tab 同样回首页
  10. 延迟 2 秒模拟用户操作间隙,进入下一轮

步骤 65 轮结束后的累积断言(核心)

# 对比 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前端内存快照对比

  1. 在 Alice tab 打开 DevTools → Memory → 拍 heap snapshot #25 轮结束后)
  2. 对比 #1baseline与 #2
    • 关键类:MediaStream / RTCPeerConnection / AudioContext / Consumer / Producer 的对象数量
    • 允许少量增长mediasoup-client Device 缓存),但必须远小于 5×
  3. 记录 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 会议数 = 0left_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._eventsVue reactive 持有 Consumer
localStateended 不回 idle onUnload 未调 exitEndedRoomroom.vue 组件未卸载(检查 H5 uni.reLaunch 行为)

延伸测试(可选)

  • 10 轮:循环次数改 10观察是否线性无增长
  • 跨 tab 并发:第 3 轮时 Bob 主动 leave而非等 Alice end验证双路径清理一致
  • 主动断网:第 4 轮打通后 Alice 关网卡 2 分钟 → 触发 host grace → 进入场景 04 路径