From 4579008479470caa953e46d74f7f0a063ea827c7 Mon Sep 17 00:00:00 2001 From: Benoit Girard Date: Mon, 17 Jun 2024 06:34:36 -0700 Subject: [PATCH] Introduce FuseboxTracer for DevTools tracing (#44840) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44840 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. Differential Revision: D57981944 --- .../jsinspector-modern/HostAgent.cpp | 46 +++++--- .../webperformance/NativePerformance.cpp | 14 ++- .../reactperflogger/fusebox/FuseboxTracer.cpp | 102 ++++++++++++++++++ .../reactperflogger/fusebox/FuseboxTracer.h | 52 +++++++++ .../fusebox/tests/FuseboxTracerTest.cpp | 90 ++++++++++++++++ 5 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.cpp create mode 100644 packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.h create mode 100644 packages/react-native/ReactCommon/reactperflogger/fusebox/tests/FuseboxTracerTest.cpp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 46c1a94df312e1..2fa6fd299d7edf 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -15,6 +15,8 @@ #include +#include + using namespace std::chrono; using namespace std::literals::string_view_literals; @@ -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_ && diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp index 1d8af0bcdb495a..01db0d57e4a904 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp @@ -13,9 +13,10 @@ #include #include +#include #include #include - +#include "NativePerformance.h" #include "Plugins.h" #ifdef WITH_PERFETTO @@ -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); } diff --git a/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.cpp b/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.cpp new file mode 100644 index 00000000000000..263830c37fea66 --- /dev/null +++ b/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.cpp @@ -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 +#include + +#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& + 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 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 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 diff --git a/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.h b/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.h new file mode 100644 index 00000000000000..d4fabca26ad935 --- /dev/null +++ b/packages/react-native/ReactCommon/reactperflogger/fusebox/FuseboxTracer.h @@ -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 +#include +#include "folly/json/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& + 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 buffer_; + std::mutex mutex_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/reactperflogger/fusebox/tests/FuseboxTracerTest.cpp b/packages/react-native/ReactCommon/reactperflogger/fusebox/tests/FuseboxTracerTest.cpp new file mode 100644 index 00000000000000..bae13b26a790a0 --- /dev/null +++ b/packages/react-native/ReactCommon/reactperflogger/fusebox/tests/FuseboxTracerTest.cpp @@ -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 + +#include "fusebox/FuseboxTracer.h" + +using namespace ::testing; + +namespace facebook::react { + +namespace { + +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; +} + +class FuseboxTracerTest : public ::testing::Test { + protected: + FuseboxTracerTest() = default; + + ~FuseboxTracerTest() override = default; + + void SetUp() override { + stopTracingAndCollect(); + } + + void TearDown() override { + stopTracingAndCollect(); + } +}; + +} // 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