Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce FuseboxTracer for DevTools tracing #44840

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ target_link_libraries(jsinspector
glog
react_featureflags
runtimeexecutor
reactperflogger
)
46 changes: 32 additions & 14 deletions packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

#include <chrono>

#include <reactperflogger/fusebox/FuseboxTracer.h>

using namespace std::chrono;
using namespace std::literals::string_view_literals;

Expand Down Expand Up @@ -146,25 +148,41 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
} else if (req.method == "Tracing.start") {
// @cdp Tracing.start is implemented as a stub only.
frontendChannel_(cdp::jsonNotification(
// @cdp Tracing.bufferUsage is implemented as a stub only.
"Tracing.bufferUsage",
folly::dynamic::object("percentFull", 0)("eventCount", 0)("value", 0)));
shouldSendOKResponse = true;
// @cdp Tracing.start support is experimental.
if (FuseboxTracer::getFuseboxTracer().startTracing()) {
shouldSendOKResponse = true;
} else {
frontendChannel_(cdp::jsonError(
req.id,
cdp::ErrorCode::InternalError,
"Tracing session already started"));
return;
}
isFinishedHandlingRequest = true;
} else if (req.method == "Tracing.end") {
// @cdp Tracing.end is implemented as a stub only.
frontendChannel_(cdp::jsonNotification(
// @cdp Tracing.dataCollected is implemented as a stub only.
"Tracing.dataCollected",
folly::dynamic::object("value", folly::dynamic::array())));
// @cdp Tracing.end support is experimental.
bool firstChunk = true;
auto id = req.id;
bool wasStopped = FuseboxTracer::getFuseboxTracer().stopTracing(
[this, firstChunk, id](const folly::dynamic& eventsChunk) {
if (firstChunk) {
frontendChannel_(cdp::jsonResult(id));
}
frontendChannel_(cdp::jsonNotification(
"Tracing.dataCollected",
folly::dynamic::object("value", eventsChunk)));
});
if (!wasStopped) {
frontendChannel_(cdp::jsonError(
req.id,
cdp::ErrorCode::InternalError,
"Tracing session not started"));
return;
}
frontendChannel_(cdp::jsonNotification(
// @cdp Tracing.tracingComplete is implemented as a stub only.
"Tracing.tracingComplete",
folly::dynamic::object("dataLossOccurred", false)));
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
return;
}

if (!isFinishedHandlingRequest && instanceAgent_ &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Pod::Spec.new do |s|
s.dependency "DoubleConversion"
s.dependency "React-runtimeexecutor", version
s.dependency "React-jsi"
s.dependency "React-perflogger", version
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
#include <cxxreact/ReactMarker.h>
#include <jsi/instrumentation.h>
#include <react/performance/timeline/PerformanceEntryReporter.h>

#include <reactperflogger/fusebox/FuseboxTracer.h>
#include "NativePerformance.h"
#include "Plugins.h"

#ifdef WITH_PERFETTO
Expand Down Expand Up @@ -112,6 +113,17 @@ void NativePerformance::measure(
}
}
#endif
std::string trackName = "Web Performance";
const int TRACK_PREFIX = 6;
if (name.starts_with("Track:")) {
const auto trackNameDelimiter = name.find(':', TRACK_PREFIX);
if (trackNameDelimiter != std::string::npos) {
trackName = name.substr(TRACK_PREFIX, trackNameDelimiter - TRACK_PREFIX);
name = name.substr(trackNameDelimiter + 1);
}
}
FuseboxTracer::getFuseboxTracer().addEvent(
name, (uint64_t)startTime, (uint64_t)endTime, trackName);
PerformanceEntryReporter::getInstance()->measure(
name, startTime, endTime, duration, startMark, endMark);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ add_compile_options(
-Wall
-Wpedantic)

file(GLOB reactperflogger_SRC CONFIGURE_DEPENDS reactperflogger/*.cpp)

file(GLOB reactperflogger_SRC CONFIGURE_DEPENDS
reactperflogger/*.cpp
fusebox/*.cpp)
add_library(reactperflogger STATIC ${reactperflogger_SRC})

target_include_directories(reactperflogger PUBLIC .)

target_link_libraries(reactperflogger folly_runtime)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
boost_compiler_flags = '-Wno-documentation'

header_search_paths = [
"\"$(PODS_TARGET_SRCROOT)/..\"",
"\"$(PODS_ROOT)/RCT-Folly\"",
"\"$(PODS_ROOT)/DoubleConversion\"",
"\"$(PODS_ROOT)/fmt/include\""
]

Pod::Spec.new do |s|
s.name = "React-perflogger"
Expand All @@ -30,7 +36,15 @@ Pod::Spec.new do |s|
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "**/*.{cpp,h}"
s.source_files = "reactperflogger/*.{cpp,h}", "fusebox/*.{cpp,h}"
s.header_dir = "reactperflogger"
s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20" }
s.compiler_flags = folly_compiler_flags
s.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
}

s.dependency "RCT-Folly", folly_version
s.dependency "DoubleConversion"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <mutex>
#include <unordered_map>

#include "FuseboxTracer.h"

namespace facebook::react {

bool FuseboxTracer::isTracing() {
std::lock_guard lock(mutex_);
return tracing_;
}

bool FuseboxTracer::startTracing() {
std::lock_guard lock(mutex_);
if (tracing_) {
return false;
}
tracing_ = true;
return true;
}

bool FuseboxTracer::stopTracing(
const std::function<void(const folly::dynamic& eventsChunk)>&
resultCallback) {
std::lock_guard lock(mutex_);

if (!tracing_) {
return false;
}

tracing_ = false;
if (buffer_.empty()) {
return true;
}

auto traceEvents = folly::dynamic::array();
auto savedBuffer = std::move(buffer_);
buffer_.clear();

std::unordered_map<std::string, uint64_t> trackIdMap;
uint64_t nextTrack = 1000;

// Name the main process. Only one process is supported currently.
traceEvents.push_back(folly::dynamic::object(
"args", folly::dynamic::object("name", "Main App"))("cat", "__metadata")(
"name", "process_name")("ph", "M")("pid", 1000)("tid", 0)("ts", 0));

for (auto& event : savedBuffer) {
if (!trackIdMap.contains(event.track)) {
auto trackId = nextTrack++;
trackIdMap[event.track] = trackId;
// New track
traceEvents.push_back(folly::dynamic::object(
"args", folly::dynamic::object("name", event.track))(
"cat", "__metadata")("name", "thread_name")("ph", "M")("pid", 1000)(
"tid", trackId)("ts", 0));
}
auto trackId = trackIdMap[event.track];

// New event
traceEvents.push_back(folly::dynamic::object(
"args", folly::dynamic::object())("cat", "react.native")(
"dur", (event.end - event.start) * 1000)("name", event.name)("ph", "X")(
"ts", event.start * 1000)("pid", 1000)("tid", trackId));

if (traceEvents.size() >= 1000) {
resultCallback(traceEvents);
traceEvents = folly::dynamic::array();
}
}

if (traceEvents.size() >= 1) {
resultCallback(traceEvents);
}
return true;
}

void FuseboxTracer::addEvent(
const std::string& name,
uint64_t start,
uint64_t end,
const std::string& track) {
std::lock_guard<std::mutex> lock(mutex_);
if (!tracing_) {
return;
}
buffer_.push_back(BufferEvent{start, end, name, track});
}

/* static */ FuseboxTracer& FuseboxTracer::getFuseboxTracer() {
static FuseboxTracer tracer;
return tracer;
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <functional>
#include <vector>
#include "folly/dynamic.h"

namespace facebook::react {

struct BufferEvent {
uint64_t start;
uint64_t end;
std::string name;
std::string track;
};

class FuseboxTracer {
public:
FuseboxTracer(const FuseboxTracer&) = delete;

bool isTracing();
// Verifies that tracing isn't started and starts tracing all in one step.
// Returns true if we were able to successful start tracing.
bool startTracing();
// Verifies that we're tracing and dumps the trace all in one step to avoid
// TOCTOU bugs. Returns false if we're not tracing. No result callbacks
// are expected in that scenario.
bool stopTracing(const std::function<void(const folly::dynamic& eventsChunk)>&
resultCallback);
void addEvent(
const std::string& name,
uint64_t start,
uint64_t end,
const std::string& track);

static FuseboxTracer& getFuseboxTracer();

private:
FuseboxTracer() {}

bool tracing_{false};
std::vector<BufferEvent> buffer_;
std::mutex mutex_;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <gtest/gtest.h>

#include <reactperflogger/fusebox/FuseboxTracer.h>

using namespace ::testing;

namespace facebook::react {

namespace {

class FuseboxTracerTest : public ::testing::Test {
protected:
FuseboxTracerTest() = default;

~FuseboxTracerTest() override = default;

void SetUp() override {
stopTracingAndCollect();
}

void TearDown() override {
stopTracingAndCollect();
}

folly::dynamic stopTracingAndCollect() {
folly::dynamic trace = folly::dynamic::array;
FuseboxTracer::getFuseboxTracer().stopTracing(
[&trace](const folly::dynamic& eventsChunk) {
for (const auto& event : eventsChunk) {
trace.push_back(event);
}
});
return trace;
}
};

} // namespace

TEST_F(FuseboxTracerTest, TracingOffByDefault) {
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
}

TEST_F(FuseboxTracerTest, TracingOn) {
FuseboxTracer::getFuseboxTracer().startTracing();
EXPECT_TRUE(FuseboxTracer::getFuseboxTracer().isTracing());
stopTracingAndCollect();
}

TEST_F(FuseboxTracerTest, DiscardEventWhenNotOn) {
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
EXPECT_EQ(stopTracingAndCollect().size(), 0);
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
EXPECT_EQ(stopTracingAndCollect().size(), 0);
}

TEST_F(FuseboxTracerTest, NoDefaultEvents) {
FuseboxTracer::getFuseboxTracer().startTracing();
EXPECT_EQ(stopTracingAndCollect().size(), 0);
}

TEST_F(FuseboxTracerTest, SimpleEvent) {
FuseboxTracer::getFuseboxTracer().startTracing();
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
EXPECT_GE(stopTracingAndCollect().size(), 1);
}

TEST_F(FuseboxTracerTest, MultiEvents) {
FuseboxTracer::getFuseboxTracer().startTracing();
for (int i = 0; i < 10; i++) {
FuseboxTracer::getFuseboxTracer().addEvent("test", 0, 0, "default track");
}
EXPECT_GE(stopTracingAndCollect().size(), 10);
EXPECT_EQ(stopTracingAndCollect().size(), 0);
}

TEST_F(FuseboxTracerTest, ShouldEndTracingEvenIfThereIsNoEvents) {
FuseboxTracer::getFuseboxTracer().startTracing();
EXPECT_EQ(stopTracingAndCollect().size(), 0);
EXPECT_FALSE(FuseboxTracer::getFuseboxTracer().isTracing());
}

} // namespace facebook::react