tp: add tree_propagate_down operator for tree transformations (#5257)
## Summary
- Add `PropagateDown()` method to TreeTransformer for root-to-leaf BFS
propagation
- Support SUM, MIN, MAX, FIRST, LAST aggregate operations
- Propagated columns become real columns enabling
filter→propagate→filter chains
- PropagateTreeDown bytecode takes spec_start/spec_count with separate
source/dest columns
## Stack
- #5262 tp: introduce TreeColumns and refactor TreeTransformer to use it
- **#5257 tp: add tree_propagate_down operator for tree
transformations** (this PR)
## Test plan
- 13 new propagate diff tests including chained propagation and
filter→propagate→filter
- All 23 tree tests pass
diff --git a/Android.bp b/Android.bp
index ff488f4..7493b53 100644
--- a/Android.bp
+++ b/Android.bp
@@ -15270,6 +15270,7 @@
filegroup {
name: "perfetto_src_trace_processor_core_tree_tree",
srcs: [
+ "src/trace_processor/core/tree/propagate_spec.cc",
"src/trace_processor/core/tree/tree_columns_builder.cc",
"src/trace_processor/core/tree/tree_transformer.cc",
],
@@ -16433,6 +16434,7 @@
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_conversion.cc",
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_filter.cc",
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.cc",
],
}
@@ -16773,6 +16775,7 @@
"src/trace_processor/perfetto_sql/stdlib/std/traceinfo/metadata_for_primary_scope.sql",
"src/trace_processor/perfetto_sql/stdlib/std/traceinfo/trace.sql",
"src/trace_processor/perfetto_sql/stdlib/std/trees/filter.sql",
+ "src/trace_processor/perfetto_sql/stdlib/std/trees/propagate.sql",
"src/trace_processor/perfetto_sql/stdlib/std/trees/table_conversion.sql",
"src/trace_processor/perfetto_sql/stdlib/time/conversion.sql",
"src/trace_processor/perfetto_sql/stdlib/traced/stats.sql",
diff --git a/BUILD b/BUILD
index 9f593d0..aa15dee 100644
--- a/BUILD
+++ b/BUILD
@@ -2183,6 +2183,8 @@
perfetto_filegroup(
name = "src_trace_processor_core_tree_tree",
srcs = [
+ "src/trace_processor/core/tree/propagate_spec.cc",
+ "src/trace_processor/core/tree/propagate_spec.h",
"src/trace_processor/core/tree/tree_columns.h",
"src/trace_processor/core/tree/tree_columns_builder.cc",
"src/trace_processor/core/tree/tree_columns_builder.h",
@@ -3331,6 +3333,8 @@
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_filter.h",
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc",
"src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.h",
+ "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h",
],
)
@@ -3956,6 +3960,7 @@
name = "src_trace_processor_perfetto_sql_stdlib_std_trees_trees",
srcs = [
"src/trace_processor/perfetto_sql/stdlib/std/trees/filter.sql",
+ "src/trace_processor/perfetto_sql/stdlib/std/trees/propagate.sql",
"src/trace_processor/perfetto_sql/stdlib/std/trees/table_conversion.sql",
],
)
diff --git a/src/trace_processor/core/common/tree_types.h b/src/trace_processor/core/common/tree_types.h
index 5a0622d..5bd9b0c 100644
--- a/src/trace_processor/core/common/tree_types.h
+++ b/src/trace_processor/core/common/tree_types.h
@@ -26,6 +26,15 @@
// Root nodes have this value as their parent.
inline constexpr uint32_t kNullParent = std::numeric_limits<uint32_t>::max();
+// Aggregation operation for tree propagation (propagate-down).
+enum class PropagateAggOp : uint8_t {
+ kSum,
+ kMin,
+ kMax,
+ kFirst,
+ kLast,
+};
+
} // namespace perfetto::trace_processor::core
#endif // SRC_TRACE_PROCESSOR_CORE_COMMON_TREE_TYPES_H_
diff --git a/src/trace_processor/core/interpreter/bytecode_instructions.h b/src/trace_processor/core/interpreter/bytecode_instructions.h
index e57b667..5014364 100644
--- a/src/trace_processor/core/interpreter/bytecode_instructions.h
+++ b/src/trace_processor/core/interpreter/bytecode_instructions.h
@@ -637,6 +637,22 @@
indices_register);
};
+// Propagates values from root toward leaves using BFS. For each spec in the
+// range [spec_start, spec_start + spec_count), copies source column data to
+// the dest column, then applies the aggregate operation downward via BFS.
+//
+// Requires the P2C CSR cache to be rebuilt if stale.
+struct PropagateTreeDown : Bytecode {
+ static constexpr Cost kCost = LinearPerRowCost{20};
+
+ PERFETTO_DATAFRAME_BYTECODE_IMPL_3(RwHandle<std::unique_ptr<TreeState>>,
+ tree_state_register,
+ uint32_t,
+ spec_start,
+ uint32_t,
+ spec_count);
+};
+
// Bytecode ops that require FilterValueFetcher access.
#define PERFETTO_DATAFRAME_BYTECODE_FVF_LIST(X) \
X(CastFilterValue<Id>) \
@@ -799,7 +815,8 @@
X(FinalizeRanksInMap) \
X(SortRowLayout) \
X(Reverse) \
- X(FilterTreeState)
+ X(FilterTreeState) \
+ X(PropagateTreeDown)
// Combined list of all bytecode instruction types.
#define PERFETTO_DATAFRAME_BYTECODE_LIST(X) \
diff --git a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc
index 4095fdc..10da636 100644
--- a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc
+++ b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc
@@ -453,6 +453,78 @@
ts->p2c_valid = true;
}
+// Runs a single propagate-down pass over the BFS order for one column.
+// |Combine| is a lambda (parent_val, child_val) -> new_child_val.
+template <typename T, typename Combine>
+void PropagateDownBfs(T* d,
+ Combine combine,
+ const uint32_t* queue,
+ uint32_t queue_end,
+ const uint32_t* parent_arr) {
+ for (uint32_t qi = 0; qi < queue_end; ++qi) {
+ uint32_t node = queue[qi];
+ uint32_t p = parent_arr[node];
+ if (p != kNullParent) {
+ d[node] = combine(d[p], d[node]);
+ }
+ }
+}
+
+// Dispatches on storage type then agg_op for a single propagate-down spec.
+template <typename T>
+void PropagateDownColumnTyped(T* d,
+ PropagateAggOp agg_op,
+ const uint32_t* queue,
+ uint32_t queue_end,
+ const uint32_t* parent_arr) {
+ switch (agg_op) {
+ case PropagateAggOp::kSum:
+ PropagateDownBfs(
+ d, [](auto p, auto c) { return p + c; }, queue, queue_end,
+ parent_arr);
+ break;
+ case PropagateAggOp::kMin:
+ PropagateDownBfs(
+ d, [](auto p, auto c) { return std::min(p, c); }, queue, queue_end,
+ parent_arr);
+ break;
+ case PropagateAggOp::kMax:
+ PropagateDownBfs(
+ d, [](auto p, auto c) { return std::max(p, c); }, queue, queue_end,
+ parent_arr);
+ break;
+ case PropagateAggOp::kFirst:
+ PropagateDownBfs(
+ d, [](auto p, auto) { return p; }, queue, queue_end, parent_arr);
+ break;
+ case PropagateAggOp::kLast:
+ break;
+ }
+}
+
+void PropagateDownColumn(uint8_t* data,
+ StorageType storage_type,
+ PropagateAggOp agg_op,
+ const uint32_t* queue,
+ uint32_t queue_end,
+ const uint32_t* parent_arr) {
+ if (storage_type.Is<Uint32>()) {
+ PropagateDownColumnTyped(reinterpret_cast<uint32_t*>(data), agg_op, queue,
+ queue_end, parent_arr);
+ } else if (storage_type.Is<Int32>()) {
+ PropagateDownColumnTyped(reinterpret_cast<int32_t*>(data), agg_op, queue,
+ queue_end, parent_arr);
+ } else if (storage_type.Is<Int64>()) {
+ PropagateDownColumnTyped(reinterpret_cast<int64_t*>(data), agg_op, queue,
+ queue_end, parent_arr);
+ } else if (storage_type.Is<Double>()) {
+ PropagateDownColumnTyped(reinterpret_cast<double*>(data), agg_op, queue,
+ queue_end, parent_arr);
+ } else {
+ PERFETTO_FATAL("Unsupported storage type for propagate down");
+ }
+}
+
} // namespace
void FilterTreeState(InterpreterState& state,
@@ -569,4 +641,50 @@
indices.e = indices.b + new_count;
}
+void PropagateTreeDown(InterpreterState& state,
+ const struct PropagateTreeDown& bc) {
+ using B = struct PropagateTreeDown;
+ auto& ts = state.ReadFromRegister(bc.arg<B::tree_state_register>());
+ uint32_t spec_start = bc.arg<B::spec_start>();
+ uint32_t spec_count = bc.arg<B::spec_count>();
+
+ if (ts->row_count == 0 || spec_count == 0) {
+ return;
+ }
+
+ BuildP2CFromTreeState(ts.get());
+
+ // BFS from roots, building a topological order in scratch1.
+ uint32_t* queue = ts->scratch1.begin();
+ uint32_t queue_end = 0;
+ for (uint32_t i = 0; i < ts->p2c_root_count; ++i) {
+ queue[queue_end++] = ts->p2c_roots[i];
+ }
+ for (uint32_t qi = 0; qi < queue_end; ++qi) {
+ uint32_t cs = ts->p2c_offsets[queue[qi]];
+ uint32_t ce = ts->p2c_offsets[queue[qi] + 1];
+ for (uint32_t ci = cs; ci < ce; ++ci) {
+ queue[queue_end++] = ts->p2c_children[ci];
+ }
+ }
+
+ // Process only the specs in [spec_start, spec_start + spec_count).
+ // For each: copy source → dest, then BFS propagation on dest.
+ const uint32_t* parent_arr = ts->parent.begin();
+ uint32_t n = ts->row_count;
+ for (uint32_t si = spec_start; si < spec_start + spec_count; ++si) {
+ const auto& spec = ts->propagate_down_specs[si];
+ uint8_t* src_data = ts->columns[spec.source_ts_col].data.begin();
+ uint8_t* dst_data = ts->columns[spec.dest_ts_col].data.begin();
+ uint32_t byte_count = n * ts->columns[spec.dest_ts_col].elem_size;
+
+ // Copy source data into dest column.
+ memcpy(dst_data, src_data, byte_count);
+
+ // BFS propagation on dest data.
+ PropagateDownColumn(dst_data, spec.storage_type, spec.agg_op, queue,
+ queue_end, parent_arr);
+ }
+}
+
} // namespace perfetto::trace_processor::core::interpreter::ops
diff --git a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h
index 863a341..efb8bb1 100644
--- a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h
+++ b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h
@@ -1726,6 +1726,12 @@
// the TreeState, and resets the indices span to [0..new_row_count-1].
void FilterTreeState(InterpreterState& state, const struct FilterTreeState& bc);
+// Propagates column values from roots toward leaves using BFS.
+// For each parent→child edge, applies the aggregate operation from
+// TreeState::propagate_down_specs.
+void PropagateTreeDown(InterpreterState& state,
+ const struct PropagateTreeDown& bc);
+
} // namespace ops
// Macros for generating case statements that dispatch to ops:: free functions.
diff --git a/src/trace_processor/core/interpreter/interpreter_types.h b/src/trace_processor/core/interpreter/interpreter_types.h
index 8b7ec73..500298e 100644
--- a/src/trace_processor/core/interpreter/interpreter_types.h
+++ b/src/trace_processor/core/interpreter/interpreter_types.h
@@ -29,6 +29,7 @@
#include "src/trace_processor/core/common/null_types.h"
#include "src/trace_processor/core/common/op_types.h"
#include "src/trace_processor/core/common/storage_types.h"
+#include "src/trace_processor/core/common/tree_types.h"
#include "src/trace_processor/core/util/bit_vector.h"
#include "src/trace_processor/core/util/flex_vector.h"
#include "src/trace_processor/core/util/slab.h"
@@ -267,6 +268,16 @@
// Scratch buffers (allocated once at max size, reused).
Slab<uint32_t> scratch1; // initial_row_count * 2
Slab<uint32_t> scratch2; // initial_row_count
+
+ // Propagation specs: set by TreeTransformer, consumed by PropagateTreeDown.
+ // Each PropagateTreeDown bytecode processes a contiguous range of specs.
+ struct PropagateDownSpec {
+ PropagateAggOp agg_op;
+ uint32_t source_ts_col; // index into |columns| to copy FROM
+ uint32_t dest_ts_col; // index into |columns| to propagate INTO
+ StorageType storage_type;
+ };
+ std::vector<PropagateDownSpec> propagate_down_specs;
};
} // namespace perfetto::trace_processor::core::interpreter
diff --git a/src/trace_processor/core/tree/BUILD.gn b/src/trace_processor/core/tree/BUILD.gn
index af7fb37..676b16c 100644
--- a/src/trace_processor/core/tree/BUILD.gn
+++ b/src/trace_processor/core/tree/BUILD.gn
@@ -16,6 +16,8 @@
source_set("tree") {
sources = [
+ "propagate_spec.cc",
+ "propagate_spec.h",
"tree_columns.h",
"tree_columns_builder.cc",
"tree_columns_builder.h",
diff --git a/src/trace_processor/core/tree/propagate_spec.cc b/src/trace_processor/core/tree/propagate_spec.cc
new file mode 100644
index 0000000..dd9208d
--- /dev/null
+++ b/src/trace_processor/core/tree/propagate_spec.cc
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/core/tree/propagate_spec.h"
+
+#include <string>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+
+namespace perfetto::trace_processor::core::tree {
+
+base::StatusOr<PropagateSpec> ParsePropagateSpec(const std::string& spec) {
+ std::string trimmed = base::TrimWhitespace(spec);
+ if (trimmed.empty()) {
+ return base::ErrStatus("propagate spec: empty string");
+ }
+
+ // Find '(' to extract agg function name.
+ auto lparen = trimmed.find('(');
+ if (lparen == std::string::npos) {
+ return base::ErrStatus("propagate spec: expected '(' in '%s'",
+ trimmed.c_str());
+ }
+ std::string agg_str =
+ base::ToUpper(base::TrimWhitespace(trimmed.substr(0, lparen)));
+
+ PropagateAggOp agg_op;
+ if (agg_str == "SUM") {
+ agg_op = PropagateAggOp::kSum;
+ } else if (agg_str == "MIN") {
+ agg_op = PropagateAggOp::kMin;
+ } else if (agg_str == "MAX") {
+ agg_op = PropagateAggOp::kMax;
+ } else if (agg_str == "FIRST") {
+ agg_op = PropagateAggOp::kFirst;
+ } else if (agg_str == "LAST") {
+ agg_op = PropagateAggOp::kLast;
+ } else {
+ return base::ErrStatus("propagate spec: unknown aggregate '%s'",
+ agg_str.c_str());
+ }
+
+ // Find ')' to extract source column name.
+ auto rparen = trimmed.find(')', lparen + 1);
+ if (rparen == std::string::npos) {
+ return base::ErrStatus("propagate spec: expected ')' in '%s'",
+ trimmed.c_str());
+ }
+ std::string source_col =
+ base::TrimWhitespace(trimmed.substr(lparen + 1, rparen - lparen - 1));
+ if (source_col.empty()) {
+ return base::ErrStatus("propagate spec: empty source column in '%s'",
+ trimmed.c_str());
+ }
+
+ // Find 'AS' (case-insensitive) after ')'.
+ std::string remainder = trimmed.substr(rparen + 1);
+ std::string remainder_upper = base::ToUpper(remainder);
+ auto as_pos = remainder_upper.find("AS");
+ if (as_pos == std::string::npos) {
+ return base::ErrStatus("propagate spec: expected 'AS' in '%s'",
+ trimmed.c_str());
+ }
+
+ // Verify that 'AS' is preceded and followed by whitespace or is at the
+ // boundary.
+ if (as_pos > 0 && remainder[as_pos - 1] != ' ' &&
+ remainder[as_pos - 1] != '\t') {
+ return base::ErrStatus(
+ "propagate spec: expected whitespace before 'AS' in '%s'",
+ trimmed.c_str());
+ }
+ if (as_pos + 2 < remainder.size() && remainder[as_pos + 2] != ' ' &&
+ remainder[as_pos + 2] != '\t') {
+ return base::ErrStatus(
+ "propagate spec: expected whitespace after 'AS' in '%s'",
+ trimmed.c_str());
+ }
+
+ std::string output_col = base::TrimWhitespace(remainder.substr(as_pos + 2));
+ if (output_col.empty()) {
+ return base::ErrStatus("propagate spec: empty output column in '%s'",
+ trimmed.c_str());
+ }
+
+ PropagateSpec result;
+ result.agg_op = agg_op;
+ result.source_col_name = std::move(source_col);
+ result.output_col_name = std::move(output_col);
+ return result;
+}
+
+} // namespace perfetto::trace_processor::core::tree
diff --git a/src/trace_processor/core/tree/propagate_spec.h b/src/trace_processor/core/tree/propagate_spec.h
new file mode 100644
index 0000000..f6bdec9
--- /dev/null
+++ b/src/trace_processor/core/tree/propagate_spec.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_CORE_TREE_PROPAGATE_SPEC_H_
+#define SRC_TRACE_PROCESSOR_CORE_TREE_PROPAGATE_SPEC_H_
+
+#include <string>
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/core/common/tree_types.h"
+
+namespace perfetto::trace_processor::core::tree {
+
+struct PropagateSpec {
+ PropagateAggOp agg_op;
+ std::string source_col_name;
+ std::string output_col_name;
+};
+
+// Parses a propagate spec string of the form 'AGG(source_col) AS output_col'.
+// Case-insensitive for the aggregate function name and AS keyword.
+// Whitespace-resilient.
+base::StatusOr<PropagateSpec> ParsePropagateSpec(const std::string& spec);
+
+} // namespace perfetto::trace_processor::core::tree
+
+namespace perfetto::trace_processor {
+namespace tree = core::tree;
+} // namespace perfetto::trace_processor
+
+#endif // SRC_TRACE_PROCESSOR_CORE_TREE_PROPAGATE_SPEC_H_
diff --git a/src/trace_processor/core/tree/tree_transformer.cc b/src/trace_processor/core/tree/tree_transformer.cc
index 393b61d..faac5cc 100644
--- a/src/trace_processor/core/tree/tree_transformer.cc
+++ b/src/trace_processor/core/tree/tree_transformer.cc
@@ -44,6 +44,7 @@
#include "src/trace_processor/core/interpreter/bytecode_interpreter_impl.h" // IWYU pragma: keep
#include "src/trace_processor/core/interpreter/bytecode_registers.h"
#include "src/trace_processor/core/interpreter/interpreter_types.h"
+#include "src/trace_processor/core/tree/propagate_spec.h"
#include "src/trace_processor/core/tree/tree_columns.h"
#include "src/trace_processor/core/util/bit_vector.h"
#include "src/trace_processor/core/util/range.h"
@@ -289,6 +290,78 @@
return base::OkStatus();
}
+// Returns the size in bytes of one element for the given storage type.
+static uint32_t ElementSize(StorageType type) {
+ switch (type.index()) {
+ case StorageType::GetTypeIndex<Uint32>():
+ return sizeof(uint32_t);
+ case StorageType::GetTypeIndex<Int32>():
+ return sizeof(int32_t);
+ case StorageType::GetTypeIndex<Int64>():
+ return sizeof(int64_t);
+ case StorageType::GetTypeIndex<Double>():
+ return sizeof(double);
+ case StorageType::GetTypeIndex<String>():
+ return sizeof(StringPool::Id);
+ default:
+ PERFETTO_FATAL("Unsupported storage type");
+ }
+}
+
+base::Status TreeTransformer::PropagateDown(std::vector<PropagateSpec> specs) {
+ if (cols_.row_count == 0 || specs.empty()) {
+ return base::OkStatus();
+ }
+
+ uint32_t spec_start = static_cast<uint32_t>(propagate_specs_.size());
+
+ for (auto& spec : specs) {
+ auto src = ResolveColumn(spec.source_col_name);
+ if (!src) {
+ return base::ErrStatus("propagate_down: source column '%s' not found",
+ spec.source_col_name.c_str());
+ }
+ StorageType st = cols_.columns[*src].type;
+ if (st.Is<String>()) {
+ return base::ErrStatus(
+ "propagate_down: string columns are not supported (column '%s')",
+ spec.source_col_name.c_str());
+ }
+ if (st.Is<Id>()) {
+ return base::ErrStatus(
+ "propagate_down: Id columns are not supported (column '%s')",
+ spec.source_col_name.c_str());
+ }
+
+ // Add a new column.
+ uint32_t dest = static_cast<uint32_t>(cols_.names.size());
+ cols_.names.push_back(std::move(spec.output_col_name));
+
+ TreeColumns::Column oc;
+ oc.type = st;
+ oc.elem_size = ElementSize(st);
+ oc.data = Slab<uint8_t>::Alloc(static_cast<uint64_t>(cols_.row_count) *
+ oc.elem_size);
+ cols_.columns.push_back(std::move(oc));
+
+ propagate_specs_.push_back({*src, dest, spec.agg_op});
+ }
+
+ // Emit PropagateTreeDown bytecode with the spec range.
+ has_operations_ = true;
+ i::RwHandle<std::unique_ptr<i::TreeState>> ts_reg(tree_state_reg_index_);
+ {
+ using P = i::PropagateTreeDown;
+ auto& op = builder_->AddOpcode<P>(i::Index<P>());
+ op.arg<P::tree_state_register>() = ts_reg;
+ op.arg<P::spec_start>() = spec_start;
+ op.arg<P::spec_count>() =
+ static_cast<uint32_t>(propagate_specs_.size()) - spec_start;
+ }
+
+ return base::OkStatus();
+}
+
base::StatusOr<dataframe::Dataframe> TreeTransformer::ToDataframe() && {
uint32_t n = cols_.row_count;
@@ -338,6 +411,17 @@
ts->null_bitvectors.push_back(std::move(col.null_bv));
}
+ // Populate propagate_down_specs (column indices map 1:1).
+ for (const auto& pi : propagate_specs_) {
+ using PDS = interpreter::TreeState::PropagateDownSpec;
+ ts->propagate_down_specs.push_back(PDS{
+ pi.agg_op,
+ pi.source_col,
+ pi.dest_col,
+ cols_.columns[pi.dest_col].type,
+ });
+ }
+
ts->p2c_offsets = Slab<uint32_t>::Alloc(n + 1);
ts->p2c_children = Slab<uint32_t>::Alloc(n);
ts->p2c_roots = Slab<uint32_t>::Alloc(n);
diff --git a/src/trace_processor/core/tree/tree_transformer.h b/src/trace_processor/core/tree/tree_transformer.h
index a4db801..9f9ce85 100644
--- a/src/trace_processor/core/tree/tree_transformer.h
+++ b/src/trace_processor/core/tree/tree_transformer.h
@@ -30,6 +30,7 @@
#include "src/trace_processor/core/common/storage_types.h"
#include "src/trace_processor/core/dataframe/dataframe.h"
#include "src/trace_processor/core/dataframe/specs.h"
+#include "src/trace_processor/core/tree/propagate_spec.h"
#include "src/trace_processor/core/tree/tree_columns.h"
namespace perfetto::trace_processor::core::interpreter {
@@ -48,7 +49,9 @@
// Usage:
// TreeTransformer t(std::move(cols), pool);
// RETURN_IF_ERROR(t.FilterTree(specs, values));
-// ASSIGN_OR_RETURN(auto result, std::move(t).ToDataframe());
+// RETURN_IF_ERROR(t.PropagateDown(propagate_specs));
+// RETURN_IF_ERROR(t.FilterTree(specs2, values2)); // can filter on
+// propagated ASSIGN_OR_RETURN(auto result, std::move(t).ToDataframe());
class TreeTransformer {
public:
TreeTransformer(TreeColumns cols, StringPool* pool);
@@ -72,6 +75,14 @@
base::Status FilterTree(std::vector<dataframe::FilterSpec> specs,
std::vector<SqlValue> values);
+ // Propagates column values from root toward leaves. For each spec, a new
+ // output column is created and filled by copying the source column and
+ // then applying the aggregate operation downward through the tree via BFS.
+ //
+ // After this call, the new columns can be referenced by name in subsequent
+ // FilterTree() calls.
+ base::Status PropagateDown(std::vector<PropagateSpec> specs);
+
// Finalizes bytecode, executes it, and returns the resulting dataframe.
base::StatusOr<dataframe::Dataframe> ToDataframe() &&;
@@ -83,6 +94,12 @@
uint32_t col; // index into cols_.columns
};
+ struct PropagateInfo {
+ uint32_t source_col; // cols_.columns index
+ uint32_t dest_col; // cols_.columns index
+ PropagateAggOp agg_op;
+ };
+
TreeColumns cols_;
StringPool* pool_;
@@ -100,6 +117,9 @@
std::vector<SqlValue> filter_values_;
uint32_t filter_value_count_ = 0;
+ // Propagation specs accumulated across PropagateDown calls.
+ std::vector<PropagateInfo> propagate_specs_;
+
// Whether any bytecodes have been emitted.
bool has_operations_ = false;
};
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/BUILD.gn
index 18ef2e2..b733f6d 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/BUILD.gn
@@ -20,6 +20,8 @@
"tree_filter.h",
"tree_functions.cc",
"tree_functions.h",
+ "tree_propagate.cc",
+ "tree_propagate.h",
]
deps = [
"../../../../../../gn:default_deps",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc
index 305a70d..6f8d1eb 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_functions.cc
@@ -22,6 +22,7 @@
#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
#include "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_conversion.h"
#include "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_filter.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h"
namespace perfetto::trace_processor {
@@ -32,6 +33,7 @@
RETURN_IF_ERROR(engine.RegisterFunction<TreeConstraint>(nullptr));
RETURN_IF_ERROR(engine.RegisterFunction<TreeWhereAnd>(nullptr));
RETURN_IF_ERROR(engine.RegisterFunction<TreeFilter>(nullptr));
+ RETURN_IF_ERROR(engine.RegisterFunction<TreePropagateDown>(nullptr));
return base::OkStatus();
}
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.cc
new file mode 100644
index 0000000..e0d112c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.cc
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/trace_processor/core/tree/propagate_spec.h"
+#include "src/trace_processor/core/tree/tree_transformer.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_value.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+
+namespace perfetto::trace_processor {
+
+void TreePropagateDown::Step(sqlite3_context* ctx,
+ int argc,
+ sqlite3_value** argv) {
+ if (argc < 2) {
+ return sqlite::result::Error(
+ ctx,
+ "tree_propagate_down: expected at least 2 arguments "
+ "(tree_ptr, 'AGG(col) AS alias', ...)");
+ }
+
+ // Extract tree pointer.
+ auto* tree_ptr =
+ sqlite::value::Pointer<sqlite::utils::MovePointer<tree::TreeTransformer>>(
+ argv[0], "TREE_TRANSFORMER");
+ if (!tree_ptr) {
+ return sqlite::result::Error(
+ ctx, "tree_propagate_down: expected TREE_TRANSFORMER");
+ }
+ if (tree_ptr->taken()) {
+ return sqlite::result::Error(
+ ctx, "tree_propagate_down: tree has already been consumed");
+ }
+
+ // Parse each spec string.
+ std::vector<tree::PropagateSpec> specs;
+ for (int i = 1; i < argc; ++i) {
+ if (sqlite::value::Type(argv[i]) != sqlite::Type::kText) {
+ return sqlite::result::Error(
+ ctx, "tree_propagate_down: spec arguments must be strings");
+ }
+ std::string spec_str = sqlite::value::Text(argv[i]);
+ SQLITE_ASSIGN_OR_RETURN(ctx, auto spec, tree::ParsePropagateSpec(spec_str));
+ specs.push_back(std::move(spec));
+ }
+
+ auto transformer = tree_ptr->Take();
+ auto status = transformer.PropagateDown(std::move(specs));
+ SQLITE_RETURN_IF_ERROR(ctx, status);
+
+ return sqlite::result::UniquePointer(
+ ctx,
+ std::make_unique<sqlite::utils::MovePointer<tree::TreeTransformer>>(
+ std::move(transformer)),
+ "TREE_TRANSFORMER");
+}
+
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h
new file mode 100644
index 0000000..6726f4d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/trees/tree_propagate.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_TREES_TREE_PROPAGATE_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_TREES_TREE_PROPAGATE_H_
+
+#include "src/trace_processor/sqlite/bindings/sqlite_function.h"
+
+namespace perfetto::trace_processor {
+
+// Variadic scalar function that propagates column values from root toward
+// leaves using BFS. Takes a tree pointer followed by one or more spec strings
+// of the form 'AGG(source_col) AS output_col'.
+struct TreePropagateDown : public sqlite::Function<TreePropagateDown> {
+ static constexpr char kName[] = "__intrinsic_tree_propagate_down";
+ static constexpr int kArgCount = -1;
+
+ static void Step(sqlite3_context* ctx, int argc, sqlite3_value** argv);
+};
+
+} // namespace perfetto::trace_processor
+
+#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_FUNCTIONS_TREES_TREE_PROPAGATE_H_
diff --git a/src/trace_processor/perfetto_sql/stdlib/std/trees/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/std/trees/BUILD.gn
index f40448f..882d6d1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/std/trees/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/std/trees/BUILD.gn
@@ -17,6 +17,7 @@
perfetto_sql_source_set("trees") {
sources = [
"filter.sql",
+ "propagate.sql",
"table_conversion.sql",
]
}
diff --git a/src/trace_processor/perfetto_sql/stdlib/std/trees/propagate.sql b/src/trace_processor/perfetto_sql/stdlib/std/trees/propagate.sql
new file mode 100644
index 0000000..d0d5cae
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/std/trees/propagate.sql
@@ -0,0 +1,45 @@
+--
+-- Copyright 2026 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+-- sqlformat file off
+
+-- Propagates column values from root toward leaves using BFS.
+--
+-- For each spec, a new output column is created by copying the source column
+-- and then applying the aggregate operation downward through the tree.
+-- For each parent→child edge: output[child] = agg(output[parent], output[child]).
+--
+-- Each spec is a string of the form 'AGG(source_col) AS output_col' where AGG
+-- is one of SUM, MIN, MAX, FIRST, LAST. Case-insensitive.
+--
+-- Example usage:
+-- ```
+-- SELECT * FROM _tree_to_table!(
+-- _tree_propagate_down(
+-- _tree_from_table!((SELECT * FROM calls), (fn, ones)),
+-- 'SUM(ones) AS depth'
+-- ),
+-- (fn, depth)
+-- );
+-- ```
+CREATE PERFETTO FUNCTION _tree_propagate_down(
+ -- A TREE pointer from _tree_from_table or another tree operation.
+ tree_ptr ANY,
+ -- Propagation specs: 'AGG(source_col) AS output_col' (variadic)
+ specs ANY...
+)
+-- Returns a TREE pointer with the propagated columns added.
+RETURNS ANY
+DELEGATES TO __intrinsic_tree_propagate_down;
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 4d7d45a..094de96 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -177,6 +177,7 @@
from diff_tests.stdlib.traced.stats import TracedStats
from diff_tests.stdlib.trees.table_conversion_tests import TreeRoundtrip
from diff_tests.stdlib.trees.tree_filter_tests import TreeFilter
+from diff_tests.stdlib.trees.tree_propagate_tests import TreePropagate
from diff_tests.stdlib.viz.tests import Viz
from diff_tests.stdlib.wattson.tests import WattsonStdlib
from diff_tests.syntax.filtering_tests import PerfettoFiltering
@@ -321,6 +322,7 @@
GraphScanTests,
TreeRoundtrip,
TreeFilter,
+ TreePropagate,
ExportTests,
Frames,
GraphSearchTests,
diff --git a/test/trace_processor/diff_tests/stdlib/trees/tree_propagate_tests.py b/test/trace_processor/diff_tests/stdlib/trees/tree_propagate_tests.py
new file mode 100644
index 0000000..8cf908b
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/trees/tree_propagate_tests.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python3
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class TreePropagate(TestSuite):
+ """Tests for tree propagate_down operations."""
+
+ def test_propagate_down_depth_sum(self):
+ """SUM(ones) AS depth computes depth: root=1, children=2, etc."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'main' AS fn, 1 AS ones
+ UNION ALL SELECT 2, 1, 'parse', 1
+ UNION ALL SELECT 3, 2, 'lex', 1
+ UNION ALL SELECT 4, 1, 'emit', 1
+ UNION ALL SELECT 5, 4, 'write', 1;
+
+ SELECT fn, depth
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, ones)),
+ 'SUM(ones) AS depth'
+ ),
+ (fn, depth)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth"
+ "emit",2
+ "lex",3
+ "main",1
+ "parse",2
+ "write",3
+ """))
+
+ def test_propagate_down_multiple_columns(self):
+ """Multiple specs in one call: SUM + MAX."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'main' AS fn, 1 AS ones, 10 AS prio
+ UNION ALL SELECT 2, 1, 'parse', 1, 5
+ UNION ALL SELECT 3, 2, 'lex', 1, 8
+ UNION ALL SELECT 4, 1, 'emit', 1, 3;
+
+ SELECT fn, depth, max_prio
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, ones, prio)),
+ 'SUM(ones) AS depth',
+ 'MAX(prio) AS max_prio'
+ ),
+ (fn, depth, max_prio)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth","max_prio"
+ "emit",2,10
+ "lex",3,10
+ "main",1,10
+ "parse",2,10
+ """))
+
+ def test_propagate_down_min(self):
+ """MIN propagation: each node gets min of its value and parent's propagated value."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'root' AS fn, 5 AS val
+ UNION ALL SELECT 2, 1, 'a', 10
+ UNION ALL SELECT 3, 2, 'b', 3
+ UNION ALL SELECT 4, 1, 'c', 2;
+
+ SELECT fn, min_val
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, val)),
+ 'MIN(val) AS min_val'
+ ),
+ (fn, min_val)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","min_val"
+ "a",5
+ "b",3
+ "c",2
+ "root",5
+ """))
+
+ def test_propagate_down_first(self):
+ """FIRST propagation: each node gets the root's value."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'root' AS fn, 100 AS val
+ UNION ALL SELECT 2, 1, 'a', 50
+ UNION ALL SELECT 3, 2, 'b', 25;
+
+ SELECT fn, first_val
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, val)),
+ 'FIRST(val) AS first_val'
+ ),
+ (fn, first_val)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","first_val"
+ "a",100
+ "b",100
+ "root",100
+ """))
+
+ def test_propagate_down_last(self):
+ """LAST propagation: each node keeps its own value (no-op)."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'root' AS fn, 100 AS val
+ UNION ALL SELECT 2, 1, 'a', 50
+ UNION ALL SELECT 3, 2, 'b', 25;
+
+ SELECT fn, last_val
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, val)),
+ 'LAST(val) AS last_val'
+ ),
+ (fn, last_val)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","last_val"
+ "a",50
+ "b",25
+ "root",100
+ """))
+
+ def test_propagate_down_after_filter(self):
+ """Propagation after filtering: filter first, then propagate depth."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.filter;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'main' AS fn, 100 AS dur, 1 AS ones
+ UNION ALL SELECT 2, 1, 'parse', 60, 1
+ UNION ALL SELECT 3, 2, 'lex', 30, 1
+ UNION ALL SELECT 4, 1, 'alloc', 5, 1
+ UNION ALL SELECT 5, 1, 'emit', 35, 1;
+
+ SELECT fn, depth
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_filter(
+ _tree_from_table!((SELECT * FROM calls), (fn, dur, ones)),
+ _tree_where(_tree_constraint('dur', '>=', 10))
+ ),
+ 'SUM(ones) AS depth'
+ ),
+ (fn, depth)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth"
+ "emit",2
+ "lex",3
+ "main",1
+ "parse",2
+ """))
+
+ def test_propagate_down_multiple_roots(self):
+ """Propagation with a forest (multiple roots)."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'tree1' AS fn, 1 AS ones
+ UNION ALL SELECT 2, 1, 'a', 1
+ UNION ALL SELECT 3, NULL, 'tree2', 1
+ UNION ALL SELECT 4, 3, 'b', 1;
+
+ SELECT fn, depth
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, ones)),
+ 'SUM(ones) AS depth'
+ ),
+ (fn, depth)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth"
+ "a",2
+ "b",2
+ "tree1",1
+ "tree2",1
+ """))
+
+ def test_propagate_down_single_node(self):
+ """Edge case: single-node tree."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'root' AS fn, 42 AS val;
+
+ SELECT fn, s
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, val)),
+ 'SUM(val) AS s'
+ ),
+ (fn, s)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","s"
+ "root",42
+ """))
+
+ def test_propagate_down_filter_then_propagate_then_filter(self):
+ """filter → propagate → filter(propagated_col) chain."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.filter;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'main' AS fn, 100 AS dur, 1 AS ones
+ UNION ALL SELECT 2, 1, 'parse', 60, 1
+ UNION ALL SELECT 3, 2, 'lex', 30, 1
+ UNION ALL SELECT 4, 1, 'alloc', 5, 1
+ UNION ALL SELECT 5, 1, 'emit', 35, 1;
+
+ -- Filter short calls, propagate depth, then filter depth >= 2.
+ SELECT fn, depth
+ FROM _tree_to_table!(
+ _tree_filter(
+ _tree_propagate_down(
+ _tree_filter(
+ _tree_from_table!((SELECT * FROM calls), (fn, dur, ones)),
+ _tree_where(_tree_constraint('dur', '>=', 10))
+ ),
+ 'SUM(ones) AS depth'
+ ),
+ _tree_where(_tree_constraint('depth', '>=', 2))
+ ),
+ (fn, depth)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth"
+ "emit",2
+ "lex",3
+ "parse",2
+ """))
+
+ def test_propagate_down_chained(self):
+ """Chained propagation: use output of first propagation as source."""
+ return DiffTestBlueprint(
+ trace=DataPath('counters.json'),
+ query="""
+ INCLUDE PERFETTO MODULE std.trees.table_conversion;
+ INCLUDE PERFETTO MODULE std.trees.propagate;
+
+ CREATE PERFETTO TABLE calls AS
+ SELECT 1 AS id, NULL AS parent_id, 'main' AS fn, 1 AS ones
+ UNION ALL SELECT 2, 1, 'parse', 1
+ UNION ALL SELECT 3, 2, 'lex', 1
+ UNION ALL SELECT 4, 1, 'emit', 1;
+
+ -- First propagation: SUM(ones) AS depth -> 1,2,3,2
+ -- Second propagation: SUM(depth) AS cumulative_depth -> 1,3,6,3
+ SELECT fn, depth, cumulative_depth
+ FROM _tree_to_table!(
+ _tree_propagate_down(
+ _tree_propagate_down(
+ _tree_from_table!((SELECT * FROM calls), (fn, ones)),
+ 'SUM(ones) AS depth'
+ ),
+ 'SUM(depth) AS cumulative_depth'
+ ),
+ (fn, depth, cumulative_depth)
+ )
+ ORDER BY fn;
+ """,
+ out=Csv("""
+ "fn","depth","cumulative_depth"
+ "emit",2,3
+ "lex",3,6
+ "main",1,1
+ "parse",2,3
+ """))