This page describes how each PerfettoSQL pipeline source and operator is implemented, what data structures and fast paths it uses, and what its memory and streaming characteristics are.
Warning: Pipe syntax and its implementation are experimental and unstable. Do not rely on anything described here. Both the syntax and the execution behavior can and will change without notice.
Related pages:
Pipelines execute over columnar batches of up to 8192 rows. Each column in a batch is a view over a contiguous array, an optional validity bitvector for nulls, and an optional row-selection index buffer for filtering or reordering rows without copying values.
Before any operator runs, a backward liveness pass walks from the pipeline's final output columns back to the sources:
INTERVAL INTERSECTION OF are dropped before the operands are read into memory.SUM(...) aggregates in TREE ACCUMULATE are dropped. If none of a TREE ACCUMULATE stage's output columns are read downstream, the entire stage is removed from the plan, skipping node numbering, tree ordering, and accumulation.FROMHow a FROM source is read depends on what it names.
PERFETTO TABLEs and built-in tablesWhen FROM names an unqualified table backed by a finalized Trace Processor dataframe, such as slice, sched, or any table created with CREATE PERFETTO TABLE, the pipeline reads the dataframe's columnar storage directly in C++ without opening a SQLite cursor:
id column stored as an implicit 0, 1, 2, ... row-index sequence, downstream operators like TREE ACCUMULATE detect this storage type and skip hash-table lookups.SELECT subqueriesWhen FROM names a view, a virtual table, or a parenthesized SELECT subquery, Trace Processor prepares and steps the SQL query in SQLite and copies the returned values into columnar batches.
If column pruning finds that some columns of the SQL source are never read downstream, the source query is wrapped before preparing it in SQLite:
WITH __pipeline_source(c0, c1, c2, ...) AS (SELECT * FROM <source>) SELECT c0 AS "<col_0>", c2 AS "<col_2>" FROM __pipeline_source
Projecting only the needed positional columns lets SQLite's query planner drop unused expressions or joins when it flattens the subquery, and avoids copying unused columns out of SQLite.
INTERVAL INTERSECTION OFImplementation: /src/trace_processor/core/exec/interval_intersect.cc
INTERVAL INTERSECTION OF is a blocking source: it reads all of its operands in full before emitting the first output batch.
Each operand is executed as an independent sub-pipeline and read into memory:
ts, dur, and all PER columns must be 64-bit integers. Rows where ts or dur is NULL are skipped. Rows where ts < 0 or dur < 0 stop execution with an error.RowStore. Unreferenced columns are discarded as batches arrive.[ts, ts + dur) interval and row index are inserted into a per-operand hash map keyed by the PER columns. Each PER column contributes 1 presence byte and 8 value bytes to the partition key, so two rows with NULL in a PER column land in the same partition.A partition can only produce output if its PER key exists in every operand. The operator iterates over the keys of whichever operand has the fewest distinct PER keys, and skips any key missing from another operand.
For each key present in all operands, it chooses between two paths:
IntervalIntersector, which picks between a linear sweep, binary search, or an interval tree based on whether that operand has overlaps and how many candidate regions are currently active.Candidate and output regions are stored as two flat arrays of bounds and one flat array of operand row indices, avoiding per-region heap allocations.
Once all partitions are intersected, output is served in batches of up to 8192 regions. Each batch writes the region ts and dur into two flat Int64 columns and attaches row-selection views into each operand's buffered RowStore for the retained columns.
TREE ACCUMULATEImplementation:
/src/trace_processor/core/exec/tree_number_nodes.cc/src/trace_processor/core/exec/tree_order.cc/src/trace_processor/core/exec/tree_accumulate.ccA TREE ACCUMULATE stage lowers into three steps: node numbering, tree ordering, and accumulation.
TreeNumberNodesTree operators need per-node state: visited bitvectors, waiting lists, and running sums. Raw id values from a filtered query can be arbitrary 64-bit integers or strings scattered across a wide range, so indexing an array directly by id would either fail or allocate memory proportional to the largest id in the original table.
TreeNumberNodes solves this by appending two dense Uint32 columns to each batch: the row‘s node number, assigned 0, 1, 2, ... in order of first sighting, and its parent’s node number, using kNoNode for roots where parent_id IS NULL. Downstream tree operators only inspect these two dense Uint32 columns, so all per-node arrays are sized to the number of rows in the input.
TreeNumberNodes has three tiers of execution:
Id fast path: When scanning an unfiltered dataframe whose id column has storage type Id, whose parent_id column is Uint32, and where every row's parent appears at an earlier row, node numbers are assigned in place by copying parent_id and filling 0, 1, 2, ... with no hash map and no per-cell type dispatch.id or parent_id arrives as a general integer column from a SQLite subquery, the operator tracks whether the IDs seen so far are still 0, 1, 2, ... with parents referencing already-seen IDs. As long as that holds, each id is already its own node number and the hash map stays empty.id, the operator backfills its FlatHashMap with any previously seen dense IDs and maps all subsequent id and parent_id values through the hash map.TreeChildFirst and TreeParentFirstAccumulating up or down a tree requires rows to be ordered so that the values a node depends on have already been computed when the node is visited. Neither direction requires strict depth-first traversal order. Subtrees may be interleaved because the accumulator stores a running total per node rather than a single root-to-leaf stack.
UP: TreeChildFirstTREE ACCUMULATE UP requires every child to appear before its parent. Because a streaming operator cannot know whether an arriving node has children until the end of the input, TreeChildFirst is a blocking operator that reads all input rows before emitting:
DOWN: TreeParentFirstTREE ACCUMULATE DOWN requires every parent to appear before its children. Unlike UP, this is not a full pipeline breaker: a row can be emitted as soon as its parent has been emitted.
TreeParentFirst keeps a bitvector of nodes that have already been emitted.TreeAccumulateUp and TreeAccumulateDownEach SUM(col) in the stage runs a streaming TreeAccumulateUp or TreeAccumulateDown operator over the ordered batches:
Int64, with NULL values contributing 0. Checked 64-bit addition is used so integer overflow is reported as an error rather than wrapping silently.std::vector<int64_t> indexed by node number:TreeAccumulateUp: When a row for node arrives, every descendant has already been processed and added its subtree sum into by_node[node]. The operator sets total = value + by_node[node], writes total to the output batch, and adds total into by_node[parent].TreeAccumulateDown: When a row for node arrives, its parent has already been processed and stored its path sum in by_node[parent]. The operator sets total = value + by_node[parent], stores by_node[node] = total, and writes total to the output batch.TREE ACCUMULATE stagesWhen a pipeline chains multiple TREE ACCUMULATE stages:
TREE ACCUMULATE stages share the same TreeNumberNodes columns as long as id and parent_id still refer to the same underlying columns.TREE ACCUMULATE UP stages, or two consecutive DOWN stages, reuse the tree ordering from the first stage and skip inserting a second ordering operator.UP and DOWN reuses the node numbering and re-orders the rows for the new direction.SELECT, EXTEND, DROP, RENAME, SET, and ASImplementation: /src/trace_processor/perfetto_sql/pipeline/compiler.cc
The column-reshaping stages SELECT, EXTEND, DROP, RENAME, SET, and AS have no runtime operators and do no work during batch execution.
During compilation, the pipeline compiler maintains the current row as a vector of (column_name, ColumnId, qualified_only) entries, alongside a list of active table aliases. Each active alias holds a snapshot of that vector from when the alias was bound:
DROP removes entries from the compile-time row vector.RENAME changes the column_name string on matching entries.SET updates a target entry‘s ColumnId to point at the source column’s ColumnId.EXTEND appends new (column_name, ColumnId) entries to the row vector.AS replaces the active alias list with a copy of the current row vector.SELECT replaces the row vector with the selected (column_name, ColumnId) entries and clears the active alias list.At the end of compilation, only the ColumnIds present in the final row vector are marked live for column pruning and mapped to physical batch column indices when returning results to SQLite. Reordering, duplicating, renaming, or dropping columns in pipe stages never copies column data at runtime.