FlexUI DFX 多引擎分桶设计(P2-H,待立项)
状态:设计草案(2026-08-25)。当前实现为进程级单 collector(P1 假设单引擎); 多引擎(同进程多个
FlexUIEngine.createEngine)场景存在语义缺陷,本设计给出完整方案与迁移路径。 相关:DFX 监控设计 §8.2 第 9 项、P1 状态
1. 现状与问题
1.1 现状(单 collector,root 全局唯一)
DfxCollector是进程级单例;Dfx_Init(engineKey, scopeKey, dataDir, ...)第二次调用会覆盖config_/data_dir_/engine_key_并ResetWindowStats()(清掉第一个引擎的计数)- 埋点(
StampUpdateStart(rootId,...)等)无 engine 上下文;frame_slots_按 rootId 分槽(rootId 全局递增唯一 → 帧数据天然跨引擎隔离),但错误计数/窗口/trace 全局共享 - 上报 JSON 无 engineKey 字段(
DfxEvent.engine_key定义了但未使用);onSlowUpdate.scopeKey已带真实 scope id(E-a) - ETS
DfxBridge单例 +inited守卫:第二个引擎的dfx/dfxClient不生效
1.2 多引擎场景的语义缺陷
| 场景 | 当前行为 | 期望 |
|---|---|---|
| 引擎 A、B 各自 createEngine(不同 dataDir/config) | B 的 Dfx_Init 覆盖 A:A 的配置失效、计数被清、事件标 B 的 engineKey | 各自独立采集/配置/上报 |
| 宿主对 A、B 传不同 dfxClient | 只有第一个 client 生效(DfxBridge inited 守卫) | 按引擎路由到各自 client |
| 同一 rootId 跨引擎 | rootId 全局唯一(不冲突),但上报无法区分来源引擎 | 事件带 engineKey |
2. 目标与范围
- 目标:同进程多引擎各自独立——采集(帧槽/轨迹/计数/异常持久化)、配置(阈值/开关/上报周期)、出站路由(每个引擎的事件 → 对应引擎的 dfxClient)
- 非目标:跨引擎聚合(P2 第 9 项"跨版本对比");引擎销毁后的延迟清理
3. 设计方案:per-engine collector 注册表
3.1 C++ 侧(modules/dfx)
// DfxCollector 从"单例"改为"注册表 + per-engine 实例"
class DfxCollector {
public:
// 保持进程级外观:按 engine_key 路由到 per-engine 实例
static DfxCollector& Instance(int32_t engine_key = 0);
static void Destroy(int32_t engine_key); // 引擎销毁时清理(flush + 停 worker?)
private:
static std::mutex registry_mutex_;
static std::unordered_map<int32_t, std::unique_ptr<DfxCollector>> registry_;
// 以下全部从"单例成员"变为"per-engine 实例成员":
DfxConfig config_; // per-engine(不再被覆盖)
std::string data_dir_; // per-engine(异常/崩溃文件分目录)
FrameSlot frame_slots_[kMaxRoots]; // 仍按全局 rootId 分槽(root 全局唯一)
TraceEntry trace_ring_[kTraceRingSize];
std::atomic<uint64_t> error_counts_[kMaxCount];
// watchdog 线程从"全局一个"变为"per-engine 一个"?→ 见 3.3 决策
};
关键接口变化:
- 埋点入口带 engine 上下文:热路径调用点(
StampUpdateStart等 10+ 处)需要知道 engine_key。不改变埋点签名(避免热路径增加参数传递)——方案:rootId → engine_key 反向映射(connector 侧维护,仿RegisterRootScope):// DfxCollector 维护 root_id → engine_key(低频注册,SetRootNode 时) void RegisterRootEngine(uint32_t root_id, int32_t engine_key); // Stamp* 内部查表(热路径一次 map 查找?→ 性能权衡见 3.3) - sink 出站带 engine_key:
DfxReportSink::Report(engine_key, method, json);DfxReporter把 engineKey 传给 ETSdispatchDfxEvent(engineKey, method, payload) - Dfx_Init/Dfx_UpdateConfig/Dfx_Flush 已带 engineKey 参数(现有),改为路由到对应实例
3.2 ETS 侧(DfxBridge)
// 从单 client 改为 Map<engineKey, DfxClient>
private clients: Map<number, DfxClient> = new Map()
// init 支持多次(每 engineKey 一次,不再 inited 全局守卫)
init(jsDriver, engineKey, scopeKey, dataDir, config, client) {
this.jsDriver = jsDriver
if (client) { this.clients.set(engineKey, client) }
// 每引擎调一次 Dfx_Init(C++ 侧路由)
}
dispatchDfxEvent(engineKey: number, method: string, payload: string): void {
this.clients.get(engineKey)?.[method]?.(payload)
}
3.3 关键权衡(性能约束优先——用户明确"不影响业务流程性能")
| 方案点 | 选项 | 决策与理由 |
|---|---|---|
| watchdog 线程 | A. per-engine 一个线程;B. 全局一个线程轮询所有实例 | B:进程内一个 dfx 线程遍历 per-engine 实例(避免多引擎时 N 个 1s 线程;线程数不随引擎数增长) |
| 埋点查 engine | A. Stamp* 签名加 engine_key 参数(改 10+ 调用点);B. rootId→engine 反向查表(热路径 map 查找) | A 或 B 都违背"热路径零侵入":A 增加参数传递(调用点改动大),B 增加热路径查找。折中:默认单引擎时走 Instance(0) 直通(零额外开销);多引擎(注册表 size>1)才启用查表路由——惰性分叉:inline DfxCollector& FastInstance() { static auto& c = Instance(0); return c; } 单引擎不变,多引擎用查表版埋点 |
| Init 覆盖语义 | per-engine 实例天然隔离 | 第二个引擎的 Init 不再影响第一个 |
| 窗口统计 | per-engine 实例各自窗口 | 各自 15s 周期 + destroy 快照 |
4. 迁移路径(分两步,每步可验证)
Step 1(本轮未做,建议立项第一步):sink/ETS 路由层
DfxReportSink::Report带 engine_key;DfxReporter→ ETS 透传;DfxBridge改 Map<engineKey, client>- C++ collector 仍单实例(事件标"当前 engine_key_"),但 ETS 已具备多 client 分发骨架
- 验证:单引擎行为不变(回归);双引擎 demo 可观察"事件到达最后引擎的 client"
Step 2(完整分桶):per-engine collector 注册表 + 埋点惰性分叉(§3.3)
- 验证:双引擎 demo——A/B 各自 config 生效、计数独立、事件路由正确、A 销毁 B 不受影响
5. 验证方案(需双引擎 demo)
- 临时构造第二个引擎(
createEngine({engineKey:'e2', ...})+ 第二张 FlexView) - 断言:A 的异常 → A 的 client;B 的异常 → B 的 client;A 的 errorCounters 独立于 B
- A 的 config(如 threshold=3ms)不影响 B(B 保持 100ms)
- 性能回归:双引擎下热路径开销对比单引擎(惰性分叉生效,无回归)
6. 风险与不做的理由(当前)
- 不做完整实现的原因:per-engine 注册表 + 埋点分叉是架构级改动(10+ 调用点 + NAPI + ETS + watchdog),且当前 demo 单引擎无法真机验证多引擎行为——违背"不做无法验证的改动"原则
- 风险:热路径惰性分叉引入分支预测开销(可忽略);registry 生命周期(引擎销毁时实例清理与 worker 线程交互)需谨慎
- 当前保障:单引擎场景零回归(Instance(0) 直通);rootId 全局唯一已提供天然数据隔离