blob: 752c4f8cad594bb5fd9670f02ebdc2e140a25f2c [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 "src/tracing/service/zstd_compressor.h"
#include "perfetto/base/build_config.h"
#if PERFETTO_BUILDFLAG(PERFETTO_ZSTD)
#include <memory>
#include <zstd.h>
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/scoped_file.h"
#include "protos/perfetto/trace/trace.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
#include "src/tracing/service/packet_compressor_common.h"
namespace perfetto {
namespace {
using packet_compressor::GetPreamble;
using packet_compressor::kCompressSliceSize;
using packet_compressor::Preamble;
using packet_compressor::PreambleToSlice;
// ScopedResource's close function must return int; ZSTD_freeCStream returns
// size_t, so wrap it. It only fails for a static-allocated stream, not the
// heap one ZSTD_createCStream gives us, so returning 0 is safe.
inline int ZstdFreeCStream(ZSTD_CStream* cstream) {
ZSTD_freeCStream(cstream);
return 0;
}
using ScopedZstdCStream = base::ScopedResource<ZSTD_CStream*,
ZstdFreeCStream,
/*InvalidValue=*/nullptr,
/*CheckClose=*/false>;
// A compressor for `TracePacket`s that uses zstd's streaming API: data is fed
// in with ZSTD_compressStream2(ZSTD_e_continue) and the frame is finalized with
// ZSTD_e_end, emitting output in fixed-size slices (kCompressSliceSize).
// zstd API reference: https://facebook.github.io/zstd/zstd_manual.html
class ZstdPacketCompressor {
public:
explicit ZstdPacketCompressor(int level);
// Can be called multiple times, before Finish() is called.
void PushPacket(const TracePacket& packet);
// Returns the compressed data. Can be called at most once. After this call,
// the object is unusable (PushPacket should not be called) and must be
// destroyed.
TracePacket Finish();
private:
void PushData(const void* data, uint32_t size);
void NewOutputSlice();
void PushCurSlice();
ScopedZstdCStream cstream_;
// Points into `cur_slice_`. Zero-initialized so that the first compression
// call observes a full output buffer and allocates the initial slice.
ZSTD_outBuffer out_{/*dst=*/nullptr, /*size=*/0, /*pos=*/0};
size_t total_new_slices_size_ = 0;
std::vector<Slice> new_slices_;
std::unique_ptr<uint8_t[]> cur_slice_;
};
ZstdPacketCompressor::ZstdPacketCompressor(int level) {
cstream_.reset(ZSTD_createCStream());
PERFETTO_CHECK(cstream_);
// zstd maps 0 to its default level, clamps levels above its max, and treats
// negatives as fast modes, so any int is safe to pass through here.
size_t rc =
ZSTD_CCtx_setParameter(cstream_.get(), ZSTD_c_compressionLevel, level);
PERFETTO_CHECK(!ZSTD_isError(rc));
}
void ZstdPacketCompressor::PushPacket(const TracePacket& packet) {
// Prefix each packet with its proto preamble so the compressed stream itself
// parses as a valid Trace proto, and its packets can be tokenized back out.
Preamble preamble =
GetPreamble<protos::pbzero::Trace::kPacketFieldNumber>(packet.size());
PushData(preamble.buf.data(), preamble.size);
for (const Slice& slice : packet.slices()) {
PushData(slice.start, static_cast<uint32_t>(slice.size));
}
}
void ZstdPacketCompressor::PushData(const void* data, uint32_t size) {
// ZSTD_e_continue hands data to the encoder, which buffers and emits at its
// own discretion. It may not consume all input in one call (e.g. when the
// output slice fills), so loop until `in` is drained, giving it a fresh slice
// whenever `out_` is full.
ZSTD_inBuffer in = {/*src=*/data, /*size=*/size, /*pos=*/0};
while (in.pos < in.size) {
if (out_.pos == out_.size) {
NewOutputSlice();
}
size_t rc =
ZSTD_compressStream2(cstream_.get(), &out_, &in, ZSTD_e_continue);
PERFETTO_CHECK(!ZSTD_isError(rc));
}
}
TracePacket ZstdPacketCompressor::Finish() {
// ZSTD_e_end flushes buffered data and writes the frame epilogue. Per the
// contract, keep calling (draining `out_` into new slices) until it reports 0
// bytes remaining.
size_t remaining;
do {
if (out_.pos == out_.size) {
NewOutputSlice();
}
ZSTD_inBuffer in = {/*src=*/nullptr, /*size=*/0, /*pos=*/0};
remaining = ZSTD_compressStream2(cstream_.get(), &out_, &in, ZSTD_e_end);
PERFETTO_CHECK(!ZSTD_isError(remaining));
} while (remaining != 0);
PushCurSlice();
TracePacket packet;
packet.AddSlice(PreambleToSlice(
GetPreamble<
protos::pbzero::TracePacket::kZstdCompressedPacketsFieldNumber>(
total_new_slices_size_)));
for (auto& slice : new_slices_) {
packet.AddSlice(std::move(slice));
}
return packet;
}
void ZstdPacketCompressor::NewOutputSlice() {
PushCurSlice();
cur_slice_ = std::make_unique<uint8_t[]>(kCompressSliceSize);
out_.dst = cur_slice_.get();
out_.size = kCompressSliceSize;
out_.pos = 0;
}
void ZstdPacketCompressor::PushCurSlice() {
if (!cur_slice_) {
return;
}
total_new_slices_size_ += out_.pos;
new_slices_.push_back(Slice::TakeOwnership(std::move(cur_slice_), out_.pos));
}
} // namespace
void ZstdCompressFn(std::vector<TracePacket>* packets, int level) {
if (packets->empty()) {
return;
}
ZstdPacketCompressor stream(level);
for (const TracePacket& packet : *packets) {
stream.PushPacket(packet);
}
TracePacket packet = stream.Finish();
packets->clear();
packets->push_back(std::move(packet));
}
} // namespace perfetto
#endif // PERFETTO_BUILDFLAG(PERFETTO_ZSTD)