diff --git a/Android.bp b/Android.bp index 8fd151c442..ff2ab4ac75 100644 --- a/Android.bp +++ b/Android.bp @@ -17773,6 +17773,7 @@ filegroup { "src/trace_processor/core/exec/row_batch.cc", "src/trace_processor/core/exec/row_cursor.cc", "src/trace_processor/core/exec/row_store.cc", + "src/trace_processor/core/exec/tree_number_nodes.cc", ], } @@ -17790,6 +17791,7 @@ filegroup { "src/trace_processor/core/exec/operator_unittest.cc", "src/trace_processor/core/exec/row_batch_unittest.cc", "src/trace_processor/core/exec/row_store_unittest.cc", + "src/trace_processor/core/exec/tree_number_nodes_unittest.cc", "src/trace_processor/core/exec/variant_unittest.cc", ], } diff --git a/gn/perfetto_benchmarks.gni b/gn/perfetto_benchmarks.gni index e7701db439..3ba634d3dc 100644 --- a/gn/perfetto_benchmarks.gni +++ b/gn/perfetto_benchmarks.gni @@ -24,6 +24,7 @@ perfetto_benchmarks_targets = [ "src/trace_processor:benchmarks", "src/trace_processor/containers:benchmarks", "src/trace_processor/importers/common:benchmarks", + "src/trace_processor/core/exec:benchmarks", "src/trace_processor/core/util:benchmarks", "src/trace_processor/core/interpreter:benchmarks", "src/trace_processor/core/tree:benchmarks", diff --git a/src/trace_processor/core/exec/BUILD.gn b/src/trace_processor/core/exec/BUILD.gn index 191a5b2324..6f4b8aebf4 100644 --- a/src/trace_processor/core/exec/BUILD.gn +++ b/src/trace_processor/core/exec/BUILD.gn @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import("../../../../gn/perfetto.gni") import("../../../../gn/test.gni") source_set("exec") { @@ -34,6 +35,8 @@ source_set("exec") { "row_selection.h", "row_store.cc", "row_store.h", + "tree_number_nodes.cc", + "tree_number_nodes.h", "variant.h", ] deps = [ @@ -60,6 +63,7 @@ perfetto_unittest_source_set("unittests") { "operator_unittest.cc", "row_batch_unittest.cc", "row_store_unittest.cc", + "tree_number_nodes_unittest.cc", "variant_unittest.cc", ] deps = [ @@ -74,3 +78,18 @@ perfetto_unittest_source_set("unittests") { "../util", ] } + +if (enable_perfetto_benchmarks) { + source_set("benchmarks") { + testonly = true + sources = [ "tree_number_nodes_benchmark.cc" ] + deps = [ + ":exec", + "../../../../gn:benchmark", + "../../../../gn:default_deps", + "../../containers", + "../common", + "../dataframe", + ] + } +} diff --git a/src/trace_processor/core/exec/tree_number_nodes.cc b/src/trace_processor/core/exec/tree_number_nodes.cc new file mode 100644 index 0000000000..b834cd729c --- /dev/null +++ b/src/trace_processor/core/exec/tree_number_nodes.cc @@ -0,0 +1,309 @@ +/* + * 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/exec/tree_number_nodes.h" + +#include +#include +#include + +#include "perfetto/base/status.h" +#include "src/trace_processor/containers/string_pool.h" +#include "src/trace_processor/core/common/storage_types.h" +#include "src/trace_processor/core/exec/column_view.h" +#include "src/trace_processor/core/exec/operator.h" +#include "src/trace_processor/core/exec/row_batch.h" +#include "src/trace_processor/core/exec/row_selection.h" +#include "src/trace_processor/core/exec/variant.h" +#include "src/trace_processor/core/util/bit_vector.h" +#include "src/trace_processor/core/util/flex_vector.h" + +namespace perfetto::trace_processor::core::exec { +namespace { + +// Reads a column of any width into one key per row. The type is dispatched on +// once per batch, so the loop itself has no per-row dispatch. +Variant AsKey(uint32_t v) { + return Variant::Int64(v); +} +Variant AsKey(int32_t v) { + return Variant::Int64(v); +} +Variant AsKey(int64_t v) { + return Variant::Int64(v); +} +Variant AsKey(StringPool::Id v) { + return Variant::String(v); +} + +template +void KeysOf(const ColumnView& column, uint32_t count, Variant* keys) { + const auto* data = static_cast(column.data()); + RowSelection selection = column.selection(); + if (selection.is_range()) { + const T* from = data + selection.offset(); + for (uint32_t i = 0; i < count; ++i) { + keys[i] = AsKey(from[i]); + } + return; + } + const uint32_t* rows = selection.data(); + for (uint32_t i = 0; i < count; ++i) { + keys[i] = AsKey(data[rows[i]]); + } +} + +void SequenceKeys(const ColumnView& column, uint32_t count, Variant* keys) { + RowSelection selection = column.selection(); + for (uint32_t i = 0; i < count; ++i) { + keys[i] = Variant::Int64(selection.GetIndex(i)); + } +} + +// Reads a column of any type into one Variant per row. A null row is only +// allowed where `nullable`. +base::Status ReadKeys(const ColumnView& column, + uint32_t count, + bool nullable, + FlexVector* out) { + Variant* keys = out->data(); + if (column.kind() == ColumnView::Kind::kVariant) { + const auto* cells = static_cast(column.data()); + RowSelection selection = column.selection(); + for (uint32_t i = 0; i < count; ++i) { + const Variant& cell = cells[selection.GetIndex(i)]; + if (cell.type == Variant::Type::kDouble) { + return base::ErrStatus("TREE NUMBER NODES: an id cannot be a float"); + } + if (cell.type == Variant::Type::kNull && !nullable) { + return base::ErrStatus("TREE NUMBER NODES: a row has no id"); + } + keys[i] = cell; + } + return base::OkStatus(); + } + + StorageType type = column.type(); + if (type.Is()) { + SequenceKeys(column, count, keys); + } else if (type.Is()) { + KeysOf(column, count, keys); + } else if (type.Is()) { + KeysOf(column, count, keys); + } else if (type.Is()) { + KeysOf(column, count, keys); + } else if (type.Is()) { + KeysOf(column, count, keys); + } else { + return base::ErrStatus("TREE NUMBER NODES: an id cannot be a float"); + } + const BitVector* validity = column.validity(); + if (!validity) { + return base::OkStatus(); + } + // A column can carry a validity bitvector without any row being null, so + // check whether a row is actually null rather than whether it could be. + RowSelection selection = column.selection(); + for (uint32_t i = 0; i < count; ++i) { + if (validity->is_set(selection.GetIndex(i))) { + continue; + } + if (!nullable) { + return base::ErrStatus("TREE NUMBER NODES: a row has no id"); + } + keys[i] = Variant::Null(); + } + return base::OkStatus(); +} + +// A table scanned in row order: the ids are the rows, so `start` onwards, and +// each parent refers to a row at or before its own. Copies the parents out, +// nulls as kNoNode, or gives up on the first row which is not like that. +bool ParentsInOrder(const ColumnView& ids, + const ColumnView& parents, + uint32_t count, + uint32_t start, + uint32_t* out) { + if (ids.kind() == ColumnView::Kind::kVariant || !ids.type().Is() || + ids.validity() || !ids.selection().is_range() || + ids.selection().offset() != start || + parents.kind() == ColumnView::Kind::kVariant || + !parents.type().Is()) { + return false; + } + const auto* data = static_cast(parents.data()); + const BitVector* validity = parents.validity(); + RowSelection selection = parents.selection(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t index = selection.GetIndex(i); + if (validity && !validity->is_set(index)) { + out[i] = kNoNode; + } else if (data[index] <= start + i) { + out[i] = data[index]; + } else { + return false; + } + } + return true; +} + +} // namespace + +TreeNumberNodes::TreeNumberNodes(uint32_t id_column, uint32_t parent_column) + : id_column_(id_column), parent_column_(parent_column) {} + +TreeNumberNodes::~TreeNumberNodes() = default; +TreeNumberNodes::State::~State() = default; + +std::unique_ptr TreeNumberNodes::MakeState() const { + auto state = std::make_unique(); + state->ids = FlexVector::CreateWithSize(kMaxBatchRows); + state->parents = FlexVector::CreateWithSize(kMaxBatchRows); + state->nodes = FlexVector::CreateWithSize(kMaxBatchRows); + state->parent_nodes = FlexVector::CreateWithSize(kMaxBatchRows); + return state; +} + +void TreeNumberNodes::Rewind(OperatorState& state) const { + State& s = state.Cast(); + s.dense = true; + s.numbered = 0; + s.numbers.Clear(); + s.has_row.clear(); + s.status = base::OkStatus(); +} + +base::Status TreeNumberNodes::status(const OperatorState& state) const { + return state.Cast().status; +} + +bool TreeNumberNodes::IsDenseForTesting(const OperatorState& state) const { + return state.Cast().dense; +} + +uint32_t TreeNumberNodes::Number(State& s, const Variant& id) const { + Key key = id.type == Variant::Type::kString + ? Key{id.AsString().raw_id(), true} + : Key{id.AsInt64(), false}; + if (s.dense) { + if (!key.is_string) { + // Every integer id below `numbered` was handed out in order. + if (key.value >= 0 && static_cast(key.value) < s.numbered) { + return static_cast(key.value); + } + if (key.value == static_cast(s.numbered)) { + if (s.numbered == kNoNode) { + s.status = base::ErrStatus( + "TREE NUMBER NODES: the relation has too many nodes"); + return kNoNode; + } + return s.numbered++; + } + } + // Not dense after all, so record the numbering identity had implied. + s.dense = false; + for (uint32_t n = 0; n < s.numbered; ++n) { + s.numbers.Insert(Key{static_cast(n), false}, n); + } + } + if (uint32_t* existing = s.numbers.Find(key); existing) { + return *existing; + } + if (s.numbered == kNoNode) { + s.status = + base::ErrStatus("TREE NUMBER NODES: the relation has too many nodes"); + return kNoNode; + } + uint32_t assigned = s.numbered++; + s.numbers.Insert(key, assigned); + return assigned; +} + +bool TreeNumberNodes::NumberInOrder(const RowBatch& in, + uint32_t count, + State& s) const { + uint32_t start = s.numbered; + if (count > kNoNode - start || + !ParentsInOrder(in.column(id_column_), in.column(parent_column_), count, + start, s.parent_nodes.data())) { + return false; + } + for (uint32_t i = 0; i < count; ++i) { + s.nodes[i] = start + i; + } + s.numbered = start + count; + // A parent numbered ahead of its row must not be marked as seen. + s.has_row.resize(start); + s.has_row.resize(s.numbered, true); + return true; +} + +bool TreeNumberNodes::NumberByKey(const RowBatch& in, + uint32_t count, + State& s) const { + base::Status status = ReadKeys(in.column(id_column_), count, false, &s.ids); + if (status.ok()) { + status = ReadKeys(in.column(parent_column_), count, true, &s.parents); + } + if (!status.ok()) { + s.status = status; + return false; + } + for (uint32_t i = 0; i < count; ++i) { + uint32_t node = Number(s, s.ids[i]); + if (!s.status.ok()) { + return false; + } + if (s.has_row.size() <= node) { + // Grown geometrically, as resize allocates exactly what it is asked for. + s.has_row.resize(std::max(node + 1, s.has_row.size() * 2)); + } + if (s.has_row.is_set(node)) { + s.status = base::ErrStatus( + "TREE NUMBER NODES: more than one row has the same id"); + return false; + } + s.has_row.set(node); + s.nodes[i] = node; + if (s.parents[i].type == Variant::Type::kNull) { + s.parent_nodes[i] = kNoNode; + continue; + } + s.parent_nodes[i] = Number(s, s.parents[i]); + if (!s.status.ok()) { + return false; + } + } + return true; +} + +OpResult TreeNumberNodes::Execute(const RowBatch& in, + RowBatch& out, + OperatorState& state) const { + State& s = state.Cast(); + uint32_t count = in.size(); + bool in_order = s.dense && NumberInOrder(in, count, s); + if (!in_order && !NumberByKey(in, count, s)) { + return OpResult::kError; + } + out.CopyFrom(in); + out.AddColumn(ColumnView::Reference(StorageType{Uint32{}}, s.nodes.data())); + out.AddColumn( + ColumnView::Reference(StorageType{Uint32{}}, s.parent_nodes.data())); + return OpResult::kNeedMoreInput; +} + +} // namespace perfetto::trace_processor::core::exec diff --git a/src/trace_processor/core/exec/tree_number_nodes.h b/src/trace_processor/core/exec/tree_number_nodes.h new file mode 100644 index 0000000000..c965f9e022 --- /dev/null +++ b/src/trace_processor/core/exec/tree_number_nodes.h @@ -0,0 +1,105 @@ +/* + * 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_EXEC_TREE_NUMBER_NODES_H_ +#define SRC_TRACE_PROCESSOR_CORE_EXEC_TREE_NUMBER_NODES_H_ + +#include +#include +#include +#include + +#include "perfetto/base/status.h" +#include "perfetto/ext/base/flat_hash_map.h" +#include "src/trace_processor/core/exec/operator.h" +#include "src/trace_processor/core/exec/row_batch.h" +#include "src/trace_processor/core/exec/variant.h" +#include "src/trace_processor/core/util/bit_vector.h" +#include "src/trace_processor/core/util/flex_vector.h" + +namespace perfetto::trace_processor::core::exec { + +// The parent of a root row. +inline constexpr uint32_t kNoNode = std::numeric_limits::max(); + +// Appends node and parent-node Uint32 columns to a relation. Numbers are handed +// out densely from zero in order of first sighting. +// +// This is the only operator which has to know how a relation stores its ids: +// they can be of any width, and a filtered relation's ids are scattered over a +// wide range. Numbering them densely means an array indexed by node is the +// size of the input rather than of the table it was filtered from, and lets +// every operator downstream deal only in node numbers. +// +// A table scanned in row order with every parent an earlier row is the common +// case: the ids are the numbers, so nothing is looked up or checked. +class TreeNumberNodes : public Operator { + public: + TreeNumberNodes(uint32_t id_column, uint32_t parent_column); + ~TreeNumberNodes() override; + + std::unique_ptr MakeState() const override; + OpResult Execute(const RowBatch&, RowBatch&, OperatorState&) const override; + void Rewind(OperatorState&) const override; + base::Status status(const OperatorState&) const override; + + // Whether every id seen so far was already its own node number. + bool IsDenseForTesting(const OperatorState&) const; + + private: + struct Key { + int64_t value; + bool is_string; + + bool operator==(const Key& other) const { + return value == other.value && is_string == other.is_string; + } + template + friend H PerfettoHashValue(H h, const Key& key) { + return H::Combine(std::move(h), key.value, key.is_string); + } + }; + struct State : OperatorState { + ~State() override; + // While the ids arriving are 0, 1, 2, ... they are already node numbers, + // so the map stays empty. + bool dense = true; + uint32_t numbered = 0; + base::FlatHashMap numbers; + // Which nodes have had a row of their own, to catch an id used twice. + BitVector has_row; + // The batch's ids and parent ids, whatever type they arrived as. + FlexVector ids; + FlexVector parents; + // The two columns appended to the batch. + FlexVector nodes; + FlexVector parent_nodes; + base::Status status = base::OkStatus(); + }; + + // Numbers a batch of a table scanned in row order whose parents all point + // back, or returns false having changed nothing. + bool NumberInOrder(const RowBatch&, uint32_t count, State&) const; + bool NumberByKey(const RowBatch&, uint32_t count, State&) const; + uint32_t Number(State&, const Variant& id) const; + + uint32_t id_column_; + uint32_t parent_column_; +}; + +} // namespace perfetto::trace_processor::core::exec + +#endif // SRC_TRACE_PROCESSOR_CORE_EXEC_TREE_NUMBER_NODES_H_ diff --git a/src/trace_processor/core/exec/tree_number_nodes_benchmark.cc b/src/trace_processor/core/exec/tree_number_nodes_benchmark.cc new file mode 100644 index 0000000000..3489985ea9 --- /dev/null +++ b/src/trace_processor/core/exec/tree_number_nodes_benchmark.cc @@ -0,0 +1,151 @@ +/* + * 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/exec/tree_number_nodes.h" + +#include + +#include +#include +#include +#include + +#include "src/trace_processor/containers/string_pool.h" +#include "src/trace_processor/core/common/storage_types.h" +#include "src/trace_processor/core/dataframe/dataframe.h" +#include "src/trace_processor/core/exec/dataframe_scan.h" +#include "src/trace_processor/core/exec/operator.h" +#include "src/trace_processor/core/exec/row_batch.h" + +namespace perfetto::trace_processor::core::exec { +namespace { + +// The shape of stack_profile_callsite: the id is the row, the parent is a +// sparse-null reference to an earlier row. +inline constexpr auto kIdColumn = dataframe::CreateTypedDataframeSpec( + {"id", "parent_id"}, + dataframe::CreateTypedColumnSpec(Id{}, + NonNull{}, + IdSorted{}, + NoDuplicates{}), + dataframe::CreateTypedColumnSpec(Uint32{}, + SparseNullWithPopcountAlways{}, + Unsorted{}, + HasDuplicates{})); + +// The same tree, but with ids spread over three times their range, as they +// would be after filtering a larger table. +inline constexpr auto kScatteredIds = dataframe::CreateTypedDataframeSpec( + {"id", "parent_id"}, + dataframe::CreateTypedColumnSpec(Uint32{}, + NonNull{}, + Unsorted{}, + NoDuplicates{}), + dataframe::CreateTypedColumnSpec(Uint32{}, + SparseNullWithPopcountAlways{}, + Unsorted{}, + HasDuplicates{})); + +std::optional ParentOf(uint32_t row) { + if (row == 0) { + return std::nullopt; + } + return (row - 1) / 2; +} + +void RunTreeNumberNodes(benchmark::State& state, + const dataframe::Dataframe& df) { + DataframeScan scan(df, {0, 1}); + std::unique_ptr scan_state = scan.MakeState(); + TreeNumberNodes op(0, 1); + std::unique_ptr op_state = op.MakeState(); + RowBatch in; + RowBatch out; + for (auto _ : state) { + scan.Rewind(*scan_state); + op.Rewind(*op_state); + while (scan.GetData(in, *scan_state)) { + OpResult result = op.Execute(in, out, *op_state); + if (result == OpResult::kError) { + state.SkipWithError(op.status(*op_state).c_message()); + return; + } + benchmark::DoNotOptimize(out.column(3).data()); + } + } + state.SetItemsProcessed(static_cast(state.iterations()) * + df.row_count()); +} + +dataframe::Dataframe BuildIdColumn(uint32_t rows, StringPool* pool) { + dataframe::Dataframe df = + dataframe::Dataframe::CreateFromTypedSpec(kIdColumn, pool); + for (uint32_t row = 0; row < rows; ++row) { + df.InsertUnchecked(kIdColumn, std::monostate{}, ParentOf(row)); + } + df.Finalize(); + return df; +} + +// The cost of the scan alone, to compare the operator against. +void BM_TreeNumberNodesScanOnly(benchmark::State& state) { + StringPool pool; + dataframe::Dataframe df = + BuildIdColumn(static_cast(state.range(0)), &pool); + DataframeScan scan(df, {0, 1}); + std::unique_ptr scan_state = scan.MakeState(); + RowBatch in; + for (auto _ : state) { + scan.Rewind(*scan_state); + while (scan.GetData(in, *scan_state)) { + benchmark::DoNotOptimize(in.column(1).data()); + } + } + state.SetItemsProcessed(static_cast(state.iterations()) * + df.row_count()); +} +BENCHMARK(BM_TreeNumberNodesScanOnly)->Arg(10000)->Arg(100000)->Arg(1000000); + +void BM_TreeNumberNodesIdColumn(benchmark::State& state) { + StringPool pool; + dataframe::Dataframe df = + BuildIdColumn(static_cast(state.range(0)), &pool); + RunTreeNumberNodes(state, df); +} +BENCHMARK(BM_TreeNumberNodesIdColumn)->Arg(10000)->Arg(100000)->Arg(1000000); + +void BM_TreeNumberNodesScatteredIds(benchmark::State& state) { + StringPool pool; + dataframe::Dataframe df = + dataframe::Dataframe::CreateFromTypedSpec(kScatteredIds, &pool); + const auto rows = static_cast(state.range(0)); + for (uint32_t row = 0; row < rows; ++row) { + std::optional parent = ParentOf(row); + if (parent) { + *parent *= 3; + } + df.InsertUnchecked(kScatteredIds, row * 3, parent); + } + df.Finalize(); + RunTreeNumberNodes(state, df); +} +BENCHMARK(BM_TreeNumberNodesScatteredIds) + ->Arg(10000) + ->Arg(100000) + ->Arg(1000000); + +} // namespace +} // namespace perfetto::trace_processor::core::exec diff --git a/src/trace_processor/core/exec/tree_number_nodes_unittest.cc b/src/trace_processor/core/exec/tree_number_nodes_unittest.cc new file mode 100644 index 0000000000..c2196c2d64 --- /dev/null +++ b/src/trace_processor/core/exec/tree_number_nodes_unittest.cc @@ -0,0 +1,366 @@ +/* + * 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/exec/tree_number_nodes.h" + +#include +#include +#include +#include +#include +#include + +#include "src/trace_processor/containers/string_pool.h" +#include "src/trace_processor/core/common/storage_types.h" +#include "src/trace_processor/core/dataframe/dataframe.h" +#include "src/trace_processor/core/exec/column_view.h" +#include "src/trace_processor/core/exec/dataframe_scan.h" +#include "src/trace_processor/core/exec/operator.h" +#include "src/trace_processor/core/exec/row_batch.h" +#include "src/trace_processor/core/exec/row_selection.h" +#include "src/trace_processor/core/exec/test_utils.h" +#include "src/trace_processor/core/exec/variant.h" +#include "src/trace_processor/core/util/bit_vector.h" +#include "test/gtest_and_gmock.h" + +namespace perfetto::trace_processor::core::exec { +namespace { + +using ::testing::ElementsAre; + +// Runs one batch of ids and parent ids, of any type, through the operator. +template +struct Numbered { + Numbered(StorageType type, + std::vector ids, + std::vector parents, + std::vector has_parent) + : op(0, 1), + state(op.MakeState()), + ids_(std::move(ids)), + parents_(std::move(parents)) { + auto count = static_cast(ids_.size()); + validity_ = BitVector::CreateWithSize(count); + for (uint32_t i = 0; i < count; ++i) { + if (has_parent[i]) { + validity_.set(i); + } + } + in.AddColumn(ColumnView::Reference(type, ids_.data())); + in.AddColumn(ColumnView::Reference(type, parents_.data(), &validity_)); + in.Compose(RowSelection::Range(0), count); + in.SetCardinality(count); + } + + OpResult Execute() { return op.Execute(in, out, *state); } + + TreeNumberNodes op; + std::unique_ptr state; + std::vector ids_; + std::vector parents_; + BitVector validity_; + RowBatch in; + RowBatch out; +}; + +TEST(TreeNumberNodesTest, IdsWhichAreAlreadyNodeNumbersAreLeftAlone) { + Numbered run(StorageType{Int64{}}, {0, 1, 2}, {0, 0, 1}, + {false, true, true}); + ASSERT_EQ(run.Execute(), OpResult::kNeedMoreInput); + + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(0u, 1u, 2u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), + ElementsAre(kNoNode, 0u, 1u)); + EXPECT_TRUE(run.op.IsDenseForTesting(*run.state)); +} + +// A filtered relation's ids are scattered over a wide range; numbering them +// makes an array indexed by node the size of the input. +TEST(TreeNumberNodesTest, AScatteringOfIdsIsNumberedDensely) { + Numbered run(StorageType{Int64{}}, {500, 900, 700}, {0, 500, 900}, + {false, true, true}); + ASSERT_EQ(run.Execute(), OpResult::kNeedMoreInput); + + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(0u, 1u, 2u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), + ElementsAre(kNoNode, 0u, 1u)); + EXPECT_FALSE(run.op.IsDenseForTesting(*run.state)); +} + +// A parent not yet seen is numbered on sight, so a child-first stream works. +TEST(TreeNumberNodesTest, AParentNotYetSeenIsNumberedAnyway) { + Numbered run(StorageType{Int64{}}, {2, 1, 0}, {1, 0, 0}, + {true, true, false}); + ASSERT_EQ(run.Execute(), OpResult::kNeedMoreInput); + + std::vector nodes = test::ReadColumn(run.out, 2); + std::vector parents = test::ReadColumn(run.out, 3); + EXPECT_EQ(parents[0], nodes[1]); + EXPECT_EQ(parents[1], nodes[2]); + EXPECT_EQ(parents[2], kNoNode); +} + +TEST(TreeNumberNodesTest, AnIdOfAnyWidthIsNamed) { + Numbered run(StorageType{Uint32{}}, {7, 8}, {0, 7}, {false, true}); + ASSERT_EQ(run.Execute(), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(0u, 1u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), ElementsAre(kNoNode, 0u)); +} + +TEST(TreeNumberNodesTest, AStringIsAnIdLikeAnythingElse) { + StringPool pool; + StringPool::Id a = pool.InternString("a"); + StringPool::Id b = pool.InternString("b"); + Numbered run(StorageType{String{}}, {a, b}, {a, a}, + {false, true}); + ASSERT_EQ(run.Execute(), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(0u, 1u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), ElementsAre(kNoNode, 0u)); +} + +// An Id column has no storage: its value is the row it sits at. +TEST(TreeNumberNodesTest, AnIdColumnIsTheRowItSitsAt) { + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + std::vector parents = {0, 0}; + BitVector validity = BitVector::CreateWithSize(2); + validity.set(1); + RowBatch in; + in.AddColumn(ColumnView::Reference(StorageType{Id{}}, nullptr, nullptr)); + in.AddColumn( + ColumnView::Reference(StorageType{Int64{}}, parents.data(), &validity)); + in.Compose(RowSelection::Range(0), 2); + in.SetCardinality(2); + + RowBatch out; + ASSERT_EQ(op.Execute(in, out, *state), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(out, 2), ElementsAre(0u, 1u)); +} + +TEST(TreeNumberNodesTest, AVariantIdIsNamed) { + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + std::vector ids = {Variant::Int64(5), Variant::Int64(9)}; + std::vector parents = {Variant::Null(), Variant::Int64(5)}; + RowBatch in; + in.AddColumn(ColumnView::Variants(ids.data())); + in.AddColumn(ColumnView::Variants(parents.data())); + in.Compose(RowSelection::Range(0), 2); + in.SetCardinality(2); + + RowBatch out; + ASSERT_EQ(op.Execute(in, out, *state), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(out, 2), ElementsAre(0u, 1u)); + EXPECT_THAT(test::ReadColumn(out, 3), ElementsAre(kNoNode, 0u)); +} + +TEST(TreeNumberNodesTest, VariantStringsAndIntegersHaveSeparateKeys) { + StringPool pool; + StringPool::Id string = pool.InternString("id"); + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + std::vector ids = {Variant::Int64(string.raw_id()), + Variant::String(string)}; + std::vector parents = {Variant::Null(), Variant::Null()}; + RowBatch in; + in.AddColumn(ColumnView::Variants(ids.data())); + in.AddColumn(ColumnView::Variants(parents.data())); + in.SetCardinality(2); + + RowBatch out; + ASSERT_EQ(op.Execute(in, out, *state), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(out, 2), ElementsAre(0u, 1u)); +} + +TEST(TreeNumberNodesTest, DuplicateIdsAreReported) { + Numbered run(StorageType{Int64{}}, {1, 1}, {0, 0}, {false, false}); + EXPECT_EQ(run.Execute(), OpResult::kError); + EXPECT_THAT(run.op.status(*run.state).message(), + testing::HasSubstr("same id")); +} + +TEST(TreeNumberNodesTest, ARowWithNoIdIsReported) { + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + std::vector ids = {1, 2}; + BitVector validity = BitVector::CreateWithSize(2); + validity.set(0); + RowBatch in; + in.AddColumn( + ColumnView::Reference(StorageType{Int64{}}, ids.data(), &validity)); + in.AddColumn(ColumnView::Reference(StorageType{Int64{}}, ids.data())); + in.Compose(RowSelection::Range(0), 2); + in.SetCardinality(2); + + RowBatch out; + EXPECT_EQ(op.Execute(in, out, *state), OpResult::kError); + EXPECT_THAT(op.status(*state).message(), testing::HasSubstr("no id")); +} + +// A parent keeps the number it was first given across later batches. +TEST(TreeNumberNodesTest, NumberingIsStableAcrossBatches) { + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + std::vector ids = {40, 50, 60}; + std::vector parents = {40, 40, 40}; + RowBatch in; + RowBatch out; + in.AddColumn(ColumnView::Reference(StorageType{Int64{}}, ids.data())); + in.AddColumn(ColumnView::Reference(StorageType{Int64{}}, parents.data())); + + in.Compose(RowSelection::Range(0), 2); + in.SetCardinality(2); + ASSERT_EQ(op.Execute(in, out, *state), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(out, 2), ElementsAre(0u, 1u)); + + RowBatch again; + again.AddColumn(ColumnView::Reference(StorageType{Int64{}}, ids.data())); + again.AddColumn(ColumnView::Reference(StorageType{Int64{}}, parents.data())); + again.Compose(RowSelection::Range(2), 1); + again.SetCardinality(1); + ASSERT_EQ(op.Execute(again, out, *state), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(out, 2), ElementsAre(2u)); + EXPECT_THAT(test::ReadColumn(out, 3), ElementsAre(0u)); +} + +// Batches of a table's id column and Uint32 parent ids, run through one +// operator in turn. +struct Scanned { + explicit Scanned(std::vector parents, std::vector has_parent) + : op(0, 1), state(op.MakeState()), parents_(std::move(parents)) { + validity_ = + BitVector::CreateWithSize(static_cast(parents_.size())); + for (uint32_t i = 0; i < parents_.size(); ++i) { + if (has_parent[i]) { + validity_.set(i); + } + } + } + + OpResult Execute(uint32_t offset, uint32_t count) { + RowBatch in; + in.AddColumn(ColumnView::Reference(StorageType{Id{}}, nullptr, nullptr)); + in.AddColumn(ColumnView::Reference(StorageType{Uint32{}}, parents_.data(), + &validity_)); + in.Compose(RowSelection::Range(offset), count); + in.SetCardinality(count); + return op.Execute(in, out, *state); + } + + TreeNumberNodes op; + std::unique_ptr state; + std::vector parents_; + BitVector validity_; + RowBatch out; +}; + +// A parent pointing at a later row in an otherwise in-order table takes the +// general path, which numbers it identically. +TEST(TreeNumberNodesTest, AParentPointingForwardIsNumberedTheSameWay) { + Scanned run({0, 2, 0, 1}, {false, true, true, true}); + ASSERT_EQ(run.Execute(0, 4), OpResult::kNeedMoreInput); + + EXPECT_THAT(test::ReadColumn(run.out, 2), + ElementsAre(0u, 1u, 2u, 3u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), + ElementsAre(kNoNode, 2u, 0u, 1u)); + EXPECT_TRUE(run.op.IsDenseForTesting(*run.state)); +} + +// A parent numbered ahead of its row is not mistaken for a row seen when an +// in-order batch comes between the reference and the row itself. +TEST(TreeNumberNodesTest, AParentNumberedAheadStillGetsItsRow) { + // Rows 0, 2, 3 in order, then the row with id 1, which row 0 referred to. + Scanned run({1, 0, 0, 2}, {true, false, true, true}); + ASSERT_EQ(run.Execute(0, 1), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(run.out, 3), ElementsAre(1u)); + + ASSERT_EQ(run.Execute(2, 2), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(2u, 3u)); + + ASSERT_EQ(run.Execute(1, 1), OpResult::kNeedMoreInput); + EXPECT_THAT(test::ReadColumn(run.out, 2), ElementsAre(1u)); + EXPECT_THAT(test::ReadColumn(run.out, 3), ElementsAre(kNoNode)); + EXPECT_TRUE(run.op.IsDenseForTesting(*run.state)); +} + +// Rows numbered in order are still known to exist when a later batch repeats +// one of their ids. +TEST(TreeNumberNodesTest, ADuplicateOfAnInOrderRowIsReported) { + Scanned run({0, 0}, {false, true}); + ASSERT_EQ(run.Execute(0, 2), OpResult::kNeedMoreInput); + EXPECT_EQ(run.Execute(1, 1), OpResult::kError); + EXPECT_THAT(run.op.status(*run.state).message(), + testing::HasSubstr("same id")); +} + +// The shape of a table like stack_profile_callsite: an id column and a +// sparse-null parent id, scanned straight from the dataframe. +inline constexpr auto kCallsiteShaped = dataframe::CreateTypedDataframeSpec( + {"id", "parent_id"}, + dataframe::CreateTypedColumnSpec(Id{}, + NonNull{}, + IdSorted{}, + NoDuplicates{}), + dataframe::CreateTypedColumnSpec(Uint32{}, + SparseNullWithPopcountAlways{}, + Unsorted{}, + HasDuplicates{})); + +// A whole table's ids are already node numbers, so scanning one through the +// operator hands back the row numbers and never builds a map, however many +// batches it takes. +TEST(TreeNumberNodesTest, AScannedIdColumnIsItsOwnNumbering) { + StringPool pool; + dataframe::Dataframe df = + dataframe::Dataframe::CreateFromTypedSpec(kCallsiteShaped, &pool); + constexpr uint32_t kRows = 3 * kMaxBatchRows + 7; + for (uint32_t row = 0; row < kRows; ++row) { + std::optional parent; + if (row > 0) { + parent = (row - 1) / 2; + } + df.InsertUnchecked(kCallsiteShaped, std::monostate{}, parent); + } + df.Finalize(); + + DataframeScan scan(df, {0, 1}); + std::unique_ptr scan_state = scan.MakeState(); + TreeNumberNodes op(0, 1); + std::unique_ptr state = op.MakeState(); + + RowBatch in; + RowBatch out; + uint32_t row = 0; + uint32_t batches = 0; + while (scan.GetData(in, *scan_state)) { + ASSERT_EQ(op.Execute(in, out, *state), OpResult::kNeedMoreInput); + ASSERT_TRUE(op.IsDenseForTesting(*state)); + std::vector nodes = test::ReadColumn(out, 2); + std::vector parents = test::ReadColumn(out, 3); + ASSERT_EQ(nodes.size(), out.size()); + for (uint32_t i = 0; i < out.size(); ++i, ++row) { + ASSERT_EQ(nodes[i], row); + ASSERT_EQ(parents[i], row == 0 ? kNoNode : (row - 1) / 2); + } + ++batches; + } + EXPECT_EQ(row, kRows); + EXPECT_EQ(batches, 4u); +} + +} // namespace +} // namespace perfetto::trace_processor::core::exec