Files
MapLibreParticleLab/docs/40_任务/STEP-101_RenderOrchestrator销毁生命周期修复_Codex任务_v2.md
tei 64cf8f4435
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
fix: make render orchestrator teardown safe
2026-10-07 08:41:08 +09:00

14 KiB
Raw Blame History

STEP-101 RenderOrchestrator teardown 生命周期修复 — Codex 任务 v1

0. 任务目的

修复 Particle V18 Android 在正常 Renderer teardown / MapView reset 时发生的 Native SIGABRT。

现场已在 NavSea Android x86_64 设备稳定复现:

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 新增:

RenderSegmentRevisionBook retainedSegmentRevisions;
mutable std::mutex readinessDiagnosticsMutex;
uint64_t readinessEventSequence;
std::unordered_map<std::string, SourceReadinessEventState> sourceReadinessEvents;
RenderMutationDiagnostics lastReadinessMutation;

多个 RenderSource / Tile observer callback 会进入:

advanceRetainedSourceRevision(...)
recordReadinessMutation(...)
onTileChanged(...)
onTileAction(...)
buildReadinessDiagnostics(...)
retainedSegmentSourceState(...)

并 lock:

readinessDiagnosticsMutex

V18 当前成员声明中:

renderSources
renderLayers
...
retainedSegmentRevisions
...
readinessDiagnosticsMutex
sourceReadinessEvents
...
threadPool

C++ 成员按声明反序析构,因此 readiness state / mutex 会早于 renderSources 销毁。

而 RenderSource 在创建时:

renderSource->setObserver(this);

observer 指向 RenderOrchestrator。

当前 RenderOrchestrator::~RenderOrchestrator() 只:

  1. contextLost 时标记 layer;
  2. threadPool.waitForEmpty();

没有在 readiness state 仍有效时显式 detach RenderSource observer / 清理 renderSources。

因此 V18 存在明确生命周期危险:

readinessDiagnosticsMutex destroyed
        ↓
renderSources still destroying/alive
        ↓
late tile/source callback
        ↓
RenderOrchestrator::onTile*
        ↓
lock destroyed mutex
        ↓
FORTIFY SIGABRT

3. 修复原则

不要只依赖调整成员声明顺序。

本任务应建立显式 teardown contract:

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 语义

当前:

void RenderSource::setObserver(RenderSourceObserver* observer_) {
    observer = observer_;
}

改成与 Style Source / Layer 一致的安全语义:

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。

至少提供:

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:

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. 最终原则

正常:

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

最低安全顺序调整为:

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 发布门槛修正为:

STEP-101 source code PASS
→ 构建未公开的 V19 candidate artifacts
→ Validation App 实机 PASS
→ NavSea 使用 candidate 做 5 次冷启动/teardown 验收
→ 公开发布不可变 V19
→ public readback
→ finalize/tag

不得先公开 V19 再做 NavSea 验收。