26-26.md 8.1 KB

26: 诊断票 —— 主场景等待后画面抽搐的根因定位

Type: research Status: resolved Assignee: ASUS Label: ready-for-agent Category: frontend-diagnosis

来源

票 18 真人走查第 3 项(2026-10-10):进入主场景后等待一段时间,整个画面进入抽搐状态,与期望不符。

已知观测

  • 现象为「等待一段时间后」出现,说明与时间或后台加载相关,而非首帧。
  • 抖动是整个画面级别的,不是单个图层。

纪律

当前不猜成因。 候选方向仅作为待排除清单,不是结论:雾过渡(domain/fog.js 的 altitude 收口与 slew 限速)、主场景区限位(domain/areaLimits.js)、3D Tiles 批次懒加载与渲染节流(services/tilesetLoading.js、requestRenderMode)、影像粗→精交接(domain/imageryHandover.js)、相机控制与动画循环。

必须先建立可复现、可判定的反馈回路,再给结论;不得凭读代码猜一个原因就改。

范围

  1. 构建可重复的复现步骤(进入模式、相机动作、等待时长、是否必现、是否与移动/静止相关),并记录可观察指标(帧率、长任务、渲染节流状态、图层批次加载事件)。
  2. 逐项排除候选方向,给出证据链;必要时做最小对照实验(关闭单个子系统看抖动是否消失)。
  3. 定位到根因后给出修复方案与回归验证方法。

验收

产出:可复现步骤 + 判定依据 + 根因 + 修复建议;若判定为非缺陷(例如加载期正常表现),需用证据说明并给出期望行为的重新定义。

关联

发现于票 18;修复属前端执行票范围,本票只做诊断。

进度

  • 2026-10-12:认领;建反馈回路(CDP 无头探针,仓库既有 cdp-driver.mjs),两轮干净复跑 + 一轮消融,根因定位完成(见 Answer)。

Answer

结论:抽搐 = 每 30 秒的新鲜度 tick 触发地图摘要全量分页重载,每一批到达都把整个管线层(5000+ 实体)判定为「变化」并重同步,主线程每次阻塞 ~175ms、一轮 8 批连续发生 —— 静置画面每 30 秒规律性卡顿数秒,即真人看到的「抽搐」。

可复现步骤

  1. dev server(:8000)+ 真实后端(config.json → real),冷启动进 /cockpit,点「点击进入系统」。
  2. 不做任何交互,静置 ≥70 秒。
  3. 现象:进入后前 ~25 秒画面平顺;~30s 与 ~60s 各出现一段持续数秒的卡顿窗口,与 map-snapshot 的 8 连发分页重取(间隔 ~250-550ms)精确重合。

判定依据(探针数据,两轮独立复跑一致)

探针 verify/artifacts/t26/probe-jitter.mjs(页内 rAF 帧间隔 + longtask + WS/REST 时刻 + __cockpitMap 相机/雾/交接/区域状态轮询):

观测 结果 判定
帧间隔(5s 分桶 p50/p95/max) 平时 p50=16.7ms(60fps);30s/60s 窗口 p95 冲到 400-666ms、max 700ms 卡顿是周期性的,非随机性能问题
长任务与 REST 对齐 start=61123ms dur=176ms、start=61623ms dur=175ms,±400ms 内恰有 map-snapshot 响应到达 每批快照到达 → 一次 ~175ms 主线程阻塞
相机 h/p/r、雾状态、交接阶段、区域状态 全程 0 次翻转(201 采样,振荡检测 flips=0) 排除票面候选里的「雾 slew/限位打架/交接循环/相机控制」四族
WS 帧 0 帧(期间无增量推送) 排除 WS 增量触发
控制台错误 0 排除异常路径

根因(代码链,逐行核实)

freshness tick(DEFAULT_REFRESH_SECONDS=30,freshnessService.js:22)
  → CockpitShell tick: mapStore.refresh() + refreshCurrentModePanels()(CockpitShell.vue:1443-1452)
  → mapStore.refresh() = setup + runLoadChain('refresh')(mapStore.js:211-215)
  → loader.refresh() 先 emptyMapSummary() 再逐页 loadNext(),auto-batch 间隔 120ms —— 共 ~8 页
  → 每页到达 summary.value = 新对象 → MapCanvas watcher(summary.objects)触发 applyMarkers()
  → applyMarkers 每批都做两件事(MapCanvas.vue:257-268):
      ① syncPointMarkers:entities.removeAll() + 重建全部标记(cesiumEngine.js:767-769)
      ② syncPipeLayerForPlan → syncPipeLayer:
         对全部 ~5268 管线 + 设施实体无条件执行 existing.polyline = spec.polyline /
         existing.point = spec.point / existing.label = spec.label(cesiumPipeLayer.js:144-169)

要害在 ②:syncPipeLayer 注释声称「只做 upsert 不全量重建」,但 upsert 分支每次都把整个 PolylineGraphics/PointGraphics 对象重新赋值——Cesium 的 Entity.polyline setter 是属性级订阅,重赋值即 definitionChanged → 几何失效 → 贴地折线全网重新 tessellate。也就是说「数据没变」时它照样做全量重建,entityIndex 只挡住了 add/remove,没挡住属性重赋值。modeLayerPlan 每次重算还 pipelines.slice() 造新数组引用(modeLayerPlan.js:106),watcher deep:false 照样触发——两层防线全部绕过。

放大器:引擎开 requestRenderMode: true(cesiumEngine.js:77),每批实体变更都驱动一次重渲染;8 批 × (5000+ 属性失效 + 标记 removeAll/重建) 在 ~4-8 秒内连续发生 → 主线程分块阻塞 + 标记层肉眼可见地闪 8 次 → 「整个画面抽搐」。与票面「等待一段时间后」吻合:首屏后第一个 tick 就在 30 秒。

消融验证

  • 消融 A(probe-jitter-ablate.mjs,15s 后拦截 map-snapshot):30s/60s 周期性 stall 消失(p95 回 16.7ms),仅剩单次 ~250-500ms 尖峰(拦截后 refresh 仍会做一次空载重同步)。注意局限:loader.refresh() 失败会把 summary 清空,故消融 A 不能单独定罪——已用代码链 + 下述旁证补齐。
  • 旁证:尖峰单次(消融 A)vs 连续数秒(基线)的差异 = 8 批连续重同步 vs 单次,与「每批一次全量重同步」的机制预测一致。
  • 消融 B(缓存回放)因探针补丁干扰实验(回放计数 0 却出现 40 秒持续 1.5fps 崩塌,疑补丁破坏 XHR 路径)作废不采信;其意外副产品记录在案:该场景若被强制连续渲染,每帧 ~130ms(SwiftShader 软渲染口径,仅作参考)。

硬件口径(如实声明)

全部测量在无头 Chrome + --disable-gpu(软件渲染)下取得:绝对帧时与真机 GPU 不可比,但机制(每 tick 8 批 × 全管线重同步、每批 ~175ms JS 阻塞)与硬件无关,真机表现为「每 30 秒数秒规律性卡顿/闪烁」与走查描述一致。本会话未在真机 GPU 上复测(浏览器会话在诊断中途已关)。

修复建议(归前端执行票,本票不改代码)

  1. syncPipeLayer 真 diff:upsert 前比较 spec(内容等值或脏标记),未变不重赋值——这是根治点,把「每批 5000+ 实体失效」降为 0。
  2. 计划层防抖:modeLayerPlan 对 pipelines/facilities 做引用记忆(输入未变不 slice() 新数组);或 applyMarkers 在 auto-batch 期间合并(批间 120ms 足够 debounce 到每轮 refresh 只重同步 1-2 次)。
  3. 标记层 diff(次要):syncPointMarkers 去掉 removeAll() 改增删差量,消除刷新期标记闪烁。
  4. 回归方法:重跑本票探针,验收判据 = 70s 静置内 (a) 无任何 ≥100ms 长任务落在 map-snapshot 响应后 ±500ms;(b) 30s/60s 窗口帧 p95 ≤ 33ms。消融 A 脚本可直接复用为反向对照。

关联事实(顺带)

  • refreshCurrentModePanels() 每 tick 还重取四块面板数据(Vue DOM 重渲染,量级远小于地图层),修复后可一并观察是否仍有小尖峰。
  • 票 27 修复(台账 pageNote)与本缺陷无交集,互不影响。

Status: resolved Type: research Blocked by: #19, #22 Labels: ready-for-agent, frontend-diagnosis, wayfinder:task

Comments