RFC-0020: Trees in Perfetto #5136
Replies: 2 comments
|
📝 RFC Document Updated View changes: Commit History |
0 replies
|
📝 RFC Document Updated View changes: Commit History |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
📄 RFC Doc: 0020-trees.md
Trees in Perfetto
Authors: @LalitMaganti
Status: Deferred
Some prototype tree operations were implemented, but work on this design is
paused. It will be replaced by PerfettoSQL Next.
Problem
unintuitive and straight up does not allow aggregation per node.
reasons:
"passthrough" columns
step so if you want to do a bunch of operations one after another, you end
up converting back/forth a lot, wasting a ton of performance
introduces tremendous cognitive load making it impossible to reason about
anything beyond most basic things: case in point flamegraph.sql
Examples
Before diving into the design, here are concrete examples showing what the tree
SQL syntax looks like in practice.
Basic concepts
Trees are an opaque type in SQL. You convert a table into a tree, perform
operations on it, and convert back to a table when you need results.
Conversion functions:
tree_from_table!((<query>), (<columns>)): converts a table withidandparent_idcolumns into a tree, carrying the listed columns along.tree_to_table!(<tree>, (<columns>)): converts a tree back to a table withtree_id(dense 0-based index) andtree_parent_idcolumns, plus theoriginal columns.
Tree operations (all take a tree and return a new tree):
tree_filter: remove nodes, reparenting surviving children.tree_propagate_down: compute cumulative values from root to leaves.tree_propagate_up: compute cumulative values from leaves to root.tree_merge_siblings: merge sibling nodes with the same group key.tree_merge_siblings_ordered: like above, but only merges consecutive runs.tree_merge_into_parent: merge a node into its parent if they share a group.tree_invert: flip a tree so leaves become roots, merging by group key.All operations compose: the output of one is a valid input to the next.
Filter
Removes nodes matching a condition. Surviving children of removed nodes are
reparented to the nearest surviving ancestor (or become roots).
When an intermediate node is removed, its children are reparented:
Multiple constraints are ANDed. Filters can also be chained by nesting
tree_filter()calls.Propagate down
Computes cumulative values from root toward leaves. For each node:
column[child] = f(column[parent], column[child]).The 4th argument names the output column. Supported aggregate functions:
SUM,MIN,MAX,FIRST,LAST.Propagate up
Computes cumulative values from leaves toward root. Multiple children's values
are reduced into a single value before combining with the parent.
Merge siblings (unordered)
Merges all sibling nodes that share the same group key, regardless of their
position among siblings. Requires specifying how to aggregate remaining columns.
Merge siblings (ordered)
Like unordered merge, but only merges siblings that are consecutive in the same
group. When the group key changes, a new merged node begins.
Nodes 2 and 3 (both "A", consecutive) merged into dur=25. Node 5 ("A") stays
separate because node 4 ("B") breaks the run.
Merge into parent
If a node shares the same group key as its parent, it is merged into the parent.
Nodes 3 and 5 ("lock") merged into node 2 ("lock") because they share the
group key with their ancestor. Node 4 ("work") is reparented to the surviving
"lock" node.
Invert tree
Flips a tree so leaves become roots and roots become leaves, merging nodes by
group key as the structure is reversed. Useful for converting "top-down" call
stacks into "bottom-up" views.
Composing operations
Operations compose naturally by nesting. Here's a realistic example: building a
simplified flamegraph from a call stack.
Step by step:
share names, so no merging happens here (alloc and lex are different; alloc
and write are different). The tree structure is unchanged.
Design
Start with a problem, explain how we can solve it with trees. Best to explain to
peopel not used to it.
Overarching principles: organized in multiple layers:
SQLite's JSON functions. Of course will have functions to convert between
trees and tables.
"lazy" operations on top of trees.
of individual rows (generally speaking) so is very efficient
on the tree.
The building blocks for SQL:
unique identifier) and parent_id (representing parent in tree).
processed by higher level oeprations
"groups" which can then be handled by higher level operations.
it's really more accurately a forest if you're pedantic).
Converting between tables and trees is the foundation.
tree_from_tabletakesa table with
id,parent_idand any additional columns, aggregates the rowsand produces an opaque tree object.
tree_to_tableconverts back, but with acrucial transformation: the original (potentially sparse)
idandparent_idvalues are normalized to dense row indices stored in
_tree_id(always 0, 1,2, ..., n-1) and
_tree_parent_id(the row index of the parent, NULL forroots). The original columns are preserved alongside these tree columns. This
normalization enables O(1) parent lookups via direct array indexing - the key
to making tree operations fast.
The SQL operations:
value, child value (for simplicitly we will just use aggregate functions).
e.g. computing depth is a sum operation over a column full of 1s
"reduce" multiple values coming from children into a single value
reparents any children which survive to a surviving parent (or they become
roots if they don't)
then merge recursively by the node grouping
nodes together. Does not care about order (i.e. merging is global per parent
node)
nodes which are part of same group but only in sequence. Every time the
value changes, merging no longer happens
parent, merge into it's parent. Requires giving aggregators for remaining
columns as well.
These need to be translated to a bunch of very composable bytecodes making use
of all the existing ones we already have to sort/filter etc data.
Bytecode layer
Data structures:
ChildToParent: parent index per node + original_rows mapping back to table.This is the in-memory representation of
_tree_id(implicit, just row index)and
_tree_parent_idcolumns produced bytree_to_table.ParentToChild: CSR (offsets + children) + roots list for BFSDesign decisions:
normalization done by
tree_from_tableensures this invariant.Bytecodes for filter operation:
MakeChildToParentTreeStructure- table (id, parent_id columns) → ChildToParentMakeParentToChildTreeStructure- ChildToParent → ParentToChild (CSR with roots)IndexSpanToBitvector- Span<uint32_t> → BitVector (general utility)FilterTree- BFS from roots using CSR, compute surviving ancestors, reparent,compact ChildToParent
Filter flow:
Bytecode for propagate down:
PropagateDown<T, AggOp>- BFS from roots, applies aggregate operationupdate[child] = f(update[parent], update[child])Propagate down flow (e.g., compute depth):
Alternatives considered
is rock solid first.
Open questions
N/A
💬 Discussion Guidelines:
All reactions