Data Explorer 架构

本文档解释了 Perfetto 的 Data Explorer 的工作原理,从创建可视化查询图到执行 SQL 查询和显示结果。它涵盖了使 Data Explorer 能够提供用于 trace 分析的交互式基于节点的 SQL 查询构建器的关键组件、数据流和架构模式。

概述

Data Explorer 是一个可视化查询构建器,允许用户通过在有向无环图(DAG)中连接节点来构建复杂的 SQL 查询。每个节点代表数据源(表、Slice、自定义 SQL)或操作(过滤、聚合、连接等)。系统将此可视化图转换为结构化的 SQL 查询,通过 trace processor 执行它们,并在交互式数据网格中显示结果。

核心数据流

用户交互 → 节点图 → 结构化查询生成 → 查询分析(验证) → 查询物化 → 结果显示

节点图结构

QueryNode (ui/src/plugins/dev.perfetto.DataExplorer/query_node.ts:128-161)

节点连接 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph_utils.ts)

节点注册和创建

NodeRegistry (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_registry.ts)

核心节点 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/core_nodes.ts)

registerCoreNodes() { nodeRegistry.register('table', {...}); nodeRegistry.register('slice', {...}); nodeRegistry.register('sql', {...}); nodeRegistry.register('filter', {...}); nodeRegistry.register('aggregation', {...}); // ... 更多节点 }

节点类型

1. 源节点(数据源)

TableSourceNode - 查询特定的 SQL 表 SlicesSourceNode - 用于 trace Slice 的预配置查询 SqlSourceNode - 作为数据源的自定义 SQL 查询 TimeRangeSourceNode - 生成时间间隔

2. 单输入修改节点

FilterNode - 添加 WHERE 条件 SortNode - 添加 ORDER BY 子句 AggregationNode - 带聚合函数的 GROUP BY ModifyColumnsNode - 重命名/删除列 AddColumnsNode - 通过 LEFT JOIN 和/或计算表达式从次源添加列 LimitAndOffsetNode - 分页

3. 多输入节点

UnionNode - 组合来自多个源的行 JoinNode - 通过 JOIN 条件组合列 IntervalIntersectNode - 查找重叠的时间间隔 FilterDuringNode - 使用次间隔输入进行过滤 CreateSlicesNode - 将来自两个次源的开始/结束事件配对到 Slice 中

UI 组件

Builder (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/builder.ts)

Graph (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph/graph.ts)

NodePanel (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/node_panel.ts)

DataExplorer (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/data_explorer.ts)

查询执行模型

两阶段执行

阶段 1:分析(验证)

节点图 → 结构化查询 Protobuf → Engine.updateSummarizerSpec() + querySummarizer() → 查询 {sql, textproto, columns} | 错误

阶段 2:物化(执行)

engine.querySummarizer(summarizerId, nodeId) → TP 创建/重用表 → {tableName, rowCount, columns, durationMs} → SQLDataSource → DataGrid 显示

QueryExecutionService

目的 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/query_execution_service.ts)

Trace Processor 作为单一事实来源

所有物化状态由 Trace Processor (TP)管理,而不是 UI:

UI 按需查询 TP 而不是缓存:

// 需要从 TP 获取表名时(例如,用于"复制表名"或导出) async getTableName(nodeId: string): Promise<string | undefined> { const result = await engine.querySummarizer(DATA_EXPLORER_SUMMARIZER_ID, nodeId); if (result.exists !== true || result.error) { return undefined; } return result.tableName; }

这消除了 UI 和 TP 之间的状态同步错误。

FIFO 执行队列

快速节点点击处理 (ui/src/base/async_limiter.ts)

AsyncLimiter 确保在快速单击节点时只运行最新的排队任务:

// AsyncLimiter 行为: while ((task = taskQueue.shift())) { if (taskQueue.length > 0) { task.deferred.resolve(); // 跳过 - 更新的任务在等待 } else { await task.work(); // 运行 - 这是最新的 } }

示例:在 A 处理时快速单击 A → B → C:

  1. A 开始处理
  2. B 排队,C 排队
  3. A 完成
  4. B 跳过(队列有 C),C 运行

这确保处理当前选定的节点(C),跳过中间的单击(B)。

通过 TP API 物化

// 与 TP 同步所有查询,然后获取目标节点的结果 async processNode(node: QueryNode): Promise<void> { // 1. 确保 summarizer 存在(每个会话创建一次) await engine.createSummarizer(DATA_EXPLORER_SUMMARIZER_ID); // 2. 向 TP 注册所有查询(处理更改检测) const spec = buildTraceSummarySpec(allNodes); await engine.updateSummarizerSpec(DATA_EXPLORER_SUMMARIZER_ID, spec); // 3. 获取结果 - 触发延迟物化 const result = await engine.querySummarizer(DATA_EXPLORER_SUMMARIZER_ID, node.nodeId); // 返回:tableName, rowCount, columns, durationMs, sql, textproto }

自动执行逻辑 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/query_execution_service.ts)

autoExecute manual 行为
true false 自动分析 + 执行
true true 分析 + 执行(强制)
false false 跳过 - 显示"运行查询"按钮
false true 分析 + 执行(用户点击)

自动执行禁用于:SqlSourceNode、IntervalIntersectNode、UnionNode、FilterDuringNode、CreateSlicesNode

状态管理

DataExplorerState (ui/src/plugins/dev.perfetto.DataExplorer/data_explorer.ts)

interface DataExplorerState { rootNodes: QueryNode[]; // 没有父节点的节点(起点) selectedNodes: ReadonlySet<string>; // 选定节点 ID 的集合(多选) nodeLayouts: Map<string, {x, y}>; // 可视化位置 labels: Array<{...}>; // 注释 isExplorerCollapsed?: boolean; sidebarWidth?: number; loadGeneration?: number; // 内容加载时递增 clipboardNodes?: ClipboardEntry[]; // 多节点复制/粘贴 clipboardConnections?: ClipboardConnection[]; }

查询状态管理 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/builder.ts:60-86)

Builder 维护 this.query 作为查询状态的单一事实来源:

查询状态流:

自动执行(autoExecute=true): NodePanel.updateQuery() → processNode({ manual: false }) → onAnalysisComplete → 设置 NodePanel.currentQuery → onAnalysisComplete → 调用 onQueryAnalyzed 回调 → 设置 Builder.query → Builder 将 query 作为 prop 传递给 NodePanel → NodePanel.renderContent() 使用 attrs.query ?? this.currentQuery 手动执行(autoExecute=false): 用户单击"运行查询" → Builder 调用 processNode({ manual: true }) → onAnalysisComplete → 设置 Builder.query → onAnalysisComplete → 调用 onNodeQueryAnalyzed 回调 → 设置 Builder.query → Builder 将 query 作为 prop 传递给 NodePanel → NodePanel.renderContent() 使用 attrs.query(this.currentQuery 可能未定义)

这确保了 SQL/Proto 选项卡在自动和手动执行模式下都能正确显示。

竞态条件预防 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/builder.ts:283-292)

回调在创建时捕获选定节点以防止陈旧查询泄漏:

const callbackNode = selectedNode; this.onNodeQueryAnalyzed = (query) => { // 仅当仍在同一节点上时才更新 if (callbackNode === this.previousSelectedNode) { this.query = query; } };

没有此检查,快速节点切换可能会导致:

  1. 用户选择节点 A → 异步分析开始
  2. 用户快速切换到节点 B → 节点 A 的组件被销毁
  3. 节点 A 的分析完成 → 回调使用节点 A 的查询触发
  4. 节点 B 在 SQL/Proto 选项卡中错误地显示节点 A 的查询

验证确保切换后忽略来自旧节点的回调。

HistoryManager (ui/src/plugins/dev.perfetto.DataExplorer/history_manager.ts)

图操作

节点创建 (ui/src/plugins/dev.perfetto.DataExplorer/node_crud_operations.ts)

// 源节点 addSourceNode(deps, state, id) { const descriptor = nodeRegistry.get(id); const initialState = await descriptor.preCreate?.(); // 可选模态框 const newNode = descriptor.factory(initialState); rootNodes.push(newNode); } // 操作节点 addOperationNode(deps, state, parentNode, id) { const newNode = descriptor.factory(initialState); if (singleNodeOperation(newNode.type)) { insertNodeBetween(parentNode, newNode); // A → C 变为 A → B → C } else { addConnection(parentNode, newNode); // 多输入:只需连接 } }

节点删除 (ui/src/plugins/dev.perfetto.DataExplorer/node_crud_operations.ts)

// 复杂的重连接逻辑保留数据流 deleteNode(deps, state, node) { 1. await cleanupManager.cleanupNode(node); // 删除 SQL 表 2. 捕获图结构(父节点、子节点、端口连接) 3. disconnectNodeFromGraph(node) 4. 将主父节点重新连接到子节点(绕过已删除的节点) - 仅主连接(portIndex === undefined) - 删除次连接(特定于已删除的节点) 5. 更新根节点(添加孤立节点) 6. 将布局转移到停靠的子节点 7. 通过 onPrevNodesUpdated() 通知受影响的节点 }

图遍历 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph_utils.ts)

失效和缓存

TP 管理的缓存

查询哈希缓存和更改检测完全由 Trace Processor 处理:

延迟物化

物化是延迟的 - TP 仅在为该特定查询调用 querySummarizer() 时才物化查询。调用 updateSummarizerSpec() 时,图中所有有效的查询都在 TP 中注册,但不执行 SQL。仅当调用 querySummarizer(nodeId) 时,TP 才实际物化该查询(及其依赖项)。这避免了用户尚未查看的节点的不必要工作。

智能重新物化优化

当通过 updateSummarizerSpec() 与 TP 同步查询时,TP 执行智能更改检测和依赖跟踪,以最大限度地减少冗余工作:

  1. 基于 proto 的更改检测:每个查询的结构化查询 proto 字节被哈希(而不是生成的 SQL)。这对于具有 inner_query_id 引用的查询可以正确工作,这些查询的 SQL 无法独立生成。

  2. 依赖传播:如果查询 B 通过 inner_query_id 依赖于查询 A,并且 A 的 proto 更改,则即使 B 的 proto 未更改(因为 B 的输出依赖于 A 的数据),B 也必须重新物化。TP 通过整个依赖链传递传播此依赖关系。

  3. 表源替换:对于已经物化的未更改查询,TP 用引用物化表的简单表源结构化查询替换它们。为更改的查询生成 SQL 时,它们直接引用这些表,而不是重新展开完整的查询链。

示例:对于链 A → B → C → D,如果 C 更改:

此优化通过避免冗余 SQL 生成和执行显著加速长查询链中的增量编辑。TP 端实现位于 src/trace_processor/trace_summary/summarizer.cc

按需状态查询

UI 在需要时从 TP 查询物化状态:

// 从 TP 获取当前状态(用于"复制表名"、导出等) const result = await engine.querySummarizer(DATA_EXPLORER_SUMMARIZER_ID, nodeId); // 返回:{ exists: boolean, tableName?: string, error?: string, ... }

此设计确保:

Trace Processor 重启处理

如果 Trace Processor 重启或崩溃,所有 summarizer 状态(包括物化表)都会丢失。UI 可能仍然持有 TP 中不再存在的陈旧 summarizerId。当下一次 querySummarizer() 调用发生时,TP 将返回错误,指示 summarizer 不存在。UI 会优雅地处理此错误,将其视为需要在下一次执行尝试时重新创建 summarizer 并重新同步所有查询。用户可能会看到错误消息,但再次单击"运行查询"将恢复状态。

结构化查询生成

查询构建 (ui/src/plugins/dev.perfetto.DataExplorer/query_builder/query_builder_utils.ts)

getStructuredQueries(finalNode) { const queries: PerfettoSqlStructuredQuery[] = []; let currentNode = finalNode; // 从叶到根遍历图 while (currentNode) { queries.push(currentNode.getStructuredQuery()); currentNode = currentNode.primaryInput; // 遵循主输入链 } return queries.reverse(); // 根 → 叶顺序 } analyzeNode(node, engine) { const structuredQueries = getStructuredQueries(node); const spec = new TraceSummarySpec(); spec.query = structuredQueries; await engine.createSummarizer(ANALYZE_NODE_SUMMARIZER_ID); // 确保 summarizer 存在 await engine.updateSummarizerSpec(ANALYZE_NODE_SUMMARIZER_ID, spec); // 向 TP 注册 const result = await engine.querySummarizer(ANALYZE_NODE_SUMMARIZER_ID, node.nodeId); // 获取结果 return {sql: result.sql, textproto: result.textproto}; }

序列化和示例

JSON 序列化 (ui/src/plugins/dev.perfetto.DataExplorer/json_handler.ts)

示例系统 (ui/src/plugins/dev.perfetto.DataExplorer/examples_modal.ts)

关键架构模式

1. 基于节点的查询构建

所有查询通过可组合节点构建:

2. 双向图连接

节点维护前向和后向链接:

3. 两阶段执行与延迟物化

4. TP 管理状态的 FIFO 队列

5. 结构化查询协议

6. 模块化纯函数架构

data_explorer.ts 将业务逻辑委托给专注于纯函数的模块:

模块:

7. GraphCallbacks 接口(减少 Prop Drilling)

14 个回调从 data_explorer.ts 流向 BuilderGraph:

文件路径参考

核心基础设施

业务逻辑模块(具有显式依赖注入的纯函数):

节点系统

UI 组件

工具

**Trace Processor (C++)**: