blob: e266934c1d858cc6d0b23dbd4fbbcf29d0277521 [file] [edit]
// 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 <cstdint>
#include <cstring>
#include <random>
#include <string>
#include <vector>
#include "perfetto/base/build_config.h"
#include "perfetto/ext/tracing/core/slice.h"
#include "perfetto/ext/tracing/core/trace_packet.h"
#include "perfetto/protozero/packed_repeated_fields.h"
#include "perfetto/protozero/scattered_heap_buffer.h"
#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
#include "protos/perfetto/trace/test_event.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
#include "protos/third_party/android/art/heap_graph.pbzero.h"
#if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
#include "src/tracing/service/zlib_compressor.h"
#endif
#if PERFETTO_BUILDFLAG(PERFETTO_ZSTD)
#include "src/tracing/service/zstd_compressor.h"
#endif
namespace perfetto {
namespace {
// Workload policies: Make(i) returns one serialized TracePacket, varied by `i`
// so a batch isn't trivially deduped away (zstd dedupes across its window) and
// the measured ratios stay representative.
// Structured, highly repetitive: a sched_switch ftrace bundle (~64 events), the
// bulk of a system trace.
struct FtraceSched {
static std::vector<uint8_t> Make(uint32_t i) {
protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
auto* bundle = packet->set_ftrace_events();
bundle->set_cpu(i % 8);
uint64_t ts = static_cast<uint64_t>(i) * 1000000;
for (uint32_t e = 0; e < 64; e++) {
auto* evt = bundle->add_event();
evt->set_pid(1000 + ((i + e) % 256));
evt->set_timestamp(ts + e * 1000);
auto* ss = evt->set_sched_switch();
ss->set_prev_comm("thread-" + std::to_string((i + e) % 50));
ss->set_prev_pid(1000 + ((i + e) % 256));
ss->set_prev_state(e % 4);
ss->set_next_comm("thread-" + std::to_string((i + e + 1) % 50));
ss->set_next_pid(1000 + ((i + e + 1) % 256));
}
return packet.SerializeAsArray();
}
};
// Java heap dump: a HeapGraph with types and cross-referencing objects (mostly
// packed varint ids). heap_graph is an out-of-tree field (number 56), not
// declared on TracePacket, so append it by field number.
struct JavaHeapDump {
static std::vector<uint8_t> Make(uint32_t i) {
namespace art = com::android::art::tracing::pbzero;
protozero::HeapBuffered<art::HeapGraph> hg;
hg->set_pid(static_cast<int32_t>(1000 + i));
for (uint32_t t = 0; t < 8; t++) {
auto* type = hg->add_types();
type->set_id(t + 1);
type->set_class_name("com.example.Class" + std::to_string((i + t) % 64));
type->set_object_size(16 * (t + 1));
}
for (uint32_t o = 0; o < 64; o++) {
uint64_t id = (static_cast<uint64_t>(i) << 16) + o + 1;
auto* obj = hg->add_objects();
obj->set_id(id);
obj->set_type_id((o % 8) + 1);
obj->set_self_size(16 + (o % 4) * 8);
protozero::PackedVarInt refs;
refs.Append(id + 1);
refs.Append(id + 7);
obj->set_reference_object_id(refs);
}
std::vector<uint8_t> heap_graph = hg.SerializeAsArray();
protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
// 56 = HeapGraph heap_graph (out-of-tree field, see heap_graph.proto).
packet->AppendBytes(56, heap_graph.data(), heap_graph.size());
return packet.SerializeAsArray();
}
};
// String-heavy: a TrackEvent slice with a name and categories.
struct TrackEvent {
static std::vector<uint8_t> Make(uint32_t i) {
protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
packet->set_timestamp(static_cast<uint64_t>(i) * 1000);
auto* te = packet->set_track_event();
te->set_track_uuid(i % 32);
te->add_categories("category-" + std::to_string(i % 16));
te->set_name("SomeEventName::DoWork_" + std::to_string(i % 128));
return packet.SerializeAsArray();
}
};
// Worst case: incompressible random bytes (e.g. an already-compressed payload).
struct Incompressible {
static std::vector<uint8_t> Make(uint32_t i) {
std::minstd_rand rng(i + 1);
std::string s(1024, '\0');
for (char& c : s)
c = static_cast<char>(rng());
protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
packet->set_for_testing()->set_str(s);
return packet.SerializeAsArray();
}
};
// Backend policies: each runs one compile-time-selected codec at the given
// level.
#if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
struct Zlib {
static void Compress(std::vector<TracePacket>* packets, int /*level*/) {
ZlibCompressFn(packets);
}
};
#endif
#if PERFETTO_BUILDFLAG(PERFETTO_ZSTD)
struct Zstd {
static void Compress(std::vector<TracePacket>* packets, int level) {
ZstdCompressFn(packets, level);
}
};
#endif
TracePacket MakePacket(const std::vector<uint8_t>& buf) {
Slice slice = Slice::Allocate(buf.size());
memcpy(slice.own_data(), buf.data(), buf.size());
TracePacket packet;
packet.AddSlice(std::move(slice));
return packet;
}
// The compressor consumes (clears) its input, so each iteration needs a fresh
// copy of the packets.
std::vector<TracePacket> CopyPackets(const std::vector<TracePacket>& src) {
std::vector<TracePacket> out;
out.reserve(src.size());
for (const TracePacket& packet : src) {
TracePacket copy;
for (const Slice& slice : packet.slices()) {
Slice new_slice = Slice::Allocate(slice.size);
memcpy(new_slice.own_data(), slice.start, slice.size);
copy.AddSlice(std::move(new_slice));
}
out.push_back(std::move(copy));
}
return out;
}
bool IsBenchmarkFunctionalOnly() {
return getenv("BENCHMARK_FUNCTIONAL_TEST_ONLY") != nullptr;
}
// Sweep the block size (packets per compress call) at the codec's default
// level.
static void BlockSizeArgs(benchmark::internal::Benchmark* b) {
b->Unit(benchmark::kMicrosecond);
b->ArgNames({"packets", "level"});
if (IsBenchmarkFunctionalOnly()) {
b->Args({1, 0})->Iterations(1);
return;
}
b->RangeMultiplier(8)->Ranges({{1, 4096}, {0, 0}});
}
// Sweep the zstd level (fastest..max) at a fixed, representative block size.
static void ZstdLevelArgs(benchmark::internal::Benchmark* b) {
b->Unit(benchmark::kMicrosecond);
b->ArgNames({"packets", "level"});
if (IsBenchmarkFunctionalOnly()) {
b->Args({1, 0})->Iterations(1);
return;
}
for (int level : {1, 3, 9, 19, 22})
b->Args({512, level});
}
// Compresses a batch of `state.range(0)` packets per iteration. The reported
// time is the per-batch compression latency; SetBytesProcessed gives throughput
// and the counters report the compression ratio.
template <typename Backend, typename Workload>
static void BM_Compress(benchmark::State& state) {
const size_t num_packets = static_cast<size_t>(state.range(0));
const int level = static_cast<int>(state.range(1));
std::vector<TracePacket> packets;
size_t total_in = 0;
for (size_t i = 0; i < num_packets; i++) {
packets.push_back(MakePacket(Workload::Make(static_cast<uint32_t>(i))));
total_in += packets.back().size();
}
size_t total_out = 0;
for (auto _ : state) {
state.PauseTiming();
std::vector<TracePacket> input = CopyPackets(packets);
state.ResumeTiming();
Backend::Compress(&input, level);
total_out = 0;
for (const TracePacket& packet : input)
total_out += packet.size();
benchmark::DoNotOptimize(total_out);
}
state.counters["packets"] = static_cast<double>(num_packets);
state.counters["in_bytes"] = static_cast<double>(total_in);
state.counters["out_bytes"] = static_cast<double>(total_out);
state.counters["ratio"] =
total_out ? static_cast<double>(total_in) / static_cast<double>(total_out)
: 0;
state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(total_in));
}
#if PERFETTO_BUILDFLAG(PERFETTO_ZLIB)
BENCHMARK_TEMPLATE(BM_Compress, Zlib, FtraceSched)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zlib, JavaHeapDump)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zlib, TrackEvent)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zlib, Incompressible)->Apply(BlockSizeArgs);
#endif
#if PERFETTO_BUILDFLAG(PERFETTO_ZSTD)
BENCHMARK_TEMPLATE(BM_Compress, Zstd, FtraceSched)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, JavaHeapDump)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, TrackEvent)->Apply(BlockSizeArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, Incompressible)->Apply(BlockSizeArgs);
// Level sweep (zstd only): how ratio and latency trade off across levels.
BENCHMARK_TEMPLATE(BM_Compress, Zstd, FtraceSched)->Apply(ZstdLevelArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, JavaHeapDump)->Apply(ZstdLevelArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, TrackEvent)->Apply(ZstdLevelArgs);
BENCHMARK_TEMPLATE(BM_Compress, Zstd, Incompressible)->Apply(ZstdLevelArgs);
#endif
} // namespace
} // namespace perfetto