This document explains how Perfetto's Explore Page works, from creating visual query graphs to executing SQL queries and displaying results. It covers the key components, data flow, and architectural patterns that enable the Explore Page to provide an interactive, node-based SQL query builder for trace analysis.
The Explore Page is a visual query builder that allows users to construct complex SQL queries by connecting nodes in a directed acyclic graph (DAG). Each node represents either a data source (table, slices, custom SQL) or an operation (filter, aggregation, join, etc.). The system converts this visual graph into structured SQL queries, executes them via the trace processor, and displays results in an interactive data grid.
User Interaction → Node Graph → Structured Query Generation → Query Analysis (Validation) → Query Materialization → Result Display
QueryNode (ui/src/plugins/dev.perfetto.ExplorePage/query_node.ts:128-161)
primaryInput (upstream), nextNodes (downstream), secondaryInputs (side connections)getStructuredQuery()Node Connections (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph_utils.ts)
addConnection()/removeConnection()NodeRegistry (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_registry.ts)
preCreate() hook for interactive setup (e.g., table selection modal)Core Nodes (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/core_nodes.ts)
registerCoreNodes() { nodeRegistry.register('table', {...}); nodeRegistry.register('slice', {...}); nodeRegistry.register('sql', {...}); nodeRegistry.register('filter', {...}); nodeRegistry.register('aggregation', {...}); // ... more nodes }
TableSourceNode - Queries a specific SQL table SlicesSourceNode - Pre-configured query for trace slices SqlSourceNode - Custom SQL query as data source TimeRangeSourceNode - Generates time intervals
FilterNode - Adds WHERE conditions SortNode - Adds ORDER BY clauses AggregationNode - GROUP BY with aggregate functions ModifyColumnsNode - Renames/removes columns AddColumnsNode - Adds columns from secondary source via LEFT JOIN and/or computed expressions LimitAndOffsetNode - Pagination
UnionNode - Combines rows from multiple sources JoinNode - Combines columns via JOIN conditions IntervalIntersectNode - Finds overlapping time intervals FilterDuringNode - Filters using secondary interval input CreateSlicesNode - Pairs start/end events from two secondary sources into slices
Builder (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts)
Graph (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.ts)
NodeExplorer (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts)
DataExplorer (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/data_explorer.ts)
Phase 1: Analysis (Validation)
Node Graph → Structured Query Protobuf → Engine.analyzeStructuredQuery() →
Query {sql, textproto, modules, preambles, columns} | Error
Phase 2: Materialization (Execution)
Query → CREATE PERFETTO TABLE _exp_materialized_{nodeId} AS {sql} →
COUNT(*) + Column Metadata → SQLDataSource → DataGrid Display
Purpose (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts:27-71)
FIFO Execution Queue (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts:442-659)
Materialization Lifecycle (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts:122-279)
// Create/reuse materialized table materializeNode(node, query, queryHash) { if (canReuseTable(node, queryHash)) return existingTable; if (node.state.materialized) await dropMaterialization(node); const tableName = await createTable(query); node.state.materializedQueryHash = queryHash; return tableName; } // Critical: State updated BEFORE dropping table to prevent race conditions dropMaterialization(node) { node.state.materialized = false; // ← Update first await engine.query(`DROP TABLE ${tableName}`); // ← Then drop }
Auto-Execute Logic (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts:892-1028)
| autoExecute | manual | materialized | Behavior |
|---|---|---|---|
| true | false | - | Analyze + execute if query changed |
| true | true | - | Analyze + execute (forced) |
| false | false | true | Load existing data from table |
| false | false | false | Skip - show “Run Query” button |
| false | true | - | Analyze + execute (user clicked) |
Auto-execute disabled for: SqlSourceNode, IntervalIntersectNode, UnionNode, FilterDuringNode, CreateSlicesNode
ExplorePageState (ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts:57-70)
interface ExplorePageState { rootNodes: QueryNode[]; // Nodes without parents (starting points) selectedNode?: QueryNode; // Currently selected node nodeLayouts: Map<string, {x, y}>; // Visual positions labels?: Array<{...}>; // Annotations isExplorerCollapsed?: boolean; sidebarWidth?: number; }
Query State Management (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts:60-86)
Builder maintains this.query as the single source of truth for query state:
Query State Flow:
Automatic execution (autoExecute=true):
NodeExplorer.updateQuery() → processNode({ manual: false })
→ onAnalysisComplete → sets NodeExplorer.currentQuery
→ onAnalysisComplete → calls onQueryAnalyzed callback → sets Builder.query
→ Builder passes query as prop to NodeExplorer
→ NodeExplorer.renderContent() uses attrs.query ?? this.currentQuery
Manual execution (autoExecute=false):
User clicks "Run Query" → Builder calls processNode({ manual: true })
→ onAnalysisComplete → sets Builder.query
→ onAnalysisComplete → calls onNodeQueryAnalyzed callback → sets Builder.query
→ Builder passes query as prop to NodeExplorer
→ NodeExplorer.renderContent() uses attrs.query (this.currentQuery may be undefined)
This ensures SQL/Proto tabs display correctly for both automatic and manual execution modes.
Race Condition Prevention (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts:283-292)
The callback captures the selected node at creation time to prevent stale query leakage:
const callbackNode = selectedNode; this.onNodeQueryAnalyzed = (query) => { // Only update if still on the same node if (callbackNode === this.previousSelectedNode) { this.query = query; } };
Without this check, rapid node switching can cause:
The validation ensures callbacks from old nodes are ignored after switching.
HistoryManager (ui/src/plugins/dev.perfetto.ExplorePage/history_manager.ts)
serializeState() for each nodeNode Creation (ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts:260-308)
// Source nodes handleAddSourceNode(id) { const descriptor = nodeRegistry.get(id); const initialState = await descriptor.preCreate?.(); // Optional modal const newNode = descriptor.factory(initialState); rootNodes.push(newNode); } // Operation nodes handleAddOperationNode(id, parentNode) { const newNode = descriptor.factory(initialState); if (singleNodeOperation(newNode.type)) { insertNodeBetween(parentNode, newNode); // A → C becomes A → B → C } else { addConnection(parentNode, newNode); // Multi-input: just connect } }
Node Deletion (ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts:599-775)
// Complex reconnection logic preserves data flow async handleDeleteNode(node) { 1. await cleanupManager.cleanupNode(node); // Drop SQL tables 2. Capture graph structure (parent, children, port connections) 3. disconnectNodeFromGraph(node) 4. Reconnect primary parent to children (bypass deleted node) - Only primary connections (portIndex === undefined) - Secondary connections dropped (specific to deleted node) 5. Update root nodes (add orphaned nodes) 6. Transfer layouts to docked children 7. Notify affected nodes via onPrevNodesUpdated() }
Graph Traversal (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph_utils.ts)
getAllNodes(): BFS traversal (both forward and backward)getAllDownstreamNodes(): Forward traversal (for invalidation)getAllUpstreamNodes(): Backward traversal (for dependency checking)insertNodeBetween(): Rewires connections when inserting operationsNode Invalidation (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts:369-440)
invalidateNode(node) { const downstreamNodes = getAllDownstreamNodes(node); for (const downstream of downstreamNodes) { queryHashCache.delete(downstream.nodeId); // Force hash recomputation downstream.state.materializedQueryHash = undefined; // Mark table stale } }
Query Hash Caching (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_builder_utils.ts)
hashNodeQuery(): Expensive JSON stringification of entire query treeQuery Construction (ui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_builder_utils.ts)
getStructuredQueries(finalNode) { const queries: PerfettoSqlStructuredQuery[] = []; let currentNode = finalNode; // Walk up the graph from leaf to root while (currentNode) { queries.push(currentNode.getStructuredQuery()); currentNode = currentNode.primaryInput; // Follow primary input chain } return queries.reverse(); // Root → Leaf order } analyzeNode(node, engine) { const structuredQueries = getStructuredQueries(node); const result = await engine.analyzeStructuredQuery(structuredQueries); return {sql, textproto, modules, preambles, columns}; }
JSON Serialization (ui/src/plugins/dev.perfetto.ExplorePage/json_handler.ts)
exportStateAsJson(): Serializes entire graph state to JSON filedeserializeState(): Reconstructs graph from JSONserializeState() for node-specific stateExamples System (ui/src/plugins/dev.perfetto.ExplorePage/examples_modal.ts)
ui/src/assets/explore_page/All queries constructed via composable nodes:
Nodes maintain both forward and backward links:
primaryInput: Single parent (vertical data flow)secondaryInputs: Map of port → parent (side connections)nextNodes: Array of children (consumers of this node's output)PerfettoSqlStructuredQueryanalyzeStructuredQuery()Core Infrastructure:
ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts - Main plugin and state managementui/src/plugins/dev.perfetto.ExplorePage/query_node.ts - Node abstraction and type definitionsui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts - Main UI componentui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_execution_service.ts - Execution coordinationNode System:
ui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_registry.ts - Node registrationui/src/plugins/dev.perfetto.ExplorePage/query_builder/core_nodes.ts - Core node registrationui/src/plugins/dev.perfetto.ExplorePage/query_builder/nodes/ - Individual node implementationsUI Components:
ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph/graph.ts - Visual graph canvasui/src/plugins/dev.perfetto.ExplorePage/query_builder/node_explorer.ts - Node sidebarui/src/plugins/dev.perfetto.ExplorePage/query_builder/data_explorer.ts - Results drawerUtilities:
ui/src/plugins/dev.perfetto.ExplorePage/query_builder/graph_utils.ts - Graph traversal and connection managementui/src/plugins/dev.perfetto.ExplorePage/query_builder/query_builder_utils.ts - Query analysis and utilitiesui/src/plugins/dev.perfetto.ExplorePage/query_builder/cleanup_manager.ts - Resource cleanupui/src/plugins/dev.perfetto.ExplorePage/history_manager.ts - Undo/redo managementui/src/plugins/dev.perfetto.ExplorePage/json_handler.ts - Serialization