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