Skip to content

Commit 30087a6

Browse files
Benoit Girardfacebook-github-bot
authored andcommitted
Introduce FuseboxTracer for DevTools tracing (facebook#44840)
Summary: Pull Request resolved: facebook#44840 Changelog: [Internal] Introduce a simplified and minimal tracing backend for Fusebox. This backend is sufficient to implement a pretty usable performance panel. Although the more I see how easy this is and how annoying working with Perfetto is, the more I think we should just maintain this going forward. Anyways we can figure that out incrementally. For now the plan is still for this to be temporary. Reviewed By: motiz88 Differential Revision: D57981944 fbshipit-source-id: b3d8c6e8c5a18311bbe98254f8ddf3810fa1334b
1 parent 82325b4 commit 30087a6

File tree

9 files changed

+313
-18
lines changed

9 files changed

+313
-18
lines changed

packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ target_link_libraries(jsinspector
2323
glog
2424
react_featureflags
2525
runtimeexecutor
26+
reactperflogger
2627
)

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

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
#include <chrono>
1717

18+
#include <reactperflogger/fusebox/FuseboxTracer.h>
19+
1820
using namespace std::chrono;
1921
using namespace std::literals::string_view_literals;
2022

@@ -146,25 +148,41 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
146148
shouldSendOKResponse = true;
147149
isFinishedHandlingRequest = true;
148150
} else if (req.method == "Tracing.start") {
149-
// @cdp Tracing.start is implemented as a stub only.
150-
frontendChannel_(cdp::jsonNotification(
151-
// @cdp Tracing.bufferUsage is implemented as a stub only.
152-
"Tracing.bufferUsage",
153-
folly::dynamic::object("percentFull", 0)("eventCount", 0)("value", 0)));
154-
shouldSendOKResponse = true;
151+
// @cdp Tracing.start support is experimental.
152+
if (FuseboxTracer::getFuseboxTracer().startTracing()) {
153+
shouldSendOKResponse = true;
154+
} else {
155+
frontendChannel_(cdp::jsonError(
156+
req.id,
157+
cdp::ErrorCode::InternalError,
158+
"Tracing session already started"));
159+
return;
160+
}
155161
isFinishedHandlingRequest = true;
156162
} else if (req.method == "Tracing.end") {
157-
// @cdp Tracing.end is implemented as a stub only.
158-
frontendChannel_(cdp::jsonNotification(
159-
// @cdp Tracing.dataCollected is implemented as a stub only.
160-
"Tracing.dataCollected",
161-
folly::dynamic::object("value", folly::dynamic::array())));
163+
// @cdp Tracing.end support is experimental.
164+
bool firstChunk = true;
165+
auto id = req.id;
166+
bool wasStopped = FuseboxTracer::getFuseboxTracer().stopTracing(
167+
[this, firstChunk, id](const folly::dynamic& eventsChunk) {
168+
if (firstChunk) {
169+
frontendChannel_(cdp::jsonResult(id));
170+
}
171+
frontendChannel_(cdp::jsonNotification(
172+
"Tracing.dataCollected",
173+
folly::dynamic::object("value", eventsChunk)));
174+
});
175+
if (!wasStopped) {
176+
frontendChannel_(cdp::jsonError(
177+
req.id,
178+
cdp::ErrorCode::InternalError,
179+
"Tracing session not started"));
180+
return;
181+
}
162182
frontendChannel_(cdp::jsonNotification(
163-
// @cdp Tracing.tracingComplete is implemented as a stub only.
164183
"Tracing.tracingComplete",
165184
folly::dynamic::object("dataLossOccurred", false)));
166-
shouldSendOKResponse = true;
167-
isFinishedHandlingRequest = true;
185+
return;
168186
}
169187

170188
if (!isFinishedHandlingRequest && instanceAgent_ &&

packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Pod::Spec.new do |s|
5454
s.dependency "DoubleConversion"
5555
s.dependency "React-runtimeexecutor", version
5656
s.dependency "React-jsi"
57+
s.dependency "React-perflogger", version
5758
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
5859
s.dependency "hermes-engine"
5960
end

packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
#include <cxxreact/ReactMarker.h>
1616
#include <jsi/instrumentation.h>
1717
#include <react/performance/timeline/PerformanceEntryReporter.h>
18-
18+
#include <reactperflogger/fusebox/FuseboxTracer.h>
19+
#include "NativePerformance.h"
1920
#include "Plugins.h"
2021

2122
#ifdef WITH_PERFETTO
@@ -112,6 +113,17 @@ void NativePerformance::measure(
112113
}
113114
}
114115
#endif
116+
std::string trackName = "Web Performance";
117+
const int TRACK_PREFIX = 6;
118+
if (name.starts_with("Track:")) {
119+
const auto trackNameDelimiter = name.find(':', TRACK_PREFIX);
120+
if (trackNameDelimiter != std::string::npos) {
121+
trackName = name.substr(TRACK_PREFIX, trackNameDelimiter - TRACK_PREFIX);
122+
name = name.substr(trackNameDelimiter + 1);
123+
}
124+
}
125+
FuseboxTracer::getFuseboxTracer().addEvent(
126+
name, (uint64_t)startTime, (uint64_t)endTime, trackName);
115127
PerformanceEntryReporter::getInstance()->measure(
116128
name, startTime, endTime, duration, startMark, endMark);
117129
}

packages/react-native/ReactCommon/reactperflogger/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ add_compile_options(
1313
-Wall
1414
-Wpedantic)
1515

16-
file(GLOB reactperflogger_SRC CONFIGURE_DEPENDS reactperflogger/*.cpp)
16+
17+
file(GLOB reactperflogger_SRC CONFIGURE_DEPENDS
18+
reactperflogger/*.cpp
19+
fusebox/*.cpp)
1720
add_library(reactperflogger STATIC ${reactperflogger_SRC})
1821

1922
target_include_directories(reactperflogger PUBLIC .)
23+
24+
target_link_libraries(reactperflogger folly_runtime)

packages/react-native/ReactCommon/reactperflogger/React-perflogger.podspec

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ end
1919
folly_config = get_folly_config()
2020
folly_compiler_flags = folly_config[:compiler_flags]
2121
folly_version = folly_config[:version]
22-
boost_compiler_flags = '-Wno-documentation'
22+
23+
header_search_paths = [
24+
"\"$(PODS_TARGET_SRCROOT)/..\"",
25+
"\"$(PODS_ROOT)/RCT-Folly\"",
26+
"\"$(PODS_ROOT)/DoubleConversion\"",
27+
"\"$(PODS_ROOT)/fmt/include\""
28+
]
2329

2430
Pod::Spec.new do |s|
2531
s.name = "React-perflogger"
@@ -30,7 +36,15 @@ Pod::Spec.new do |s|
3036
s.author = "Meta Platforms, Inc. and its affiliates"
3137
s.platforms = min_supported_versions
3238
s.source = source
33-
s.source_files = "**/*.{cpp,h}"
39+
s.source_files = "reactperflogger/*.{cpp,h}", "fusebox/*.{cpp,h}"
3440
s.header_dir = "reactperflogger"
3541
s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20" }
42+
s.compiler_flags = folly_compiler_flags
43+
s.pod_target_xcconfig = {
44+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
45+
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
46+
}
47+
48+
s.dependency "RCT-Folly", folly_version
49+
s.dependency "DoubleConversion"
3650
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
#include <mutex>
9+
#include <unordered_map>
10+
11+
#include "FuseboxTracer.h"
12+
13+
namespace facebook::react {
14+
15+
bool FuseboxTracer::isTracing() {
16+
std::lock_guard lock(mutex_);
17+
return tracing_;
18+
}
19+
20+
bool FuseboxTracer::startTracing() {
21+
std::lock_guard lock(mutex_);
22+
if (tracing_) {
23+
return false;
24+
}
25+
tracing_ = true;
26+
return true;
27+
}
28+
29+
bool FuseboxTracer::stopTracing(
30+
const std::function<void(const folly::dynamic& eventsChunk)>&
31+
resultCallback) {
32+
std::lock_guard lock(mutex_);
33+
34+
if (!tracing_) {
35+
return false;
36+
}
37+
38+
tracing_ = false;
39+
if (buffer_.empty()) {
40+
return true;
41+
}
42+
43+
auto traceEvents = folly::dynamic::array();
44+
auto savedBuffer = std::move(buffer_);
45+
buffer_.clear();
46+
47+
std::unordered_map<std::string, uint64_t> trackIdMap;
48+
uint64_t nextTrack = 1000;
49+
50+
// Name the main process. Only one process is supported currently.
51+
traceEvents.push_back(folly::dynamic::object(
52+
"args", folly::dynamic::object("name", "Main App"))("cat", "__metadata")(
53+
"name", "process_name")("ph", "M")("pid", 1000)("tid", 0)("ts", 0));
54+
55+
for (auto& event : savedBuffer) {
56+
if (!trackIdMap.contains(event.track)) {
57+
auto trackId = nextTrack++;
58+
trackIdMap[event.track] = trackId;
59+
// New track
60+
traceEvents.push_back(folly::dynamic::object(
61+
"args", folly::dynamic::object("name", event.track))(
62+
"cat", "__metadata")("name", "thread_name")("ph", "M")("pid", 1000)(
63+
"tid", trackId)("ts", 0));
64+
}
65+
auto trackId = trackIdMap[event.track];
66+
67+
// New event
68+
traceEvents.push_back(folly::dynamic::object(
69+
"args", folly::dynamic::object())("cat", "react.native")(
70+
"dur", (event.end - event.start) * 1000)("name", event.name)("ph", "X")(
71+
"ts", event.start * 1000)("pid", 1000)("tid", trackId));
72+
73+
if (traceEvents.size() >= 1000) {
74+
resultCallback(traceEvents);
75+
traceEvents = folly::dynamic::array();
76+
}
77+
}
78+
79+
if (traceEvents.size() >= 1) {
80+
resultCallback(traceEvents);
81+
}
82+
return true;
83+
}
84+
85+
void FuseboxTracer::addEvent(
86+
const std::string& name,
87+
uint64_t start,
88+
uint64_t end,
89+
const std::string& track) {
90+
std::lock_guard<std::mutex> lock(mutex_);
91+
if (!tracing_) {
92+
return;
93+
}
94+
buffer_.push_back(BufferEvent{start, end, name, track});
95+
}
96+
97+
/* static */ FuseboxTracer& FuseboxTracer::getFuseboxTracer() {
98+
static FuseboxTracer tracer;
99+
return tracer;
100+
}
101+
102+
} // namespace facebook::react
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 <functional>
11+
#include <vector>
12+
#include "folly/dynamic.h"
13+
14+
namespace facebook::react {
15+
16+
struct BufferEvent {
17+
uint64_t start;
18+
uint64_t end;
19+
std::string name;
20+
std::string track;
21+
};
22+
23+
class FuseboxTracer {
24+
public:
25+
FuseboxTracer(const FuseboxTracer&) = delete;
26+
27+
bool isTracing();
28+
// Verifies that tracing isn't started and starts tracing all in one step.
29+
// Returns true if we were able to successful start tracing.
30+
bool startTracing();
31+
// Verifies that we're tracing and dumps the trace all in one step to avoid
32+
// TOCTOU bugs. Returns false if we're not tracing. No result callbacks
33+
// are expected in that scenario.
34+
bool stopTracing(const std::function<void(const folly::dynamic& eventsChunk)>&
35+
resultCallback);
36+
void addEvent(
37+
const std::string& name,
38+
uint64_t start,
39+
uint64_t end,
40+
const std::string& track);
41+
42+
static FuseboxTracer& getFuseboxTracer();
43+
44+
private:
45+
FuseboxTracer() {}
46+
47+
bool tracing_{false};
48+
std::vector<BufferEvent> buffer_;
49+
std::mutex mutex_;
50+
};
51+
52+
} // namespace facebook::react
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
#include <gtest/gtest.h>
9+
10+
#include <reactperflogger/fusebox/FuseboxTracer.h>
11+
12+
using namespace ::testing;
13+
14+
namespace facebook::react {
15+
16+
namespace {
17+
18+
class FuseboxTracerTest : public ::testing::Test {
19+
protected:
20+
FuseboxTracerTest() = default;
21+
22+
~FuseboxTracerTest() override = default;
23+
24+
void SetUp() override {
25+
stopTracingAndCollect();
26+
}
27+
28+
void TearDown() override {
29+
stopTracingAndCollect();
30+
}
31+
32+
folly::dynamic stopTracingAndCollect() {
33+
folly::dynamic trace = folly::dynamic::array;
34+
FuseboxTracer::getFuseboxTracer().stopTracing(
35+
[&trace](const folly::dynamic& eventsChunk) {
36+
for (const auto& event : eventsChunk) {
37+
trace.push_back(event);
38+
}
39+
});
40+
return trace;
41+
}
42+
};
43+
44+
} // namespace
45+
46+
TEST_F(FuseboxTracerTest, TracingOffByDefault) {
47+
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
48+
}
49+
50+
TEST_F(FuseboxTracerTest, TracingOn) {
51+
FuseboxTracer::getFuseboxTracer().startTracing();
52+
EXPECT_TRUE(FuseboxTracer::getFuseboxTracer().isTracing());
53+
stopTracingAndCollect();
54+
}
55+
56+
TEST_F(FuseboxTracerTest, DiscardEventWhenNotOn) {
57+
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
58+
EXPECT_EQ(stopTracingAndCollect().size(), 0);
59+
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
60+
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
61+
EXPECT_EQ(stopTracingAndCollect().size(), 0);
62+
}
63+
64+
TEST_F(FuseboxTracerTest, NoDefaultEvents) {
65+
FuseboxTracer::getFuseboxTracer().startTracing();
66+
EXPECT_EQ(stopTracingAndCollect().size(), 0);
67+
}
68+
69+
TEST_F(FuseboxTracerTest, SimpleEvent) {
70+
FuseboxTracer::getFuseboxTracer().startTracing();
71+
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
72+
EXPECT_GE(stopTracingAndCollect().size(), 1);
73+
}
74+
75+
TEST_F(FuseboxTracerTest, MultiEvents) {
76+
FuseboxTracer::getFuseboxTracer().startTracing();
77+
for (int i = 0; i < 10; i++) {
78+
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
79+
}
80+
EXPECT_GE(stopTracingAndCollect().size(), 10);
81+
EXPECT_EQ(stopTracingAndCollect().size(), 0);
82+
}
83+
84+
TEST_F(FuseboxTracerTest, ShouldEndTracingEvenIfThereIsNoEvents) {
85+
FuseboxTracer::getFuseboxTracer().startTracing();
86+
EXPECT_EQ(stopTracingAndCollect().size(), 0);
87+
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
88+
}
89+
90+
} // namespace facebook::react

0 commit comments

Comments
 (0)