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)、相机控制与动画循环。
必须先建立可复现、可判定的反馈回路,再给结论;不得凭读代码猜一个原因就改。
产出:可复现步骤 + 判定依据 + 根因 + 修复建议;若判定为非缺陷(例如加载期正常表现),需用证据说明并给出期望行为的重新定义。
发现于票 18;修复属前端执行票范围,本票只做诊断。
cdp-driver.mjs),两轮干净复跑 + 一轮消融,根因定位完成(见 Answer)。结论:抽搐 = 每 30 秒的新鲜度 tick 触发地图摘要全量分页重载,每一批到达都把整个管线层(5000+ 实体)判定为「变化」并重同步,主线程每次阻塞 ~175ms、一轮 8 批连续发生 —— 静置画面每 30 秒规律性卡顿数秒,即真人看到的「抽搐」。
config.json → real),冷启动进 /cockpit,点「点击进入系统」。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 秒。
probe-jitter-ablate.mjs,15s 后拦截 map-snapshot):30s/60s 周期性 stall 消失(p95 回 16.7ms),仅剩单次 ~250-500ms 尖峰(拦截后 refresh 仍会做一次空载重同步)。注意局限:loader.refresh() 失败会把 summary 清空,故消融 A 不能单独定罪——已用代码链 + 下述旁证补齐。全部测量在无头 Chrome + --disable-gpu(软件渲染)下取得:绝对帧时与真机 GPU 不可比,但机制(每 tick 8 批 × 全管线重同步、每批 ~175ms JS 阻塞)与硬件无关,真机表现为「每 30 秒数秒规律性卡顿/闪烁」与走查描述一致。本会话未在真机 GPU 上复测(浏览器会话在诊断中途已关)。
syncPipeLayer 真 diff:upsert 前比较 spec(内容等值或脏标记),未变不重赋值——这是根治点,把「每批 5000+ 实体失效」降为 0。modeLayerPlan 对 pipelines/facilities 做引用记忆(输入未变不 slice() 新数组);或 applyMarkers 在 auto-batch 期间合并(批间 120ms 足够 debounce 到每轮 refresh 只重同步 1-2 次)。syncPointMarkers 去掉 removeAll() 改增删差量,消除刷新期标记闪烁。map-snapshot 响应后 ±500ms;(b) 30s/60s 窗口帧 p95 ≤ 33ms。消融 A 脚本可直接复用为反向对照。refreshCurrentModePanels() 每 tick 还重取四块面板数据(Vue DOM 重渲染,量级远小于地图层),修复后可一并观察是否仍有小尖峰。pageNote)与本缺陷无交集,互不影响。Status: resolved Type: research Blocked by: #19, #22 Labels: ready-for-agent, frontend-diagnosis, wayfinder:task