blob: bf41735b845c4319f2429de5fff9a26bd5743771 [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 <cstdint>
#include <cstring>
#include <limits>
#include <optional>
#include <string>
#include <vector>
#include "perfetto/base/logging.h"
#include "src/trace_processor/containers/string_pool.h"
#include "src/trace_processor/core/tree/tree.h"
#include "src/trace_processor/core/tree/tree_column_ops.h"
#include "src/trace_processor/core/util/slab.h"
#include "test/gtest_and_gmock.h"
namespace perfetto::trace_processor::flamegraph {
namespace {
struct Frame {
std::string name;
std::optional<uint32_t> parent;
int64_t value;
};
template <typename T>
core::Tree::Column ColumnFromValues(const std::vector<T>& values) {
auto column =
core::Tree::Column::Create<T>(static_cast<uint32_t>(values.size()));
memcpy(column.data.begin(), values.data(), values.size() * sizeof(T));
return column;
}
core::Tree MakeTree(StringPool* pool, const std::vector<Frame>& frames) {
core::Tree tree;
tree.row_count = static_cast<uint32_t>(frames.size());
tree.parent = core::Slab<uint32_t>::Alloc(frames.size());
std::vector<StringPool::Id> names;
std::vector<int64_t> values;
for (uint32_t i = 0; i < frames.size(); ++i) {
tree.parent[i] = frames[i].parent.value_or(core::Tree::kNullParent);
names.push_back(pool->InternString(base::StringView(frames[i].name)));
values.push_back(frames[i].value);
}
tree.names = {"name", "value"};
tree.columns.push_back(ColumnFromValues(names));
tree.columns.push_back(ColumnFromValues(values));
return tree;
}
template <typename T>
void AddColumn(core::Tree* tree,
const char* name,
const std::vector<T>& values) {
tree->names.emplace_back(name);
tree->columns.push_back(ColumnFromValues(values));
}
void AddStringColumn(core::Tree* tree,
StringPool* pool,
const char* name,
const std::vector<std::string>& values) {
std::vector<StringPool::Id> ids;
ids.reserve(values.size());
for (const std::string& value : values) {
ids.push_back(pool->InternString(base::StringView(value)));
}
AddColumn(tree, name, ids);
}
Config MakeConfig(const core::Tree& tree, StringPool& pool) {
Config config(pool);
config.name = &tree.columns[0];
config.value_columns.push_back(&tree.columns[1]);
return config;
}
template <typename T>
T Value(const core::Tree& tree, const char* name, uint32_t row) {
auto column = tree.Find(name);
PERFETTO_CHECK(column);
return (*column)->unchecked_data<T>()[row];
}
TEST(FlamegraphTest, TopDownMergesSiblings) {
StringPool pool;
core::Tree input = MakeTree(
&pool,
{{"main", std::nullopt, 1}, {"b", 0, 2}, {"b", 0, 3}, {"c", 1, 4}});
Config config = MakeConfig(input, pool);
auto result = Build(std::move(input), config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 3u);
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 0)).ToStdString(),
"main");
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 0), 10);
EXPECT_EQ(Value<int64_t>(*result, "self_value", 1), 5);
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 1), 9);
EXPECT_EQ(result->parent[2], 1u);
}
TEST(FlamegraphTest, LayoutPacksWidestFirstBreadthFirst) {
StringPool pool;
core::Tree input = MakeTree(
&pool,
{{"main", std::nullopt, 4}, {"a", 0, 3}, {"b", 0, 6}, {"c", 1, 2}});
Config config = MakeConfig(input, pool);
auto result = Build(input, config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 4u);
auto cumulative = result->Find("cumulative_value");
auto depth = result->Find("depth");
ASSERT_TRUE(cumulative && depth);
Layout layout = ComputeLayout(*result, **cumulative, **depth);
auto name_at = [&](uint32_t row) {
return pool.Get(Value<StringPool::Id>(*result, "name", layout.node[row]))
.ToStdString();
};
EXPECT_EQ(name_at(0), "main");
EXPECT_EQ(name_at(1), "b");
EXPECT_EQ(name_at(2), "a");
EXPECT_EQ(name_at(3), "c");
EXPECT_EQ(layout.parent_row[0], core::Tree::kNullParent);
EXPECT_EQ(layout.parent_row[1], 0u);
EXPECT_EQ(layout.parent_row[2], 0u);
EXPECT_EQ(layout.parent_row[3], 2u);
EXPECT_DOUBLE_EQ(layout.x_start[0], 0.0);
EXPECT_DOUBLE_EQ(layout.x_start[1], 0.0);
EXPECT_DOUBLE_EQ(layout.x_start[2], 6.0);
EXPECT_DOUBLE_EQ(layout.x_start[3], 6.0);
}
TEST(FlamegraphTest, StringAggregates) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"x", 0, 2}, {"x", 0, 3}});
AddStringColumn(&input, &pool, "source", {"root", "a", "b"});
AddColumn(&input, "num", std::vector<int64_t>{5, 7, 8});
AddColumn(&input, "frac", std::vector<double>{1.5, 2.0, 2.0});
Config config = MakeConfig(input, pool);
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kOneOrSummary, "summary"});
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kConcatWithComma, "concatenated"});
config.aggregate_columns.push_back(
{&input.columns[3], Config::Aggregate::kOneOrSummary, "num_summary"});
config.aggregate_columns.push_back(
{&input.columns[3], Config::Aggregate::kConcatWithComma, "nums"});
config.aggregate_columns.push_back(
{&input.columns[4], Config::Aggregate::kOneOrSummary, "frac_summary"});
config.aggregate_columns.push_back(
{&input.columns[4], Config::Aggregate::kConcatWithComma, "fracs"});
auto result = Build(input, config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 2u);
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "summary", 0)).ToStdString(),
"root");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "summary", 1)).ToStdString(),
"a and 2 others");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "concatenated", 1)).ToStdString(),
"a,b");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "num_summary", 0)).ToStdString(),
"5");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "num_summary", 1)).ToStdString(),
"7 and 2 others");
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "nums", 1)).ToStdString(),
"7,8");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "frac_summary", 0)).ToStdString(),
"1.5");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "frac_summary", 1)).ToStdString(),
"2.0");
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "fracs", 1)).ToStdString(),
"2.0,2.0");
}
// The dataframe layer materializes a column whose rows are all null as a
// null-typed column. Every aggregate mode over such a column produces an
// all-null output.
TEST(FlamegraphTest, AllNullAggregates) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"x", 0, 2}, {"x", 0, 3}});
input.names.emplace_back("source");
input.columns.push_back(core::Tree::Column::CreateNull(3));
Config config = MakeConfig(input, pool);
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kOneOrSummary, "summary"});
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kConcatWithComma, "concatenated"});
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kSum, "summed"});
auto result = Build(input, config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 2u);
for (uint32_t row = 0; row < result->row_count; ++row) {
for (const char* name : {"summary", "concatenated", "summed"}) {
auto output = result->Find(name);
ASSERT_TRUE(output);
ASSERT_TRUE((*output)->null_bv.size() > 0);
EXPECT_FALSE((*output)->null_bv.is_set(row));
}
}
}
// 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 still a well-formed flamegraph.
TEST(FlamegraphTest, AllNullNames) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"a", 0, 2}, {"b", 0, 3}});
input.columns[0] = core::Tree::Column::CreateNull(3);
Config config = MakeConfig(input, pool);
auto result = Build(input, config);
ASSERT_TRUE(result.ok());
// Both children are unnamed, so they merge into a single child frame.
ASSERT_EQ(result->row_count, 2u);
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 0), 6);
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 1), 5);
auto name = result->Find("name");
ASSERT_TRUE(name);
ASSERT_TRUE((*name)->null_bv.size() > 0);
for (uint32_t row = 0; row < result->row_count; ++row) {
EXPECT_FALSE((*name)->null_bv.is_set(row));
}
}
// Filters read the name of every frame, which must not touch the absent
// payload of a Null-typed column. Nothing can match, so nothing is retained.
TEST(FlamegraphTest, AllNullNamesWithFilters) {
StringPool pool;
core::Tree input = MakeTree(&pool, {{"main", std::nullopt, 1}, {"a", 0, 2}});
input.columns[0] = core::Tree::Column::CreateNull(2);
Config config = MakeConfig(input, pool);
config.show_stack_filters.push_back(base::Regex::CreateOrCheck("^main$"));
auto result = Build(input, config);
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_count, 0u);
}
TEST(FlamegraphTest, BottomUp) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"a", 0, 2}, {"b", 1, 4}});
Config config = MakeConfig(input, pool);
config.view = Config::View(Config::BottomUp{});
auto result = Build(std::move(input), config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 6u);
EXPECT_EQ(Value<int64_t>(*result, "depth", 0), -1);
EXPECT_EQ(Value<int64_t>(*result, "self_value", 4), 0);
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 4), 4);
}
TEST(FlamegraphTest, PivotAndFromFrameViews) {
StringPool pool;
const std::vector<Frame> frames = {{"main", std::nullopt, 1},
{"pivot", 0, 2},
{"leaf", 1, 4},
{"other", 0, 8}};
for (bool pivot : {false, true}) {
core::Tree input = MakeTree(&pool, frames);
Config config = MakeConfig(input, pool);
config.view = pivot ? Config::View(Config::Pivot{})
: Config::View(Config::FromFrame{});
config.view_pattern = base::Regex::CreateOrCheck("^pivot$");
auto result = Build(std::move(input), config);
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_count, pivot ? 4u : 2u);
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 0)).ToStdString(),
"pivot");
EXPECT_EQ(Value<int64_t>(*result, "depth", 0), 1);
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 1)).ToStdString(),
"leaf");
if (pivot) {
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "name", 2)).ToStdString(),
"pivot");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "name", 3)).ToStdString(),
"main");
EXPECT_EQ(Value<int64_t>(*result, "depth", 3), -2);
}
}
}
TEST(FlamegraphTest, ShowAndHideStackFilters) {
StringPool pool;
core::Tree input = MakeTree(&pool, {{"main", std::nullopt, 1},
{"show", 0, 2},
{"leaf", 1, 4},
{"hidden", 0, 8},
{"hidden_leaf", 3, 16}});
Config config = MakeConfig(input, pool);
config.show_stack_filters.push_back(base::Regex::CreateOrCheck("^leaf$"));
config.hide_stack_filters.push_back(
base::Regex::CreateOrCheck("^hidden_leaf$"));
auto result = Build(std::move(input), config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 3u);
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 0)).ToStdString(),
"main");
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 1)).ToStdString(),
"show");
EXPECT_EQ(pool.Get(Value<StringPool::Id>(*result, "name", 2)).ToStdString(),
"leaf");
}
TEST(FlamegraphTest, RejectsInvalidConfig) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"child", 0, 2}});
Config missing_value(pool);
missing_value.name = &input.columns[0];
EXPECT_FALSE(Build(input, missing_value).ok());
Config non_numeric = MakeConfig(input, pool);
non_numeric.value_columns = {&input.columns[0]};
EXPECT_FALSE(Build(input, non_numeric).ok());
Config duplicate_output = MakeConfig(input, pool);
duplicate_output.aggregate_columns.push_back(
{&input.columns[1], Config::Aggregate::kSum, "self_value"});
EXPECT_FALSE(Build(input, duplicate_output).ok());
}
TEST(FlamegraphTest, RejectsNegativeValues) {
StringPool pool;
// Negative values are rejected in downward views (TOP_DOWN).
{
core::Tree input = MakeTree(&pool, {{"main", std::nullopt, -1}});
Config config = MakeConfig(input, pool);
EXPECT_FALSE(Build(std::move(input), config).ok());
}
// Negative values are rejected in upward views (BOTTOM_UP) too.
{
core::Tree input = MakeTree(&pool, {{"main", std::nullopt, -1}});
Config config = MakeConfig(input, pool);
config.view = Config::View(Config::BottomUp{});
EXPECT_FALSE(Build(std::move(input), config).ok());
}
}
TEST(FlamegraphTest, RejectsIntegerOverflow) {
StringPool pool;
{
core::Tree input = MakeTree(
&pool, {{"main", std::nullopt, std::numeric_limits<int64_t>::max()},
{"main", std::nullopt, 1}});
Config config = MakeConfig(input, pool);
EXPECT_FALSE(Build(std::move(input), config).ok());
}
{
core::Tree input = MakeTree(
&pool, {{"main", std::nullopt, std::numeric_limits<int64_t>::max()},
{"child", 0, 1}});
Config config = MakeConfig(input, pool);
EXPECT_FALSE(Build(std::move(input), config).ok());
}
}
TEST(FlamegraphTest, EmptyLayout) {
core::Tree tree;
core::Tree::Column cumulative = core::Tree::Column::Create<int64_t>(0);
core::Tree::Column depth = core::Tree::Column::Create<int64_t>(0);
Layout layout = ComputeLayout(tree, cumulative, depth);
EXPECT_EQ(layout.node.size(), 0u);
EXPECT_EQ(layout.parent_row.size(), 0u);
EXPECT_EQ(layout.x_start.size(), 0u);
}
TEST(FlamegraphTest, HideFrameFoldsValue) {
StringPool pool;
core::Tree input =
MakeTree(&pool, {{"main", std::nullopt, 1}, {"hide", 0, 2}, {"b", 1, 4}});
AddStringColumn(&input, &pool, "source", {"root", "hidden", "child"});
Config config = MakeConfig(input, pool);
config.hide_frame_filters.push_back(base::Regex::CreateOrCheck("hide"));
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kOneOrSummary, "summary"});
config.aggregate_columns.push_back(
{&input.columns[2], Config::Aggregate::kConcatWithComma, "concatenated"});
auto result = Build(std::move(input), config);
ASSERT_TRUE(result.ok());
ASSERT_EQ(result->row_count, 2u);
EXPECT_EQ(Value<int64_t>(*result, "self_value", 0), 3);
EXPECT_EQ(Value<int64_t>(*result, "cumulative_value", 0), 7);
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "summary", 0)).ToStdString(),
"root and 2 others");
EXPECT_EQ(
pool.Get(Value<StringPool::Id>(*result, "concatenated", 0)).ToStdString(),
"root,hidden");
}
} // namespace
} // namespace perfetto::trace_processor::flamegraph