Skip to content

Commit

Permalink
Introduce FuseboxTracer for DevTools tracing (facebook#44840)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#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
  • Loading branch information
bgirard authored and facebook-github-bot committed Jun 17, 2024
1 parent 16380aa commit de911ba
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 15 deletions.
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 <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 @@ -13,9 +13,10 @@

#include <cxxreact/JSExecutor.h>
#include <cxxreact/ReactMarker.h>
#include <fusebox/FuseboxTracer.h>
#include <jsi/instrumentation.h>
#include <react/performance/timeline/PerformanceEntryReporter.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
@@ -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/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<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 "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

0 comments on commit de911ba

Please sign in to comment.