| /* |
| * 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/plugins/flamegraph/flamegraph.h" |
| |
| #include <algorithm> |
| #include <cinttypes> |
| #include <cstddef> |
| #include <cstdint> |
| #include <cstring> |
| #include <string> |
| #include <tuple> |
| #include <type_traits> |
| #include <utility> |
| #include <vector> |
| |
| #include "perfetto/base/logging.h" |
| #include "perfetto/base/status.h" |
| #include "perfetto/ext/base/endian.h" |
| #include "perfetto/ext/base/flat_hash_map.h" |
| #include "perfetto/ext/base/hash.h" |
| #include "perfetto/ext/base/murmur_hash.h" |
| #include "perfetto/ext/base/status_macros.h" |
| #include "perfetto/ext/base/status_or.h" |
| #include "perfetto/ext/base/string_utils.h" |
| #include "perfetto/ext/base/utils.h" |
| #include "src/trace_processor/core/tree/tree_column_ops.h" |
| #include "src/trace_processor/core/tree/tree_path_interner.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" |
| #include "src/trace_processor/core/util/sort.h" |
| |
| namespace perfetto::trace_processor::flamegraph { |
| namespace { |
| |
| // A bit outside the SHOW_STACK mask. Once set, equality with the required |
| // SHOW_STACK mask is impossible, rejecting this path and all its descendants. |
| constexpr uint64_t kHideStackBit = uint64_t{1} << 63; |
| |
| template <typename T> |
| bool TryAdd(T value, T* total) { |
| static_assert(std::is_same_v<T, int64_t> || std::is_same_v<T, double>); |
| if constexpr (std::is_same_v<T, int64_t>) { |
| return base::CheckedAdd(*total, value, total); |
| } else { |
| *total += value; |
| return true; |
| } |
| } |
| |
| template <typename T> |
| bool AccumulateSum(T value, |
| uint32_t destination, |
| core::Span<T> sums, |
| core::BitVector* has_value) { |
| PERFETTO_DCHECK(destination < sums.size()); |
| PERFETTO_DCHECK(has_value->size() == sums.size()); |
| if (!has_value->is_set(destination)) { |
| sums[destination] = value; |
| has_value->set(destination); |
| return true; |
| } |
| return TryAdd(value, &sums[destination]); |
| } |
| |
| template <typename T> |
| core::Slab<T> AllocFilled(uint64_t size, T value) { |
| core::Slab<T> slab = core::Slab<T>::Alloc(size); |
| std::fill_n(slab.data(), size, value); |
| return slab; |
| } |
| |
| void UpdateNonZeroRows(const core::Tree::Column& column, |
| uint32_t offset, |
| core::Span<uint8_t> rows) { |
| if (column.type.Is<core::Int64>()) { |
| core::Span<const int64_t> values = |
| column.unchecked_span<int64_t>().subspan(offset, rows.size()); |
| for (uint32_t row = 0; row < rows.size(); ++row) { |
| rows[row] |= !base::IsZero(values[row]); |
| } |
| return; |
| } |
| PERFETTO_DCHECK(column.type.Is<core::Double>()); |
| core::Span<const double> values = |
| column.unchecked_span<double>().subspan(offset, rows.size()); |
| for (uint32_t row = 0; row < rows.size(); ++row) { |
| rows[row] |= !base::IsZero(values[row]); |
| } |
| } |
| |
| struct FilterMatches { |
| uint64_t show_stack : Config::kMaxShowStackFilters; |
| uint64_t view_pattern : 1; |
| uint64_t hide_stack : 1; |
| uint64_t hide_frame : 1; |
| }; |
| static_assert(sizeof(FilterMatches) == 8); |
| |
| constexpr FilterMatches kNoFilterMatches{}; |
| |
| // Everything later phases need from the root-to-row path traversal. Keeping |
| // the path-compressed retained frame avoids any later ancestor search. |
| struct PathState { |
| // SHOW_STACK matches on retained frames, plus kHideStackBit if a retained |
| // frame on this path matched HIDE_STACK. Removed frames inherit this value |
| // unchanged, so their names cannot affect stack filtering. |
| uint64_t stack_bits; |
| |
| // The nearest retained input row. A retained frame points to itself, while a |
| // removed frame inherits its nearest retained ancestor. |
| uint32_t retained_frame; |
| uint8_t padding[4]; |
| |
| bool IsRetained(uint32_t row) const; |
| bool StackIsHidden() const; |
| bool StackContributes(uint64_t required_show_stack_bits) const; |
| }; |
| static_assert(sizeof(PathState) == 16); |
| |
| static constexpr PathState kEmptyPathState = |
| PathState{0, core::Tree::kNullParent, {}}; |
| |
| struct UpwardPath { |
| uint32_t root; |
| uint32_t leaf; |
| }; |
| static_assert(sizeof(UpwardPath) == 8); |
| |
| struct UpwardAnchor { |
| uint32_t frame; |
| UpwardPath path; |
| }; |
| static_assert(sizeof(UpwardAnchor) == 12); |
| |
| struct TreeConstituent { |
| uint32_t frame; |
| uint32_t node; |
| }; |
| static_assert(sizeof(TreeConstituent) == 8); |
| |
| // Self and cumulative values for one value column, indexed by combined node |
| // id: downward nodes first, then upward nodes offset by the downward size. |
| // Aggregate columns use the same combined id space. |
| struct MetricColumns { |
| core::Tree::Column self; |
| core::Tree::Column cumulative; |
| }; |
| |
| // Outputs of the three build stages, produced in order by Run(). |
| |
| // View-independent per-row analysis: frame identity, filter matching and |
| // path compression. Depends only on the input tree and the config's |
| // regexes; both halves are built from it. |
| struct PathAnalysis { |
| std::vector<base::MurmurHashCombiner> frame_hashes; |
| core::Slab<PathState> states; |
| // Retained rows whose frame matches the view pattern. Allocated only when |
| // the config has a view pattern. |
| core::BitVector view_pattern_matches; |
| }; |
| |
| // The merged downward (descendant) half of the view. |
| struct DownwardHalf { |
| explicit DownwardHalf(uint32_t interner_capacity) : tree(interner_capacity) {} |
| |
| core::TreePathInterner tree; |
| // Input row -> merged node receiving its values, kNullParent when the row |
| // is outside the downward half. |
| core::Slab<uint32_t> node; |
| }; |
| |
| // The merged upward (ancestor) half of the view. |
| struct UpwardHalf { |
| explicit UpwardHalf(uint32_t interner_capacity) : tree(interner_capacity) {} |
| |
| core::TreePathInterner tree; |
| core::Slab<UpwardPath> bottom_up_paths; |
| core::FlexVector<UpwardAnchor> pivot_anchors; |
| core::FlexVector<TreeConstituent> aggregate_constituents; |
| }; |
| |
| // Compact two-level routing for aggregates. Output nodes reference retained |
| // frames, and retained frames reference the input rows folded into them. |
| struct AggregateRouting { |
| core::Slab<uint32_t> output_frame_offsets; |
| core::Slab<uint32_t> output_frames; |
| core::Slab<uint32_t> frame_row_offsets; |
| core::Slab<uint32_t> frame_rows; |
| bool frame_rows_are_identity = false; |
| }; |
| |
| // Appends the textual form of an aggregated value: strings verbatim, |
| // numbers formatted directly so numeric columns need no per-row interning. |
| void AppendValue(StringPool::Id value, |
| const StringPool& pool, |
| std::string* out) { |
| const NullTermStringView str = pool.Get(value); |
| out->append(str.data(), str.size()); |
| } |
| void AppendValue(int64_t value, const StringPool&, std::string* out) { |
| base::StackString<32> str("%" PRId64, value); |
| out->append(str.c_str(), str.len()); |
| } |
| void AppendValue(double value, const StringPool&, std::string* out) { |
| base::StackString<32> str("%.15g", value); |
| out->append(str.c_str(), str.len()); |
| // Match SQLite's REAL rendering: integral values keep a trailing ".0". |
| // The scan set also skips infinities and NaNs. |
| if (strcspn(str.c_str(), ".eEnN") == str.len()) { |
| out->append(".0"); |
| } |
| } |
| |
| // Canonical 64-bit pattern used to count distinct aggregate values without |
| // comparing the values themselves (which -Wfloat-equal forbids for doubles). |
| uint64_t DistinctKey(StringPool::Id value) { |
| return value.raw_id(); |
| } |
| uint64_t DistinctKey(int64_t value) { |
| return static_cast<uint64_t>(value); |
| } |
| uint64_t DistinctKey(double value) { |
| if (base::IsZero(value)) { |
| return 0; // Canonicalize -0.0 to +0.0. |
| } |
| uint64_t bits; |
| memcpy(&bits, &value, sizeof(bits)); |
| return bits; |
| } |
| |
| struct NativeFrameRouting { |
| core::Span<const PathState> states; |
| core::Span<const uint32_t> downward_nodes; |
| core::Span<const TreeConstituent> upward_constituents; |
| uint32_t downward_size; |
| uint64_t required_show_stack_bits; |
| bool has_downward; |
| }; |
| |
| class AggregateOperator { |
| public: |
| enum class InputMode { |
| kRowsToOutputs, |
| kNativeRowsToFramesThenFramesToOutputs, |
| }; |
| |
| virtual ~AggregateOperator(); |
| virtual InputMode input_mode() const { return InputMode::kRowsToOutputs; } |
| virtual base::Status UpdateBatch(core::Span<const uint32_t>, |
| core::Span<const uint32_t>) { |
| PERFETTO_FATAL("Aggregate operator does not support direct row updates"); |
| } |
| virtual base::Status UpdateFrames(const NativeFrameRouting&) { |
| PERFETTO_FATAL("Aggregate operator does not support frame aggregation"); |
| } |
| virtual base::Status MergeFrames(const NativeFrameRouting&) { |
| PERFETTO_FATAL("Aggregate operator does not support frame merging"); |
| } |
| virtual base::Status Finalize() { return base::OkStatus(); } |
| virtual core::Tree::Column TakeOutput() = 0; |
| }; |
| |
| AggregateOperator::~AggregateOperator() = default; |
| |
| template <typename T> |
| class SumAggregateOperator final : public AggregateOperator { |
| public: |
| SumAggregateOperator(const core::Tree::Column& input, uint32_t output_rows) |
| : input_(input), |
| frame_totals_(core::Tree::Column::Create<T>( |
| static_cast<uint32_t>(input.unchecked_span<T>().size()))), |
| output_(core::Tree::Column::Create<T>(output_rows)) { |
| frame_totals_.null_bv = core::BitVector::CreateWithSize( |
| input.unchecked_span<T>().size(), false); |
| output_.null_bv = core::BitVector::CreateWithSize(output_rows, false); |
| } |
| |
| InputMode input_mode() const override { |
| return InputMode::kNativeRowsToFramesThenFramesToOutputs; |
| } |
| |
| base::Status UpdateFrames(const NativeFrameRouting& routing) override { |
| core::Span<const T> input = input_.unchecked_span<T>(); |
| core::Span<T> totals = frame_totals_.unchecked_span<T>(); |
| for (uint32_t row = 0; row < input.size(); ++row) { |
| if (input_.null_bv.size() > 0 && !input_.null_bv.is_set(row)) { |
| continue; |
| } |
| const PathState& state = routing.states[row]; |
| if (state.retained_frame == core::Tree::kNullParent || |
| state.StackIsHidden()) { |
| continue; |
| } |
| if (!AccumulateSum(input[row], state.retained_frame, totals, |
| &frame_totals_.null_bv)) { |
| return base::ErrStatus("flamegraph: integer aggregate overflow"); |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status MergeFrames(const NativeFrameRouting& routing) override { |
| const core::Tree::Column& frame_totals = frame_totals_; |
| core::Span<const T> totals = frame_totals.unchecked_span<T>(); |
| core::Span<T> output = output_.unchecked_span<T>(); |
| if (routing.has_downward) { |
| for (uint32_t frame = 0; frame < routing.states.size(); ++frame) { |
| const PathState& state = routing.states[frame]; |
| if (!state.IsRetained(frame) || !frame_totals_.null_bv.is_set(frame) || |
| routing.downward_nodes[frame] == core::Tree::kNullParent || |
| !state.StackContributes(routing.required_show_stack_bits)) { |
| continue; |
| } |
| if (!AccumulateSum(totals[frame], routing.downward_nodes[frame], output, |
| &output_.null_bv)) { |
| return base::ErrStatus("flamegraph: integer aggregate overflow"); |
| } |
| } |
| } |
| for (const TreeConstituent& constituent : routing.upward_constituents) { |
| if (!frame_totals_.null_bv.is_set(constituent.frame)) { |
| continue; |
| } |
| if (!AccumulateSum(totals[constituent.frame], |
| routing.downward_size + constituent.node, output, |
| &output_.null_bv)) { |
| return base::ErrStatus("flamegraph: integer aggregate overflow"); |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| core::Tree::Column TakeOutput() override { return std::move(output_); } |
| |
| private: |
| const core::Tree::Column& input_; |
| core::Tree::Column frame_totals_; |
| core::Tree::Column output_; |
| }; |
| |
| template <typename T> |
| class OneOrSummaryAggregateOperator final : public AggregateOperator { |
| public: |
| OneOrSummaryAggregateOperator(const core::Tree::Column& input, |
| StringPool& pool, |
| uint32_t output_rows) |
| : input_(input), |
| pool_(pool), |
| output_(core::Tree::Column::Create<StringPool::Id>(output_rows)) { |
| output_.null_bv = core::BitVector::CreateWithSize(output_rows, false); |
| } |
| |
| base::Status UpdateBatch(core::Span<const uint32_t> output_rows, |
| core::Span<const uint32_t> input_rows) override { |
| core::Span<const T> input = input_.unchecked_span<T>(); |
| for (uint32_t i = 0; i < input_rows.size(); ++i) { |
| const uint32_t input_row = input_rows[i]; |
| if (input_.null_bv.size() > 0 && !input_.null_bv.is_set(input_row)) { |
| continue; |
| } |
| if (output_rows[i] != current_output_row_) { |
| FinishGroup(); |
| current_output_row_ = output_rows[i]; |
| } |
| Add(input[input_row]); |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status Finalize() override { |
| FinishGroup(); |
| return base::OkStatus(); |
| } |
| |
| core::Tree::Column TakeOutput() override { return std::move(output_); } |
| |
| private: |
| void Add(T value) { |
| const uint64_t key = DistinctKey(value); |
| if (!has_value_) { |
| representative_ = value; |
| representative_key_ = key; |
| has_value_ = true; |
| } else if (!has_multiple_values_ && key != representative_key_) { |
| distinct_keys_.push_back(representative_key_); |
| distinct_keys_.push_back(key); |
| has_multiple_values_ = true; |
| } else if (has_multiple_values_) { |
| distinct_keys_.push_back(key); |
| } |
| } |
| |
| void FinishGroup() { |
| if (!has_value_) { |
| return; |
| } |
| core::Span<StringPool::Id> output = |
| output_.unchecked_span<StringPool::Id>(); |
| if constexpr (std::is_same_v<T, StringPool::Id>) { |
| if (!has_multiple_values_) { |
| output[current_output_row_] = representative_; |
| output_.null_bv.set(current_output_row_); |
| ResetGroup(); |
| return; |
| } |
| } |
| summary_.clear(); |
| AppendValue(representative_, pool_, &summary_); |
| if (has_multiple_values_) { |
| std::sort(distinct_keys_.begin(), distinct_keys_.end()); |
| const size_t distinct_count = static_cast<size_t>(std::distance( |
| distinct_keys_.begin(), |
| std::unique(distinct_keys_.begin(), distinct_keys_.end()))); |
| summary_ += " and "; |
| summary_ += std::to_string(distinct_count); |
| summary_ += " others"; |
| } |
| output[current_output_row_] = |
| pool_.InternString(base::StringView(summary_)); |
| output_.null_bv.set(current_output_row_); |
| ResetGroup(); |
| } |
| |
| void ResetGroup() { |
| distinct_keys_.clear(); |
| has_value_ = false; |
| has_multiple_values_ = false; |
| } |
| |
| const core::Tree::Column& input_; |
| StringPool& pool_; |
| core::Tree::Column output_; |
| std::vector<uint64_t> distinct_keys_; |
| std::string summary_; |
| T representative_{}; |
| uint64_t representative_key_ = 0; |
| uint32_t current_output_row_ = core::Tree::kNullParent; |
| bool has_value_ = false; |
| bool has_multiple_values_ = false; |
| }; |
| |
| template <typename T> |
| class ConcatAggregateOperator final : public AggregateOperator { |
| public: |
| ConcatAggregateOperator(const core::Tree::Column& input, |
| StringPool& pool, |
| uint32_t output_rows) |
| : input_(input), |
| pool_(pool), |
| output_(core::Tree::Column::Create<StringPool::Id>(output_rows)) { |
| output_.null_bv = core::BitVector::CreateWithSize(output_rows, false); |
| } |
| |
| base::Status UpdateBatch(core::Span<const uint32_t> output_rows, |
| core::Span<const uint32_t> input_rows) override { |
| core::Span<const T> input = input_.unchecked_span<T>(); |
| for (uint32_t i = 0; i < input_rows.size(); ++i) { |
| const uint32_t input_row = input_rows[i]; |
| if (input_.null_bv.size() > 0 && !input_.null_bv.is_set(input_row)) { |
| continue; |
| } |
| if (output_rows[i] != current_output_row_) { |
| FinishGroup(); |
| current_output_row_ = output_rows[i]; |
| } |
| if (!text_.empty()) { |
| text_.push_back(','); |
| } |
| AppendValue(input[input_row], pool_, &text_); |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status Finalize() override { |
| FinishGroup(); |
| return base::OkStatus(); |
| } |
| |
| core::Tree::Column TakeOutput() override { return std::move(output_); } |
| |
| private: |
| void FinishGroup() { |
| if (text_.empty()) { |
| return; |
| } |
| output_.unchecked_span<StringPool::Id>()[current_output_row_] = |
| pool_.InternString(base::StringView(text_)); |
| output_.null_bv.set(current_output_row_); |
| text_.clear(); |
| } |
| |
| const core::Tree::Column& input_; |
| StringPool& pool_; |
| core::Tree::Column output_; |
| std::string text_; |
| uint32_t current_output_row_ = core::Tree::kNullParent; |
| }; |
| |
| struct PackedTree { |
| explicit PackedTree(uint32_t capacity) |
| : depths(core::Slab<int64_t>::Alloc(capacity)), |
| source_nodes(core::Slab<uint32_t>::Alloc(capacity)), |
| parents(core::Slab<uint32_t>::Alloc(capacity)), |
| representative_frames(core::Slab<uint32_t>::Alloc(capacity)) {} |
| |
| void Append(int64_t depth, |
| uint32_t source_node, |
| uint32_t parent, |
| uint32_t representative_frame) { |
| PERFETTO_DCHECK(rows < depths.size()); |
| depths[rows] = depth; |
| source_nodes[rows] = source_node; |
| parents[rows] = parent; |
| representative_frames[rows] = representative_frame; |
| ++rows; |
| } |
| |
| uint32_t size() const { return rows; } |
| core::Span<const uint32_t> source_node_span() const { |
| return source_nodes.span().subspan(0, rows); |
| } |
| core::Span<const uint32_t> representative_frame_span() const { |
| return representative_frames.span().subspan(0, rows); |
| } |
| |
| core::Slab<int64_t> depths; |
| core::Slab<uint32_t> source_nodes; |
| core::Slab<uint32_t> parents; |
| core::Slab<uint32_t> representative_frames; |
| uint32_t rows = 0; |
| }; |
| |
| // Owns all temporary state for one flamegraph computation. The methods are the |
| // algorithm phases in execution order; helpers are outlined below to keep the |
| // class declaration readable. |
| class FlamegraphBuilder { |
| public: |
| FlamegraphBuilder(const core::Tree& input, const Config& config); |
| |
| base::StatusOr<core::Tree> Run(); |
| |
| private: |
| FilterMatches MatchText(const char* value) const; |
| |
| template <typename T> |
| FilterMatches MatchNumber(T value) const; |
| |
| FilterMatches MatchColumn(const core::Tree::Column& column, |
| uint32_t row) const; |
| FilterMatches MatchFrame(uint32_t row) const; |
| void PrepareFrameHashes(); |
| void AnalyzePaths(); |
| void BuildDownwardHalf(); |
| base::Status BuildUpwardHalf(); |
| UpwardPath BuildUpwardPath(uint32_t frame, bool include_aggregates); |
| bool ContributesToDownwardCumulative(uint32_t row) const; |
| base::Status AccumulateMetrics(); |
| void BuildAggregateRouting(); |
| base::Status RunAggregateOperator(AggregateOperator* op); |
| base::Status ComputeAggregates(); |
| base::StatusOr<core::Tree> PackOutput(); |
| |
| template <typename T> |
| base::Status MarkActiveBottomUpAnchors(const core::Tree::Column& column, |
| core::Span<uint8_t> active); |
| |
| template <typename T> |
| static void InitializeMetricColumns(uint32_t rows, MetricColumns* output); |
| |
| template <typename T> |
| base::Status AccumulateMetric(const core::Tree::Column& input, |
| MetricColumns* output); |
| |
| template <typename T> |
| base::Status AccumulateDownwardMetric(const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative); |
| |
| template <typename T> |
| base::Status AccumulateUpwardMetric(const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative); |
| |
| template <typename T> |
| base::Status AccumulateBottomUpMetric(const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative); |
| |
| template <typename T> |
| base::Status AccumulatePivotUpwardMetric(const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative); |
| |
| template <typename T> |
| static base::Status PropagateCumulative(const core::TreePathInterner& tree, |
| core::Span<T> cumulative); |
| |
| void AppendPackedNodes(const core::TreePathInterner& tree, |
| uint32_t id_offset, |
| bool upward, |
| PackedTree* output) const; |
| |
| const core::Tree& input_; |
| const Config& config_; |
| uint64_t required_show_stack_bits_; |
| |
| PathAnalysis analysis_; |
| DownwardHalf downward_; |
| UpwardHalf upward_; |
| AggregateRouting aggregate_routing_; |
| std::vector<MetricColumns> metrics_; |
| std::vector<core::Tree::Column> aggregate_columns_; |
| }; |
| |
| bool IsValidConfig(const core::Tree& input, const Config& config) { |
| // A name column holding no values at all is typed Null rather than String |
| // (e.g. a heap graph whose class names were all stripped). Every frame is |
| // then unnamed, which is an unhelpful but perfectly well-formed flamegraph, |
| // so accept it instead of failing the whole build. |
| const bool name_is_valid = |
| config.name && (config.name->type.Is<core::String>() || |
| config.name->type.Is<core::Null>()); |
| if (!name_is_valid || config.value_columns.empty()) { |
| return false; |
| } |
| for (const core::Tree::Column* value : config.value_columns) { |
| if (!value || !IsNumericColumn(*value)) { |
| return false; |
| } |
| } |
| |
| std::vector<std::string> output_names{"depth", "name"}; |
| const auto add_output_name = [&](std::string name) { |
| if (name.empty() || std::find(output_names.begin(), output_names.end(), |
| name) != output_names.end()) { |
| return false; |
| } |
| output_names.push_back(std::move(name)); |
| return true; |
| }; |
| for (const core::Tree::Column* grouping : config.grouping_columns) { |
| if (!grouping || |
| !add_output_name(std::string(input.ColumnName(grouping)))) { |
| return false; |
| } |
| } |
| for (const core::Tree::Column* value : config.value_columns) { |
| const std::string name(input.ColumnName(value)); |
| if (!add_output_name("self_" + name) || |
| !add_output_name("cumulative_" + name)) { |
| return false; |
| } |
| } |
| for (const Config::AggregateColumn& aggregate : config.aggregate_columns) { |
| if (!aggregate.input || !add_output_name(aggregate.output_name)) { |
| return false; |
| } |
| if (aggregate.input->type.Is<core::Null>()) { |
| // A column with no values aggregates to no values under any mode. |
| } else if (aggregate.aggregate == Config::Aggregate::kSum && |
| !IsNumericColumn(*aggregate.input)) { |
| return false; |
| } |
| } |
| if (config.show_stack_filters.size() > Config::kMaxShowStackFilters) { |
| return false; |
| } |
| const bool is_pattern_view = config.view.IsAnyOf<Config::PatternViews>(); |
| return is_pattern_view == config.view_pattern.has_value(); |
| } |
| |
| bool PathState::IsRetained(uint32_t row) const { |
| return retained_frame == row; |
| } |
| |
| bool PathState::StackIsHidden() const { |
| return (stack_bits & kHideStackBit) != 0; |
| } |
| |
| bool PathState::StackContributes(uint64_t required_show_stack_bits) const { |
| return retained_frame != core::Tree::kNullParent && |
| stack_bits == required_show_stack_bits; |
| } |
| |
| bool FlamegraphBuilder::ContributesToDownwardCumulative(uint32_t row) const { |
| return downward_.node[row] != core::Tree::kNullParent && |
| analysis_.states[row].StackContributes(required_show_stack_bits_); |
| } |
| |
| FlamegraphBuilder::FlamegraphBuilder(const core::Tree& input, |
| const Config& config) |
| : input_(input), |
| config_(config), |
| required_show_stack_bits_( |
| (uint64_t{1} << config.show_stack_filters.size()) - 1), |
| downward_(config.view.Is<Config::TopDown>() ? input.row_count : 0), |
| upward_(config.view.Is<Config::BottomUp>() ? input.row_count : 0) {} |
| |
| base::StatusOr<core::Tree> FlamegraphBuilder::Run() { |
| AnalyzePaths(); |
| BuildDownwardHalf(); |
| RETURN_IF_ERROR(BuildUpwardHalf()); |
| analysis_.frame_hashes.clear(); |
| analysis_.frame_hashes.shrink_to_fit(); |
| RETURN_IF_ERROR(AccumulateMetrics()); |
| upward_.bottom_up_paths = {}; |
| upward_.pivot_anchors = {}; |
| RETURN_IF_ERROR(ComputeAggregates()); |
| analysis_.states = {}; |
| upward_.aggregate_constituents = {}; |
| return PackOutput(); |
| } |
| |
| FilterMatches FlamegraphBuilder::MatchText(const char* value) const { |
| FilterMatches matches{}; |
| if (config_.view_pattern) { |
| matches.view_pattern = config_.view_pattern->PartialMatch(value); |
| } |
| for (size_t i = 0; i < config_.show_stack_filters.size(); ++i) { |
| if (config_.show_stack_filters[i].PartialMatch(value)) { |
| matches.show_stack |= uint64_t{1} << i; |
| } |
| } |
| for (const base::Regex& filter : config_.hide_stack_filters) { |
| if (filter.PartialMatch(value)) { |
| matches.hide_stack = 1; |
| break; |
| } |
| } |
| for (const base::Regex& filter : config_.hide_frame_filters) { |
| if (filter.PartialMatch(value)) { |
| matches.hide_frame = 1; |
| break; |
| } |
| } |
| return matches; |
| } |
| |
| void FlamegraphBuilder::PrepareFrameHashes() { |
| analysis_.frame_hashes.resize(input_.row_count); |
| core::Span<base::MurmurHashCombiner> hashes = |
| core::MakeMutableSpan(analysis_.frame_hashes); |
| core::tree_ops::UpdateRowHashes(*config_.name, hashes); |
| for (const core::Tree::Column* column : config_.grouping_columns) { |
| core::tree_ops::UpdateRowHashes(*column, hashes); |
| } |
| } |
| |
| template <typename T> |
| FilterMatches FlamegraphBuilder::MatchNumber(T value) const { |
| if constexpr (std::is_same_v<T, int64_t>) { |
| return MatchText(base::StackString<32>("%" PRId64, value).c_str()); |
| } else { |
| return MatchText(base::StackString<32>("%.15g", value).c_str()); |
| } |
| } |
| |
| FilterMatches FlamegraphBuilder::MatchColumn(const core::Tree::Column& column, |
| uint32_t row) const { |
| if (column.null_bv.size() > 0 && !column.null_bv.is_set(row)) { |
| return kNoFilterMatches; |
| } |
| if (column.type.Is<core::String>()) { |
| const StringPool::Id value = column.unchecked_data<StringPool::Id>()[row]; |
| return value.is_null() ? kNoFilterMatches |
| : MatchText(config_.pool.Get(value).c_str()); |
| } |
| if (column.type.Is<core::Int64>()) { |
| return MatchNumber(column.unchecked_data<int64_t>()[row]); |
| } |
| return MatchNumber(column.unchecked_data<double>()[row]); |
| } |
| |
| FilterMatches FlamegraphBuilder::MatchFrame(uint32_t row) const { |
| FilterMatches matches = MatchColumn(*config_.name, row); |
| for (const core::Tree::Column* column : config_.grouping_columns) { |
| const FilterMatches column_matches = MatchColumn(*column, row); |
| matches.show_stack |= column_matches.show_stack; |
| matches.view_pattern |= column_matches.view_pattern; |
| matches.hide_stack |= column_matches.hide_stack; |
| matches.hide_frame |= column_matches.hide_frame; |
| } |
| return matches; |
| } |
| |
| void FlamegraphBuilder::AnalyzePaths() { |
| PrepareFrameHashes(); |
| analysis_.states = core::Slab<PathState>::Alloc(input_.row_count); |
| if (config_.view_pattern) { |
| analysis_.view_pattern_matches = |
| core::BitVector::CreateWithSize(input_.row_count, false); |
| } |
| const bool needs_matching = config_.HasFilters() || config_.view_pattern; |
| base::FlatHashMapV2<uint64_t, uint32_t, base::AlreadyHashed<uint64_t>> |
| frame_match_index; |
| base::FlatHashMapV2<StringPool::Id, uint32_t> name_match_index; |
| std::vector<FilterMatches> match_pool; |
| // A Null-typed name column carries no payload, so its data must not be |
| // read; every row reads as unnamed instead. |
| const bool name_is_null_typed = config_.name->type.Is<core::Null>(); |
| const StringPool::Id* names = |
| name_is_null_typed ? nullptr |
| : config_.name->unchecked_data<StringPool::Id>(); |
| |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| // Tree guarantees parents precede children, so inheritance is an O(1) |
| // reference rather than an ancestor walk or a copy. Roots inherit the |
| // shared empty path. |
| const uint32_t parent = input_.parent[row]; |
| const PathState& inheritance = parent == core::Tree::kNullParent |
| ? kEmptyPathState |
| : analysis_.states[parent]; |
| PERFETTO_DCHECK(parent == core::Tree::kNullParent || parent < row); |
| |
| // A HIDE_FRAME row folds into its parent's path and cannot affect stack |
| // filtering. |
| PathState& state = analysis_.states[row]; |
| const FilterMatches* matches = &kNoFilterMatches; |
| if (needs_matching) { |
| const uint32_t next_index = static_cast<uint32_t>(match_pool.size()); |
| uint32_t* index; |
| bool inserted; |
| StringPool::Id matched_name; |
| if (config_.grouping_columns.empty()) { |
| const bool is_null = |
| name_is_null_typed || (config_.name->null_bv.size() > 0 && |
| !config_.name->null_bv.is_set(row)); |
| matched_name = is_null ? StringPool::Id() : names[row]; |
| std::tie(index, inserted) = |
| name_match_index.Insert(matched_name, next_index); |
| } else { |
| const uint64_t frame_hash = analysis_.frame_hashes[row].digest(); |
| std::tie(index, inserted) = |
| frame_match_index.Insert(frame_hash, next_index); |
| } |
| if (inserted) { |
| if (config_.grouping_columns.empty()) { |
| match_pool.push_back( |
| matched_name.is_null() |
| ? kNoFilterMatches |
| : MatchText(config_.pool.Get(matched_name).c_str())); |
| } else { |
| match_pool.push_back(MatchFrame(row)); |
| } |
| } |
| matches = &match_pool[*index]; |
| } |
| if (matches->hide_frame) { |
| state = inheritance; |
| PERFETTO_DCHECK(!state.IsRetained(row)); |
| continue; |
| } |
| |
| // Every other row is retained, even when it is outside the built halves. |
| // This lets matching descendants start roots and lets upward paths walk |
| // through retained ancestors. |
| state.retained_frame = row; |
| if (config_.view_pattern && matches->view_pattern) { |
| analysis_.view_pattern_matches.set(row); |
| } |
| state.stack_bits = matches->hide_stack |
| ? kHideStackBit |
| : matches->show_stack | inheritance.stack_bits; |
| } |
| } |
| |
| void FlamegraphBuilder::BuildDownwardHalf() { |
| if (!config_.view.IsAnyOf<Config::DownwardViews>()) { |
| // Every row sits outside the downward half. |
| downward_.node = |
| AllocFilled<uint32_t>(input_.row_count, core::Tree::kNullParent); |
| return; |
| } |
| downward_.node = core::Slab<uint32_t>::Alloc(input_.row_count); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| const uint32_t parent = input_.parent[row]; |
| const uint32_t parent_node = parent == core::Tree::kNullParent |
| ? core::Tree::kNullParent |
| : downward_.node[parent]; |
| const PathState& state = analysis_.states[row]; |
| if (!state.IsRetained(row)) { |
| downward_.node[row] = parent_node; |
| continue; |
| } |
| |
| // Top-down starts at every retained root. Pivot and from-frame start only |
| // at matching frames; nested matches deliberately start another root. |
| // Bottom-up has no downward half and never starts one. |
| const bool has_retained_parent = |
| parent != core::Tree::kNullParent && |
| analysis_.states[parent].retained_frame != core::Tree::kNullParent; |
| const bool is_top_down_root = |
| config_.view.Is<Config::TopDown>() && !has_retained_parent; |
| const bool starts_downward_tree = |
| (config_.view_pattern && analysis_.view_pattern_matches.is_set(row)) || |
| is_top_down_root; |
| if (!starts_downward_tree && parent_node == core::Tree::kNullParent) { |
| downward_.node[row] = core::Tree::kNullParent; |
| continue; |
| } |
| |
| // Equivalent frames under the same merged parent share a node. The first |
| // row which creates the node remains its representative. |
| downward_.node[row] = downward_.tree.Intern( |
| starts_downward_tree ? core::Tree::kNullParent : parent_node, |
| analysis_.frame_hashes[row], row); |
| } |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::MarkActiveBottomUpAnchors( |
| const core::Tree::Column& column, |
| core::Span<uint8_t> active) { |
| core::Span<const T> values = column.unchecked_span<T>(); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (column.null_bv.size() > 0 && !column.null_bv.is_set(row)) { |
| continue; |
| } |
| const T value = values[row]; |
| if (value < 0) { |
| return base::ErrStatus("flamegraph: value columns must be non-negative"); |
| } |
| const PathState& state = analysis_.states[row]; |
| if (!base::IsZero(value) && |
| state.StackContributes(required_show_stack_bits_)) { |
| active[state.retained_frame] = 1; |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| base::Status FlamegraphBuilder::BuildUpwardHalf() { |
| if (config_.view.Is<Config::Pivot>()) { |
| // Anchors are the retained rows matching the view pattern, in row order. |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (!analysis_.view_pattern_matches.is_set(row)) { |
| continue; |
| } |
| const bool include_aggregates = |
| !config_.aggregate_columns.empty() && |
| analysis_.states[row].StackContributes(required_show_stack_bits_); |
| upward_.pivot_anchors.push_back( |
| UpwardAnchor{row, BuildUpwardPath(row, include_aggregates)}); |
| } |
| return base::OkStatus(); |
| } |
| if (!config_.view.Is<Config::BottomUp>()) { |
| return base::OkStatus(); |
| } |
| |
| auto active = AllocFilled<uint8_t>(input_.row_count, 0); |
| for (const core::Tree::Column* column : config_.value_columns) { |
| if (column->type.Is<core::Int64>()) { |
| RETURN_IF_ERROR( |
| MarkActiveBottomUpAnchors<int64_t>(*column, active.mutable_span())); |
| } else { |
| RETURN_IF_ERROR( |
| MarkActiveBottomUpAnchors<double>(*column, active.mutable_span())); |
| } |
| } |
| constexpr UpwardPath kNoPath{core::Tree::kNullParent, |
| core::Tree::kNullParent}; |
| upward_.bottom_up_paths = AllocFilled<UpwardPath>(input_.row_count, kNoPath); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (active[row]) { |
| upward_.bottom_up_paths[row] = BuildUpwardPath(row, true); |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| UpwardPath FlamegraphBuilder::BuildUpwardPath(uint32_t frame, |
| bool include_aggregates) { |
| UpwardPath result{core::Tree::kNullParent, core::Tree::kNullParent}; |
| uint32_t upward_parent = core::Tree::kNullParent; |
| while (frame != core::Tree::kNullParent) { |
| upward_parent = upward_.tree.Intern(upward_parent, |
| analysis_.frame_hashes[frame], frame); |
| if (result.root == core::Tree::kNullParent) { |
| result.root = upward_parent; |
| } |
| if (include_aggregates) { |
| upward_.aggregate_constituents.push_back( |
| TreeConstituent{frame, upward_parent}); |
| } |
| const uint32_t parent = input_.parent[frame]; |
| frame = parent == core::Tree::kNullParent |
| ? core::Tree::kNullParent |
| : analysis_.states[parent].retained_frame; |
| } |
| result.leaf = upward_parent; |
| return result; |
| } |
| |
| template <typename T> |
| void FlamegraphBuilder::InitializeMetricColumns(uint32_t rows, |
| MetricColumns* output) { |
| output->self = core::Tree::Column::Create<T>(rows); |
| output->cumulative = core::Tree::Column::Create<T>(rows); |
| core::Span<T> self = output->self.unchecked_span<T>(); |
| core::Span<T> cumulative = output->cumulative.unchecked_span<T>(); |
| std::fill(self.begin(), self.end(), T{}); |
| std::fill(cumulative.begin(), cumulative.end(), T{}); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::AccumulateMetric( |
| const core::Tree::Column& input, |
| MetricColumns* output) { |
| const uint32_t downward_size = downward_.tree.size(); |
| const uint32_t upward_size = upward_.tree.size(); |
| InitializeMetricColumns<T>(downward_size + upward_size, output); |
| core::Span<T> self = output->self.unchecked_span<T>(); |
| core::Span<T> cumulative = output->cumulative.unchecked_span<T>(); |
| RETURN_IF_ERROR( |
| AccumulateDownwardMetric<T>(input, self.subspan(0, downward_size), |
| cumulative.subspan(0, downward_size))); |
| return AccumulateUpwardMetric<T>( |
| input, self.subspan(downward_size, upward_size), |
| cumulative.subspan(downward_size, upward_size)); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::PropagateCumulative( |
| const core::TreePathInterner& tree, |
| core::Span<T> cumulative) { |
| PERFETTO_DCHECK(tree.size() == cumulative.size()); |
| for (uint32_t node = tree.size(); node-- > 0;) { |
| const uint32_t parent = tree.parent(node); |
| if (parent != core::Tree::kNullParent && |
| !TryAdd(cumulative[node], &cumulative[parent])) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::AccumulateDownwardMetric( |
| const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative) { |
| if (!config_.view.IsAnyOf<Config::DownwardViews>()) { |
| return base::OkStatus(); |
| } |
| |
| core::Span<const T> values = input.unchecked_span<T>(); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (input.null_bv.size() > 0 && !input.null_bv.is_set(row)) { |
| continue; |
| } |
| const T value = values[row]; |
| if (value < 0) { |
| return base::ErrStatus("flamegraph: value columns must be non-negative"); |
| } |
| |
| const uint32_t node = downward_.node[row]; |
| if (node == core::Tree::kNullParent) { |
| continue; |
| } |
| if (analysis_.states[row].StackContributes(required_show_stack_bits_) && |
| (!TryAdd(value, &self[node]) || !TryAdd(value, &cumulative[node]))) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| |
| return PropagateCumulative(downward_.tree, cumulative); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::AccumulateUpwardMetric( |
| const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative) { |
| if (!config_.view.IsAnyOf<Config::UpwardViews>()) { |
| return base::OkStatus(); |
| } |
| return config_.view.Is<Config::BottomUp>() |
| ? AccumulateBottomUpMetric<T>(input, self, cumulative) |
| : AccumulatePivotUpwardMetric<T>(input, self, cumulative); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::AccumulateBottomUpMetric( |
| const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative) { |
| core::Span<const T> values = input.unchecked_span<T>(); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (input.null_bv.size() > 0 && !input.null_bv.is_set(row)) { |
| continue; |
| } |
| const T value = values[row]; |
| const PathState& state = analysis_.states[row]; |
| if (base::IsZero(value) || |
| !state.StackContributes(required_show_stack_bits_)) { |
| continue; |
| } |
| const uint32_t retained = state.retained_frame; |
| const UpwardPath& path = upward_.bottom_up_paths[retained]; |
| const uint32_t root = path.root; |
| const uint32_t leaf = path.leaf; |
| PERFETTO_DCHECK(root != core::Tree::kNullParent); |
| PERFETTO_DCHECK(leaf != core::Tree::kNullParent); |
| if (!TryAdd(value, &self[root]) || !TryAdd(value, &cumulative[leaf])) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| return PropagateCumulative(upward_.tree, cumulative); |
| } |
| |
| template <typename T> |
| base::Status FlamegraphBuilder::AccumulatePivotUpwardMetric( |
| const core::Tree::Column& input, |
| core::Span<T> self, |
| core::Span<T> cumulative) { |
| core::Span<const T> values = input.unchecked_span<T>(); |
| |
| auto anchor_weights = AllocFilled<T>(input_.row_count, T{}); |
| for (uint32_t row = 0; row < input_.row_count; ++row) { |
| if (input.null_bv.size() > 0 && !input.null_bv.is_set(row)) { |
| continue; |
| } |
| const PathState& state = analysis_.states[row]; |
| if (ContributesToDownwardCumulative(row) && |
| !TryAdd(values[row], &anchor_weights[state.retained_frame])) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| |
| for (uint32_t row = input_.row_count; row-- > 0;) { |
| const PathState& state = analysis_.states[row]; |
| const uint32_t node = downward_.node[row]; |
| if (!state.IsRetained(row) || node == core::Tree::kNullParent || |
| downward_.tree.parent(node) == core::Tree::kNullParent) { |
| continue; |
| } |
| const uint32_t parent = input_.parent[row]; |
| PERFETTO_DCHECK(parent != core::Tree::kNullParent); |
| const uint32_t retained_parent = analysis_.states[parent].retained_frame; |
| PERFETTO_DCHECK(retained_parent != core::Tree::kNullParent); |
| if (!TryAdd(anchor_weights[row], &anchor_weights[retained_parent])) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| |
| for (const UpwardAnchor& anchor : upward_.pivot_anchors) { |
| const T value = anchor_weights[anchor.frame]; |
| if (!TryAdd(value, &self[anchor.path.root]) || |
| !TryAdd(value, &cumulative[anchor.path.leaf])) { |
| return base::ErrStatus("flamegraph: integer value overflow"); |
| } |
| } |
| return PropagateCumulative(upward_.tree, cumulative); |
| } |
| |
| base::Status FlamegraphBuilder::AccumulateMetrics() { |
| metrics_.resize(config_.value_columns.size()); |
| for (uint32_t i = 0; i < config_.value_columns.size(); ++i) { |
| const core::Tree::Column& column = *config_.value_columns[i]; |
| switch (column.type.index()) { |
| case core::Tree::Column::Type::GetTypeIndex<core::Int64>(): |
| RETURN_IF_ERROR(AccumulateMetric<int64_t>(column, &metrics_[i])); |
| break; |
| case core::Tree::Column::Type::GetTypeIndex<core::Double>(): |
| RETURN_IF_ERROR(AccumulateMetric<double>(column, &metrics_[i])); |
| break; |
| default: |
| PERFETTO_FATAL("Unsupported flamegraph value column type"); |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| void FlamegraphBuilder::BuildAggregateRouting() { |
| const uint32_t rows = input_.row_count; |
| aggregate_routing_.frame_rows_are_identity = true; |
| for (uint32_t row = 0; row < rows; ++row) { |
| const PathState& state = analysis_.states[row]; |
| if (state.retained_frame != row || state.StackIsHidden()) { |
| aggregate_routing_.frame_rows_are_identity = false; |
| break; |
| } |
| } |
| if (!aggregate_routing_.frame_rows_are_identity) { |
| aggregate_routing_.frame_row_offsets = |
| core::Slab<uint32_t>::Alloc(uint64_t{rows} + 1); |
| std::fill(aggregate_routing_.frame_row_offsets.begin(), |
| aggregate_routing_.frame_row_offsets.end(), 0u); |
| uint32_t routed_rows = 0; |
| for (uint32_t row = 0; row < rows; ++row) { |
| const PathState& state = analysis_.states[row]; |
| if (state.retained_frame != core::Tree::kNullParent && |
| !state.StackIsHidden()) { |
| aggregate_routing_.frame_row_offsets[state.retained_frame + 1]++; |
| routed_rows++; |
| } |
| } |
| for (uint32_t frame = 0; frame < rows; ++frame) { |
| aggregate_routing_.frame_row_offsets[frame + 1] += |
| aggregate_routing_.frame_row_offsets[frame]; |
| } |
| aggregate_routing_.frame_rows = core::Slab<uint32_t>::Alloc(routed_rows); |
| core::Slab<uint32_t> frame_fill = core::Slab<uint32_t>::Alloc(rows); |
| memcpy(frame_fill.data(), aggregate_routing_.frame_row_offsets.data(), |
| rows * sizeof(uint32_t)); |
| for (uint32_t row = 0; row < rows; ++row) { |
| const PathState& state = analysis_.states[row]; |
| if (state.retained_frame != core::Tree::kNullParent && |
| !state.StackIsHidden()) { |
| aggregate_routing_.frame_rows[frame_fill[state.retained_frame]++] = row; |
| } |
| } |
| } |
| |
| const uint32_t downward_size = downward_.tree.size(); |
| const uint32_t nodes = downward_size + upward_.tree.size(); |
| aggregate_routing_.output_frame_offsets = |
| core::Slab<uint32_t>::Alloc(uint64_t{nodes} + 1); |
| std::fill(aggregate_routing_.output_frame_offsets.begin(), |
| aggregate_routing_.output_frame_offsets.end(), 0u); |
| uint32_t routed_frames = 0; |
| if (config_.view.IsAnyOf<Config::DownwardViews>()) { |
| for (uint32_t frame = 0; frame < rows; ++frame) { |
| if (analysis_.states[frame].IsRetained(frame) && |
| ContributesToDownwardCumulative(frame)) { |
| aggregate_routing_.output_frame_offsets[downward_.node[frame] + 1]++; |
| routed_frames++; |
| } |
| } |
| } |
| for (const TreeConstituent& constituent : upward_.aggregate_constituents) { |
| aggregate_routing_ |
| .output_frame_offsets[downward_size + constituent.node + 1]++; |
| routed_frames++; |
| } |
| for (uint32_t node = 0; node < nodes; ++node) { |
| aggregate_routing_.output_frame_offsets[node + 1] += |
| aggregate_routing_.output_frame_offsets[node]; |
| } |
| aggregate_routing_.output_frames = core::Slab<uint32_t>::Alloc(routed_frames); |
| core::Slab<uint32_t> output_fill = core::Slab<uint32_t>::Alloc(nodes); |
| memcpy(output_fill.data(), aggregate_routing_.output_frame_offsets.data(), |
| nodes * sizeof(uint32_t)); |
| if (config_.view.IsAnyOf<Config::DownwardViews>()) { |
| for (uint32_t frame = 0; frame < rows; ++frame) { |
| if (analysis_.states[frame].IsRetained(frame) && |
| ContributesToDownwardCumulative(frame)) { |
| const uint32_t node = downward_.node[frame]; |
| aggregate_routing_.output_frames[output_fill[node]++] = frame; |
| } |
| } |
| } |
| for (const TreeConstituent& constituent : upward_.aggregate_constituents) { |
| const uint32_t node = downward_size + constituent.node; |
| aggregate_routing_.output_frames[output_fill[node]++] = constituent.frame; |
| } |
| } |
| |
| base::Status FlamegraphBuilder::RunAggregateOperator(AggregateOperator* op) { |
| constexpr size_t kBatchSize = 4096; |
| std::vector<uint32_t> destinations; |
| std::vector<uint32_t> sources; |
| destinations.reserve(kBatchSize); |
| sources.reserve(kBatchSize); |
| |
| if (op->input_mode() == |
| AggregateOperator::InputMode::kNativeRowsToFramesThenFramesToOutputs) { |
| const core::Slab<PathState>& states = analysis_.states; |
| const core::Slab<uint32_t>& downward_nodes = downward_.node; |
| const core::FlexVector<TreeConstituent>& upward_constituents = |
| upward_.aggregate_constituents; |
| NativeFrameRouting routing{ |
| states.span(), |
| downward_nodes.span(), |
| upward_constituents.span(), |
| downward_.tree.size(), |
| required_show_stack_bits_, |
| config_.view.IsAnyOf<Config::DownwardViews>(), |
| }; |
| RETURN_IF_ERROR(op->UpdateFrames(routing)); |
| RETURN_IF_ERROR(op->MergeFrames(routing)); |
| return op->Finalize(); |
| } |
| |
| const uint32_t nodes = downward_.tree.size() + upward_.tree.size(); |
| for (uint32_t node = 0; node < nodes; ++node) { |
| for (uint32_t fi = aggregate_routing_.output_frame_offsets[node]; |
| fi < aggregate_routing_.output_frame_offsets[node + 1]; ++fi) { |
| const uint32_t frame = aggregate_routing_.output_frames[fi]; |
| if (aggregate_routing_.frame_rows_are_identity) { |
| destinations.push_back(node); |
| sources.push_back(frame); |
| } else { |
| for (uint32_t ri = aggregate_routing_.frame_row_offsets[frame]; |
| ri < aggregate_routing_.frame_row_offsets[frame + 1]; ++ri) { |
| destinations.push_back(node); |
| sources.push_back(aggregate_routing_.frame_rows[ri]); |
| if (sources.size() == kBatchSize) { |
| RETURN_IF_ERROR(op->UpdateBatch(core::MakeSpan(destinations), |
| core::MakeSpan(sources))); |
| destinations.clear(); |
| sources.clear(); |
| } |
| } |
| } |
| if (sources.size() == kBatchSize) { |
| RETURN_IF_ERROR(op->UpdateBatch(core::MakeSpan(destinations), |
| core::MakeSpan(sources))); |
| destinations.clear(); |
| sources.clear(); |
| } |
| } |
| } |
| if (!sources.empty()) { |
| RETURN_IF_ERROR( |
| op->UpdateBatch(core::MakeSpan(destinations), core::MakeSpan(sources))); |
| } |
| return op->Finalize(); |
| } |
| |
| base::Status FlamegraphBuilder::ComputeAggregates() { |
| bool needs_materialized_routing = false; |
| for (const Config::AggregateColumn& aggregate : config_.aggregate_columns) { |
| needs_materialized_routing |= |
| aggregate.aggregate != Config::Aggregate::kSum; |
| } |
| if (needs_materialized_routing) { |
| BuildAggregateRouting(); |
| } |
| const uint32_t nodes = downward_.tree.size() + upward_.tree.size(); |
| aggregate_columns_.resize(config_.aggregate_columns.size()); |
| for (uint32_t i = 0; i < config_.aggregate_columns.size(); ++i) { |
| const Config::AggregateColumn& aggregate = config_.aggregate_columns[i]; |
| const core::Tree::Column& input = *aggregate.input; |
| if (input.type.Is<core::Null>()) { |
| aggregate_columns_[i] = core::Tree::Column::CreateNull(nodes); |
| continue; |
| } |
| switch (aggregate.aggregate) { |
| case Config::Aggregate::kSum: |
| if (input.type.Is<core::Int64>()) { |
| SumAggregateOperator<int64_t> op(input, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } else { |
| SumAggregateOperator<double> op(input, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } |
| break; |
| case Config::Aggregate::kOneOrSummary: |
| if (input.type.Is<core::String>()) { |
| OneOrSummaryAggregateOperator<StringPool::Id> op(input, config_.pool, |
| nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } else if (input.type.Is<core::Int64>()) { |
| OneOrSummaryAggregateOperator<int64_t> op(input, config_.pool, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } else { |
| OneOrSummaryAggregateOperator<double> op(input, config_.pool, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } |
| break; |
| case Config::Aggregate::kConcatWithComma: |
| if (input.type.Is<core::String>()) { |
| ConcatAggregateOperator<StringPool::Id> op(input, config_.pool, |
| nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } else if (input.type.Is<core::Int64>()) { |
| ConcatAggregateOperator<int64_t> op(input, config_.pool, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } else { |
| ConcatAggregateOperator<double> op(input, config_.pool, nodes); |
| RETURN_IF_ERROR(RunAggregateOperator(&op)); |
| aggregate_columns_[i] = op.TakeOutput(); |
| } |
| break; |
| } |
| } |
| return base::OkStatus(); |
| } |
| |
| void FlamegraphBuilder::AppendPackedNodes(const core::TreePathInterner& tree, |
| uint32_t id_offset, |
| bool upward, |
| PackedTree* output) const { |
| auto keep = AllocFilled<uint8_t>(tree.size(), 0); |
| for (const MetricColumns& metric : metrics_) { |
| UpdateNonZeroRows(metric.cumulative, id_offset, keep.mutable_span()); |
| } |
| |
| auto output_row = AllocFilled<uint32_t>(tree.size(), core::Tree::kNullParent); |
| for (uint32_t node = 0; node < tree.size(); ++node) { |
| if (!keep[node]) { |
| continue; |
| } |
| const uint32_t parent = tree.parent(node); |
| const uint32_t packed_parent = parent == core::Tree::kNullParent |
| ? core::Tree::kNullParent |
| : output_row[parent]; |
| PERFETTO_DCHECK(parent == core::Tree::kNullParent || |
| packed_parent != core::Tree::kNullParent); |
| const int64_t depth = |
| packed_parent == core::Tree::kNullParent |
| ? (upward ? -1 : 1) |
| : output->depths[packed_parent] + (upward ? -1 : 1); |
| output_row[node] = output->size(); |
| output->Append(depth, id_offset + node, packed_parent, |
| tree.representative_row(node)); |
| } |
| } |
| |
| base::StatusOr<core::Tree> FlamegraphBuilder::PackOutput() { |
| PackedTree packed(downward_.tree.size() + upward_.tree.size()); |
| AppendPackedNodes(downward_.tree, 0, false, &packed); |
| AppendPackedNodes(upward_.tree, downward_.tree.size(), true, &packed); |
| |
| core::Tree output; |
| output.row_count = packed.size(); |
| const size_t column_count = 2 + config_.grouping_columns.size() + |
| 2 * config_.value_columns.size() + |
| config_.aggregate_columns.size(); |
| output.names.reserve(column_count); |
| output.columns.reserve(column_count); |
| |
| // The packed arrays are already in output order: adopt them. |
| packed.parents.Truncate(packed.size()); |
| output.parent = std::move(packed.parents); |
| packed.depths.Truncate(packed.size()); |
| core::Tree::Column depth; |
| depth.data = std::move(packed.depths).TakeAsBytes(); |
| output.names.push_back("depth"); |
| output.columns.push_back(std::move(depth)); |
| |
| // TODO(lalitm): the copies below exist because every Tree operation |
| // materializes a full rewritten tree for the next one to consume. If tree |
| // operations composed as a linear pipeline of ops over the source instead, |
| // the intermediate rewrites (and this packing step with them) would |
| // largely disappear. |
| const core::Span<const uint32_t> representatives = |
| packed.representative_frame_span(); |
| output.names.push_back("name"); |
| output.columns.push_back( |
| core::tree_ops::Gather(*config_.name, representatives)); |
| for (const core::Tree::Column* grouping : config_.grouping_columns) { |
| output.names.emplace_back(input_.ColumnName(grouping)); |
| output.columns.push_back( |
| core::tree_ops::Gather(*grouping, representatives)); |
| } |
| |
| // Kept nodes are appended in id order, so the packed sources are a strictly |
| // increasing filter over the combined id space. When every node is kept the |
| // filter is the identity and the builder-owned columns can be adopted |
| // outright instead of gathered. |
| const core::Span<const uint32_t> packed_sources = packed.source_node_span(); |
| const bool keep_all = |
| packed.size() == downward_.tree.size() + upward_.tree.size(); |
| const auto take_or_gather = [&](core::Tree::Column& column) { |
| return keep_all ? std::move(column) |
| : core::tree_ops::Gather(column, packed_sources); |
| }; |
| for (uint32_t i = 0; i < config_.value_columns.size(); ++i) { |
| const std::string name(input_.ColumnName(config_.value_columns[i])); |
| output.names.push_back("self_" + name); |
| output.columns.push_back(take_or_gather(metrics_[i].self)); |
| output.names.push_back("cumulative_" + name); |
| output.columns.push_back(take_or_gather(metrics_[i].cumulative)); |
| } |
| for (uint32_t i = 0; i < config_.aggregate_columns.size(); ++i) { |
| output.names.push_back(config_.aggregate_columns[i].output_name); |
| output.columns.push_back(take_or_gather(aggregate_columns_[i])); |
| } |
| return std::move(output); |
| } |
| |
| } // namespace |
| |
| base::StatusOr<core::Tree> Build(const core::Tree& tree, const Config& config) { |
| if (!IsValidConfig(tree, config)) { |
| return base::ErrStatus("flamegraph: invalid configuration"); |
| } |
| return FlamegraphBuilder(tree, config).Run(); |
| } |
| |
| namespace {} // namespace |
| |
| // Lays out siblings widest first: a stable radix sort orders all nodes by |
| // descending width, and distributing that order into per-parent buckets |
| // keeps it within every sibling list. A breadth-first walk then packs each |
| // node's children left-to-right from the node's own position; the walk |
| // order is the render order. |
| Layout ComputeLayout(const core::Tree& tree, |
| const core::Tree::Column& cumulative, |
| const core::Tree::Column& depth) { |
| const uint32_t row_count = tree.row_count; |
| if (row_count == 0) { |
| return {}; |
| } |
| const core::Span<const int64_t> depths = depth.unchecked_span<int64_t>(); |
| core::Slab<double> width = core::Slab<double>::Alloc(row_count); |
| const bool cumulative_is_int = cumulative.type.Is<core::Int64>(); |
| for (uint32_t node = 0; node < row_count; ++node) { |
| if (cumulative.null_bv.size() > 0 && !cumulative.null_bv.is_set(node)) { |
| width[node] = 0; |
| continue; |
| } |
| const double value = |
| cumulative_is_int |
| ? static_cast<double>(cumulative.unchecked_data<int64_t>()[node]) |
| : cumulative.unchecked_data<double>()[node]; |
| width[node] = std::max(value, 0.0); |
| } |
| |
| // Order all nodes by descending width, ties in input order. The key is the |
| // complemented big-endian IEEE representation, whose unsigned order matches |
| // descending width for the non-negative widths used here. |
| core::Slab<uint64_t> keys = core::Slab<uint64_t>::Alloc(row_count); |
| core::Slab<uint32_t> by_width = core::Slab<uint32_t>::Alloc(row_count); |
| for (uint32_t node = 0; node < row_count; ++node) { |
| uint64_t bits; |
| memcpy(&bits, &width[node], sizeof(bits)); |
| keys[node] = base::HostToBE64(~bits); |
| by_width[node] = node; |
| } |
| core::Slab<uint32_t> scratch = core::Slab<uint32_t>::Alloc(row_count); |
| core::Slab<uint32_t> radix_counts = core::Slab<uint32_t>::Alloc(1u << 16); |
| const uint32_t* by_width_sorted = core::RadixSort( |
| by_width.begin(), by_width.end(), scratch.begin(), radix_counts.data(), |
| sizeof(uint64_t), [&](uint32_t node) { |
| return reinterpret_cast<const uint8_t*>(&keys[node]); |
| }); |
| |
| // Distribute the width order into per-parent buckets. Roots go to one |
| // strip per traversal direction, each starting at x = 0. |
| uint32_t downward_root_count = 0; |
| uint32_t upward_root_count = 0; |
| core::Slab<uint32_t> child_offset = |
| core::Slab<uint32_t>::Alloc(row_count + 1); |
| std::fill(child_offset.begin(), child_offset.end(), 0u); |
| for (uint32_t node = 0; node < row_count; ++node) { |
| if (tree.parent[node] != core::Tree::kNullParent) { |
| child_offset[tree.parent[node] + 1]++; |
| } else if (depths[node] >= 0) { |
| downward_root_count++; |
| } else { |
| upward_root_count++; |
| } |
| } |
| for (uint32_t node = 0; node < row_count; ++node) { |
| child_offset[node + 1] += child_offset[node]; |
| } |
| core::Slab<uint32_t> child_fill = core::Slab<uint32_t>::Alloc(row_count); |
| memcpy(child_fill.data(), child_offset.data(), row_count * sizeof(uint32_t)); |
| core::Slab<uint32_t> children = |
| core::Slab<uint32_t>::Alloc(child_offset[row_count]); |
| core::Slab<uint32_t> downward_roots = |
| core::Slab<uint32_t>::Alloc(downward_root_count); |
| core::Slab<uint32_t> upward_roots = |
| core::Slab<uint32_t>::Alloc(upward_root_count); |
| uint32_t downward_fill = 0; |
| uint32_t upward_fill = 0; |
| for (uint32_t i = 0; i < row_count; ++i) { |
| const uint32_t node = by_width_sorted[i]; |
| const uint32_t parent = tree.parent[node]; |
| if (parent != core::Tree::kNullParent) { |
| children[child_fill[parent]++] = node; |
| } else if (depths[node] >= 0) { |
| downward_roots[downward_fill++] = node; |
| } else { |
| upward_roots[upward_fill++] = node; |
| } |
| } |
| |
| core::Slab<double> x = core::Slab<double>::Alloc(row_count); |
| double strip_x = 0; |
| for (const uint32_t root : downward_roots) { |
| x[root] = strip_x; |
| strip_x += width[root]; |
| } |
| strip_x = 0; |
| for (const uint32_t root : upward_roots) { |
| x[root] = strip_x; |
| strip_x += width[root]; |
| } |
| |
| Layout layout; |
| layout.node = core::Slab<uint32_t>::Alloc(row_count); |
| layout.parent_row = core::Slab<uint32_t>::Alloc(row_count); |
| layout.x_start = core::Slab<double>::Alloc(row_count); |
| uint32_t tail = 0; |
| const auto emit = [&](uint32_t node, uint32_t parent_row) { |
| layout.node[tail] = node; |
| layout.parent_row[tail] = parent_row; |
| layout.x_start[tail] = x[node]; |
| tail++; |
| }; |
| |
| // Each strip is in x order, so merging them by position (the downward |
| // strip first on ties) yields the roots' render order; the walk keeps it |
| // level by level. |
| for (uint32_t di = 0, ui = 0; |
| di < downward_roots.size() || ui < upward_roots.size();) { |
| if (ui >= upward_roots.size() || |
| (di < downward_roots.size() && |
| x[downward_roots[di]] <= x[upward_roots[ui]])) { |
| emit(downward_roots[di++], core::Tree::kNullParent); |
| } else { |
| emit(upward_roots[ui++], core::Tree::kNullParent); |
| } |
| } |
| for (uint32_t row = 0; row < tail; ++row) { |
| const uint32_t node = layout.node[row]; |
| double offset = layout.x_start[row]; |
| for (uint32_t i = child_offset[node]; i < child_offset[node + 1]; ++i) { |
| const uint32_t child = children[i]; |
| x[child] = offset; |
| offset += width[child]; |
| emit(child, row); |
| } |
| } |
| PERFETTO_DCHECK(tail == row_count); |
| return layout; |
| } |
| |
| } // namespace perfetto::trace_processor::flamegraph |