blob: 89fb220c133f380d99eb5e55148cc0f59283f037 [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 <benchmark/benchmark.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
#include "perfetto/base/logging.h"
#include "src/trace_processor/containers/string_pool.h"
#include "src/trace_processor/core/dataframe/dataframe.h"
#include "src/trace_processor/core/dataframe/specs.h"
#include "src/trace_processor/plugins/flamegraph/flamegraph.h"
namespace perfetto::trace_processor::flamegraph {
namespace {
// Deterministic LCG so runs are comparable.
struct Rng {
uint64_t state = 42;
uint32_t operator()() {
state = state * 6364136223846793005ull + 1442695040888963407ull;
return static_cast<uint32_t>(state >> 33);
}
};
// A forest plus the per-frame property columns the collection step would
// intern alongside it: |mapping| groups (and is folded into the merge
// key), |source_file| aggregates.
struct BenchInput {
Forest forest;
std::vector<StringPool::Id> mapping;
std::vector<StringPool::Id> source_file;
};
BenchInput GenForest(StringPool* pool,
uint32_t n,
uint32_t metric_count,
bool with_properties) {
constexpr uint32_t kNameCardinality = 10000;
constexpr uint32_t kMappingCardinality = 40;
constexpr uint32_t kSourceFileCardinality = 2000;
Rng rng;
BenchInput in;
Forest& forest = in.forest;
forest.metric_count = metric_count;
for (uint32_t t = 0; t < kNameCardinality; ++t) {
forest.name_table.push_back(
pool->InternString(base::StringView("frame_" + std::to_string(t))));
}
std::vector<StringPool::Id> mappings;
for (uint32_t t = 0; t < kMappingCardinality && with_properties; ++t) {
mappings.push_back(
pool->InternString(base::StringView("mapping_" + std::to_string(t))));
}
std::vector<StringPool::Id> files;
for (uint32_t t = 0; t < kSourceFileCardinality && with_properties; ++t) {
files.push_back(
pool->InternString(base::StringView("file_" + std::to_string(t))));
}
std::vector<uint32_t> stack;
for (uint32_t i = 0; i < n; ++i) {
if (!stack.empty() && rng() % 100 < 40) {
stack.resize(stack.size() - (rng() % stack.size()));
}
forest.parent.push_back(stack.empty() ? kNoParent : stack.back());
uint32_t name = rng() % kNameCardinality;
forest.name.push_back(name);
// The merge key folds the grouping properties in; the generated
// mapping is a function of the name, so the name index is exactly the
// distinct (name, mapping) id.
forest.key.push_back(name);
for (uint32_t m = 0; m < metric_count; ++m) {
forest.metrics.push_back(rng() % 3 == 0 ? rng() % 100 : 0);
}
if (with_properties) {
in.mapping.push_back(mappings[name % kMappingCardinality]);
in.source_file.push_back(files[rng() % kSourceFileCardinality]);
}
stack.push_back(i);
}
return in;
}
// Converts the trees to a flat dataframe like the library's ToDataframe,
// additionally emitting the grouping property of the representative frame
// and the ONE_OR_SUMMARY aggregation of the constituents' values: the
// work the SQL intrinsic glue does for property columns.
constexpr auto PropsRowSpec() {
using core::dataframe::CreateTypedColumnSpec;
using core::dataframe::Int64;
using core::dataframe::NonNull;
using core::dataframe::String;
using core::dataframe::Unsorted;
return core::dataframe::CreateTypedDataframeSpec(
{"id", "parentId", "depth", "name", "mapping", "sourceFile", "selfValue",
"cumulativeValue"},
CreateTypedColumnSpec(Int64{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(Int64{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(Int64{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(String{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(String{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(String{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(Int64{}, NonNull{}, Unsorted{}),
CreateTypedColumnSpec(Int64{}, NonNull{}, Unsorted{}));
}
core::dataframe::Dataframe TreesToDataframeWithProps(const Flamegraph& fg,
const BenchInput& in,
StringPool* pool) {
static constexpr auto kSpec = PropsRowSpec();
auto df = core::dataframe::Dataframe::CreateFromTypedSpec(kSpec, pool);
std::vector<StringPool::Id> seen;
int64_t base = 0;
for (const Tree* tree : {&fg.down, &fg.up}) {
uint32_t k = tree->metric_count;
std::vector<int64_t> depth(tree->size());
for (uint32_t i = 0; i < tree->size(); ++i) {
bool root = tree->parent[i] == kNoParent;
depth[i] = root ? 1 : depth[tree->parent[i]] + 1;
// ONE_OR_SUMMARY: the representative frame's value, with a summary
// suffix when the merged frames had several distinct ones. The
// double space and the count including the shown value mirror the
// SQL surface this replaces.
seen.clear();
for (uint32_t c = tree->constituents_offset[i];
c < tree->constituents_offset[i + 1]; ++c) {
StringPool::Id file = in.source_file[tree->constituents[c]];
if (std::find(seen.begin(), seen.end(), file) == seen.end()) {
seen.push_back(file);
}
}
StringPool::Id file = in.source_file[tree->rep_frame[i]];
if (seen.size() > 1) {
std::string text = pool->Get(file).ToStdString() + " and " +
std::to_string(seen.size()) + " others";
file = pool->InternString(base::StringView(text));
}
df.InsertUnchecked(
kSpec, base + i, root ? -1 : base + tree->parent[i],
tree == &fg.up ? -depth[i] : depth[i],
in.forest.name_table[in.forest.name[tree->rep_frame[i]]],
in.mapping[tree->rep_frame[i]], file,
static_cast<int64_t>(tree->self[i * k]),
static_cast<int64_t>(tree->cumulative[i * k]));
}
base += static_cast<int64_t>(tree->size());
}
df.Finalize();
return df;
}
void Run(benchmark::State& state,
const BenchInput& in,
const Config& config,
StringPool* pool) {
for (auto _ : state) {
auto fg = Build(in.forest, config, *pool);
PERFETTO_CHECK(fg.ok());
benchmark::DoNotOptimize(fg);
}
state.counters["frames/s"] =
benchmark::Counter(static_cast<double>(in.forest.size()),
benchmark::Counter::kIsIterationInvariantRate);
}
// Build plus the dataframe conversion (and property aggregation when the
// input has properties).
void RunWithRows(benchmark::State& state,
const BenchInput& in,
const Config& config,
StringPool* pool) {
for (auto _ : state) {
auto fg = Build(in.forest, config, *pool);
PERFETTO_CHECK(fg.ok());
if (in.mapping.empty()) {
auto df = ToDataframe(*fg, in.forest, pool);
PERFETTO_CHECK(df.ok());
benchmark::DoNotOptimize(df);
} else {
auto df = TreesToDataframeWithProps(*fg, in, pool);
benchmark::DoNotOptimize(df);
}
}
state.counters["frames/s"] =
benchmark::Counter(static_cast<double>(in.forest.size()),
benchmark::Counter::kIsIterationInvariantRate);
}
void BM_FlamegraphTreeTopDown(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Run(state, in, Config{}, &pool);
}
BENCHMARK(BM_FlamegraphTreeTopDown)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeTopDown4Metrics(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 4, false);
Run(state, in, Config{}, &pool);
}
BENCHMARK(BM_FlamegraphTreeTopDown4Metrics)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeTopDownFiltered(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Config config;
config.filters.push_back({Config::FilterKind::kShowStack, "frame_1\\d\\d"});
config.filters.push_back({Config::FilterKind::kHideFrame, "frame_2\\d\\d"});
Run(state, in, config, &pool);
}
BENCHMARK(BM_FlamegraphTreeTopDownFiltered)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeBottomUp(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Config config;
config.view = Config::View::kBottomUp;
Run(state, in, config, &pool);
}
BENCHMARK(BM_FlamegraphTreeBottomUp)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreePivot(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Config config;
config.view = Config::View::kPivot;
config.pivot_pattern = "frame_12";
Run(state, in, config, &pool);
}
BENCHMARK(BM_FlamegraphTreePivot)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeTopDownRows(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
RunWithRows(state, in, Config{}, &pool);
}
BENCHMARK(BM_FlamegraphTreeTopDownRows)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeTopDownPropertiesRows(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, true);
RunWithRows(state, in, Config{}, &pool);
}
BENCHMARK(BM_FlamegraphTreeTopDownPropertiesRows)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreeBottomUpRows(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Config config;
config.view = Config::View::kBottomUp;
RunWithRows(state, in, config, &pool);
}
BENCHMARK(BM_FlamegraphTreeBottomUpRows)->Arg(100000)->Arg(1000000);
void BM_FlamegraphTreePivotRows(benchmark::State& state) {
StringPool pool;
BenchInput in =
GenForest(&pool, static_cast<uint32_t>(state.range(0)), 1, false);
Config config;
config.view = Config::View::kPivot;
config.pivot_pattern = "frame_12";
RunWithRows(state, in, config, &pool);
}
BENCHMARK(BM_FlamegraphTreePivotRows)->Arg(100000)->Arg(1000000);
} // namespace
} // namespace perfetto::trace_processor::flamegraph