docs: record STEP-086 continuous repaint investigation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
370
docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md
Normal file
370
docs/20_调查/STEP-086_Particle连续渲染唤醒MapLibre整图成本调查_v1.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# STEP-086 Particle 连续渲染唤醒 MapLibre 整图成本调查 v1
|
||||||
|
|
||||||
|
状态:`INVESTIGATION / SOURCE_EVIDENCE_CONFIRMED / RUNTIME_PROFILE_PENDING`
|
||||||
|
日期:2026-09-21
|
||||||
|
|
||||||
|
## 1. 调查目标
|
||||||
|
|
||||||
|
本调查针对 NavSea 完整场景中出现的以下现象:
|
||||||
|
|
||||||
|
- Particle 独立 Host / NavSea 最小 Particle Host 中,Particle 本体性能基本正常。
|
||||||
|
- 完整 NavSea 在 Particle OFF、camera 静止时能够正常 idle。
|
||||||
|
- 完整 NavSea 在 Particle ON 后,RenderThread、JS、main CPU 明显高于最小 Particle Host。
|
||||||
|
- 已排除 NavSea 侧两个已确认的 JS 问题:NavigationEngine renderPulse 导致整树重渲染,以及 JsSchedulerProbe 常驻 RAF/16ms timer。
|
||||||
|
|
||||||
|
核心问题是验证:
|
||||||
|
|
||||||
|
> Particle 连续动画是否通过 MapLibre 的全局 repaint/update/render 生命周期,把本来可 idle 的 PBF/vector/style/map scene 持续唤醒。
|
||||||
|
|
||||||
|
本调查优先检查 Shared Core / Renderer / Android frame scheduling,不修改 NavSea ↔ Particle C-001 公共责任边界。
|
||||||
|
|
||||||
|
## 2. 调查基线
|
||||||
|
|
||||||
|
Integration 仓库已同步最新 main:
|
||||||
|
- repo:`/root/sourceserver/navsea-particle-integration`
|
||||||
|
- branch:`main`
|
||||||
|
- HEAD:`39ad8415fbdb76c1822460bb09f3a3f35fe141e0`
|
||||||
|
- Contract:`C-001`
|
||||||
|
Particle 源码调查基线:
|
||||||
|
- repo:`/mnt/sda1/MapLibreParticleLab`
|
||||||
|
- branch:`origin/particle/main`
|
||||||
|
- HEAD:`70f112ea27acaaa4da0a307563b7868937f69691`
|
||||||
|
- commit:`fix: close STEP-085 V4 renderer efficiency`
|
||||||
|
- 当前该 HEAD 中 package/version 字段仍为 `13.4.1-particle.7`。
|
||||||
|
|
||||||
|
注意:用户当前运行环境称为 `.8`,而本次可直接审计的最新远端源码 HEAD 仍显示 `.7` 版本字段。
|
||||||
|
因此本文的源码结论针对 `70f112ea2` 的 MapLibre/Particle 调度架构;后续如 `.8` 对应精确发布提交不同,应再做一次 commit-level 对照。
|
||||||
|
不过本文确认的全局 repaint/render 机制位于 MapLibre Renderer 主链,不依赖 NavSea 业务代码。
|
||||||
|
|
||||||
|
## 3. 当前实测背景
|
||||||
|
|
||||||
|
用户提供的 Android 模拟环境参考量级:
|
||||||
|
|
||||||
|
### 最小 Particle Host
|
||||||
|
- RenderThread:约 38–40%
|
||||||
|
- JS `mqt_v_js`:约 4–5%
|
||||||
|
- main:约 11%
|
||||||
|
|
||||||
|
### 完整 NavSea,Particle OFF
|
||||||
|
- JS:7.08%
|
||||||
|
- RenderThread:1.31%
|
||||||
|
- main:3.34%
|
||||||
|
|
||||||
|
说明完整 NavSea 的静止地图、PBF、UI 和业务逻辑本身能够进入低成本 idle。
|
||||||
|
|
||||||
|
### 完整 NavSea,Particle ON,且已排除两个 NavSea JS 问题
|
||||||
|
- JS:29.07%
|
||||||
|
- RenderThread:80.13%
|
||||||
|
- main:15.51%
|
||||||
|
|
||||||
|
相对于最小 Particle Host,大约额外:
|
||||||
|
- JS:+24 percentage points
|
||||||
|
- RenderThread:+40 percentage points
|
||||||
|
- main:+4~5 percentage points
|
||||||
|
## 4. 源码确认的核心结论
|
||||||
|
|
||||||
|
核心假设成立:
|
||||||
|
|
||||||
|
> 当前 Particle 没有独立的“只绘制 Particle Layer”的动画循环。Particle 的连续动画通过 MapLibre 全局 `needsRepaint` / `onUpdate` / `Renderer::render` 链维持,因此一次合法的 Particle animation frame 会进入完整 MapLibre frame orchestration。
|
||||||
|
|
||||||
|
主链如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderRasterParticleLayer::needsRepaint()
|
||||||
|
↓
|
||||||
|
RenderOrchestrator::hasTransitions()
|
||||||
|
↓
|
||||||
|
RenderTreeParameters::needsRepaint = true
|
||||||
|
↓
|
||||||
|
Renderer::Impl::render() 完成当前整帧
|
||||||
|
↓
|
||||||
|
Map::Impl::onDidFinishRenderingFrame()
|
||||||
|
↓
|
||||||
|
if (needsRepaint) onUpdate()
|
||||||
|
↓
|
||||||
|
重新构造完整 UpdateParameters
|
||||||
|
↓
|
||||||
|
AndroidRendererFrontend::update()
|
||||||
|
↓
|
||||||
|
MapRenderer.requestRender()
|
||||||
|
↓
|
||||||
|
Renderer::render()
|
||||||
|
↓
|
||||||
|
RenderOrchestrator::createRenderTree()
|
||||||
|
↓
|
||||||
|
完整 Map frame
|
||||||
|
```
|
||||||
|
|
||||||
|
因此 `needsRepaint()` 的语义不是“仅让 Particle Layer 再绘制一次”,而是“MapLibre 需要下一张完整地图 frame”。
|
||||||
|
|
||||||
|
## 5. Particle 每帧如何请求下一帧
|
||||||
|
|
||||||
|
`RenderRasterParticleLayer::needsRepaint()` 在 Particle active、visible、field-ready 且 cadence 到期时返回 true。
|
||||||
|
|
||||||
|
`RenderOrchestrator::hasTransitions()` 会遍历全部 RenderLayer:
|
||||||
|
- `hasTransition()`
|
||||||
|
- `needsRepaint()`
|
||||||
|
|
||||||
|
任一 layer 返回 true,整个 RenderTree 即被标记为 `needsRepaint`。
|
||||||
|
帧结束后,`Map::Impl::onDidFinishRenderingFrame()` 在 Continuous 模式下执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
if (needsRepaint || transform.inTransition()) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
而 `Map::triggerRepaint()` 本身也只是:
|
||||||
|
```text
|
||||||
|
impl->onUpdate();
|
||||||
|
```
|
||||||
|
|
||||||
|
Android 显式 `FPS_30/FPS_60` 还存在一层 platform wake:
|
||||||
|
- FPS_30:Handler 约每 34ms 调用一次 `map.triggerRepaint()`
|
||||||
|
- FPS_60:Handler 约每 17ms 调用一次 `map.triggerRepaint()`
|
||||||
|
- AUTO:不使用该 Handler,但 Particle 的 `needsRepaint()` 仍可使连续 frame 自行串联。
|
||||||
|
|
||||||
|
结论:当前没有 layer-scoped repaint 调度语义。
|
||||||
|
|
||||||
|
## 6. 一次 Particle repaint 会重新进入哪些 MapLibre 工作
|
||||||
|
|
||||||
|
`Map::Impl::onUpdate()` 每次都会重新生成完整 `UpdateParameters`,其中包括:
|
||||||
|
- transform state
|
||||||
|
- style loaded state
|
||||||
|
- light
|
||||||
|
- images
|
||||||
|
- sources
|
||||||
|
- layers
|
||||||
|
- glyph/font/sprite 状态
|
||||||
|
- annotation manager
|
||||||
|
- tile LOD 参数等
|
||||||
|
|
||||||
|
随后 `Renderer::render(updateParameters)` 会重新调用:
|
||||||
|
- `RenderOrchestrator::createRenderTree()`
|
||||||
|
- `renderTree->prepare()`
|
||||||
|
- `Renderer::Impl::render()`
|
||||||
|
|
||||||
|
因此不是复用一张静态 RenderTree 后只执行 Particle pass。
|
||||||
|
|
||||||
|
## 7. camera 静止时仍重复执行的非 Particle 工作
|
||||||
|
在当前源码中,即使 camera 完全静止,连续 frame 仍会重复:
|
||||||
|
|
||||||
|
1. 遍历全部 source。
|
||||||
|
2. 对每个 source 遍历 ordered layers,计算 source/layer 关系、visibility、zoom fit、renderability。
|
||||||
|
3. 对启用 source 调用 `source->update()`。
|
||||||
|
4. Vector/PBF source 进入 `RenderTileSetSource::update()` → `TilePyramid::update()`。
|
||||||
|
5. TilePyramid 会重新处理 wrap、zoom、covering tiles、retain 等逻辑。
|
||||||
|
6. 对全部 enabled source 执行 `prepare()`。
|
||||||
|
7. 对全部 renderable layer 执行 `prepare()`。
|
||||||
|
8. symbol layer 进入 cross-tile symbol bookkeeping。
|
||||||
|
9. 根据 placement freshness 条件周期性重新执行 placement。
|
||||||
|
10. Renderer upload pass 遍历 source/layer RenderItem。
|
||||||
|
11. `updateLayers()`、layer-group tweaker、layer-group upload 继续执行。
|
||||||
|
12. opaque pass 重新遍历/绘制普通 layer groups。
|
||||||
|
13. translucent pass 重新遍历/绘制普通 layer groups。
|
||||||
|
14. 重新 present 整个默认 framebuffer。
|
||||||
|
|
||||||
|
因此“大量 PBF/vector/fill/line/symbol 在静止 camera 下由 Particle frame 唤醒”从源码结构上成立。
|
||||||
|
|
||||||
|
## 8. 需要精确区分:不是所有 Style 工作都每帧完整重算
|
||||||
|
|
||||||
|
当前代码并非每帧无条件重新 evaluate 所有 style property。
|
||||||
|
|
||||||
|
普通 RenderLayer 的 `evaluate()` 通常受以下条件约束:
|
||||||
|
- layer added/changed
|
||||||
|
- zoom changed 且该 layer 依赖 zoom
|
||||||
|
- crossfade
|
||||||
|
- active transition
|
||||||
|
|
||||||
|
所以静止 camera、style 不变时,很多 property evaluation 可以跳过。
|
||||||
|
|
||||||
|
同样,完整 symbol placement 也不是严格 60 次/秒。
|
||||||
|
`PlacementController::placementIsRecent()` 使用 placement update period 控制重新 placement;默认逻辑与约 300ms 的 transition/update period 相关。
|
||||||
|
但这些优化不改变主结论:
|
||||||
|
|
||||||
|
> 即使 property evaluation 和完整 placement 并非每帧执行,整张地图仍持续进入 source/layer traversal、source update/prepare、layer prepare、command encoding、普通 layer draw 与 framebuffer present。
|
||||||
|
|
||||||
|
这足以解释复杂 Style 与简单 Test Host 之间的大幅 RenderThread 差异。
|
||||||
|
|
||||||
|
## 9. Particle 是否每帧制造不必要的全局状态变化
|
||||||
|
|
||||||
|
本次静态审计没有发现 Particle 每个 simulation frame 都在修改以下全局状态的直接证据:
|
||||||
|
- camera state
|
||||||
|
- 普通 vector/PBF source data
|
||||||
|
- 全局 Style revision
|
||||||
|
- 普通 layer property
|
||||||
|
- sprite/image revision
|
||||||
|
|
||||||
|
STEP-085 已针对 Particle-local 的不必要成本做过修复,包括:
|
||||||
|
- V4 WeatherColor 静态状态不再因为没有 legacy timeline 而永久 `needsRepaint()`。
|
||||||
|
- V4 Particle 增加 Source-owned renderer cadence。
|
||||||
|
- V4 snapshot 改用 immutable shared pointer,减少稳定 window 的大数组深复制。
|
||||||
|
- WeatherColor 尽量复用 drawable,仅 wrap 变化时重建。
|
||||||
|
- 增加 renderer debug counters。
|
||||||
|
|
||||||
|
所以当前问题与 STEP-085 并不矛盾。
|
||||||
|
|
||||||
|
STEP-085 解决的是:
|
||||||
|
```text
|
||||||
|
Particle 自己是否产生了多余 repaint / copy / drawable rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
本调查发现的是更外层问题:
|
||||||
|
```text
|
||||||
|
即使是“合法的 Particle frame”,当前仍等价于一次完整 MapLibre frame
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 为什么简单 Particle Host 不能证明完整 NavSea 成本正常
|
||||||
|
简单 Host 的 Style 很小,因此:
|
||||||
|
```text
|
||||||
|
Particle simulation/render
|
||||||
|
+ 少量普通 MapLibre layer 成本
|
||||||
|
≈ 当前约 40% RenderThread
|
||||||
|
```
|
||||||
|
|
||||||
|
完整 NavSea Style 包含大量:
|
||||||
|
- PBF/vector source
|
||||||
|
- fill
|
||||||
|
- line
|
||||||
|
- symbol
|
||||||
|
- text/icon placement
|
||||||
|
- navigation marks
|
||||||
|
- 其他 MapLibre layers
|
||||||
|
|
||||||
|
当 Particle 让 MapLibre 持续出帧后:
|
||||||
|
```text
|
||||||
|
Particle 自身成本
|
||||||
|
+ 大型静态 map scene 的重复 orchestration / prepare / draw
|
||||||
|
≈ 当前约 80% RenderThread
|
||||||
|
```
|
||||||
|
|
||||||
|
因此必须使用复杂真实 Style 做性能判断,不能只使用 Particle Test Host。
|
||||||
|
|
||||||
|
## 11. JsSchedulerProbe 联动现象的当前解释边界
|
||||||
|
|
||||||
|
用户实测:
|
||||||
|
- Particle OFF:Probe OFF 7.08%,Probe ON 8.93%,约 +1.85pp JS。
|
||||||
|
- Particle ON:Probe OFF 约 27~29%,Probe ON 96.47%,约 +69pp JS。
|
||||||
|
|
||||||
|
当前 Native 源码审计没有发现 Particle 每帧主动通过 RN bridge 回调 JS 的证据。
|
||||||
|
|
||||||
|
更合理的当前解释是:
|
||||||
|
- Particle 让 MapLibre/RenderThread 持续处于 frame pressure。
|
||||||
|
- FPS_30/60 时 Android Main Looper 还有 cadence wake。
|
||||||
|
- RN RAF / Choreographer / timer / JS scheduler 在不再 idle 的系统中发生严重放大或竞争。
|
||||||
|
|
||||||
|
但这部分尚不能仅靠 Native 静态源码判定根因。
|
||||||
|
需要 RN scheduler trace / perfetto 才能进一步归因。
|
||||||
|
由于 JsSchedulerProbe 将退出正常运行路径,它不应阻塞当前 Native Renderer 主问题。
|
||||||
|
## 12. 下一阶段必须做的运行时 Profile
|
||||||
|
|
||||||
|
需要固定 camera,并至少构造以下矩阵:
|
||||||
|
|
||||||
|
| 场景 | Style | Particle |
|
||||||
|
|---|---|---|
|
||||||
|
| A0 | 简单 Style | OFF |
|
||||||
|
| A1 | 简单 Style | ON |
|
||||||
|
| B0 | 大量 PBF/vector/symbol/line/fill | OFF |
|
||||||
|
| B1 | 与 B0 完全相同 | ON |
|
||||||
|
|
||||||
|
B1 还应分别验证:
|
||||||
|
- AUTO
|
||||||
|
- 60fps
|
||||||
|
- 30fps
|
||||||
|
|
||||||
|
建议记录以下 Native 阶段的调用次数与 CPU 时间:
|
||||||
|
- `Map::Impl::onUpdate`
|
||||||
|
- `RenderOrchestrator::createRenderTree`
|
||||||
|
- source × layer traversal
|
||||||
|
- VectorSource / TilePyramid update
|
||||||
|
- source prepare
|
||||||
|
- layer prepare
|
||||||
|
- cross-tile symbol bookkeeping
|
||||||
|
- full placement
|
||||||
|
- `updateLayers`
|
||||||
|
- layer-group tweaker
|
||||||
|
- upload pass
|
||||||
|
- opaque pass
|
||||||
|
- translucent pass
|
||||||
|
- present
|
||||||
|
- Particle simulation advance
|
||||||
|
- Particle render/composite
|
||||||
|
|
||||||
|
同时记录 MapLibre 已有 `gfx::RenderingStats`:
|
||||||
|
- encodingTime
|
||||||
|
- renderingTime
|
||||||
|
- numDrawCalls
|
||||||
|
- texture/buffer/uniform updates
|
||||||
|
|
||||||
|
并结合 STEP-085 的 Particle debug counters,区分 Particle 自身和非 Particle 成本。
|
||||||
|
预期验证特征:
|
||||||
|
|
||||||
|
```text
|
||||||
|
B0 + stable camera
|
||||||
|
→ frame/update 很快降到接近 idle
|
||||||
|
|
||||||
|
B1 + stable camera
|
||||||
|
→ onUpdate/createRenderTree 与 Particle frame 数量高度相关
|
||||||
|
→ Vector/PBF source update/prepare 被持续调用
|
||||||
|
→ 普通非 Particle draw calls 持续存在
|
||||||
|
→ B1 non-Particle CPU 显著高于 A1
|
||||||
|
```
|
||||||
|
|
||||||
|
如果该特征成立,则“continuous repaint 唤醒完整 scene”的运行时证据闭环完成。
|
||||||
|
|
||||||
|
## 13. 优化方向分层
|
||||||
|
|
||||||
|
### 第一层:低风险缓解
|
||||||
|
优先验证 30fps / adaptive fps。
|
||||||
|
|
||||||
|
原因:在当前 full-map repaint 架构下,降低 Particle frame 数不仅减少 Particle 本身成本,也会近似同比减少被唤醒的整个 NavSea map scene 成本。
|
||||||
|
|
||||||
|
### 第二层:Renderer animation-only fast path
|
||||||
|
值得优先调查是否可在 MapLibre Native 内部实现:
|
||||||
|
- 复用稳定 map state / RenderTree
|
||||||
|
- Particle 下一帧不重新进入完整 `Map::Impl::onUpdate`
|
||||||
|
- 跳过普通 source update、TilePyramid update、部分 prepare 和 placement
|
||||||
|
- 仍允许必要的 framebuffer redraw
|
||||||
|
|
||||||
|
这属于内部性能实现优化,原则上不需要改变 C-001。
|
||||||
|
|
||||||
|
### 第三层:静态地图 framebuffer cache + Particle composition
|
||||||
|
若目标是让静态 fill/line/symbol/PBF draw call 在 Particle 动画期间也消失,则需要 retained/offscreen composition。
|
||||||
|
|
||||||
|
这比 animation-only fast path 复杂得多,需要处理:
|
||||||
|
- layer ordering
|
||||||
|
- transparent layers
|
||||||
|
- depth/stencil
|
||||||
|
- 3D
|
||||||
|
- world wrap
|
||||||
|
- custom layer/render target
|
||||||
|
- camera/style/source invalidation
|
||||||
|
如果最终要求 Particle 固定作为静态地图之上的独立 overlay,架构可显著简化;但若这种限制成为 Host 可观察的公共语义,则必须重新进入 Integration RFC/Contract 评审。
|
||||||
|
|
||||||
|
## 14. 当前结论
|
||||||
|
|
||||||
|
源码层面可以确认:
|
||||||
|
|
||||||
|
1. Particle 当前通过 MapLibre 全局 `needsRepaint` 维持动画。
|
||||||
|
2. Particle 没有独立 layer-only render loop。
|
||||||
|
3. 一次 Particle repaint 会进入完整 `Renderer::render`。
|
||||||
|
4. camera 静止时仍会重复大量普通 source/layer update、prepare、render 工作。
|
||||||
|
5. 普通 fill/line/symbol/PBF draw 会在连续 frame 中再次提交。
|
||||||
|
6. 并非所有 style property 和 placement 都每帧完整重算,但不足以让复杂地图回到 idle。
|
||||||
|
7. 当前没有证据表明主要原因是 Particle 每帧修改全局 Style/Source/Camera revision。
|
||||||
|
8. STEP-085 已降低 Particle-local 的多余 repaint/copy/rebuild,但没有改变“合法 Particle frame = 完整 MapLibre frame”的架构。
|
||||||
|
9. Test Host Style 太简单,不能用于排除真实大型 Style 下的 full-scene amplification。
|
||||||
|
10. 下一步应先做 A0/A1/B0/B1 定量 profile,再决定 animation-only fast path 或 framebuffer composition。
|
||||||
|
11. 现阶段不需要修改 Particle 公共 API,也不需要修改 NavSea ↔ Particle C-001。
|
||||||
|
|
||||||
|
## 15. 建议的下一任务
|
||||||
|
|
||||||
|
建议下一任务只做 Renderer observability / performance profile,不先做架构重构。
|
||||||
|
|
||||||
|
目标是把完整 frame 拆成 Particle 与 non-Particle 阶段,定量证明:
|
||||||
|
- 每秒进入多少次完整 map update/render;
|
||||||
|
- 大型 Style 中额外 CPU 具体落在哪些 source/layer/placement/draw 阶段;
|
||||||
|
- 30fps 对完整 NavSea 的实际收益;
|
||||||
|
- 哪些阶段可以通过复用 RenderTree 或 animation-only fast path 安全跳过。
|
||||||
|
|
||||||
|
完成该 profile 后,再决定是否进入具体 Renderer 优化实现。
|
||||||
Reference in New Issue
Block a user