Skip to content

Commit 08af104

Browse files
committed
protozero: let transports read directly into ProtoRingBuffer
Every transport kept a staging buffer, read into it, then handed those bytes to Append(), which copied them again. BeginWrite()/EndWrite() lets read(2) deposit them in the ring buffer directly, so they are copied once. unixd additionally kept a ProtoRingBuffer per connection, re-serialized each tokenized message back into its TraceProcessorRpcStream framing and pushed it through a second ring buffer inside Rpc. It now reads straight into Rpc's tokenizer: a session serves one client at a time, so there is no second byte stream that could interleave with it, and the per-connection framing buys nothing. out/linux_clang_release/perfetto_benchmarks \ --benchmark_filter='ProtoRingBufferIngest|ProtoRingBufferDispatch' ingest, 4KB reads 142 ns -> 98.3 ns ingest, 1MB reads 38.4 us -> 19.2 us dispatch 171 ns -> 99.1 ns traceconv also drops a 64MB read buffer: peak RSS 62.7MB -> 13.6MB.
1 parent afe0173 commit 08af104

10 files changed

Lines changed: 288 additions & 95 deletions

File tree

‎include/perfetto/ext/protozero/proto_ring_buffer.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#ifndef INCLUDE_PERFETTO_EXT_PROTOZERO_PROTO_RING_BUFFER_H_
1818
#define INCLUDE_PERFETTO_EXT_PROTOZERO_PROTO_RING_BUFFER_H_
1919

20+
#include <stddef.h>
2021
#include <stdint.h>
2122

2223
#include "perfetto/ext/base/paged_memory.h"
@@ -115,6 +116,14 @@ class RingBufferMessageReader {
115116
// Will invaildate the pointers previously handed out.
116117
void Append(const void* data, size_t len);
117118

119+
// Zero-copy counterpart of Append(), for transports that can deposit their
120+
// bytes straight into the ring buffer rather than into a staging buffer of
121+
// their own. BeginWrite() reserves `size` bytes at the write cursor; the
122+
// caller writes at most that many there (e.g. by passing the pointer to
123+
// read(2)) and calls EndWrite() with the number actually written.
124+
uint8_t* BeginWrite(size_t size);
125+
void EndWrite(size_t size_written);
126+
118127
// If a message can be read, it returns the boundaries of the message
119128
// (without including the preamble) and advances the read cursor.
120129
// If no message is available, returns a null range.
@@ -136,6 +145,7 @@ class RingBufferMessageReader {
136145
bool failed_ = false; // Set in case of an unrecoverable framing faiulre.
137146
size_t rd_ = 0; // Offset of the read cursor in |buf_|.
138147
size_t wr_ = 0; // Offset of the write cursor in |buf_|.
148+
size_t writable_ = 0; // Bytes reserved by the last BeginWrite().
139149
};
140150

141151
class ProtoRingBuffer final : public RingBufferMessageReader {

‎src/protozero/proto_ring_buffer.cc‎

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "perfetto/ext/protozero/proto_ring_buffer.h"
1818

19+
#include "perfetto/base/compiler.h"
1920
#include "perfetto/base/logging.h"
2021
#include "perfetto/ext/base/paged_memory.h"
2122
#include "perfetto/protozero/proto_utils.h"
@@ -76,40 +77,28 @@ RingBufferMessageReader::RingBufferMessageReader()
7677
: buf_(perfetto::base::PagedMemory::Allocate(kGrowBytes)) {}
7778
RingBufferMessageReader::~RingBufferMessageReader() = default;
7879

79-
void RingBufferMessageReader::Append(const void* data_void, size_t data_len) {
80-
if (failed_)
81-
return;
82-
const uint8_t* data = static_cast<const uint8_t*>(data_void);
80+
uint8_t* RingBufferMessageReader::BeginWrite(size_t data_len) {
81+
PERFETTO_CHECK(data_len <= kMaxMsgSize);
8382
PERFETTO_DCHECK(wr_ <= buf_.size());
8483
PERFETTO_DCHECK(wr_ >= rd_);
8584

85+
// Nothing can be tokenized any more, so recycle the whole buffer for the
86+
// bytes EndWrite() is about to drop.
87+
if (PERFETTO_UNLIKELY(failed_))
88+
rd_ = wr_ = 0;
89+
8690
// If the last call to ReadMessage() consumed all the data in the buffer and
8791
// there are no incomplete messages pending, restart from the beginning rather
8892
// than keep ringing. This is the most common case.
89-
if (rd_ == wr_)
93+
if (PERFETTO_LIKELY(rd_ == wr_))
9094
rd_ = wr_ = 0;
9195

92-
// The caller is expected to always issue a ReadMessage() after each Append().
93-
PERFETTO_CHECK(!fastpath_.valid());
94-
if (rd_ == wr_) {
95-
auto msg = TryReadMessage(data, data + data_len);
96-
if (msg.valid() && msg.end() == (data + data_len)) {
97-
// Fastpath: in many cases, the underlying stream will effectively
98-
// preserve the atomicity of messages for most small messages.
99-
// In this case we can avoid the extra buf_ roundtrip and just pass a
100-
// pointer to |data| + (proto preamble len).
101-
// The next call to ReadMessage)= will return |fastpath_|.
102-
fastpath_ = std::move(msg);
103-
return;
104-
}
105-
}
106-
10796
size_t avail = buf_.size() - wr_;
10897
if (data_len > avail) {
10998
// This whole section should be hit extremely rarely.
11099

111100
// Try first just recompacting the buffer by moving everything to the left.
112-
// This can happen if we received "a message and a bit" on each Append call
101+
// This can happen if we received "a message and a bit" on each write call
113102
// so we ended pup in a situation like:
114103
// buf_: [unused space] [msg1 incomplete]
115104
// ^rd_ ^wr_
@@ -133,21 +122,53 @@ void RingBufferMessageReader::Append(const void* data_void, size_t data_len) {
133122
while (data_len > new_size - wr_)
134123
new_size += kGrowBytes;
135124
if (new_size > kMaxMsgSize * 2) {
125+
// These bytes can never amount to a message (e.g. a never-ending
126+
// varint). |buf_| exceeds kMaxMsgSize by now, so dropping them leaves
127+
// room for the write.
136128
failed_ = true;
137-
return;
129+
rd_ = wr_ = 0;
130+
return static_cast<uint8_t*>(buf_.Get());
138131
}
139132
auto new_buf = perfetto::base::PagedMemory::Allocate(new_size);
140133
memcpy(new_buf.Get(), buf_.Get(), buf_.size());
141134
buf_ = std::move(new_buf);
142-
avail = new_size - wr_;
143135
// No need to touch rd_ / wr_ cursors.
144136
}
145137
}
146138

147-
// Append the received data at the end of the ring buffer.
148-
uint8_t* buf = static_cast<uint8_t*>(buf_.Get());
149-
memcpy(&buf[wr_], data, data_len);
150-
wr_ += data_len;
139+
writable_ = data_len;
140+
return static_cast<uint8_t*>(buf_.Get()) + wr_;
141+
}
142+
143+
void RingBufferMessageReader::EndWrite(size_t size_written) {
144+
PERFETTO_CHECK(size_written <= writable_);
145+
writable_ = 0;
146+
if (PERFETTO_LIKELY(!failed_))
147+
wr_ += size_written;
148+
}
149+
150+
void RingBufferMessageReader::Append(const void* data_void, size_t data_len) {
151+
if (failed_)
152+
return;
153+
const uint8_t* data = static_cast<const uint8_t*>(data_void);
154+
155+
// The caller is expected to always issue a ReadMessage() after each Append().
156+
PERFETTO_CHECK(!fastpath_.valid());
157+
if (rd_ == wr_) {
158+
auto msg = TryReadMessage(data, data + data_len);
159+
if (msg.valid() && msg.end() == (data + data_len)) {
160+
// Fastpath: in many cases, the underlying stream will effectively
161+
// preserve the atomicity of messages for most small messages.
162+
// In this case we can avoid the extra buf_ roundtrip and just pass a
163+
// pointer to |data| + (proto preamble len).
164+
// The next call to ReadMessage)= will return |fastpath_|.
165+
fastpath_ = std::move(msg);
166+
return;
167+
}
168+
}
169+
170+
memcpy(BeginWrite(data_len), data, data_len);
171+
EndWrite(data_len);
151172
}
152173

153174
RingBufferMessageReader::Message RingBufferMessageReader::ReadMessage() {

‎src/protozero/proto_ring_buffer_unittest.cc‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include <stdint.h>
2020
#include <sys/types.h>
2121

22+
#include <algorithm>
23+
#include <cstring>
2224
#include <list>
2325
#include <ostream>
2426
#include <random>
@@ -264,5 +266,28 @@ TEST(RingBufferTest, FixedLengthRingBuffer) {
264266
"abc");
265267
}
266268

269+
// A read(2) can come back short, so EndWrite() may report fewer bytes than
270+
// BeginWrite() reserved. The tests above always write what they reserve.
271+
TEST_F(ProtoRingBufferTest, PartialWrites) {
272+
ProtoRingBuffer buf;
273+
auto expected = MakeProtoMessage(/*field_id=*/7, /*len=*/4096);
274+
275+
buf.BeginWrite(4096);
276+
buf.EndWrite(0); // The socket had nothing for us.
277+
EXPECT_FALSE(buf.ReadMessage().valid());
278+
279+
for (size_t written = 0; written < last_msg_.size();) {
280+
// Reserve far more than we intend to write, as a socket reader would.
281+
uint8_t* dst = buf.BeginWrite(4096);
282+
size_t n = std::min<size_t>(37, last_msg_.size() - written);
283+
memcpy(dst, &last_msg_[written], n);
284+
buf.EndWrite(n);
285+
written += n;
286+
if (written < last_msg_.size())
287+
ASSERT_FALSE(buf.ReadMessage().valid());
288+
}
289+
EXPECT_EQ(buf.ReadMessage(), expected);
290+
}
291+
267292
} // namespace
268293
} // namespace protozero

‎src/protozero/test/proto_ring_buffer_benchmark.cc‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,147 @@
1515
#include <benchmark/benchmark.h>
1616

1717
#include <algorithm>
18+
#include <cstring>
1819
#include <string>
20+
#include <vector>
1921

22+
#include "perfetto/base/compiler.h"
2023
#include "perfetto/ext/base/file_utils.h"
2124
#include "perfetto/ext/protozero/proto_ring_buffer.h"
25+
#include "perfetto/protozero/proto_utils.h"
2226
#include "src/base/test/utils.h"
2327

28+
namespace {
29+
30+
std::string LoadTestTrace() {
31+
std::string trace_data;
32+
static const char kTestTrace[] = "test/data/example_android_trace_30s.pb";
33+
perfetto::base::ReadFile(perfetto::base::GetTestDataPath(kTestTrace),
34+
&trace_data);
35+
PERFETTO_CHECK(!trace_data.empty());
36+
return trace_data;
37+
}
38+
39+
// Stands in for the read(2)/recv(2) that brings bytes in from the outside
40+
// world: like those, it fills a buffer chosen by the caller. That is the one
41+
// copy no scheme avoids; the benchmarks below differ only in whether the buffer
42+
// belongs to the transport or to the ring buffer.
43+
PERFETTO_NO_INLINE void ProduceBytes(void* dst, const void* src, size_t size) {
44+
memcpy(dst, src, size);
45+
}
46+
47+
// Ingestion as the transports used to do it: read into a staging buffer of the
48+
// transport's own, then hand those bytes to Append(), which copies them again.
49+
void BM_ProtoRingBufferIngestStaged(benchmark::State& state) {
50+
const std::string trace_data = LoadTestTrace();
51+
const auto read_size = static_cast<size_t>(state.range(0));
52+
std::vector<uint8_t> staging(read_size);
53+
54+
protozero::ProtoRingBuffer buffer;
55+
size_t offset = 0, bytes_ingested = 0, total_packet_size = 0;
56+
for (auto _ : state) {
57+
size_t n = std::min(read_size, trace_data.size() - offset);
58+
ProduceBytes(staging.data(), trace_data.data() + offset, n);
59+
buffer.Append(staging.data(), n);
60+
// The trace holds whole messages only, so by the time we get back to the
61+
// start the buffer has drained and the wrap is seamless.
62+
offset = (offset + n) % trace_data.size();
63+
bytes_ingested += n;
64+
for (;;) {
65+
auto msg = buffer.ReadMessage();
66+
if (!msg.valid())
67+
break;
68+
total_packet_size += msg.len;
69+
}
70+
}
71+
benchmark::DoNotOptimize(total_packet_size);
72+
state.SetBytesProcessed(static_cast<int64_t>(bytes_ingested));
73+
}
74+
75+
// Ingestion as they do it now: read straight into the ring buffer, so the bytes
76+
// are copied exactly once.
77+
void BM_ProtoRingBufferIngestZeroCopy(benchmark::State& state) {
78+
const std::string trace_data = LoadTestTrace();
79+
const auto read_size = static_cast<size_t>(state.range(0));
80+
81+
protozero::ProtoRingBuffer buffer;
82+
size_t offset = 0, bytes_ingested = 0, total_packet_size = 0;
83+
for (auto _ : state) {
84+
size_t n = std::min(read_size, trace_data.size() - offset);
85+
ProduceBytes(buffer.BeginWrite(n), trace_data.data() + offset, n);
86+
buffer.EndWrite(n);
87+
offset = (offset + n) % trace_data.size();
88+
bytes_ingested += n;
89+
for (;;) {
90+
auto msg = buffer.ReadMessage();
91+
if (!msg.valid())
92+
break;
93+
total_packet_size += msg.len;
94+
}
95+
}
96+
benchmark::DoNotOptimize(total_packet_size);
97+
state.SetBytesProcessed(static_cast<int64_t>(bytes_ingested));
98+
}
99+
100+
// unixd used to tokenize each message out of a per-connection ProtoRingBuffer,
101+
// re-serialize its TraceProcessorRpcStream preamble, and push preamble +
102+
// payload through a second ProtoRingBuffer inside Rpc, so every message was
103+
// copied and tokenized twice. It now reads straight into Rpc's tokenizer.
104+
void RunDispatch(benchmark::State& state, bool reframe) {
105+
namespace pu = protozero::proto_utils;
106+
const std::string trace_data = LoadTestTrace();
107+
constexpr size_t kReadSize = 4096;
108+
109+
protozero::ProtoRingBuffer conn_buf, rpc_buf;
110+
size_t offset = 0, bytes_ingested = 0, total_packet_size = 0;
111+
for (auto _ : state) {
112+
size_t n = std::min(kReadSize, trace_data.size() - offset);
113+
ProduceBytes(conn_buf.BeginWrite(n), trace_data.data() + offset, n);
114+
conn_buf.EndWrite(n);
115+
offset = (offset + n) % trace_data.size();
116+
bytes_ingested += n;
117+
for (;;) {
118+
auto msg = conn_buf.ReadMessage();
119+
if (!msg.valid())
120+
break;
121+
if (!reframe) {
122+
total_packet_size += msg.len;
123+
continue;
124+
}
125+
uint8_t preamble[16];
126+
uint8_t* end = preamble;
127+
end = pu::WriteVarInt(pu::MakeTagLengthDelimited(1), end);
128+
end = pu::WriteVarInt(msg.len, end);
129+
rpc_buf.Append(preamble, static_cast<size_t>(end - preamble));
130+
rpc_buf.Append(msg.start, msg.len);
131+
for (;;) {
132+
auto inner = rpc_buf.ReadMessage();
133+
if (!inner.valid())
134+
break;
135+
total_packet_size += inner.len;
136+
}
137+
}
138+
}
139+
benchmark::DoNotOptimize(total_packet_size);
140+
state.SetBytesProcessed(static_cast<int64_t>(bytes_ingested));
141+
}
142+
143+
void BM_ProtoRingBufferDispatchReframed(benchmark::State& state) {
144+
RunDispatch(state, /*reframe=*/true);
145+
}
146+
147+
void BM_ProtoRingBufferDispatchDirect(benchmark::State& state) {
148+
RunDispatch(state, /*reframe=*/false);
149+
}
150+
151+
} // namespace
152+
153+
// 4096 is what the socket transports read; 1MB is what traceconv reads.
154+
BENCHMARK(BM_ProtoRingBufferIngestStaged)->Arg(4096)->Arg(1024 * 1024);
155+
BENCHMARK(BM_ProtoRingBufferIngestZeroCopy)->Arg(4096)->Arg(1024 * 1024);
156+
BENCHMARK(BM_ProtoRingBufferDispatchReframed);
157+
BENCHMARK(BM_ProtoRingBufferDispatchDirect);
158+
24159
static void BM_ProtoRingBufferReadLargeChunks(benchmark::State& state) {
25160
std::string trace_data;
26161
static const char kTestTrace[] = "test/data/example_android_trace_30s.pb";

‎src/trace_processor/rpc/remote_trace_processor.cc‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,6 @@ base::Status RemoteTraceProcessor::SendStream(
291291
}
292292

293293
base::Status RemoteTraceProcessor::ReadResponse(std::vector<uint8_t>* out) {
294-
// Hoisted out of the loop: ProtoRingBuffer's fastpath can return a message
295-
// pointing into the last buffer passed to Append(), so it must stay alive.
296-
uint8_t buf[4096];
297294
for (;;) {
298295
auto msg = rxbuf_.ReadMessage();
299296
if (msg.fatal_framing_error)
@@ -302,10 +299,12 @@ base::Status RemoteTraceProcessor::ReadResponse(std::vector<uint8_t>* out) {
302299
out->assign(msg.start, msg.start + msg.len);
303300
return base::OkStatus();
304301
}
305-
ASSIGN_OR_RETURN(size_t n, transport_->Recv(buf, sizeof(buf)));
302+
constexpr size_t kReadSize = 4096;
303+
uint8_t* buf = rxbuf_.BeginWrite(kReadSize);
304+
ASSIGN_OR_RETURN(size_t n, transport_->Recv(buf, kReadSize));
305+
rxbuf_.EndWrite(n);
306306
if (n == 0)
307307
return base::ErrStatus("Session closed the connection");
308-
rxbuf_.Append(buf, n);
309308
}
310309
}
311310

‎src/trace_processor/rpc/rpc.cc‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,19 @@ void Rpc::ResetTraceProcessorInternal(const Config& config) {
253253

254254
void Rpc::OnRpcRequest(const void* data, size_t len) {
255255
rxbuf_.Append(data, len);
256+
DrainRxBuf();
257+
}
258+
259+
uint8_t* Rpc::BeginRpcRequest(size_t size) {
260+
return rxbuf_.BeginWrite(size);
261+
}
262+
263+
void Rpc::EndRpcRequest(size_t size_written) {
264+
rxbuf_.EndWrite(size_written);
265+
DrainRxBuf();
266+
}
267+
268+
void Rpc::DrainRxBuf() {
256269
for (;;) {
257270
auto msg = rxbuf_.ReadMessage();
258271
if (!msg.valid()) {

0 commit comments

Comments
 (0)