Skip to content

Commit d3b33f5

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Switch trace event chunks from event count to size based (#56080)
Summary: Pull Request resolved: #56080 Trace event chunks were previously split by a fixed event count (1000). This caused issues with Frame Timing events containing screenshot data, where a single chunk could produce CDP messages of tens or hundreds of MBs — exceeding the OkHttp WebSocket limit (app crash). Address this problem by replacing count-based chunking with size-based chunking (max 10 MiB per chunk) for `emitFrameTimings()` and `emitPerformanceTraceEvents()`. A new `TraceEventSerializer::estimateJsonSize()` utility recursively estimates the serialized byte size of a `folly::dynamic` value to avoid double-serialization overhead. **Notes** - RuntimeSamplingProfile chunks (separate code path) remain count-based (inherently small, no screenshot data). Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D96206962 fbshipit-source-id: 6b6e4db76441f174c46bf34905292585d0e30899
1 parent d309cda commit d3b33f5

5 files changed

Lines changed: 111 additions & 39 deletions

File tree

packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ void emitNotificationsForTracingProfile(
3535
std::convertible_to<std::ranges::range_value_t<ChannelsRange>, FrontendChannel>
3636
{
3737
/**
38-
* Threshold for the size Trace Event chunk, that will be flushed out with
39-
* Tracing.dataCollected event.
38+
* Maximum serialized byte size of a Trace Event chunk before it is flushed
39+
* with a Tracing.dataCollected event.
4040
*/
41-
static constexpr uint16_t TRACE_EVENT_CHUNK_SIZE = 1000;
41+
static constexpr size_t TRACE_EVENT_CHUNK_MAX_BYTES = 10 * 1024 * 1024; // 10 MiB
4242

4343
/**
4444
* The maximum number of ProfileChunk trace events
@@ -65,7 +65,7 @@ void emitNotificationsForTracingProfile(
6565
cdp::jsonNotification("Tracing.dataCollected", folly::dynamic::object("value", serializedChunk)));
6666
}
6767
},
68-
TRACE_EVENT_CHUNK_SIZE,
68+
TRACE_EVENT_CHUNK_MAX_BYTES,
6969
PROFILE_TRACE_EVENT_CHUNK_SIZE);
7070

7171
for (auto &frontendChannel : channels) {

packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@ namespace facebook::react::jsinspector_modern::tracing {
1414

1515
namespace {
1616

17-
folly::dynamic generateNewChunk(uint16_t chunkSize) {
18-
folly::dynamic chunk = folly::dynamic::array();
19-
chunk.reserve(chunkSize);
20-
21-
return chunk;
22-
}
23-
2417
/**
2518
* Hardcoded layer tree ID for all recorded frames.
2619
* https://chromedevtools.github.io/devtools-protocol/tot/LayerTree/
@@ -32,14 +25,14 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
3225
/* static */ void HostTracingProfileSerializer::emitAsDataCollectedChunks(
3326
HostTracingProfile&& hostTracingProfile,
3427
const std::function<void(folly::dynamic&&)>& chunkCallback,
35-
uint16_t traceEventsChunkSize,
28+
size_t maxChunkBytes,
3629
uint16_t profileTraceEventsChunkSize) {
3730
emitFrameTimings(
3831
std::move(hostTracingProfile.frameTimings),
3932
hostTracingProfile.processId,
4033
hostTracingProfile.startTime,
4134
chunkCallback,
42-
traceEventsChunkSize);
35+
maxChunkBytes);
4336

4437
auto instancesProfiles =
4538
std::move(hostTracingProfile.instanceTracingProfiles);
@@ -49,7 +42,7 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
4942
emitPerformanceTraceEvents(
5043
std::move(instanceProfile.performanceTraceEvents),
5144
chunkCallback,
52-
traceEventsChunkSize);
45+
maxChunkBytes);
5346
}
5447

5548
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
@@ -63,16 +56,22 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
6356
/* static */ void HostTracingProfileSerializer::emitPerformanceTraceEvents(
6457
std::vector<TraceEvent>&& events,
6558
const std::function<void(folly::dynamic&&)>& chunkCallback,
66-
uint16_t chunkSize) {
67-
folly::dynamic chunk = generateNewChunk(chunkSize);
59+
size_t maxChunkBytes) {
60+
folly::dynamic chunk = folly::dynamic::array();
61+
size_t currentChunkBytes = 0;
6862

6963
for (auto& event : events) {
70-
if (chunk.size() == chunkSize) {
64+
auto serializedEvent = TraceEventSerializer::serialize(std::move(event));
65+
size_t eventBytes = TraceEventSerializer::estimateJsonSize(serializedEvent);
66+
67+
if (currentChunkBytes + eventBytes > maxChunkBytes && !chunk.empty()) {
7168
chunkCallback(std::move(chunk));
72-
chunk = generateNewChunk(chunkSize);
69+
chunk = folly::dynamic::array();
70+
currentChunkBytes = 0;
7371
}
7472

75-
chunk.push_back(TraceEventSerializer::serialize(std::move(event)));
73+
chunk.push_back(std::move(serializedEvent));
74+
currentChunkBytes += eventBytes;
7675
}
7776

7877
if (!chunk.empty()) {
@@ -85,26 +84,30 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
8584
ProcessId processId,
8685
HighResTimeStamp recordingStartTimestamp,
8786
const std::function<void(folly::dynamic&& chunk)>& chunkCallback,
88-
uint16_t chunkSize) {
87+
size_t maxChunkBytes) {
8988
if (frameTimings.empty()) {
9089
return;
9190
}
9291

93-
folly::dynamic chunk = generateNewChunk(chunkSize);
92+
folly::dynamic chunk = folly::dynamic::array();
93+
size_t currentChunkBytes = 0;
94+
9495
auto setLayerTreeIdEvent = TraceEventGenerator::createSetLayerTreeIdEvent(
9596
"", // Hardcoded frame name for the default (and only) layer.
9697
FALLBACK_LAYER_TREE_ID,
9798
processId,
9899
frameTimings.front().threadId,
99100
recordingStartTimestamp);
100-
chunk.push_back(
101-
TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent)));
101+
auto serializedSetLayerTreeId =
102+
TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent));
103+
currentChunkBytes +=
104+
TraceEventSerializer::estimateJsonSize(serializedSetLayerTreeId);
105+
chunk.push_back(std::move(serializedSetLayerTreeId));
102106

103107
for (auto&& frameTimingSequence : frameTimings) {
104-
if (chunk.size() >= chunkSize) {
105-
chunkCallback(std::move(chunk));
106-
chunk = generateNewChunk(chunkSize);
107-
}
108+
// Serialize all events for this frame.
109+
folly::dynamic frameEvents = folly::dynamic::array();
110+
size_t totalFrameBytes = 0;
108111

109112
auto [beginDrawingEvent, endDrawingEvent] =
110113
TraceEventGenerator::createFrameTimingsEvents(
@@ -115,10 +118,15 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
115118
processId,
116119
frameTimingSequence.threadId);
117120

118-
chunk.push_back(
119-
TraceEventSerializer::serialize(std::move(beginDrawingEvent)));
120-
chunk.push_back(
121-
TraceEventSerializer::serialize(std::move(endDrawingEvent)));
121+
auto serializedBegin =
122+
TraceEventSerializer::serialize(std::move(beginDrawingEvent));
123+
totalFrameBytes += TraceEventSerializer::estimateJsonSize(serializedBegin);
124+
frameEvents.push_back(std::move(serializedBegin));
125+
126+
auto serializedEnd =
127+
TraceEventSerializer::serialize(std::move(endDrawingEvent));
128+
totalFrameBytes += TraceEventSerializer::estimateJsonSize(serializedEnd);
129+
frameEvents.push_back(std::move(serializedEnd));
122130

123131
if (frameTimingSequence.screenshot.has_value()) {
124132
auto screenshotEvent = TraceEventGenerator::createScreenshotEvent(
@@ -129,9 +137,24 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
129137
processId,
130138
frameTimingSequence.threadId);
131139

132-
chunk.push_back(
133-
TraceEventSerializer::serialize(std::move(screenshotEvent)));
140+
auto serializedScreenshot =
141+
TraceEventSerializer::serialize(std::move(screenshotEvent));
142+
totalFrameBytes +=
143+
TraceEventSerializer::estimateJsonSize(serializedScreenshot);
144+
frameEvents.push_back(std::move(serializedScreenshot));
145+
}
146+
147+
// Flush current chunk if adding this frame would exceed the limit.
148+
if (currentChunkBytes + totalFrameBytes > maxChunkBytes && !chunk.empty()) {
149+
chunkCallback(std::move(chunk));
150+
chunk = folly::dynamic::array();
151+
currentChunkBytes = 0;
152+
}
153+
154+
for (auto& frameEvent : frameEvents) {
155+
chunk.push_back(std::move(frameEvent));
134156
}
157+
currentChunkBytes += totalFrameBytes;
135158
}
136159

137160
if (!chunk.empty()) {

packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,27 @@ class HostTracingProfileSerializer {
2424
public:
2525
/**
2626
* Transforms the profile into a sequence of serialized Trace Events, which
27-
* is split in chunks of sizes \p traceEventsChunkSize or
28-
* \p profileTraceEventsChunkSize, depending on type, and sent with \p
29-
* chunkCallback.
27+
* is split in chunks of at most \p maxChunkBytes serialized bytes or
28+
* \p profileTraceEventsChunkSize events, depending on type, and sent with
29+
* \p chunkCallback.
3030
*/
3131
static void emitAsDataCollectedChunks(
3232
HostTracingProfile &&hostTracingProfile,
3333
const std::function<void(folly::dynamic &&chunk)> &chunkCallback,
34-
uint16_t traceEventsChunkSize,
34+
size_t maxChunkBytes,
3535
uint16_t profileTraceEventsChunkSize);
3636

3737
static void emitPerformanceTraceEvents(
3838
std::vector<TraceEvent> &&events,
3939
const std::function<void(folly::dynamic &&chunk)> &chunkCallback,
40-
uint16_t chunkSize);
40+
size_t maxChunkBytes);
4141

4242
static void emitFrameTimings(
4343
std::vector<FrameTimingSequence> &&frameTimings,
4444
ProcessId processId,
4545
HighResTimeStamp recordingStartTimestamp,
4646
const std::function<void(folly::dynamic &&chunk)> &chunkCallback,
47-
uint16_t chunkSize);
47+
size_t maxChunkBytes);
4848
};
4949

5050
} // namespace facebook::react::jsinspector_modern::tracing

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,46 @@ TraceEventSerializer::serializeProfileChunkCPUProfileNodeCallFrame(
112112
return dynamicCallFrame;
113113
}
114114

115+
/* static */ size_t TraceEventSerializer::estimateJsonSize(
116+
const folly::dynamic& value) {
117+
switch (value.type()) {
118+
case folly::dynamic::Type::NULLT:
119+
return 4; // null
120+
case folly::dynamic::Type::BOOL:
121+
return 5; // false
122+
case folly::dynamic::Type::INT64:
123+
case folly::dynamic::Type::DOUBLE:
124+
return 20; // conservative max for numeric values
125+
case folly::dynamic::Type::STRING:
126+
return value.stringPiece().size() + 2; // quotes
127+
case folly::dynamic::Type::ARRAY: {
128+
size_t size = 2; // []
129+
bool first = true;
130+
for (const auto& element : value) {
131+
if (!first) {
132+
size += 1; // comma
133+
}
134+
first = false;
135+
size += estimateJsonSize(element);
136+
}
137+
return size;
138+
}
139+
case folly::dynamic::Type::OBJECT: {
140+
size_t size = 2; // {}
141+
bool first = true;
142+
for (const auto& [key, val] : value.items()) {
143+
if (!first) {
144+
size += 1; // comma
145+
}
146+
first = false;
147+
// key size + quotes + colon
148+
size += key.stringPiece().size() + 3;
149+
size += estimateJsonSize(val);
150+
}
151+
return size;
152+
}
153+
}
154+
return 0;
155+
}
156+
115157
} // namespace facebook::react::jsinspector_modern::tracing

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ class TraceEventSerializer {
8484
*/
8585
static folly::dynamic serializeProfileChunkCPUProfileNodeCallFrame(
8686
TraceEventProfileChunk::CPUProfile::Node::CallFrame &&callFrame);
87+
88+
/**
89+
* Estimates the JSON-serialized byte size of a folly::dynamic value.
90+
* This is a rough but conservative estimate to avoid the cost of
91+
* double-serialization (once to measure, once to emit).
92+
*/
93+
static size_t estimateJsonSize(const folly::dynamic &value);
8794
};
8895

8996
} // namespace facebook::react::jsinspector_modern::tracing

0 commit comments

Comments
 (0)