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 决策
};

关键接口变化:

  1. 埋点入口带 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)
    
  2. sink 出站带 engine_key:DfxReportSink::Report(engine_key, method, json);DfxReporter 把 engineKey 传给 ETS dispatchDfxEvent(engineKey, method, payload)
  3. 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)

  1. 临时构造第二个引擎(createEngine({engineKey:'e2', ...}) + 第二张 FlexView)
  2. 断言:A 的异常 → A 的 client;B 的异常 → B 的 client;A 的 errorCounters 独立于 B
  3. A 的 config(如 threshold=3ms)不影响 B(B 保持 100ms)
  4. 性能回归:双引擎下热路径开销对比单引擎(惰性分叉生效,无回归)

6. 风险与不做的理由(当前)

  • 不做完整实现的原因:per-engine 注册表 + 埋点分叉是架构级改动(10+ 调用点 + NAPI + ETS + watchdog),且当前 demo 单引擎无法真机验证多引擎行为——违背"不做无法验证的改动"原则
  • 风险:热路径惰性分叉引入分支预测开销(可忽略);registry 生命周期(引擎销毁时实例清理与 worker 线程交互)需谨慎
  • 当前保障:单引擎场景零回归(Instance(0) 直通);rootId 全局唯一已提供天然数据隔离