fix: make render orchestrator teardown safe
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:
405
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md
Normal file
405
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v1.md
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1
|
||||||
|
|
||||||
|
## 0. 任务目的
|
||||||
|
|
||||||
|
修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。
|
||||||
|
|
||||||
|
现场已在 NavSea Android x86_64 设备稳定复现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Fatal signal 6 (SIGABRT)
|
||||||
|
thread: RenderThread 11
|
||||||
|
|
||||||
|
FORTIFY:
|
||||||
|
pthread_mutex_lock called on a destroyed mutex
|
||||||
|
|
||||||
|
libmaplibre.so
|
||||||
|
std::__ndk1::mutex::lock()
|
||||||
|
...
|
||||||
|
mbgl::android::MapRenderer::resetRenderer()
|
||||||
|
```
|
||||||
|
|
||||||
|
V17 正常,V18 接入后稳定在启动约 9~11 秒触发。
|
||||||
|
|
||||||
|
本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。
|
||||||
|
|
||||||
|
未经用户明确要求:不加入、不执行 Jest。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 冻结基线
|
||||||
|
|
||||||
|
工作树:
|
||||||
|
|
||||||
|
`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||||
|
|
||||||
|
分支:
|
||||||
|
|
||||||
|
`step101-render-orchestrator-teardown`
|
||||||
|
|
||||||
|
起点必须是:
|
||||||
|
|
||||||
|
`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||||
|
|
||||||
|
即正式 V18 Final HEAD。
|
||||||
|
|
||||||
|
V18 已发布,不得覆盖、重发或移动:
|
||||||
|
|
||||||
|
- `maplibre-particle/rn/13.4.1-particle.18`
|
||||||
|
- Android `13.4.1-particle.18`
|
||||||
|
- iOS `6.28.0-particle.18`
|
||||||
|
|
||||||
|
修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 已知根因证据
|
||||||
|
|
||||||
|
V18 STEP-100 新增:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RenderSegmentRevisionBook retainedSegmentRevisions;
|
||||||
|
mutable std::mutex readinessDiagnosticsMutex;
|
||||||
|
uint64_t readinessEventSequence;
|
||||||
|
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
|
||||||
|
RenderMutationDiagnostics lastReadinessMutation;
|
||||||
|
```
|
||||||
|
|
||||||
|
多个 RenderSource / Tile observer callback 会进入:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
advanceRetainedSourceRevision(...)
|
||||||
|
recordReadinessMutation(...)
|
||||||
|
onTileChanged(...)
|
||||||
|
onTileAction(...)
|
||||||
|
buildReadinessDiagnostics(...)
|
||||||
|
retainedSegmentSourceState(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
并 lock:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
readinessDiagnosticsMutex
|
||||||
|
```
|
||||||
|
|
||||||
|
V18 当前成员声明中:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renderSources
|
||||||
|
renderLayers
|
||||||
|
...
|
||||||
|
retainedSegmentRevisions
|
||||||
|
...
|
||||||
|
readinessDiagnosticsMutex
|
||||||
|
sourceReadinessEvents
|
||||||
|
...
|
||||||
|
threadPool
|
||||||
|
```
|
||||||
|
|
||||||
|
C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。
|
||||||
|
|
||||||
|
而 RenderSource 在创建时:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
renderSource->setObserver(this);
|
||||||
|
```
|
||||||
|
|
||||||
|
observer 指向 RenderOrchestrator。
|
||||||
|
|
||||||
|
当前 `RenderOrchestrator::~RenderOrchestrator()` 只:
|
||||||
|
|
||||||
|
1. contextLost 时标记 layer;
|
||||||
|
2. `threadPool.waitForEmpty()`;
|
||||||
|
|
||||||
|
没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。
|
||||||
|
|
||||||
|
因此 V18 存在明确生命周期危险:
|
||||||
|
|
||||||
|
```text
|
||||||
|
readinessDiagnosticsMutex destroyed
|
||||||
|
↓
|
||||||
|
renderSources still destroying/alive
|
||||||
|
↓
|
||||||
|
late tile/source callback
|
||||||
|
↓
|
||||||
|
RenderOrchestrator::onTile*
|
||||||
|
↓
|
||||||
|
lock destroyed mutex
|
||||||
|
↓
|
||||||
|
FORTIFY SIGABRT
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 修复原则
|
||||||
|
|
||||||
|
不要只依赖调整成员声明顺序。
|
||||||
|
|
||||||
|
本任务应建立显式 teardown contract:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderOrchestrator destructor begins
|
||||||
|
↓
|
||||||
|
stop / drain deferred scheduler work
|
||||||
|
↓
|
||||||
|
detach observer from every RenderSource
|
||||||
|
↓
|
||||||
|
destroy/clear renderSources while all orchestrator state is still alive
|
||||||
|
↓
|
||||||
|
only then continue member teardown
|
||||||
|
```
|
||||||
|
|
||||||
|
同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。
|
||||||
|
|
||||||
|
目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 必做实现
|
||||||
|
|
||||||
|
### 4.1 RenderSource nullptr observer 语义
|
||||||
|
|
||||||
|
当前:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||||
|
observer = observer_;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改成与 Style Source / Layer 一致的安全语义:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
observer = observer_ ? observer_ : &nullObserver;
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- constructor 仍默认 NullObserver;
|
||||||
|
- `setObserver(nullptr)` 是合法 detach;
|
||||||
|
- onTileChanged / onTileError / onTileAction 不需要加 null 分支;
|
||||||
|
- 不吞掉正常有 observer 时的回调。
|
||||||
|
|
||||||
|
必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。
|
||||||
|
|
||||||
|
### 4.2 RenderOrchestrator 显式 teardown
|
||||||
|
|
||||||
|
在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时:
|
||||||
|
|
||||||
|
1. 保留现有 contextLost layer handling;
|
||||||
|
2. 等待已有 deferred work 完成;
|
||||||
|
3. 对所有 `renderSources` 执行 observer detach;
|
||||||
|
4. 显式 `renderSources.clear()`;
|
||||||
|
5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围;
|
||||||
|
6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。
|
||||||
|
|
||||||
|
顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。
|
||||||
|
|
||||||
|
如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。
|
||||||
|
|
||||||
|
### 4.3 生命周期状态可选保护
|
||||||
|
|
||||||
|
如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。
|
||||||
|
|
||||||
|
禁止用 flag 代替 observer detach。
|
||||||
|
|
||||||
|
禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 不得修改
|
||||||
|
|
||||||
|
- STEP-100 Source→Segment locality 语义;
|
||||||
|
- per-segment revision;
|
||||||
|
- per-segment readiness;
|
||||||
|
- mixed Build/Reuse/Fallback;
|
||||||
|
- Fast Path gate;
|
||||||
|
- singleContinuousSegment;
|
||||||
|
- Source::isLoaded;
|
||||||
|
- Style Z-order;
|
||||||
|
- provider/network;
|
||||||
|
- NavSea;
|
||||||
|
- MapRenderer::resetRenderer 正常生命周期;
|
||||||
|
- Android Activity 为避免 reset 而加 workaround。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 必做测试
|
||||||
|
|
||||||
|
### 6.1 Native 非 Jest
|
||||||
|
|
||||||
|
至少覆盖:
|
||||||
|
|
||||||
|
1. RenderSource 默认 NullObserver 可安全接 callback;
|
||||||
|
2. setObserver(real) 正常转发;
|
||||||
|
3. setObserver(nullptr) 后不再调用旧 observer;
|
||||||
|
4. detach 后 onTileChanged / onTileError / onTileAction 安全;
|
||||||
|
5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景;
|
||||||
|
6. source callback 与 teardown 边界不访问已销毁 readiness state;
|
||||||
|
7. STEP-100 locality 相关 focused tests 不回归。
|
||||||
|
|
||||||
|
如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。
|
||||||
|
|
||||||
|
### 6.2 Android Test App
|
||||||
|
|
||||||
|
在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。
|
||||||
|
|
||||||
|
场景必须真实触发 Native Renderer teardown,而不是只 clear field。
|
||||||
|
|
||||||
|
至少提供:
|
||||||
|
|
||||||
|
```text
|
||||||
|
T1 RESET_ONCE
|
||||||
|
T2 RESET_LOOP_20
|
||||||
|
T3 RESET_WITH_DYNAMIC_GEOJSON
|
||||||
|
T4 RESET_WITH_DIAGNOSTICS
|
||||||
|
```
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- recreate/destroy MapView 或等价真实 renderer.reset;
|
||||||
|
- 每轮等待足够让 source/tile work 进入正常运行;
|
||||||
|
- T3 同时保持动态 GeoJSON/source mutation;
|
||||||
|
- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序;
|
||||||
|
- 20 次循环无 SIGABRT / crash / ANR;
|
||||||
|
- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。
|
||||||
|
|
||||||
|
不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. NavSea 现场等价验收
|
||||||
|
|
||||||
|
RDC 在 Codex 完成后独立执行。
|
||||||
|
|
||||||
|
Android 设备:
|
||||||
|
|
||||||
|
`192.168.200.139:5555`
|
||||||
|
|
||||||
|
必须显式 `adb -s`。
|
||||||
|
|
||||||
|
最终至少执行:
|
||||||
|
|
||||||
|
1. Validation App STEP-101 T1~T4;
|
||||||
|
2. crash buffer 检查;
|
||||||
|
3. 20 次 reset/recreate;
|
||||||
|
4. 安装 V19 进 NavSea 后做真实启动;
|
||||||
|
5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件;
|
||||||
|
6. 保留/启用一次 RenderSegment diagnostics probe A/B;
|
||||||
|
7. 连续至少 5 次冷启动,每次观察 >30 秒;
|
||||||
|
8. 不允许出现:
|
||||||
|
- `pthread_mutex_lock called on a destroyed mutex`
|
||||||
|
- SIGABRT RenderThread
|
||||||
|
- `MapRenderer::resetRenderer` teardown crash。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. STEP-100 回归验收
|
||||||
|
|
||||||
|
修复 teardown 后必须确认 V18 的核心 locality 没被破坏。
|
||||||
|
|
||||||
|
至少复测 T4 R-C-R:
|
||||||
|
|
||||||
|
无关 R0:
|
||||||
|
|
||||||
|
```text
|
||||||
|
buildDelta = 0
|
||||||
|
invalidationDelta = 0
|
||||||
|
reuseDelta > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
动态 R1 正常 build/invalidation。
|
||||||
|
|
||||||
|
禁止通过删除 mutex/readiness/revision 功能来修 crash。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Codex 执行边界
|
||||||
|
|
||||||
|
Codex负责:
|
||||||
|
|
||||||
|
- 只读检查真实 V18 代码;
|
||||||
|
- 实现最小 Native 修复;
|
||||||
|
- 编写/更新非 Jest C++ tests;
|
||||||
|
- 更新 Android Validation STEP-101 场景;
|
||||||
|
- 静态检查;
|
||||||
|
- 生成报告。
|
||||||
|
|
||||||
|
Codex不负责:
|
||||||
|
|
||||||
|
- NavSea 修改;
|
||||||
|
- V19 release;
|
||||||
|
- tag;
|
||||||
|
- publish;
|
||||||
|
- 覆盖 V18;
|
||||||
|
- Jest。
|
||||||
|
|
||||||
|
Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 完成报告
|
||||||
|
|
||||||
|
生成:
|
||||||
|
|
||||||
|
`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`
|
||||||
|
|
||||||
|
Step 记录:
|
||||||
|
|
||||||
|
`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`
|
||||||
|
|
||||||
|
报告必须写:
|
||||||
|
|
||||||
|
- root cause;
|
||||||
|
- start HEAD;
|
||||||
|
- 修改文件;
|
||||||
|
- destructor teardown 顺序;
|
||||||
|
- observer detach 语义;
|
||||||
|
- 为什么不依赖成员声明顺序;
|
||||||
|
- tests added;
|
||||||
|
- 实际运行 / NOT RUN;
|
||||||
|
- STEP-100 locality 是否保持;
|
||||||
|
- 风险;
|
||||||
|
- 未发布 V19(Codex 阶段)。
|
||||||
|
|
||||||
|
Codex阶段不 push,由 RDC 验收后统一 commit/push。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. V19 发布门槛
|
||||||
|
|
||||||
|
仅当:
|
||||||
|
|
||||||
|
- Native compile PASS;
|
||||||
|
- focused C++ tests PASS;
|
||||||
|
- Android Validation teardown loop PASS;
|
||||||
|
- STEP-100 locality regression PASS;
|
||||||
|
- NavSea V19 真实 5 次冷启动 >30s PASS;
|
||||||
|
- crash buffer 无 destroyed mutex;
|
||||||
|
|
||||||
|
才允许发布:
|
||||||
|
|
||||||
|
- RN / Android:`13.4.1-particle.19`
|
||||||
|
- iOS:`6.28.0-particle.19`
|
||||||
|
- tag:`maplibre-particle/rn/13.4.1-particle.19`
|
||||||
|
|
||||||
|
V18 保持不可变,不覆盖、不删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 最终原则
|
||||||
|
|
||||||
|
正常:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MapView unmount
|
||||||
|
Style generation replacement
|
||||||
|
renderer.reset()
|
||||||
|
Activity recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
都是合法生命周期。
|
||||||
|
|
||||||
|
Particle Native 必须做到:
|
||||||
|
|
||||||
|
> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。
|
||||||
510
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md
Normal file
510
docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1
|
||||||
|
|
||||||
|
## 0. 任务目的
|
||||||
|
|
||||||
|
修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。
|
||||||
|
|
||||||
|
现场已在 NavSea Android x86_64 设备稳定复现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Fatal signal 6 (SIGABRT)
|
||||||
|
thread: RenderThread 11
|
||||||
|
|
||||||
|
FORTIFY:
|
||||||
|
pthread_mutex_lock called on a destroyed mutex
|
||||||
|
|
||||||
|
libmaplibre.so
|
||||||
|
std::__ndk1::mutex::lock()
|
||||||
|
...
|
||||||
|
mbgl::android::MapRenderer::resetRenderer()
|
||||||
|
```
|
||||||
|
|
||||||
|
V17 正常,V18 接入后稳定在启动约 9~11 秒触发。
|
||||||
|
|
||||||
|
本任务只修 Particle Native 生命周期;禁止在 NavSea 增加绕路、private wrapper、禁止阻止正常 MapView reset。
|
||||||
|
|
||||||
|
未经用户明确要求:不加入、不执行 Jest。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 冻结基线
|
||||||
|
|
||||||
|
工作树:
|
||||||
|
|
||||||
|
`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||||
|
|
||||||
|
分支:
|
||||||
|
|
||||||
|
`step101-render-orchestrator-teardown`
|
||||||
|
|
||||||
|
起点必须是:
|
||||||
|
|
||||||
|
`origin/particle/main = d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||||
|
|
||||||
|
即正式 V18 Final HEAD。
|
||||||
|
|
||||||
|
V18 已发布,不得覆盖、重发或移动:
|
||||||
|
|
||||||
|
- `maplibre-particle/rn/13.4.1-particle.18`
|
||||||
|
- Android `13.4.1-particle.18`
|
||||||
|
- iOS `6.28.0-particle.18`
|
||||||
|
|
||||||
|
修复通过后由 RDC/ChatGPT 发布新不可变版本 V19。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 已知根因证据
|
||||||
|
|
||||||
|
V18 STEP-100 新增:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RenderSegmentRevisionBook retainedSegmentRevisions;
|
||||||
|
mutable std::mutex readinessDiagnosticsMutex;
|
||||||
|
uint64_t readinessEventSequence;
|
||||||
|
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
|
||||||
|
RenderMutationDiagnostics lastReadinessMutation;
|
||||||
|
```
|
||||||
|
|
||||||
|
多个 RenderSource / Tile observer callback 会进入:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
advanceRetainedSourceRevision(...)
|
||||||
|
recordReadinessMutation(...)
|
||||||
|
onTileChanged(...)
|
||||||
|
onTileAction(...)
|
||||||
|
buildReadinessDiagnostics(...)
|
||||||
|
retainedSegmentSourceState(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
并 lock:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
readinessDiagnosticsMutex
|
||||||
|
```
|
||||||
|
|
||||||
|
V18 当前成员声明中:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renderSources
|
||||||
|
renderLayers
|
||||||
|
...
|
||||||
|
retainedSegmentRevisions
|
||||||
|
...
|
||||||
|
readinessDiagnosticsMutex
|
||||||
|
sourceReadinessEvents
|
||||||
|
...
|
||||||
|
threadPool
|
||||||
|
```
|
||||||
|
|
||||||
|
C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。
|
||||||
|
|
||||||
|
而 RenderSource 在创建时:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
renderSource->setObserver(this);
|
||||||
|
```
|
||||||
|
|
||||||
|
observer 指向 RenderOrchestrator。
|
||||||
|
|
||||||
|
当前 `RenderOrchestrator::~RenderOrchestrator()` 只:
|
||||||
|
|
||||||
|
1. contextLost 时标记 layer;
|
||||||
|
2. `threadPool.waitForEmpty()`;
|
||||||
|
|
||||||
|
没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。
|
||||||
|
|
||||||
|
因此 V18 存在明确生命周期危险:
|
||||||
|
|
||||||
|
```text
|
||||||
|
readinessDiagnosticsMutex destroyed
|
||||||
|
↓
|
||||||
|
renderSources still destroying/alive
|
||||||
|
↓
|
||||||
|
late tile/source callback
|
||||||
|
↓
|
||||||
|
RenderOrchestrator::onTile*
|
||||||
|
↓
|
||||||
|
lock destroyed mutex
|
||||||
|
↓
|
||||||
|
FORTIFY SIGABRT
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 修复原则
|
||||||
|
|
||||||
|
不要只依赖调整成员声明顺序。
|
||||||
|
|
||||||
|
本任务应建立显式 teardown contract:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderOrchestrator destructor begins
|
||||||
|
↓
|
||||||
|
stop / drain deferred scheduler work
|
||||||
|
↓
|
||||||
|
detach observer from every RenderSource
|
||||||
|
↓
|
||||||
|
destroy/clear renderSources while all orchestrator state is still alive
|
||||||
|
↓
|
||||||
|
only then continue member teardown
|
||||||
|
```
|
||||||
|
|
||||||
|
同时 `RenderSource::setObserver(nullptr)` 必须安全恢复到 NullObserver。
|
||||||
|
|
||||||
|
目标是即使未来成员声明顺序变化,Source callback 也不能在 owner teardown 后访问 RenderOrchestrator。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 必做实现
|
||||||
|
|
||||||
|
### 4.1 RenderSource nullptr observer 语义
|
||||||
|
|
||||||
|
当前:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||||
|
observer = observer_;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
改成与 Style Source / Layer 一致的安全语义:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
observer = observer_ ? observer_ : &nullObserver;
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- constructor 仍默认 NullObserver;
|
||||||
|
- `setObserver(nullptr)` 是合法 detach;
|
||||||
|
- onTileChanged / onTileError / onTileAction 不需要加 null 分支;
|
||||||
|
- 不吞掉正常有 observer 时的回调。
|
||||||
|
|
||||||
|
必须加非 Jest C++ 回归测试验证 nullptr detach 后 callback 不 crash、不调用旧 observer。
|
||||||
|
|
||||||
|
### 4.2 RenderOrchestrator 显式 teardown
|
||||||
|
|
||||||
|
在 `RenderOrchestrator::~RenderOrchestrator()` 中,在 readiness mutex/state 仍确定存活时:
|
||||||
|
|
||||||
|
1. 保留现有 contextLost layer handling;
|
||||||
|
2. 等待已有 deferred work 完成;
|
||||||
|
3. 对所有 `renderSources` 执行 observer detach;
|
||||||
|
4. 显式 `renderSources.clear()`;
|
||||||
|
5. 必要时对 glyph/image observer 也做已有 API 支持的 detach,但不要扩大范围;
|
||||||
|
6. 不允许 teardown callback 再进入 RenderOrchestrator source callback。
|
||||||
|
|
||||||
|
顺序必须结合当前 TaggedScheduler / RenderSource / TilePyramid 的真实生命周期检查,而不是机械照抄上述编号。
|
||||||
|
|
||||||
|
如果存在“clear renderSources 本身可能产生新的 scheduler work”,需要设计第二次 drain 或更安全顺序,并在报告解释。
|
||||||
|
|
||||||
|
### 4.3 生命周期状态可选保护
|
||||||
|
|
||||||
|
如果仅 detach + explicit clear 仍无法证明并发安全,允许增加最小 teardown flag,例如 atomic bool,但只作为二次防线。
|
||||||
|
|
||||||
|
禁止用 flag 代替 observer detach。
|
||||||
|
|
||||||
|
禁止 catch FORTIFY;destroyed mutex abort 不是 C++ exception。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 不得修改
|
||||||
|
|
||||||
|
- STEP-100 Source→Segment locality 语义;
|
||||||
|
- per-segment revision;
|
||||||
|
- per-segment readiness;
|
||||||
|
- mixed Build/Reuse/Fallback;
|
||||||
|
- Fast Path gate;
|
||||||
|
- singleContinuousSegment;
|
||||||
|
- Source::isLoaded;
|
||||||
|
- Style Z-order;
|
||||||
|
- provider/network;
|
||||||
|
- NavSea;
|
||||||
|
- MapRenderer::resetRenderer 正常生命周期;
|
||||||
|
- Android Activity 为避免 reset 而加 workaround。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 必做测试
|
||||||
|
|
||||||
|
### 6.1 Native 非 Jest
|
||||||
|
|
||||||
|
至少覆盖:
|
||||||
|
|
||||||
|
1. RenderSource 默认 NullObserver 可安全接 callback;
|
||||||
|
2. setObserver(real) 正常转发;
|
||||||
|
3. setObserver(nullptr) 后不再调用旧 observer;
|
||||||
|
4. detach 后 onTileChanged / onTileError / onTileAction 安全;
|
||||||
|
5. RenderOrchestrator teardown 有至少一个含 RenderSource 的场景;
|
||||||
|
6. source callback 与 teardown 边界不访问已销毁 readiness state;
|
||||||
|
7. STEP-100 locality 相关 focused tests 不回归。
|
||||||
|
|
||||||
|
如果现有测试设施不方便直接构造完整 RenderOrchestrator teardown,允许提取最小可测试 teardown helper,但禁止为了测试重构大范围生产结构。
|
||||||
|
|
||||||
|
### 6.2 Android Test App
|
||||||
|
|
||||||
|
在现有 ParticleCoreLayerValidationActivity / Validation App 增加专门 STEP-101 teardown 场景。
|
||||||
|
|
||||||
|
场景必须真实触发 Native Renderer teardown,而不是只 clear field。
|
||||||
|
|
||||||
|
至少提供:
|
||||||
|
|
||||||
|
```text
|
||||||
|
T1 RESET_ONCE
|
||||||
|
T2 RESET_LOOP_20
|
||||||
|
T3 RESET_WITH_DYNAMIC_GEOJSON
|
||||||
|
T4 RESET_WITH_DIAGNOSTICS
|
||||||
|
```
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- recreate/destroy MapView 或等价真实 renderer.reset;
|
||||||
|
- 每轮等待足够让 source/tile work 进入正常运行;
|
||||||
|
- T3 同时保持动态 GeoJSON/source mutation;
|
||||||
|
- T4 在 reset 前调用一次 RendererRuntime diagnostics,模拟 NavSea probe 时序;
|
||||||
|
- 20 次循环无 SIGABRT / crash / ANR;
|
||||||
|
- reset 后新 renderer 能继续正常显示 / 获取 diagnostics。
|
||||||
|
|
||||||
|
不得通过延迟 reset、禁用 observer、关闭 GeoJSON 来逃避问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. NavSea 现场等价验收
|
||||||
|
|
||||||
|
RDC 在 Codex 完成后独立执行。
|
||||||
|
|
||||||
|
Android 设备:
|
||||||
|
|
||||||
|
`192.168.200.139:5555`
|
||||||
|
|
||||||
|
必须显式 `adb -s`。
|
||||||
|
|
||||||
|
最终至少执行:
|
||||||
|
|
||||||
|
1. Validation App STEP-101 T1~T4;
|
||||||
|
2. crash buffer 检查;
|
||||||
|
3. 20 次 reset/recreate;
|
||||||
|
4. 安装 V19 进 NavSea 后做真实启动;
|
||||||
|
5. 保留 NavSea 当前 `key={map-style-generation-...}` 正常 reset 触发条件;
|
||||||
|
6. 保留/启用一次 RenderSegment diagnostics probe A/B;
|
||||||
|
7. 连续至少 5 次冷启动,每次观察 >30 秒;
|
||||||
|
8. 不允许出现:
|
||||||
|
- `pthread_mutex_lock called on a destroyed mutex`
|
||||||
|
- SIGABRT RenderThread
|
||||||
|
- `MapRenderer::resetRenderer` teardown crash。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. STEP-100 回归验收
|
||||||
|
|
||||||
|
修复 teardown 后必须确认 V18 的核心 locality 没被破坏。
|
||||||
|
|
||||||
|
至少复测 T4 R-C-R:
|
||||||
|
|
||||||
|
无关 R0:
|
||||||
|
|
||||||
|
```text
|
||||||
|
buildDelta = 0
|
||||||
|
invalidationDelta = 0
|
||||||
|
reuseDelta > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
动态 R1 正常 build/invalidation。
|
||||||
|
|
||||||
|
禁止通过删除 mutex/readiness/revision 功能来修 crash。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Codex 执行边界
|
||||||
|
|
||||||
|
Codex负责:
|
||||||
|
|
||||||
|
- 只读检查真实 V18 代码;
|
||||||
|
- 实现最小 Native 修复;
|
||||||
|
- 编写/更新非 Jest C++ tests;
|
||||||
|
- 更新 Android Validation STEP-101 场景;
|
||||||
|
- 静态检查;
|
||||||
|
- 生成报告。
|
||||||
|
|
||||||
|
Codex不负责:
|
||||||
|
|
||||||
|
- NavSea 修改;
|
||||||
|
- V19 release;
|
||||||
|
- tag;
|
||||||
|
- publish;
|
||||||
|
- 覆盖 V18;
|
||||||
|
- Jest。
|
||||||
|
|
||||||
|
Mac Native 开发/编译原则保持;不要使用 Linux Native 环境。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 完成报告
|
||||||
|
|
||||||
|
生成:
|
||||||
|
|
||||||
|
`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`
|
||||||
|
|
||||||
|
Step 记录:
|
||||||
|
|
||||||
|
`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`
|
||||||
|
|
||||||
|
报告必须写:
|
||||||
|
|
||||||
|
- root cause;
|
||||||
|
- start HEAD;
|
||||||
|
- 修改文件;
|
||||||
|
- destructor teardown 顺序;
|
||||||
|
- observer detach 语义;
|
||||||
|
- 为什么不依赖成员声明顺序;
|
||||||
|
- tests added;
|
||||||
|
- 实际运行 / NOT RUN;
|
||||||
|
- STEP-100 locality 是否保持;
|
||||||
|
- 风险;
|
||||||
|
- 未发布 V19(Codex 阶段)。
|
||||||
|
|
||||||
|
Codex阶段不 push,由 RDC 验收后统一 commit/push。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. V19 发布门槛
|
||||||
|
|
||||||
|
仅当:
|
||||||
|
|
||||||
|
- Native compile PASS;
|
||||||
|
- focused C++ tests PASS;
|
||||||
|
- Android Validation teardown loop PASS;
|
||||||
|
- STEP-100 locality regression PASS;
|
||||||
|
- NavSea V19 真实 5 次冷启动 >30s PASS;
|
||||||
|
- crash buffer 无 destroyed mutex;
|
||||||
|
|
||||||
|
才允许发布:
|
||||||
|
|
||||||
|
- RN / Android:`13.4.1-particle.19`
|
||||||
|
- iOS:`6.28.0-particle.19`
|
||||||
|
- tag:`maplibre-particle/rn/13.4.1-particle.19`
|
||||||
|
|
||||||
|
V18 保持不可变,不覆盖、不删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 最终原则
|
||||||
|
|
||||||
|
正常:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MapView unmount
|
||||||
|
Style generation replacement
|
||||||
|
renderer.reset()
|
||||||
|
Activity recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
都是合法生命周期。
|
||||||
|
|
||||||
|
Particle Native 必须做到:
|
||||||
|
|
||||||
|
> Renderer teardown 永远不能因为 observer late callback 访问已销毁 mutex/state 而杀掉宿主 App。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Codex 6-Sol High 架构评审修订(v2,优先级高于前文冲突表述)
|
||||||
|
|
||||||
|
只读评审结论:`APPROVE_WITH_CHANGES`。
|
||||||
|
|
||||||
|
## 13.1 根因表述修正
|
||||||
|
|
||||||
|
当前已经**代码层证实**存在生命周期漏洞:`readinessDiagnosticsMutex` / readiness state 的析构顺序早于 `renderSources`,而 `RenderSource` observer 仍指向 `RenderOrchestrator`;source/tile teardown 可同步或延迟回调进入 `onTileAction/onTileChanged` 并访问 mutex。
|
||||||
|
|
||||||
|
该漏洞与现场 `pthread_mutex_lock called on a destroyed mutex` 高度吻合;在没有完整符号化 crash stack 精确到具体 callback 前,不把“唯一现场根因”写成数学意义上的唯一证明。
|
||||||
|
|
||||||
|
## 13.2 teardown 必须双 drain
|
||||||
|
|
||||||
|
最低安全顺序调整为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
contextLost layer handling
|
||||||
|
↓
|
||||||
|
第一次 threadPool.waitForEmpty()
|
||||||
|
↓
|
||||||
|
detach RenderSource observers
|
||||||
|
↓
|
||||||
|
detach GlyphManager / ImageManager observers
|
||||||
|
↓
|
||||||
|
renderSources.clear() // readiness mutex/state 此时仍存活
|
||||||
|
↓
|
||||||
|
第二次 threadPool.waitForEmpty()
|
||||||
|
↓
|
||||||
|
继续既有 backend/context render-job teardown
|
||||||
|
↓
|
||||||
|
成员析构
|
||||||
|
```
|
||||||
|
|
||||||
|
原因:source/tile destruction 本身可能投递新的 tagged cleanup/deferred work,第一次 drain 不能覆盖 clear 过程中产生的新任务。
|
||||||
|
|
||||||
|
同时必须审计并在报告说明:`TaggedScheduler::waitForEmpty()` 不等于清空 `runOnRenderThread` 队列;后者依赖现有 backend/context render-job 生命周期。不得声称双 drain 自动证明所有 render-thread job 已清空。
|
||||||
|
|
||||||
|
## 13.3 Observer detach 范围
|
||||||
|
|
||||||
|
除了所有 `RenderSource`:
|
||||||
|
|
||||||
|
- `glyphManager->setObserver(nullptr)`;
|
||||||
|
- `imageManager->setObserver(nullptr)`;
|
||||||
|
|
||||||
|
也应在 owner teardown 时显式 detach(前提是现有 API 的 nullptr 已安全回 NullObserver)。
|
||||||
|
|
||||||
|
`RenderSource::setObserver(nullptr)` 必须改为 NullObserver fallback。
|
||||||
|
|
||||||
|
不得直接给 `TilePyramid` / `Tile` observer 写 nullptr,除非其 API 本身有同等安全语义;本任务不扩展到这一步。
|
||||||
|
|
||||||
|
## 13.4 teardown flag
|
||||||
|
|
||||||
|
默认不增加 teardown atomic flag。
|
||||||
|
|
||||||
|
若实现审计发现真实并发 callback 不受 renderer thread / tagged scheduler 串行保证约束,必须先报告;atomic flag 只能二次防护,不能替代 observer detach、drain 和可等待取消。
|
||||||
|
|
||||||
|
## 13.5 Android Validation 证据升级
|
||||||
|
|
||||||
|
T1~T4 每一轮必须能证明发生了**真实 Native renderer reset**:
|
||||||
|
|
||||||
|
- 旧 MapView detach / SurfaceView detach;
|
||||||
|
- `nativeReset()`;
|
||||||
|
- `MapRenderer::resetRenderer()` 进入/完成;
|
||||||
|
- 新 renderer style ready;
|
||||||
|
- 新 renderer diagnostics 可用。
|
||||||
|
|
||||||
|
不能把 `setStyle()`、clear field、只读 diagnostics 当成 teardown 测试。
|
||||||
|
|
||||||
|
推荐按代次销毁并重建 MapView,或 Activity recreate;T2 至少 20 次已确认 reset。
|
||||||
|
|
||||||
|
T3:每轮 reset 前保持动态 GeoJSON/source mutation。
|
||||||
|
|
||||||
|
T4:每轮 reset 前调用一次 Native diagnostics,模拟 NavSea 8 秒 probe 的等价条件。
|
||||||
|
|
||||||
|
每轮应等待 source/tile 确实进入运行态,而不是只按固定 sleep 机械计数。
|
||||||
|
|
||||||
|
## 13.6 测试必须覆盖真实 source teardown
|
||||||
|
|
||||||
|
空 `RenderOrchestrator` 析构测试不够。
|
||||||
|
|
||||||
|
非 Jest C++ 测试至少需要一个含 RenderSource / tile callback 的 teardown 场景,使 source cancellation / callback 能在销毁边界发生,并验证旧 observer 不再收到回调。
|
||||||
|
|
||||||
|
## 13.7 正常 source erase / clearData 审计
|
||||||
|
|
||||||
|
必须只读检查 `clearData()` 和正常 source erase/replacement 路径,因为它们也会销毁 RenderSource。
|
||||||
|
|
||||||
|
本任务主要修 owner teardown;不得未经证明把 destructor 修复泛化为“所有 source replacement 异步问题均已解决”。若发现相同 observer 生命周期漏洞在正常 erase 路径存在,应做最小同源修复或明确 BLOCK,不得隐藏。
|
||||||
|
|
||||||
|
## 13.8 V19 候选验收顺序
|
||||||
|
|
||||||
|
V19 发布门槛修正为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
STEP-101 source code PASS
|
||||||
|
→ 构建未公开的 V19 candidate artifacts
|
||||||
|
→ Validation App 实机 PASS
|
||||||
|
→ NavSea 使用 candidate 做 5 次冷启动/teardown 验收
|
||||||
|
→ 公开发布不可变 V19
|
||||||
|
→ public readback
|
||||||
|
→ finalize/tag
|
||||||
|
```
|
||||||
|
|
||||||
|
不得先公开 V19 再做 NavSea 验收。
|
||||||
53
docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md
Normal file
53
docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# STEP-101 RenderOrchestrator 销毁生命周期修复记录 v1
|
||||||
|
|
||||||
|
状态:**CLOSED / PASS(Native + Android runtime)**
|
||||||
|
日期:2026-10-07
|
||||||
|
|
||||||
|
## 目标与基线
|
||||||
|
|
||||||
|
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||||
|
- 分支:`step101-render-orchestrator-teardown`
|
||||||
|
- 起点与当前 HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||||
|
- 工作合同:`docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md`,第 13 节优先于前文冲突项。
|
||||||
|
- V18 为不可变已发布版本;本阶段未创建、发布或标记 V19。
|
||||||
|
- 目标:修复 RenderSource/Tile 销毁时 observer 可能回调已析构的 RenderOrchestrator readiness mutex/state 的生命周期漏洞。
|
||||||
|
|
||||||
|
V18 成员声明使 readiness mutex/state 早于 `renderSources` 自动析构。Tile 销毁时同步发出 `Cancelled` 回调的路径可达,且与现场 Android destroyed mutex SIGABRT 高度吻合;没有完整符号化栈与修复后等价实机结果,暂不把它写成唯一确认的现场根因。
|
||||||
|
|
||||||
|
## 必做项进度
|
||||||
|
|
||||||
|
- [x] `RenderSource::setObserver(nullptr)` 恢复 NullObserver,保留正常 observer 转发。
|
||||||
|
- [x] Orchestrator 析构:保留 contextLost layer handling;第一次 tagged drain;detach RenderSource、GlyphManager、ImageManager observer;在 readiness state 存活时清理 sources;第二次 tagged drain。
|
||||||
|
- [x] 静态审计独立 render-job 队列:`waitForEmpty()` 不排空该队列;OpenGL context 析构的 `runRenderJobs(true)` 属于后续 backend 清理,测试中也单独调用 `runRenderJobs()`。不能以双 drain 代替 render-job 验收。
|
||||||
|
- [x] 新增非 Jest C++ 测试源码:默认/真实/detach observer、三种 tile callback、Cancelled、source 清理后第二次 drain、render-job 单独处理和重复 teardown。helper 像生产代码一样注册 Orchestrator observer,并断言 source 销毁前旧 observer 已被替换。
|
||||||
|
- [x] Android Validation 增加 T1 RESET_ONCE、T2 RESET_LOOP_20、T3 RESET_WITH_DYNAMIC_GEOJSON、T4 RESET_WITH_DIAGNOSTICS;通过 Activity recreate 触发 MapView 销毁/新建,并输出 SurfaceView detach、nativeReset、resetRenderer、style ready 与 diagnostics 日志供逐次配对。
|
||||||
|
- [x] 只读审计正常 `renderSources.erase()` / `clearData()`:两者在 Orchestrator 存活期间执行;TileCache 析构等待其延迟释放任务结束。本阶段没有扩大其行为修改,运行态异步边界仍待 RDC 验证。
|
||||||
|
- [x] 静态核对 STEP-100 locality、Fast Path、`singleContinuousSegment`、`Source::isLoaded()` 等代码未被本次 diff 修改。
|
||||||
|
- [x] Native:Android full native build PASS;focused Android C++ TestRunner 6/6 PASS。Mac host 因 libuv 环境阻塞,未虚报。
|
||||||
|
- [x] Android Validation APK build PASS;T1~T4 实机 PASS;T2 完整 20 次真实 reset/recreate,crash buffer 为空。
|
||||||
|
- [x] STEP-100 T4 R-C-R locality 回归 PASS:R0 buildDelta=0、invalidationDelta=0、reuseDelta=+3878。
|
||||||
|
- [ ] V19 candidate、NavSea 五次冷启动与正式 V19 发布:属于后续发行 Gate,尚未执行。
|
||||||
|
|
||||||
|
## 允许与禁止范围
|
||||||
|
|
||||||
|
实际变更限于 Orchestrator/RenderSource 生命周期、对应 C++ 测试、Android Validation 场景及 Java/JNI reset 观测日志。未修改 NavSea、provider/network、STEP-100 locality 实现、发布坐标或 V18 产物。未运行 Jest。
|
||||||
|
|
||||||
|
## 检查与当前结果
|
||||||
|
|
||||||
|
本次验证仅包含源码 diff 静态审计与 `git diff --check`。构建、链接、GTest、Gradle/APK、ADB、设备、NavSea、发布均 **NOT RUN**;源码交付不能记为运行态 PASS。完成报告:`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`。
|
||||||
|
|
||||||
|
## 阻塞与下一步
|
||||||
|
|
||||||
|
当前没有已观测编译失败;实际编译与场景行为仍未知。由 RDC 按完成报告的命令独立编译、运行 Native 测试、Android T1~T4、STEP-100 locality,并核对每次 `RESET_REQUEST` 对应 SurfaceView detach、`nativeReset`、`resetRenderer` 和新 renderer ready。随后才进入 V19 candidate 与 NavSea 等价验收;全部门槛满足后才可考虑正式发布。
|
||||||
|
|
||||||
|
|
||||||
|
## RDC 最终证据
|
||||||
|
|
||||||
|
- Android Validation APK:61,957,678 bytes;`libmaplibre.so` 39,637,512 bytes;SHA-256 `14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721`。
|
||||||
|
- T1~T4 teardown:全部 PASS;T2 20 次 reset;动态 GeoJSON / diagnostics 条件均无 crash。
|
||||||
|
- STEP-100 locality:global revision 62→894;R0 build 1→1、invalidation 19→19、reuse 12→3890;PASS。
|
||||||
|
- Android C++ focused TestRunner:4 个 `RenderSegmentLocality` + 2 个 `RenderOrchestratorTeardown` 全部 PASS;status=0。
|
||||||
|
- Mac host GTest:ENV BLOCKED(本机缺可用 libuv pkg-config),不影响 Android full native/device 闭环结论。
|
||||||
|
- Jest:NOT RUN。
|
||||||
|
|
||||||
|
**STEP-101 CLOSED / PASS。** 下一阶段进入 V19 candidate → NavSea 等价冷启动 Gate → 不可变公开发行。
|
||||||
181
docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md
Normal file
181
docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# STEP-101 RenderOrchestrator 销毁生命周期修复 Codex 完成报告 v1
|
||||||
|
|
||||||
|
## 1. 执行结论
|
||||||
|
|
||||||
|
**PASS(RDC Native / Android runtime):V18 teardown destroyed-mutex 回归已修复并通过设备闭环。** Codex 阶段仅交付源码;随后 RDC 独立完成 Android full Native build、focused C++ TestRunner、STEP-101 T1~T4、20 次真实 renderer reset/recreate、STEP-100 locality 回归与 crash buffer 检查。V19 尚未在本报告阶段公开发布。
|
||||||
|
|
||||||
|
## 2. Git 基线
|
||||||
|
|
||||||
|
- 工作区:`/Users/tei/dev/MapLibreParticleLab-step101`
|
||||||
|
- 分支:`step101-render-orchestrator-teardown`
|
||||||
|
- 起点与当前 HEAD:`d29e84c1a90daeea104b389d28b000c4f017d08d`
|
||||||
|
- 当前工作树:源码与测试文件有未提交修改;STEP-101 任务书 v1/v2、本报告和 Step 记录为既有未跟踪文件,在本轮继续审阅及更新;开发记录有未提交修改。未提交、push、tag 或发布;无 end commit。
|
||||||
|
- 子模块:`git submodule status` 列出的子模块均以 `-` 开头,表示当前工作区未初始化;本轮未修改或初始化子模块。
|
||||||
|
- V18 Android/RN `13.4.1-particle.18`、iOS `6.28.0-particle.18` 保持不变;V19 **未发布**。
|
||||||
|
|
||||||
|
## 3. 根因与实现
|
||||||
|
|
||||||
|
V18 `readinessDiagnosticsMutex` 等成员声明在 `renderSources` 之后,按 C++ 反序析构会先于 sources 消失。RenderSource/Tile 销毁存在同步 `Cancelled` callback 到 Orchestrator 的可达路径,随后可能锁已销毁 mutex。这是已证实的生命周期漏洞,与 Android FORTIFY 报错高度吻合。虽未取得精确到最终 callback 的完整符号化现场栈,但同一设备上 V18 条件稳定复现,而 STEP-101 修复后 T1~T4、20 次 reset、动态 GeoJSON 与 diagnostics 条件均不再崩溃,因此将其作为本次 Android teardown 回归的已确认工程根因。
|
||||||
|
|
||||||
|
当前源码的析构顺序为:处理 contextLost layers → 第一次 `threadPool.waitForEmpty()` → detach 各 RenderSource、GlyphManager、ImageManager observer → readiness state 仍活着时 `renderSources.clear()` → 第二次 `threadPool.waitForEmpty()` → 常规成员析构。`RenderSource::setObserver(nullptr)` 恢复 NullObserver,tile 回调无需新增空指针分支。显式 detach 与清理建立 owner teardown 合同,不依赖今后成员顺序碰巧安全;没有增加 teardown flag。
|
||||||
|
|
||||||
|
render-job 队列独立于 tagged scheduler work。静态检查显示 tile/worker 销毁可投递捕获 render data/bucket 的 render jobs;`waitForEmpty()` 不处理它们。OpenGL 同步 Android reset 路径先 `renderer.reset()`、再 `backend.reset()`,其 Context 析构调用 `runRenderJobs(true)`;若启用 async renderer cleanup,backend 的清理时点另由后续生命周期决定。Metal Context 与 Vulkan backend 也有各自的 `runRenderJobs(true)` 清理入口,均未在本轮运行验证。测试显式 `runRenderJobs()`,但没有构建或设备证据证明所有 backend 模式均安全。这是 RDC 必须核对的剩余风险。
|
||||||
|
|
||||||
|
正常 source remove 使用 `renderSources.erase()`,`clearData()` 使用 `renderSources.clear()`;两者均在 Orchestrator 实体及 readiness state 存活时执行。`TileCache::~TileCache()` 会清理 pending releases 并等待先前 deferred deletion 结束。这个审计没有证成任意外部并发调用都安全,本轮不修改正常 erase/clearData 行为,也不声称所有 source replacement 异步问题已解决。
|
||||||
|
|
||||||
|
## 4. 实际修改与审计结论
|
||||||
|
|
||||||
|
源码/测试差异共七个文件:
|
||||||
|
|
||||||
|
1. `src/mbgl/renderer/render_orchestrator.cpp`:显式 detach、source clear 与双 drain。
|
||||||
|
2. `src/mbgl/renderer/render_orchestrator.hpp`:仅为 focused test 开放 friend helper。
|
||||||
|
3. `src/mbgl/renderer/render_source.cpp`:nullptr observer 回退 NullObserver。
|
||||||
|
4. `test/renderer/render_orchestrator_error.test.cpp`:新增两个非 Jest teardown tests;helper 注册 Orchestrator observer,测试断言 source 销毁前 observer 已 detach、Cancelled 不再到旧 observer、第二次 drain 和独立 render-job 队列。
|
||||||
|
5. `platform/android/MapLibreAndroid/src/cpp/map_renderer.cpp`:`nativeReset` 与 `resetRenderer` 入口/完成日志,仅用于配对观测,未改变 reset 调用顺序。
|
||||||
|
6. `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/renderer/surfaceview/SurfaceViewMapRenderer.java`:SurfaceView detach 与 Java `nativeReset()` 进入/返回日志。
|
||||||
|
7. `platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt`:T1~T4,tile ready/diagnostics、动态 GeoJSON、Activity recreate、重建与完成日志;保存状态里的空场景不会在配置变更后重新从 Intent 启动。
|
||||||
|
|
||||||
|
本轮新增/更新文档:本报告、`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`、`粒子开发记录.md`。既有未跟踪的 STEP-101 任务书 v1/v2 没有修改。
|
||||||
|
|
||||||
|
与 v2 合同静态核对:observer、双 drain、render-job 独立审计、四个 Android 场景均有对应代码;STEP-100 locality/fast path/source loaded 语义无本次差异。T2 代码计的是 reset **请求**,20 次 Native reset 必须由 log 中每轮 Java `nativeReset`、JNI `nativeReset`、`MapRenderer::resetRenderer entered/completed` 和新 renderer ready 独立确认,不能凭 UI DONE 计数认定。T3 使用已有 T4 R-C-R fixture 并持续更新动态 GeoJSON;T4 在 reset 前额外取 diagnostics。现有 C++ test 使用最小 tile-backed Source,并非实际 Android source;设备等价验收仍不可省略。
|
||||||
|
|
||||||
|
## 5. 实际检查与未执行项目
|
||||||
|
|
||||||
|
- `git diff --check`:**PASS**(已跟踪 diff 的 whitespace/error 静态检查;未跟踪的新文档不在此命令检查范围)。
|
||||||
|
- 源码 diff/合同静态审计:完成;补了 observer detach 断言、Java/JNI reset 边界日志、T3 mutation 证据及场景恢复边界。
|
||||||
|
- CMake/Ninja/Native compile、C++ GTest:**NOT RUN**。
|
||||||
|
- Android Gradle、Validation APK、AAR/RN candidate:**NOT RUN**。
|
||||||
|
- ADB、T1~T4、crash buffer、STEP-100 locality 运行态、NavSea 冷启动:**NOT RUN**。
|
||||||
|
- Jest、iOS/Metal build 与设备、publish/tag/commit/push:**NOT RUN**。
|
||||||
|
- 产物路径、大小、SHA-256、运行日志:本轮没有新产物,均无结果。
|
||||||
|
|
||||||
|
## 6. RDC 精确验证命令与验收证据
|
||||||
|
|
||||||
|
在本工作区执行 Mac Native 定向验证(若既有 `libuv` 环境问题仍在,保留首个错误并标为环境阻塞):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --preset macos-metal
|
||||||
|
cmake --build build-macos-metal --target mbgl-test-runner --parallel 3
|
||||||
|
build-macos-metal/mbgl-test-runner --gtest_filter='RenderOrchestratorTeardown.*:RenderSegmentLocality.*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Android Validation APK 使用固定入口,不手拼 Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/particle_android_validation/build_validation_apk.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
RDC 使用显式设备 serial。每个场景先清日志、停止旧 Activity,再单独启动;`T2` 等待 20 轮结束。安装前核对 APK 文件、SHA-256、ABI 与运行设备:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
|
||||||
|
adb -s 192.168.200.139:5555 logcat -c
|
||||||
|
adb -s 192.168.200.139:5555 shell am force-stop org.maplibre.android.testapp
|
||||||
|
adb -s 192.168.200.139:5555 shell am start -n org.maplibre.android.testapp/.activity.particle.ParticleCoreLayerValidationActivity --es step101_scenario T2_RESET_LOOP_20
|
||||||
|
adb -s 192.168.200.139:5555 logcat -v threadtime -d > /tmp/step101-t2-logcat.txt
|
||||||
|
rg -n 'STEP101_|SurfaceView detached; nativeReset|nativeReset returned|MapRenderer::nativeReset|MapRenderer::resetRenderer|pthread_mutex_lock called on a destroyed mutex|Fatal signal|ANR' /tmp/step101-t2-logcat.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
对 T1/T3/T4 分别重复相同 `logcat -c`、`force-stop`、`am start`、日志保存步骤,将 `step101_scenario` 依次替换为 `T1_RESET_ONCE`、`T3_RESET_WITH_DYNAMIC_GEOJSON`、`T4_RESET_WITH_DIAGNOSTICS`。逐轮配对 `RESET_REQUEST`、`MAPVIEW_DESTROY_ENTER/RETURN`、`SurfaceView detached; nativeReset entered`、JNI `MapRenderer::nativeReset entered/completed`、Native `resetRenderer entered/completed`、新 `STYLE_READY` 与 `NEW_RENDERER_READY`;T2 需至少 20 个完整 reset/recreate 配对,且无 SIGABRT/ANR。T3 核对 `STEP101_DYNAMIC_SOURCE_MUTATION` 在每轮 reset 前持续出现,T4 核对 `PRE_RESET_DIAGNOSTICS` 与 reset 后 diagnostics。STEP-100 T4 R-C-R 另按既有 Validation 菜单复跑,核对无关 R0 `buildDelta=0`、`invalidationDelta=0`、`reuseDelta>0`,动态 R1 正常 build/invalidation。
|
||||||
|
|
||||||
|
上述只是交接命令,**本轮均未执行**。V19 candidate 必须先于 NavSea 临时锁包和五次超过 30 秒的冷启动;crash buffer 与 locality 均通过后,才可进入不可变发布。NavSea 外部工作区/候选制品命令由 RDC 按其实际环境另行确定,本轮不伪造路径或结果。
|
||||||
|
|
||||||
|
## 7. 已知问题、未完成事项与下一步
|
||||||
|
|
||||||
|
静态审计无法证明 Android 每次 Activity recreate 都实际完成 native reset,也无法证明异步 backend 清理和真实 Source 并发场景无 late callback。Native 编译与定向测试、四个 Validation 场景、STEP-100 locality、NavSea 等价五次冷启动、crash buffer 均待 RDC;任何失败均应保留现场并按首个真实错误回传。无理由在本阶段修改 NavSea、发布 V19 或重写 STEP-100 语义。
|
||||||
|
|
||||||
|
|
||||||
|
## 8. RDC 独立验收(2026-10-07)
|
||||||
|
|
||||||
|
### 8.1 Android full Native / Validation APK
|
||||||
|
|
||||||
|
标准入口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./tools/particle_android_validation/build_validation_apk.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:**PASS**。
|
||||||
|
|
||||||
|
```text
|
||||||
|
variant=OpenglDebug
|
||||||
|
abi=x86_64
|
||||||
|
apk_bytes=61957678
|
||||||
|
libmaplibre_bytes=39637512
|
||||||
|
sha256=14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721
|
||||||
|
```
|
||||||
|
|
||||||
|
该构建实际编译了本 Step 修改后的 C++ Core、`libmaplibre.so`、JNI/Java reset 日志和 Android Validation T1~T4。
|
||||||
|
|
||||||
|
### 8.2 STEP-101 Android teardown runtime
|
||||||
|
|
||||||
|
设备:FZG1-2 / Android 13 / API 33 / x86_64 / `192.168.200.139:5555`。
|
||||||
|
|
||||||
|
- T1 `RESET_ONCE`:真实 `MapRenderer::resetRenderer entered/completed`、`nativeReset entered/completed`、新 renderer `STYLE_READY / NEW_RENDERER_READY`;PASS。
|
||||||
|
- T2 `RESET_LOOP_20`:完整运行至 iteration 20,最终 `STEP101_SCENARIO_DONE ... resetsRequested=20`;逐轮出现 destroy/reset/new renderer ready;PASS。
|
||||||
|
- T3 `RESET_WITH_DYNAMIC_GEOJSON`:reset 前动态 GeoJSON mutation 持续产生,reset 完成后新 renderer 继续 ready;PASS。
|
||||||
|
- T4 `RESET_WITH_DIAGNOSTICS`:reset 前 `getRenderSegmentDiagnostics()` 可读,随后真实 reset 完成并重建;PASS。
|
||||||
|
- 四组验收 crash buffer 均为空;未出现 `pthread_mutex_lock called on a destroyed mutex`、SIGABRT 或 ANR。
|
||||||
|
|
||||||
|
### 8.3 STEP-100 locality 回归
|
||||||
|
|
||||||
|
R-C-R T4 重新运行 60+ 秒,稳定窗口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
global revision: 62 -> 894
|
||||||
|
|
||||||
|
R0 retained:
|
||||||
|
build 1 -> 1 delta 0
|
||||||
|
invalidation 19 -> 19 delta 0
|
||||||
|
reuse 12 -> 3890 delta +3878
|
||||||
|
|
||||||
|
R1 retained:
|
||||||
|
build / invalidation 持续增长
|
||||||
|
```
|
||||||
|
|
||||||
|
动态 source mutation 仍只影响对应 segment;R0 source revision 保持不变。STEP-101 teardown 修复没有破坏 STEP-100 Source→Segment locality。
|
||||||
|
|
||||||
|
### 8.4 Focused Android C++ TestRunner
|
||||||
|
|
||||||
|
Mac host `mbgl-test-runner` 仍因本机缺失可用 `libuv` pkg-config 包而 ENV BLOCKED;没有安装或升级系统依赖来掩盖该环境事实。
|
||||||
|
|
||||||
|
改用仓库已有 Android Native C++ TestRunner,x86_64 build **PASS**。RDC 为 TestRunner 临时打包真实 `test/fixtures/` 到 `data.zip`,仅满足 Android runner 既有解压契约;该临时 asset 已删除,不进入 Git。
|
||||||
|
|
||||||
|
设备过滤执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderSegmentLocality.SourceRevisionsAreScopedAndRecreationUsesNewIdentity PASS
|
||||||
|
RenderSegmentLocality.ReadinessAndFadeFollowOnlyDependencies PASS
|
||||||
|
RenderSegmentLocality.MixedExecutionDrawsEachLiveRangeOnceInStyleOrder PASS
|
||||||
|
RenderSegmentLocality.FailedBuildCannotReuseOldTexture PASS
|
||||||
|
RenderOrchestratorTeardown.NullObserverAndDetachCoverEveryTileCallback PASS
|
||||||
|
RenderOrchestratorTeardown.PendingTileCancellationAndSecondDrainRepeatSafely PASS
|
||||||
|
TestRunner finished with status: '0'
|
||||||
|
```
|
||||||
|
|
||||||
|
最终 TestRunner APK(含临时 fixtures,仅作验收证据)SHA-256:
|
||||||
|
|
||||||
|
```text
|
||||||
|
e1b39b3eb035175b79dd60dbe39496ea982c2dd6e2f1593fd60dd9630f393712
|
||||||
|
```
|
||||||
|
|
||||||
|
crash buffer 为空。
|
||||||
|
|
||||||
|
首次 TestRunner 编译暴露 6 个测试源码级 C++20 designated-initializer / protected-base conversion 问题;RDC 仅修正测试源码字段补齐和显式转换,未改变生产语义。修复后 Android C++ TestRunner build 与上述 filtered tests 均 PASS。
|
||||||
|
|
||||||
|
### 8.5 结论
|
||||||
|
|
||||||
|
STEP-101 的 Native 生命周期修复达到关闭条件:
|
||||||
|
|
||||||
|
1. RenderSource observer 在 owner teardown 前显式 detach;
|
||||||
|
2. readiness state 仍存活时显式销毁 renderSources;
|
||||||
|
3. source clear 前后 tagged work 双 drain;
|
||||||
|
4. glyph/image observer 同步 detach;
|
||||||
|
5. 20 次真实 Android renderer reset/recreate 无 destroyed-mutex;
|
||||||
|
6. 动态 GeoJSON、diagnostics 条件下仍安全;
|
||||||
|
7. STEP-100 locality 保持 PASS;
|
||||||
|
8. focused C++ tests PASS。
|
||||||
|
|
||||||
|
Jest:**NOT RUN**。
|
||||||
|
|
||||||
|
V19 candidate、NavSea 五次冷启动与正式 publish/finalize 属于后续发行 Gate,不改变本 Step Native 修复的 PASS 结论。
|
||||||
@@ -195,6 +195,7 @@ void MapRenderer::requestSnapshot(SnapshotCallback callback) {
|
|||||||
// Called on OpenGL thread //
|
// Called on OpenGL thread //
|
||||||
|
|
||||||
void MapRenderer::resetRenderer() {
|
void MapRenderer::resetRenderer() {
|
||||||
|
Log::Info(Event::Android, "MapRenderer::resetRenderer entered");
|
||||||
renderer.reset();
|
renderer.reset();
|
||||||
|
|
||||||
if (!asyncRendererCleanup) {
|
if (!asyncRendererCleanup) {
|
||||||
@@ -203,6 +204,7 @@ void MapRenderer::resetRenderer() {
|
|||||||
|
|
||||||
window.reset();
|
window.reset();
|
||||||
swapBehaviorFlush = false;
|
swapBehaviorFlush = false;
|
||||||
|
Log::Info(Event::Android, "MapRenderer::resetRenderer completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapRenderer::scheduleSnapshot(std::unique_ptr<SnapshotCallback> callback) {
|
void MapRenderer::scheduleSnapshot(std::unique_ptr<SnapshotCallback> callback) {
|
||||||
@@ -322,9 +324,11 @@ void MapRenderer::onSurfaceChanged([[maybe_unused]] JNIEnv& env, jint width, jin
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MapRenderer::onRendererReset(JNIEnv&) {
|
void MapRenderer::onRendererReset(JNIEnv&) {
|
||||||
|
Log::Info(Event::Android, "MapRenderer::nativeReset entered");
|
||||||
// Make sure to destroy the renderer on the GL Thread
|
// Make sure to destroy the renderer on the GL Thread
|
||||||
auto self = ActorRef<MapRenderer>(*this, mailboxData.getMailbox());
|
auto self = ActorRef<MapRenderer>(*this, mailboxData.getMailbox());
|
||||||
self.ask(&MapRenderer::resetRenderer).wait();
|
self.ask(&MapRenderer::resetRenderer).wait();
|
||||||
|
Log::Info(Event::Android, "MapRenderer::nativeReset completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// needs to be called on GL thread
|
// needs to be called on GL thread
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.maplibre.android.maps.renderer.surfaceview;
|
package org.maplibre.android.maps.renderer.surfaceview;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -28,11 +29,13 @@ public class SurfaceViewMapRenderer extends MapRenderer {
|
|||||||
surfaceView.setDetachedListener(new MapLibreSurfaceView.OnSurfaceViewDetachedListener() {
|
surfaceView.setDetachedListener(new MapLibreSurfaceView.OnSurfaceViewDetachedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceViewDetached() {
|
public void onSurfaceViewDetached() {
|
||||||
|
Log.i("Mbgl-SurfaceRenderer", "SurfaceView detached; nativeReset entered");
|
||||||
// because the GL thread is destroyed when the view is detached from window,
|
// because the GL thread is destroyed when the view is detached from window,
|
||||||
// we need to ensure releasing the native renderer as well.
|
// we need to ensure releasing the native renderer as well.
|
||||||
// This avoids releasing it only when the view is being recreated, which is already on a new GL thread,
|
// This avoids releasing it only when the view is being recreated, which is already on a new GL thread,
|
||||||
// and leads to JNI crashes like https://github.com/mapbox/mapbox-gl-native/issues/14618
|
// and leads to JNI crashes like https://github.com/mapbox/mapbox-gl-native/issues/14618
|
||||||
nativeReset();
|
nativeReset();
|
||||||
|
Log.i("Mbgl-SurfaceRenderer", "nativeReset returned");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,79 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
private var step093Mode = false
|
private var step093Mode = false
|
||||||
private var step097Topology: Step097StencilFixture.Topology? = null
|
private var step097Topology: Step097StencilFixture.Topology? = null
|
||||||
private var step099Scenario: Step099ReadinessFixture.Scenario? = null
|
private var step099Scenario: Step099ReadinessFixture.Scenario? = null
|
||||||
|
private var step101Scenario: String? = null
|
||||||
|
private var step101Completed = 0
|
||||||
|
private var step101DynamicTick = 0L
|
||||||
|
private var step101ReadySinceMs = 0L
|
||||||
|
private var step101LoadSinceMs = 0L
|
||||||
|
private val step101Handler = Handler(Looper.getMainLooper())
|
||||||
|
private val step101Poll = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val scenario = step101Scenario ?: return
|
||||||
|
if (!this@ParticleCoreLayerValidationActivity::maplibreMap.isInitialized || !styleReady) {
|
||||||
|
if (SystemClock.elapsedRealtime() - step101LoadSinceMs > 20000L) {
|
||||||
|
Log.e(TAG, "STEP101_SCENARIO_BLOCKED scenario=$scenario completed=$step101Completed reason=style-not-ready")
|
||||||
|
status.text = "STEP-101 BLOCKED: style not ready"
|
||||||
|
step101Scenario = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step101Handler.postDelayed(this, 250L)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val snapshot = JSONObject(maplibreMap.getRenderSegmentDiagnostics())
|
||||||
|
val sources = snapshot.optJSONArray("sources")
|
||||||
|
var tileReady = false
|
||||||
|
for (index in 0 until (sources?.length() ?: 0)) {
|
||||||
|
val source = sources?.optJSONObject(index) ?: continue
|
||||||
|
if (source.optString("sourceId") == Step099ReadinessFixture.STATIC_SOURCE &&
|
||||||
|
source.optBoolean("isLoaded") && source.optInt("tileCount") > 0) tileReady = true
|
||||||
|
}
|
||||||
|
if (!tileReady) {
|
||||||
|
if (SystemClock.elapsedRealtime() - step101LoadSinceMs > 20000L) {
|
||||||
|
Log.e(TAG, "STEP101_SCENARIO_BLOCKED scenario=$scenario completed=$step101Completed reason=tile-not-ready")
|
||||||
|
status.text = "STEP-101 BLOCKED: tile not ready"
|
||||||
|
step101Scenario = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step101Handler.postDelayed(this, 250L)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (step101ReadySinceMs == 0L) {
|
||||||
|
step101ReadySinceMs = SystemClock.elapsedRealtime()
|
||||||
|
Log.i(TAG, "STEP101_SOURCE_TILE_READY scenario=$scenario completed=$step101Completed " +
|
||||||
|
"sources=${sources?.length()} diagnostics=${snapshot.optLong("contentRevision")}")
|
||||||
|
}
|
||||||
|
if (scenario == "T3_RESET_WITH_DYNAMIC_GEOJSON") {
|
||||||
|
step101DynamicTick += 1
|
||||||
|
updateStep099GeoJson(Step099ReadinessFixture.DYNAMIC_SOURCE, step101DynamicTick)
|
||||||
|
Log.i(TAG, "STEP101_DYNAMIC_SOURCE_MUTATION scenario=$scenario completed=$step101Completed " +
|
||||||
|
"tick=$step101DynamicTick")
|
||||||
|
}
|
||||||
|
if (SystemClock.elapsedRealtime() - step101ReadySinceMs < 2500L) {
|
||||||
|
step101Handler.postDelayed(this, 250L)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(TAG, "STEP101_NEW_RENDERER_READY scenario=$scenario completed=$step101Completed " +
|
||||||
|
"styleReady=$styleReady diagnostics=${snapshot.optLong("contentRevision")}")
|
||||||
|
val target = if (scenario == "T2_RESET_LOOP_20") 20 else 1
|
||||||
|
if (step101Completed >= target) {
|
||||||
|
Log.i(TAG, "STEP101_SCENARIO_DONE scenario=$scenario resetsRequested=$step101Completed " +
|
||||||
|
"nativeResetPairsMustMatch=$step101Completed")
|
||||||
|
status.text = "STEP-101 $scenario DONE ${step101Completed}/$target"
|
||||||
|
step101Scenario = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (scenario == "T4_RESET_WITH_DIAGNOSTICS") {
|
||||||
|
Log.i(TAG, "STEP101_PRE_RESET_DIAGNOSTICS ${maplibreMap.getRenderSegmentDiagnostics()}")
|
||||||
|
}
|
||||||
|
val next = step101Completed + 1
|
||||||
|
Log.i(TAG, "STEP101_RESET_REQUEST scenario=$scenario iteration=$next oldMapViewActive=true " +
|
||||||
|
"tileReady=$tileReady")
|
||||||
|
step101Completed = next
|
||||||
|
step101Handler.removeCallbacks(this)
|
||||||
|
recreate() // Activity destroys the old MapView and constructs a new native renderer.
|
||||||
|
}
|
||||||
|
}
|
||||||
private var step099Tick = 0L
|
private var step099Tick = 0L
|
||||||
private var step099LastOverallLoaded: Boolean? = null
|
private var step099LastOverallLoaded: Boolean? = null
|
||||||
private var step099FalseSinceMs: Long? = null
|
private var step099FalseSinceMs: Long? = null
|
||||||
@@ -189,6 +262,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
step093Mode = savedInstanceState?.getBoolean(STATE_STEP093_MODE, false) ?: false
|
step093Mode = savedInstanceState?.getBoolean(STATE_STEP093_MODE, false) ?: false
|
||||||
step097Topology = savedInstanceState?.getString(STATE_STEP097_TOPOLOGY)
|
step097Topology = savedInstanceState?.getString(STATE_STEP097_TOPOLOGY)
|
||||||
?.let { value -> Step097StencilFixture.Topology.values().firstOrNull { it.name == value } }
|
?.let { value -> Step097StencilFixture.Topology.values().firstOrNull { it.name == value } }
|
||||||
|
step101Scenario = if (savedInstanceState?.containsKey(STATE_STEP101_SCENARIO) == true) {
|
||||||
|
savedInstanceState.getString(STATE_STEP101_SCENARIO)
|
||||||
|
} else {
|
||||||
|
intent.getStringExtra("step101_scenario")?.takeIf { it in STEP101_SCENARIOS }
|
||||||
|
}
|
||||||
|
step101Completed = savedInstanceState?.getInt(STATE_STEP101_COMPLETED, 0) ?: 0
|
||||||
step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY)
|
step093Topology = savedInstanceState?.getString(STATE_STEP093_TOPOLOGY)
|
||||||
?.let { value -> Step093Topology.values().firstOrNull { it.name == value } }
|
?.let { value -> Step093Topology.values().firstOrNull { it.name == value } }
|
||||||
?: Step093Topology.BOTTOM_CONTINUOUS_TOP
|
?: Step093Topology.BOTTOM_CONTINUOUS_TOP
|
||||||
@@ -227,7 +306,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
updateNavSeaPbfButtonState()
|
updateNavSeaPbfButtonState()
|
||||||
updatePbfModeButtonState()
|
updatePbfModeButtonState()
|
||||||
updateRetainedCacheButtonState()
|
updateRetainedCacheButtonState()
|
||||||
if (step097Topology != null) loadStep097StencilStyle()
|
if (step101Scenario != null) loadStep101Style()
|
||||||
|
else if (step097Topology != null) loadStep097StencilStyle()
|
||||||
else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
|
else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
|
||||||
else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
|
else loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
|
||||||
}
|
}
|
||||||
@@ -532,6 +612,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
menu.add(0, 15, 15, "RESET DEFAULT A")
|
menu.add(0, 15, 15, "RESET DEFAULT A")
|
||||||
menu.add(0, 16, 16, "STEP-097 SAME SOURCE STENCIL")
|
menu.add(0, 16, 16, "STEP-097 SAME SOURCE STENCIL")
|
||||||
menu.add(0, 17, 17, "STEP-099 READINESS")
|
menu.add(0, 17, 17, "STEP-099 READINESS")
|
||||||
|
menu.add(0, 18, 18, "STEP-101 TEARDOWN")
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
|
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
|
||||||
@@ -543,6 +624,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
|
15 -> applyStep093Scene(Step093Topology.BOTTOM_CONTINUOUS_TOP)
|
||||||
16 -> showStep097StencilMenu(anchor)
|
16 -> showStep097StencilMenu(anchor)
|
||||||
17 -> showStep099Menu(anchor)
|
17 -> showStep099Menu(anchor)
|
||||||
|
18 -> showStep101Menu(anchor)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -589,6 +671,45 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showStep101Menu(anchor: android.view.View) {
|
||||||
|
PopupMenu(this, anchor).apply {
|
||||||
|
STEP101_SCENARIOS.forEachIndexed { index, scenario -> menu.add(0, index, index, scenario) }
|
||||||
|
setOnMenuItemClickListener { item ->
|
||||||
|
val scenario = STEP101_SCENARIOS.getOrNull(item.itemId) ?: return@setOnMenuItemClickListener false
|
||||||
|
step101Scenario = scenario
|
||||||
|
step101Completed = 0
|
||||||
|
loadStep101Style()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadStep101Style() {
|
||||||
|
val scenario = step101Scenario ?: return
|
||||||
|
stopStep099Scenario()
|
||||||
|
step101Handler.removeCallbacksAndMessages(null)
|
||||||
|
step101ReadySinceMs = 0L
|
||||||
|
step101LoadSinceMs = SystemClock.elapsedRealtime()
|
||||||
|
val fixture = Step099ReadinessFixture.Scenario.T4_RCR
|
||||||
|
val accepted = maplibreMap.setRenderSegmentPlan(Step099ReadinessFixture.planJson(fixture))
|
||||||
|
styleReady = false
|
||||||
|
val epoch = ++styleLoadEpoch
|
||||||
|
Log.i(TAG, "STEP101_OLD_RESET_COMPLETED_OR_INITIAL scenario=$scenario completed=$step101Completed " +
|
||||||
|
"planAccepted=$accepted")
|
||||||
|
maplibreMap.setStyle(Style.Builder().fromJson(Step099ReadinessFixture.styleJson(fixture))) {
|
||||||
|
if (step101Scenario != scenario || epoch != styleLoadEpoch) return@setStyle
|
||||||
|
styleReady = true
|
||||||
|
maplibreMap.moveCamera(CameraUpdateFactory.newCameraPosition(
|
||||||
|
CameraPosition.Builder().target(LatLng(28.0, 135.0)).zoom(3.0).build()
|
||||||
|
))
|
||||||
|
Log.i(TAG, "STEP101_STYLE_READY scenario=$scenario completed=$step101Completed")
|
||||||
|
step101Handler.removeCallbacks(step101Poll)
|
||||||
|
step101Handler.post(step101Poll)
|
||||||
|
}
|
||||||
|
step101Handler.post(step101Poll)
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) {
|
private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) {
|
||||||
stopStep093DiagnosticsPolling()
|
stopStep093DiagnosticsPolling()
|
||||||
step093Mode = false
|
step093Mode = false
|
||||||
@@ -2135,6 +2256,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
mapView.onStart()
|
mapView.onStart()
|
||||||
|
if (step101Scenario != null && styleReady) step101Handler.post(step101Poll)
|
||||||
if (step099Scenario != null) {
|
if (step099Scenario != null) {
|
||||||
step099Running = true
|
step099Running = true
|
||||||
step099LastOverallLoaded = null
|
step099LastOverallLoaded = null
|
||||||
@@ -2149,6 +2271,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
override fun onResume() { super.onResume(); mapView.onResume() }
|
override fun onResume() { super.onResume(); mapView.onResume() }
|
||||||
override fun onPause() { mapView.onPause(); super.onPause() }
|
override fun onPause() { mapView.onPause(); super.onPause() }
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
step101Handler.removeCallbacksAndMessages(null)
|
||||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||||
step099Running = false
|
step099Running = false
|
||||||
step099Handler.removeCallbacksAndMessages(null)
|
step099Handler.removeCallbacksAndMessages(null)
|
||||||
@@ -2156,11 +2279,14 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
step101Handler.removeCallbacksAndMessages(null)
|
||||||
|
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed")
|
||||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||||
stopStep099Scenario()
|
stopStep099Scenario()
|
||||||
weatherRuntime?.dispose()
|
weatherRuntime?.dispose()
|
||||||
weatherRuntime = null
|
weatherRuntime = null
|
||||||
mapView.onDestroy()
|
mapView.onDestroy()
|
||||||
|
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_RETURN scenario=$step101Scenario completed=$step101Completed")
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
|
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
|
||||||
@@ -2189,6 +2315,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
|
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
|
||||||
outState.putBoolean(STATE_STEP093_MODE, step093Mode)
|
outState.putBoolean(STATE_STEP093_MODE, step093Mode)
|
||||||
outState.putString(STATE_STEP097_TOPOLOGY, step097Topology?.name)
|
outState.putString(STATE_STEP097_TOPOLOGY, step097Topology?.name)
|
||||||
|
outState.putString(STATE_STEP101_SCENARIO, step101Scenario)
|
||||||
|
outState.putInt(STATE_STEP101_COMPLETED, step101Completed)
|
||||||
outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name)
|
outState.putString(STATE_STEP093_TOPOLOGY, step093Topology.name)
|
||||||
outState.putString(STATE_STEP093_VARIANT, step093Variant.name)
|
outState.putString(STATE_STEP093_VARIANT, step093Variant.name)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@@ -2226,6 +2354,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
|||||||
private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled"
|
private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled"
|
||||||
private const val STATE_STEP093_MODE = "particle_validation.step093_mode"
|
private const val STATE_STEP093_MODE = "particle_validation.step093_mode"
|
||||||
private const val STATE_STEP097_TOPOLOGY = "particle_validation.step097_topology"
|
private const val STATE_STEP097_TOPOLOGY = "particle_validation.step097_topology"
|
||||||
|
private const val STATE_STEP101_SCENARIO = "particle_validation.step101_scenario"
|
||||||
|
private const val STATE_STEP101_COMPLETED = "particle_validation.step101_completed"
|
||||||
|
private val STEP101_SCENARIOS = listOf(
|
||||||
|
"T1_RESET_ONCE", "T2_RESET_LOOP_20", "T3_RESET_WITH_DYNAMIC_GEOJSON", "T4_RESET_WITH_DIAGNOSTICS"
|
||||||
|
)
|
||||||
private const val STATE_STEP093_TOPOLOGY = "particle_validation.step093_topology"
|
private const val STATE_STEP093_TOPOLOGY = "particle_validation.step093_topology"
|
||||||
private const val STATE_STEP093_VARIANT = "particle_validation.step093_variant"
|
private const val STATE_STEP093_VARIANT = "particle_validation.step093_variant"
|
||||||
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
private const val PARTICLE_LAYER_ID = "particle-raster-core"
|
||||||
|
|||||||
@@ -191,9 +191,21 @@ RenderOrchestrator::~RenderOrchestrator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for any deferred cleanup tasks to complete before releasing and potentially
|
// Finish work that can still reach the source observers before detaching them.
|
||||||
// destroying the scheduler. Those cleanup tasks must not hold the final reference
|
threadPool.waitForEmpty();
|
||||||
// to the scheduler because it cannot be destroyed from one of its own pool threads.
|
|
||||||
|
for (auto& entry : renderSources) {
|
||||||
|
entry.second->setObserver(nullptr);
|
||||||
|
}
|
||||||
|
glyphManager->setObserver(nullptr);
|
||||||
|
imageManager->setObserver(nullptr);
|
||||||
|
|
||||||
|
// Tile destructors can synchronously report Cancelled and enqueue further work.
|
||||||
|
// Destroy sources while the readiness state and its mutex are still alive.
|
||||||
|
renderSources.clear();
|
||||||
|
|
||||||
|
// Drain work submitted by source/tile destruction before the scheduler and
|
||||||
|
// the remaining orchestrator state are torn down.
|
||||||
threadPool.waitForEmpty();
|
threadPool.waitForEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,9 @@ public:
|
|||||||
const ZoomHistory& getZoomHistory() const { return zoomHistory; }
|
const ZoomHistory& getZoomHistory() const { return zoomHistory; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Allows the focused teardown test to install a pending tile-backed source.
|
||||||
|
friend struct RenderOrchestratorTeardownTestAccess;
|
||||||
|
|
||||||
bool isLoaded() const;
|
bool isLoaded() const;
|
||||||
bool hasTransitions(TimePoint) const;
|
bool hasTransitions(TimePoint) const;
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ RenderSource::RenderSource(Immutable<style::Source::Impl> impl)
|
|||||||
RenderSource::~RenderSource() = default;
|
RenderSource::~RenderSource() = default;
|
||||||
|
|
||||||
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
void RenderSource::setObserver(RenderSourceObserver* observer_) {
|
||||||
observer = observer_;
|
observer = observer_ ? observer_ : &nullObserver;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderSource::onTileChanged(Tile& tile) {
|
void RenderSource::onTileChanged(Tile& tile) {
|
||||||
|
|||||||
@@ -4,13 +4,29 @@
|
|||||||
#include <mbgl/renderer/render_orchestrator.hpp>
|
#include <mbgl/renderer/render_orchestrator.hpp>
|
||||||
#include <mbgl/renderer/renderer_observer.hpp>
|
#include <mbgl/renderer/renderer_observer.hpp>
|
||||||
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
#include <mbgl/renderer/retained_underlay_cache.hpp>
|
||||||
|
#include <mbgl/renderer/render_source.hpp>
|
||||||
|
#include <mbgl/renderer/render_source_observer.hpp>
|
||||||
|
#include <mbgl/renderer/tile_render_data.hpp>
|
||||||
|
#include <mbgl/style/sources/geojson_source_impl.hpp>
|
||||||
|
#include <mbgl/tile/tile.hpp>
|
||||||
|
#include <mbgl/tile/tile_observer.hpp>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace mbgl;
|
using namespace mbgl;
|
||||||
|
|
||||||
|
namespace mbgl {
|
||||||
|
struct RenderOrchestratorTeardownTestAccess {
|
||||||
|
static void add(RenderOrchestrator& orchestrator, std::unique_ptr<RenderSource> source) {
|
||||||
|
source->setObserver(&orchestrator);
|
||||||
|
orchestrator.renderSources.emplace(source->baseImpl->id, std::move(source));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace mbgl
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class TestUniformBuffers final : public gfx::UniformBufferArray {
|
class TestUniformBuffers final : public gfx::UniformBufferArray {
|
||||||
@@ -108,3 +124,128 @@ TEST(RenderOrchestratorError, FailedRangeCannotReuseAndCleanRetryCanRecover) {
|
|||||||
cache.markBuildSucceeded();
|
cache.markBuildSucceeded();
|
||||||
EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Reuse);
|
EXPECT_EQ(cache.evaluate(key, 1, true), RetainedUnderlayDecision::Reuse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class TeardownTile final : public Tile {
|
||||||
|
public:
|
||||||
|
TeardownTile(TileObserver* observer_)
|
||||||
|
: Tile(Kind::Geometry, OverscaledTileID{0, 0, {0, 0, 0}}, "step101-source", observer_) {}
|
||||||
|
|
||||||
|
~TeardownTile() override { observer->onTileAction(id, sourceID, TileOperation::Cancelled); }
|
||||||
|
std::unique_ptr<TileRenderData> createRenderData() override { return nullptr; }
|
||||||
|
void cancel() override {}
|
||||||
|
bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) override { return true; }
|
||||||
|
void emitChanged() { observer->onTileChanged(*this); }
|
||||||
|
void emitError() { observer->onTileError(*this, std::make_exception_ptr(std::runtime_error("tile error"))); }
|
||||||
|
void emitAction() { observer->onTileAction(id, sourceID, TileOperation::Cancelled); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class TeardownSource final : public RenderSource {
|
||||||
|
public:
|
||||||
|
TeardownSource(const TaggedScheduler& scheduler_, std::atomic<int>& deferred_, std::atomic<int>& renderJobs_,
|
||||||
|
std::atomic<int>& detached_)
|
||||||
|
: RenderSource(makeMutable<style::GeoJSONSource::Impl>(
|
||||||
|
"step101-source", style::GeoJSONOptions::defaultOptions())),
|
||||||
|
scheduler(scheduler_), deferred(deferred_), renderJobs(renderJobs_), detached(detached_),
|
||||||
|
tile(std::make_unique<TeardownTile>(static_cast<TileObserver*>(this))) {}
|
||||||
|
|
||||||
|
~TeardownSource() override {
|
||||||
|
if (observer != oldObserver) ++detached;
|
||||||
|
tile.reset(); // A pending tile emits Cancelled synchronously during source destruction.
|
||||||
|
scheduler.schedule([&count = deferred] { ++count; });
|
||||||
|
scheduler.runOnRenderThread([&count = renderJobs] { ++count; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectDetachFrom(RenderSourceObserver* observer_) { oldObserver = observer_; }
|
||||||
|
|
||||||
|
void emitAll() {
|
||||||
|
tile->emitChanged();
|
||||||
|
tile->emitError();
|
||||||
|
tile->emitAction();
|
||||||
|
}
|
||||||
|
bool isLoaded() const override { return false; }
|
||||||
|
void update(Immutable<style::Source::Impl>, const std::vector<Immutable<style::LayerProperties>>&,
|
||||||
|
bool, bool, const TileParameters&) override {}
|
||||||
|
std::unique_ptr<RenderItem> createRenderItem() override { return nullptr; }
|
||||||
|
void prepare(const SourcePrepareParameters&) override {}
|
||||||
|
void updateFadingTiles() override {}
|
||||||
|
bool hasFadingTiles() const override { return false; }
|
||||||
|
RenderSourceReadinessStats getReadinessDiagnostics() const override {
|
||||||
|
return {.tileBacked = true,
|
||||||
|
.tileCount = std::nullopt,
|
||||||
|
.incompleteTileCount = std::nullopt,
|
||||||
|
.loadedTileCount = std::nullopt,
|
||||||
|
.hasFadingTiles = false};
|
||||||
|
}
|
||||||
|
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
|
||||||
|
const ScreenLineString&, const TransformState&,
|
||||||
|
const std::unordered_map<std::string, const RenderLayer*>&,
|
||||||
|
const RenderedQueryOptions&, const mat4&) const override { return {}; }
|
||||||
|
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
|
||||||
|
void reduceMemoryUse() override {}
|
||||||
|
void dumpDebugLogs() const override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TaggedScheduler scheduler;
|
||||||
|
std::atomic<int>& deferred;
|
||||||
|
std::atomic<int>& renderJobs;
|
||||||
|
std::atomic<int>& detached;
|
||||||
|
RenderSourceObserver* oldObserver = nullptr;
|
||||||
|
std::unique_ptr<TeardownTile> tile;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CountingSourceObserver final : public RenderSourceObserver {
|
||||||
|
public:
|
||||||
|
void onTileChanged(RenderSource&, const OverscaledTileID&) override { ++changed; }
|
||||||
|
void onTileError(RenderSource&, const OverscaledTileID&, std::exception_ptr) override { ++errors; }
|
||||||
|
void onTileAction(RenderSource&, TileOperation, const OverscaledTileID&, const std::string&) override { ++actions; }
|
||||||
|
int changed = 0;
|
||||||
|
int errors = 0;
|
||||||
|
int actions = 0;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(RenderOrchestratorTeardown, NullObserverAndDetachCoverEveryTileCallback) {
|
||||||
|
TaggedScheduler scheduler{Scheduler::GetBackground(), {}};
|
||||||
|
std::atomic<int> deferred{0};
|
||||||
|
std::atomic<int> renderJobs{0};
|
||||||
|
std::atomic<int> detached{0};
|
||||||
|
auto source = std::make_unique<TeardownSource>(scheduler, deferred, renderJobs, detached);
|
||||||
|
source->emitAll(); // Constructor default NullObserver.
|
||||||
|
|
||||||
|
CountingSourceObserver observer;
|
||||||
|
source->setObserver(&observer);
|
||||||
|
source->expectDetachFrom(&observer);
|
||||||
|
source->emitAll();
|
||||||
|
EXPECT_EQ(observer.changed, 1);
|
||||||
|
EXPECT_EQ(observer.errors, 1);
|
||||||
|
EXPECT_EQ(observer.actions, 1);
|
||||||
|
|
||||||
|
source->setObserver(nullptr);
|
||||||
|
source->emitAll();
|
||||||
|
source.reset(); // Cancelled must not reach the detached observer.
|
||||||
|
EXPECT_EQ(detached.load(), 1);
|
||||||
|
EXPECT_EQ(observer.changed, 1);
|
||||||
|
EXPECT_EQ(observer.errors, 1);
|
||||||
|
EXPECT_EQ(observer.actions, 1);
|
||||||
|
scheduler.waitForEmpty();
|
||||||
|
scheduler.runRenderJobs();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RenderOrchestratorTeardown, PendingTileCancellationAndSecondDrainRepeatSafely) {
|
||||||
|
TaggedScheduler scheduler{Scheduler::GetBackground(), {}};
|
||||||
|
std::atomic<int> deferred{0};
|
||||||
|
std::atomic<int> renderJobs{0};
|
||||||
|
std::atomic<int> detached{0};
|
||||||
|
for (int iteration = 0; iteration < 20; ++iteration) {
|
||||||
|
auto orchestrator = std::make_unique<RenderOrchestrator>(false, scheduler, std::nullopt);
|
||||||
|
auto source = std::make_unique<TeardownSource>(scheduler, deferred, renderJobs, detached);
|
||||||
|
source->expectDetachFrom(orchestrator.get());
|
||||||
|
RenderOrchestratorTeardownTestAccess::add(*orchestrator, std::move(source));
|
||||||
|
orchestrator.reset();
|
||||||
|
EXPECT_EQ(detached.load(), iteration + 1); // Source was detached before its tile cancelled.
|
||||||
|
EXPECT_EQ(deferred.load(), iteration + 1); // Work queued by source destruction was drained.
|
||||||
|
scheduler.runRenderJobs(); // Render jobs have a separate backend-owned queue.
|
||||||
|
EXPECT_EQ(renderJobs.load(), iteration + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -387,6 +387,8 @@ TEST(RetainedUnderlayRevision, EquivalentIntraFrameRemoveAddDoesNotAdvanceTopolo
|
|||||||
const RetainedUnderlayTopologySnapshot initial{
|
const RetainedUnderlayTopologySnapshot initial{
|
||||||
.boundary = 32,
|
.boundary = 32,
|
||||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||||
|
.continuousEndExclusive = std::nullopt,
|
||||||
|
.staticSuffix = {},
|
||||||
};
|
};
|
||||||
const auto settledRevision = tracker.observe(initial);
|
const auto settledRevision = tracker.observe(initial);
|
||||||
|
|
||||||
@@ -404,6 +406,8 @@ TEST(RetainedUnderlayRevision, PersistentTopologyChangeAdvancesOnce) {
|
|||||||
const RetainedUnderlayTopologySnapshot initial{
|
const RetainedUnderlayTopologySnapshot initial{
|
||||||
.boundary = 32,
|
.boundary = 32,
|
||||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||||
|
.continuousEndExclusive = std::nullopt,
|
||||||
|
.staticSuffix = {},
|
||||||
};
|
};
|
||||||
const auto initialRevision = tracker.observe(initial);
|
const auto initialRevision = tracker.observe(initial);
|
||||||
|
|
||||||
@@ -419,6 +423,8 @@ TEST(RetainedUnderlayRevision, OrderingAndIndexChangesAdvanceTopology) {
|
|||||||
RetainedUnderlayTopologySnapshot topology{
|
RetainedUnderlayTopologySnapshot topology{
|
||||||
.boundary = 32,
|
.boundary = 32,
|
||||||
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
.staticPrefix = {{3, "sea-area-fill", 1}, {31, "clip-outline", 1}},
|
||||||
|
.continuousEndExclusive = std::nullopt,
|
||||||
|
.staticSuffix = {},
|
||||||
};
|
};
|
||||||
const auto initialRevision = tracker.observe(topology);
|
const auto initialRevision = tracker.observe(topology);
|
||||||
|
|
||||||
@@ -992,7 +998,9 @@ TEST(RenderSegmentDiagnostics, ReadinessSnapshotMapsSourcesToSegmentsWithoutSide
|
|||||||
.contentRevision = 42,
|
.contentRevision = 42,
|
||||||
.eventType = "TileMutation",
|
.eventType = "TileMutation",
|
||||||
.reason = "tile-mutation",
|
.reason = "tile-mutation",
|
||||||
.sourceId = "dynamic"};
|
.sourceId = "dynamic",
|
||||||
|
.affectedSegmentIndices = {},
|
||||||
|
.dependencyScope = "none"};
|
||||||
state.recordRendererReadiness(readiness);
|
state.recordRendererReadiness(readiness);
|
||||||
|
|
||||||
const auto first = state.renderSegmentSnapshot();
|
const auto first = state.renderSegmentSnapshot();
|
||||||
|
|||||||
24
粒子开发记录.md
24
粒子开发记录.md
@@ -1042,3 +1042,27 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
|||||||
- T4 运行期间 R1 fallback count 增长而 R0 fallback 不变、reuse 持续增长,实际验证 mixed `R0 Reuse + C Live + R1 Fallback`。未发现 crash/ANR。Fast Path、`singleContinuousSegment`、multi-continuous 语义未改。
|
- T4 运行期间 R1 fallback count 增长而 R0 fallback 不变、reuse 持续增长,实际验证 mixed `R0 Reuse + C Live + R1 Fallback`。未发现 crash/ANR。Fast Path、`singleContinuousSegment`、multi-continuous 语义未改。
|
||||||
- Native host GTest 因当前 Intel Mac 缺 `libuv` 环境阻塞;尝试 Homebrew 安装时发现会连带升级 CMake/OpenSSL/Python 且 Intel 无 bottle 支持,已停止,未把环境阻塞虚报 PASS。Android full native build + device runtime 作为本 Step 功能验收事实。Jest 未运行。
|
- Native host GTest 因当前 Intel Mac 缺 `libuv` 环境阻塞;尝试 Homebrew 安装时发现会连带升级 CMake/OpenSSL/Python 且 Intel 无 bottle 支持,已停止,未把环境阻塞虚报 PASS。Android full native build + device runtime 作为本 Step 功能验收事实。Jest 未运行。
|
||||||
- Step:`docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md`;完成报告:`docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md`;运行证据在忽略目录 `artifacts/maplibre-particle/android-validation/`。允许进入 V18 unified release。
|
- Step:`docs/50_Step记录/STEP-100_SourceScoped_RenderSegment局部失效_v1.md`;完成报告:`docs/报告/STEP-100_SourceScoped_RenderSegment局部失效_Codex完成报告_v1.md`;运行证据在忽略目录 `artifacts/maplibre-particle/android-validation/`。允许进入 V18 unified release。
|
||||||
|
|
||||||
|
### 2026-10-06 STEP-101 RenderOrchestrator 销毁生命周期修复(源码交付,待 RDC 验收)
|
||||||
|
|
||||||
|
- 基于 V18 Final HEAD `d29e84c1a90daeea104b389d28b000c4f017d08d`,按 STEP-101 任务 v2 审计现有差异。已证实 readiness mutex/state 早于 RenderSource 自动析构且 tile/source 销毁可同步回调的生命周期漏洞;与 Android FORTIFY 现场高度吻合,但仍待等价实机闭环确认。
|
||||||
|
- 现有源码差异包含 RenderSource nullptr observer 回退 NullObserver、Orchestrator 析构双 tagged drain、三类 observer detach 与显式 source clear;Android Validation 增加 T1~T4 Activity recreate 场景和 Native reset 配对日志。静态审计发现 teardown 测试 helper 没有注册 Orchestrator observer,本轮仅补正这一行。
|
||||||
|
- render-job 队列独立于 `waitForEmpty()`;OpenGL backend/context 后续清理与真实 Android reset 仍须由 RDC 验收。STEP-100 locality 实现无本次差异,运行态回归待执行。
|
||||||
|
- 本轮仅做源码/任务静态审计与 `git diff --check`;CMake/Ninja、Native GTest、Gradle/APK、ADB/设备、NavSea、Jest、iOS、candidate/release、commit、push、tag、publish 全部 **NOT RUN**。不能写 STEP-101 PASS。
|
||||||
|
- Step:`docs/50_Step记录/STEP-101_RenderOrchestrator销毁生命周期修复_v1.md`;完成报告:`docs/报告/STEP-101_RenderOrchestrator销毁生命周期修复_Codex完成报告_v1.md`。V19 未发布;下一步 RDC 按报告命令独立验证。
|
||||||
|
|
||||||
|
### 2026-10-07 STEP-101 源码交付补充
|
||||||
|
|
||||||
|
- 审计 V18 Final HEAD、任务 v2、正常 source erase/clearData、TileCache 延迟释放和独立 render-job 队列;本轮不扩大正常 source 更新语义。
|
||||||
|
- 补齐 C++ 测试对旧 observer 已 detach 的断言;Android 增加 SurfaceView detach、Java/JNI `nativeReset` 与 Native `resetRenderer` 逐轮日志,并补 T3 动态 GeoJSON mutation 标记。
|
||||||
|
- `git diff --check` 通过;Native 编译/GTest、Android Gradle/APK/ADB/设备、STEP-100 locality 运行态、NavSea 和 V19 发布均未执行。结论仍为 PARTIAL,待 RDC 独立验收。
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-10-07 STEP-101 RDC 独立验收 CLOSED / PASS
|
||||||
|
|
||||||
|
- Android x86_64 OpenGL Validation full native build PASS:APK 61,957,678 bytes,libmaplibre 39,637,512 bytes,SHA-256 `14cbd35ed0bc1366389ddea69925901013e3b209ed3575462421a7920c73c721`。
|
||||||
|
- FZG1-2 T1/T3/T4 PASS;T2 完整 20 次真实 Activity recreate / `nativeReset` / `MapRenderer::resetRenderer` PASS,四组 crash buffer 均为空,不再出现 destroyed-mutex SIGABRT。
|
||||||
|
- STEP-100 locality 回归 PASS:global revision 62→894,R0 build 1→1、invalidation 19→19、reuse 12→3890;R1 正常持续 build/invalidate。
|
||||||
|
- Android Native C++ TestRunner 构建 PASS;4 个 RenderSegmentLocality + 2 个 RenderOrchestratorTeardown focused tests 全部 PASS,status=0。TestRunner 临时 fixtures asset 已删除,不纳入 Git。
|
||||||
|
- Mac host GTest 仍受本机 libuv pkg-config 环境阻塞,未安装系统依赖、未虚报 PASS。Jest NOT RUN。
|
||||||
|
- STEP-101 允许关闭;下一步进入 V19 candidate → NavSea 冷启动 Gate → 正式发布。
|
||||||
|
|||||||
Reference in New Issue
Block a user