blob: f6bb89b79f6a94b1d5bdf0d144176a77b4f974ae [file]
/*
* 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