Skip to content

Commit 4382a95

Browse files
motiz88facebook-github-bot
authored andcommitted
Stream background traces to multiple RNDT sessions if present (facebook#55351)
Summary: Changelog: [Internal] ## Behaviour Fixes an edge case in the way background traces are emitted when multiple RNDT frontends (or other clients with the `ReactNativeApplication` domain enabled) are connected to a single Host. Previously, only one of the clients would receive the CDP events for the trace. With this diff, *all* of them will. NOTE: This leaves another, even smaller edge case / inconsistency: if there are *no* RNDT frontends at the time a background trace ends, we still only send it to the first frontend (if any) that connects. This is logically more defensible than picking one client out of multiple active connections ( = what's fixed in this diff). ## Implementation Tracing functionality is somewhat awkwardly split between `HostTarget` (background traces) and `TracingAgent` (CDP-initiated traces). This diff doesn't attempt to fully clean this up[1], but we do reduce duplication and boilerplate by creating a helper function used by both. This function encapsulates the gnarly/surprising details of dealing with `tracing::HostTracingProfile` objects in a memory-efficient way: 1. As per the existing implementation, we serialise them to JSON chunk-by-chunk, destroying the original object in the process. 2. Each chunk is sent to all relevant sessions *and destroyed* before we begin serialising the next one. [1] In future work, we should probably move the Host's tracing state and logic into its own `TracingTarget` class. The naturally close coupling of a Target with its corresponding Agent would be helpful here. Reviewed By: huntie Differential Revision: D90888852
1 parent 25bb5e1 commit 4382a95

8 files changed

Lines changed: 150 additions & 108 deletions

File tree

packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,8 @@ class HostAgent::Impl final {
379379
}
380380
}
381381

382-
bool hasFuseboxClientConnected() const {
383-
return fuseboxClientType_ == FuseboxClientType::Fusebox;
384-
}
385-
386-
void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) {
387-
assert(
388-
hasFuseboxClientConnected() &&
389-
"Attempted to emit a trace recording to a non-Fusebox client");
390-
tracingAgent_.emitExternalHostTracingProfile(std::move(tracingProfile));
382+
bool isEligibleForBackgroundTrace() const {
383+
return sessionState_.isReactNativeApplicationDomainEnabled;
391384
}
392385

393386
void emitSystemStateChanged(bool isSingleHost) {
@@ -500,10 +493,9 @@ class HostAgent::Impl final {
500493

501494
void handleRequest(const cdp::PreparsedRequest& req) {}
502495
void setCurrentInstanceAgent(std::shared_ptr<InstanceAgent> agent) {}
503-
bool hasFuseboxClientConnected() const {
496+
bool isEligibleForBackgroundTrace() const {
504497
return false;
505498
}
506-
void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) {}
507499
void emitSystemStateChanged(bool isSingleHost) {}
508500
};
509501

@@ -535,17 +527,8 @@ void HostAgent::setCurrentInstanceAgent(
535527
impl_->setCurrentInstanceAgent(std::move(instanceAgent));
536528
}
537529

538-
bool HostAgent::hasFuseboxClientConnected() const {
539-
return impl_->hasFuseboxClientConnected();
540-
}
541-
542-
void HostAgent::emitExternalTracingProfile(
543-
tracing::HostTracingProfile tracingProfile) {
544-
impl_->emitExternalTracingProfile(std::move(tracingProfile));
545-
}
546-
547-
void HostAgent::emitSystemStateChanged(bool isSingleHost) {
548-
impl_->emitSystemStateChanged(isSingleHost);
530+
bool HostAgent::isEligibleForBackgroundTrace() const {
531+
return impl_->isEligibleForBackgroundTrace();
549532
}
550533

551534
#pragma mark - Tracing

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,10 @@ class HostAgent final {
6767
void setCurrentInstanceAgent(std::shared_ptr<InstanceAgent> agent);
6868

6969
/**
70-
* Returns whether this HostAgent is part of the session that has an active
71-
* Fusebox client connecte, i.e. with Chrome DevTools Frontend fork for React
72-
* Native.
70+
* Returns whether this HostAgent is eligible to receive notifications about
71+
* background traces.
7372
*/
74-
bool hasFuseboxClientConnected() const;
75-
76-
/**
77-
* Emits the HostTracingProfile that was captured externally, not via the
78-
* CDP-initiated request.
79-
*/
80-
void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile);
73+
bool isEligibleForBackgroundTrace() const;
8174

8275
/**
8376
* Emits a system state changed event when the number of ReactHost instances

packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "HostTarget.h"
99
#include "HostAgent.h"
1010
#include "HostTargetTraceRecording.h"
11+
#include "HostTargetTracing.h"
1112
#include "InspectorInterfaces.h"
1213
#include "InspectorUtilities.h"
1314
#include "InstanceTarget.h"
@@ -106,6 +107,10 @@ class HostTargetSession {
106107
return hostAgent_;
107108
}
108109

110+
FrontendChannel dangerouslyGetFrontendChannel() {
111+
return frontendChannel_;
112+
}
113+
109114
private:
110115
// Owned by this instance, but shared (weakly) with the frontend channel
111116
std::shared_ptr<RAIIRemoteConnection> remote_;
@@ -370,20 +375,27 @@ folly::dynamic createHostMetadataPayload(const HostTargetMetadata& metadata) {
370375
}
371376

372377
bool HostTarget::maybeEmitStashedBackgroundTrace() {
373-
bool emitted = false;
374-
sessions_.forEach([&](auto& session) {
375-
if (emitted) {
376-
return;
377-
}
378-
if (session.agent().hasFuseboxClientConnected()) {
379-
auto stashedTrace = std::exchange(stashedTracingProfile_, std::nullopt);
380-
if (stashedTrace) {
381-
session.agent().emitExternalTracingProfile(std::move(*stashedTrace));
382-
}
383-
emitted = true;
378+
std::vector<FrontendChannel> eligibleFrontendChannels;
379+
eligibleFrontendChannels.reserve(sessions_.size());
380+
sessions_.forEach([&eligibleFrontendChannels](auto& session) {
381+
if (session.agent().isEligibleForBackgroundTrace()) {
382+
eligibleFrontendChannels.push_back(
383+
session.dangerouslyGetFrontendChannel());
384384
}
385385
});
386-
return emitted;
386+
387+
if (eligibleFrontendChannels.empty()) {
388+
return false;
389+
}
390+
391+
auto stashedTrace = std::exchange(stashedTracingProfile_, std::nullopt);
392+
if (stashedTrace) {
393+
emitNotificationsForTracingProfile(
394+
std::move(*stashedTrace),
395+
eligibleFrontendChannels,
396+
/* isBackgroundTrace */ true);
397+
}
398+
return true;
387399
}
388400

389401
bool HostTarget::stopAndMaybeEmitBackgroundTrace() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class HostTargetController final {
244244
tracing::HostTracingProfile stopTracing();
245245

246246
/**
247-
* If there is a stashed background trace, emit it to the first eligible session.
247+
* If there is a stashed background trace, emit it to all eligible sessions.
248248
* \return true if an eligible session is found (even if there was no stashed background trace).
249249
*/
250250
bool maybeEmitStashedBackgroundTrace();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include "InspectorInterfaces.h"
11+
12+
#include "cdp/CdpJson.h"
13+
#include "tracing/HostTracingProfile.h"
14+
#include "tracing/HostTracingProfileSerializer.h"
15+
16+
#include <array>
17+
#include <concepts>
18+
#include <cstdint>
19+
#include <ranges>
20+
21+
namespace facebook::react::jsinspector_modern {
22+
23+
/**
24+
* Emits a captured HostTracingProfile in a series of
25+
* Tracing.dataCollected events, followed by a Tracing.tracingComplete event, to zero or more
26+
* FrontendChannels. If \p isBackgroundTrace is true, a ReactNativeApplication.traceRequested
27+
* notification is sent to each FrontendChannel before the trace events are emitted.
28+
*/
29+
template <typename ChannelsRange>
30+
void emitNotificationsForTracingProfile(
31+
tracing::HostTracingProfile &&hostTracingProfile,
32+
const ChannelsRange &channels,
33+
bool isBackgroundTrace)
34+
requires std::ranges::range<ChannelsRange> &&
35+
std::convertible_to<std::ranges::range_value_t<ChannelsRange>, FrontendChannel>
36+
{
37+
/**
38+
* Threshold for the size Trace Event chunk, that will be flushed out with
39+
* Tracing.dataCollected event.
40+
*/
41+
static constexpr uint16_t TRACE_EVENT_CHUNK_SIZE = 1000;
42+
43+
/**
44+
* The maximum number of ProfileChunk trace events
45+
* that will be sent in a single CDP Tracing.dataCollected message.
46+
*/
47+
static constexpr uint16_t PROFILE_TRACE_EVENT_CHUNK_SIZE = 10;
48+
49+
if (std::ranges::empty(channels)) {
50+
return;
51+
}
52+
53+
if (isBackgroundTrace) {
54+
for (auto &frontendChannel : channels) {
55+
frontendChannel(cdp::jsonNotification("ReactNativeApplication.traceRequested"));
56+
}
57+
}
58+
59+
// Serialize each chunk once and send it to all eligible sessions.
60+
tracing::HostTracingProfileSerializer::emitAsDataCollectedChunks(
61+
std::move(hostTracingProfile),
62+
[&](folly::dynamic &&serializedChunk) {
63+
for (auto &frontendChannel : channels) {
64+
frontendChannel(
65+
cdp::jsonNotification("Tracing.dataCollected", folly::dynamic::object("value", serializedChunk)));
66+
}
67+
},
68+
TRACE_EVENT_CHUNK_SIZE,
69+
PROFILE_TRACE_EVENT_CHUNK_SIZE);
70+
71+
for (auto &frontendChannel : channels) {
72+
frontendChannel(
73+
cdp::jsonNotification("Tracing.tracingComplete", folly::dynamic::object("dataLossOccurred", false)));
74+
}
75+
}
76+
77+
/**
78+
* Convenience overload of emitNotificationsForTracingProfile() for a single FrontendChannel.
79+
*/
80+
inline void emitNotificationsForTracingProfile(
81+
tracing::HostTracingProfile &&hostTracingProfile,
82+
const FrontendChannel &channel,
83+
bool isBackgroundTrace)
84+
{
85+
std::array<FrontendChannel, 1> channels{channel};
86+
emitNotificationsForTracingProfile(std::move(hostTracingProfile), channels, isBackgroundTrace);
87+
}
88+
89+
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,15 @@
66
*/
77

88
#include "TracingAgent.h"
9+
#include "HostTargetTracing.h"
910

10-
#include <jsinspector-modern/tracing/HostTracingProfileSerializer.h>
1111
#include <jsinspector-modern/tracing/PerformanceTracer.h>
1212
#include <jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h>
1313
#include <jsinspector-modern/tracing/TraceEventSerializer.h>
1414
#include <jsinspector-modern/tracing/TracingMode.h>
1515

1616
namespace facebook::react::jsinspector_modern {
1717

18-
namespace {
19-
20-
/**
21-
* Threshold for the size Trace Event chunk, that will be flushed out with
22-
* Tracing.dataCollected event.
23-
*/
24-
const uint16_t TRACE_EVENT_CHUNK_SIZE = 1000;
25-
26-
/**
27-
* The maximum number of ProfileChunk trace events
28-
* that will be sent in a single CDP Tracing.dataCollected message.
29-
*/
30-
const uint16_t PROFILE_TRACE_EVENT_CHUNK_SIZE = 10;
31-
32-
} // namespace
33-
3418
TracingAgent::TracingAgent(
3519
FrontendChannel frontendChannel,
3620
SessionState& sessionState,
@@ -112,38 +96,14 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
11296
// Send response to Tracing.end request.
11397
frontendChannel_(cdp::jsonResult(req.id));
11498

115-
emitHostTracingProfile(std::move(tracingProfile));
99+
emitNotificationsForTracingProfile(
100+
std::move(tracingProfile),
101+
frontendChannel_,
102+
/* isBackgroundTrace */ false);
116103
return true;
117104
}
118105

119106
return false;
120107
}
121108

122-
void TracingAgent::emitExternalHostTracingProfile(
123-
tracing::HostTracingProfile tracingProfile) {
124-
frontendChannel_(
125-
cdp::jsonNotification("ReactNativeApplication.traceRequested"));
126-
emitHostTracingProfile(std::move(tracingProfile));
127-
}
128-
129-
void TracingAgent::emitHostTracingProfile(
130-
tracing::HostTracingProfile tracingProfile) {
131-
auto dataCollectedCallback = [this](folly::dynamic&& eventsChunk) {
132-
frontendChannel_(
133-
cdp::jsonNotification(
134-
"Tracing.dataCollected",
135-
folly::dynamic::object("value", std::move(eventsChunk))));
136-
};
137-
tracing::HostTracingProfileSerializer::emitAsDataCollectedChunks(
138-
std::move(tracingProfile),
139-
dataCollectedCallback,
140-
TRACE_EVENT_CHUNK_SIZE,
141-
PROFILE_TRACE_EVENT_CHUNK_SIZE);
142-
143-
frontendChannel_(
144-
cdp::jsonNotification(
145-
"Tracing.tracingComplete",
146-
folly::dynamic::object("dataLossOccurred", false)));
147-
}
148-
149109
} // namespace facebook::react::jsinspector_modern

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ class TracingAgent {
4141
*/
4242
bool handleRequest(const cdp::PreparsedRequest &req);
4343

44-
/**
45-
* Emits the HostTracingProfile that was stashed externally by the HostTarget.
46-
*/
47-
void emitExternalHostTracingProfile(tracing::HostTracingProfile tracingProfile);
48-
4944
private:
5045
/**
5146
* A channel used to send responses and events to the frontend.
@@ -55,12 +50,6 @@ class TracingAgent {
5550
SessionState &sessionState_;
5651

5752
HostTargetController &hostTargetController_;
58-
59-
/**
60-
* Emits captured HostTracingProfile in a series of
61-
* Tracing.dataCollected events, followed by a Tracing.tracingComplete event.
62-
*/
63-
void emitHostTracingProfile(tracing::HostTracingProfile tracingProfile);
6453
};
6554

6655
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ TEST_F(TracingTest, BackgroundTracingIsRejectedWhileCDPTracingIsRunning) {
185185
page_->stopTracing();
186186
}
187187

188-
TEST_F(TracingTest, EmitsToFirstEligibleSessionWhenMultipleSessionsEnabled) {
188+
TEST_F(TracingTest, EmitsToAllSessionsWithReactNativeApplicationDomainEnabled) {
189189
auto secondaryFusebox = this->connectSecondary();
190190
auto secondaryNonFusebox = this->connectSecondary();
191191

@@ -230,9 +230,11 @@ TEST_F(TracingTest, EmitsToFirstEligibleSessionWhenMultipleSessionsEnabled) {
230230
now + HighResDuration::fromNanoseconds(10),
231231
now + HighResDuration::fromNanoseconds(50)));
232232

233-
// Primary session should receive the trace (first eligible session).
234-
// Events within a session are ordered.
233+
// Primary and secondaryFusebox sessions should receive the trace.
234+
// Events within each session are ordered, but order between sessions is
235+
// arbitrary.
235236
Sequence primarySeq;
237+
Sequence secondarySeq;
236238

237239
EXPECT_CALL(
238240
this->fromPage(),
@@ -249,12 +251,26 @@ TEST_F(TracingTest, EmitsToFirstEligibleSessionWhenMultipleSessionsEnabled) {
249251
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete"))))
250252
.InSequence(primarySeq);
251253

252-
// secondaryFusebox and secondaryNonFusebox should NOT receive anything
253-
// (only first eligible session receives the trace)
254-
EXPECT_CALL(secondaryFusebox.fromPage(), onMessage(_)).Times(0);
254+
EXPECT_CALL(
255+
secondaryFusebox.fromPage(),
256+
onMessage(JsonParsed(
257+
AtJsonPtr("/method", "ReactNativeApplication.traceRequested"))))
258+
.InSequence(secondarySeq);
259+
EXPECT_CALL(
260+
secondaryFusebox.fromPage(),
261+
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected"))))
262+
.Times(AtLeast(1))
263+
.InSequence(secondarySeq);
264+
EXPECT_CALL(
265+
secondaryFusebox.fromPage(),
266+
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete"))))
267+
.InSequence(secondarySeq);
268+
269+
// secondaryNonFusebox should NOT receive anything (it did not enable the
270+
// domain)
255271
EXPECT_CALL(secondaryNonFusebox.fromPage(), onMessage(_)).Times(0);
256272

257-
// Stop tracing and emit to first eligible session
273+
// Stop tracing and emit to all eligible sessions
258274
EXPECT_TRUE(this->page_->stopAndMaybeEmitBackgroundTrace());
259275
}
260276

0 commit comments

Comments
 (0)