first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB jsinspector_tracing_SRC CONFIGURE_DEPENDS *.cpp)
add_library(jsinspector_tracing OBJECT ${jsinspector_tracing_SRC})
target_merge_so(jsinspector_tracing)
target_include_directories(jsinspector_tracing PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector_tracing
folly_runtime
jsi
jsinspector_network
oscompat
react_timing
)
target_compile_reactnative_options(jsinspector_tracing PRIVATE)
target_compile_options(jsinspector_tracing PRIVATE -Wpedantic)

View File

@@ -0,0 +1,22 @@
/*
* 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 "ConsoleTimeStamp.h"
namespace facebook::react::jsinspector_modern::tracing {
std::optional<folly::dynamic> getConsoleTimeStampDetailFromObject(
jsi::Runtime& runtime,
const jsi::Value& detailValue) {
try {
return jsi::dynamicFromValue(runtime, detailValue);
} catch (jsi::JSIException&) {
return std::nullopt;
}
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,103 @@
/*
* 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 <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/timing/primitives.h>
#include <cassert>
#include <optional>
#include <stdexcept>
#include <string>
#include <variant>
namespace facebook::react::jsinspector_modern::tracing {
// https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp
using ConsoleTimeStampEntry = std::variant<HighResTimeStamp, std::string>;
// https://developer.chrome.com/docs/devtools/performance/extension#devtools_object
// Although warning is not listed in Chrome DevTools announcement, it is
// actually supported.
enum class ConsoleTimeStampColor {
Primary,
PrimaryLight,
PrimaryDark,
Secondary,
SecondaryLight,
SecondaryDark,
Tertiary,
TertiaryLight,
TertiaryDark,
Warning,
Error,
};
inline std::string consoleTimeStampColorToString(ConsoleTimeStampColor color)
{
switch (color) {
case ConsoleTimeStampColor::Primary:
return "primary";
case ConsoleTimeStampColor::PrimaryLight:
return "primary-light";
case ConsoleTimeStampColor::PrimaryDark:
return "primary-dark";
case ConsoleTimeStampColor::Secondary:
return "secondary";
case ConsoleTimeStampColor::SecondaryLight:
return "secondary-light";
case ConsoleTimeStampColor::SecondaryDark:
return "secondary-dark";
case ConsoleTimeStampColor::Tertiary:
return "tertiary";
case ConsoleTimeStampColor::TertiaryLight:
return "tertiary-light";
case ConsoleTimeStampColor::TertiaryDark:
return "tertiary-dark";
case ConsoleTimeStampColor::Warning:
return "warning";
case ConsoleTimeStampColor::Error:
return "error";
default:
throw std::runtime_error("Unknown ConsoleTimeStampColor");
}
};
inline std::optional<ConsoleTimeStampColor> getConsoleTimeStampColorFromString(const std::string &str)
{
if (str == "primary") {
return ConsoleTimeStampColor::Primary;
} else if (str == "primary-light") {
return ConsoleTimeStampColor::PrimaryLight;
} else if (str == "primary-dark") {
return ConsoleTimeStampColor::PrimaryDark;
} else if (str == "secondary") {
return ConsoleTimeStampColor::Secondary;
} else if (str == "secondary-light") {
return ConsoleTimeStampColor::SecondaryLight;
} else if (str == "secondary-dark") {
return ConsoleTimeStampColor::SecondaryDark;
} else if (str == "tertiary") {
return ConsoleTimeStampColor::Tertiary;
} else if (str == "tertiary-light") {
return ConsoleTimeStampColor::TertiaryLight;
} else if (str == "tertiary-dark") {
return ConsoleTimeStampColor::TertiaryDark;
} else if (str == "warning") {
return ConsoleTimeStampColor::Warning;
} else if (str == "error") {
return ConsoleTimeStampColor::Error;
} else {
return std::nullopt;
}
};
std::optional<folly::dynamic> getConsoleTimeStampDetailFromObject(jsi::Runtime &runtime, const jsi::Value &detailValue);
}; // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,49 @@
/*
* 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 "EventLoopReporter.h"
#if defined(REACT_NATIVE_DEBUGGER_ENABLED)
#include <react/timing/primitives.h>
#include "PerformanceTracer.h"
#endif
namespace facebook::react::jsinspector_modern::tracing {
#if defined(REACT_NATIVE_DEBUGGER_ENABLED)
EventLoopReporter::EventLoopReporter(EventLoopPhase phase)
: startTimestamp_(HighResTimeStamp::now()), phase_(phase) {}
EventLoopReporter::~EventLoopReporter() {
PerformanceTracer& performanceTracer = PerformanceTracer::getInstance();
if (performanceTracer.isTracing()) {
auto end = HighResTimeStamp::now();
switch (phase_) {
case EventLoopPhase::Task:
performanceTracer.reportEventLoopTask(startTimestamp_, end);
break;
case EventLoopPhase::Microtasks:
performanceTracer.reportEventLoopMicrotasks(startTimestamp_, end);
break;
default:
break;
}
}
}
#else
EventLoopReporter::EventLoopReporter(EventLoopPhase phase) {}
EventLoopReporter::~EventLoopReporter() {}
#endif
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,39 @@
/*
* 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
#if defined(REACT_NATIVE_DEBUGGER_ENABLED)
#include <react/timing/primitives.h>
#endif
namespace facebook::react::jsinspector_modern::tracing {
enum class EventLoopPhase {
Task,
Microtasks,
};
struct EventLoopReporter {
public:
explicit EventLoopReporter(EventLoopPhase phase);
EventLoopReporter(const EventLoopReporter &) = delete;
EventLoopReporter(EventLoopReporter &&) = delete;
EventLoopReporter &operator=(const EventLoopReporter &) = delete;
EventLoopReporter &operator=(EventLoopReporter &&) = delete;
~EventLoopReporter();
private:
#if defined(REACT_NATIVE_DEBUGGER_ENABLED)
HighResTimeStamp startTimestamp_;
EventLoopPhase phase_;
#endif
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,18 @@
/*
* 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 "TraceEvent.h"
namespace facebook::react::jsinspector_modern::tracing {
struct InstanceTracingProfile {
std::vector<TraceEvent> performanceTraceEvents;
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,758 @@
/*
* 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 "PerformanceTracer.h"
#include "Timing.h"
#include "TraceEventSerializer.h"
#include <jsinspector-modern/network/CdpNetwork.h>
#include <jsinspector-modern/network/HttpUtils.h>
#include <oscompat/OSCompat.h>
#include <react/timing/primitives.h>
#include <folly/json.h>
#include <mutex>
using namespace facebook::react;
namespace facebook::react::jsinspector_modern::tracing {
namespace {
/**
* Instead of validating that the indicated duration is positive, we can ensure
* the value is sensible (less than 1 seccond doesn't make much sense).
*/
const HighResDuration MIN_VALUE_FOR_MAX_TRACE_DURATION_OPTION =
HighResDuration::fromMilliseconds(1000); // 1 second
ThreadId getCurrentThreadId() {
static thread_local const auto CURRENT_THREAD_ID =
oscompat::getCurrentThreadId();
return CURRENT_THREAD_ID;
}
} // namespace
PerformanceTracer& PerformanceTracer::getInstance() {
static PerformanceTracer tracer;
return tracer;
}
PerformanceTracer::PerformanceTracer()
: processId_(oscompat::getCurrentProcessId()) {}
bool PerformanceTracer::startTracing() {
return startTracingImpl();
}
bool PerformanceTracer::startTracing(HighResDuration maxDuration) {
return startTracingImpl(maxDuration);
}
bool PerformanceTracer::startTracingImpl(
std::optional<HighResDuration> maxDuration) {
std::vector<TracingStateCallback> callbacksToNotify;
{
std::lock_guard lock(mutex_);
if (tracingAtomic_) {
return false;
}
tracingAtomic_ = true;
if (maxDuration && *maxDuration < MIN_VALUE_FOR_MAX_TRACE_DURATION_OPTION) {
throw std::invalid_argument("maxDuration should be at least 1 second");
}
currentTraceStartTime_ = HighResTimeStamp::now();
currentTraceMaxDuration_ = maxDuration;
for (const auto& [id, callback] : tracingStateCallbacks_) {
callbacksToNotify.push_back(callback);
}
}
for (const auto& callback : callbacksToNotify) {
callback(true);
}
return true;
}
std::optional<std::vector<TraceEvent>> PerformanceTracer::stopTracing() {
std::vector<TraceEvent> events;
std::vector<TracingStateCallback> callbacksToNotify;
auto currentTraceEndTime = HighResTimeStamp::now();
{
std::lock_guard lock(mutex_);
if (!tracingAtomic_) {
return std::nullopt;
}
// Collect callbacks before disabling tracing
for (const auto& [id, callback] : tracingStateCallbacks_) {
callbacksToNotify.push_back(callback);
}
}
// Notify callbacks while tracing is still enabled, so they can send final
// events
for (const auto& callback : callbacksToNotify) {
callback(false);
}
{
std::lock_guard lock(mutex_);
tracingAtomic_ = false;
performanceMeasureCount_ = 0;
alreadyEmittedEntryForComponentsTrackOrdering_ = false;
events = collectEventsAndClearBuffers(currentTraceEndTime);
}
auto currentTraceStartTime = currentTraceStartTime_;
if (currentTraceMaxDuration_ &&
currentTraceEndTime - *currentTraceMaxDuration_ > currentTraceStartTime) {
currentTraceStartTime = currentTraceEndTime - *currentTraceMaxDuration_;
}
// This is synthetic Trace Event, which should not be represented on a
// timeline. CDT is not using Profile or ProfileChunk events for determining
// trace timeline window, this is why trace that only contains JavaScript
// samples will be displayed as empty. We use these events to avoid that.
// This could happen for non-bridgeless apps, where Performance interface is
// not supported and no spec-compliant Event Loop implementation.
events.emplace_back(
TraceEvent{
.name = "TracingStartedInPage",
.cat = "disabled-by-default-devtools.timeline",
.ph = 'I',
.ts = currentTraceStartTime,
.pid = processId_,
.tid = getCurrentThreadId(),
.args = folly::dynamic::object("data", folly::dynamic::object()),
});
events.emplace_back(
TraceEvent{
.name = "ReactNative-TracingStopped",
.cat = "disabled-by-default-devtools.timeline",
.ph = 'I',
.ts = currentTraceEndTime,
.pid = processId_,
.tid = getCurrentThreadId(),
});
currentTraceMaxDuration_ = std::nullopt;
return events;
}
void PerformanceTracer::reportMark(
const std::string& name,
HighResTimeStamp start,
folly::dynamic&& detail) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerEventMark{
.name = name,
.start = start,
.detail = std::move(detail),
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportMeasure(
const std::string& name,
HighResTimeStamp start,
HighResDuration duration,
folly::dynamic&& detail) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerEventMeasure{
.name = name,
.start = start,
.duration = duration,
.detail = std::move(detail),
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportTimeStamp(
const std::string& name,
std::optional<ConsoleTimeStampEntry> start,
std::optional<ConsoleTimeStampEntry> end,
std::optional<std::string> trackName,
std::optional<std::string> trackGroup,
std::optional<ConsoleTimeStampColor> color,
std::optional<folly::dynamic> detail) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerEventTimeStamp{
.name = name,
.start = std::move(start),
.end = std::move(end),
.trackName = std::move(trackName),
.trackGroup = std::move(trackGroup),
.color = std::move(color),
.detail = std::move(detail),
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportEventLoopTask(
HighResTimeStamp start,
HighResTimeStamp end) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerEventEventLoopTask{
.start = start,
.end = end,
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportEventLoopMicrotasks(
HighResTimeStamp start,
HighResTimeStamp end) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerEventEventLoopMicrotask{
.start = start,
.end = end,
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportResourceSendRequest(
const std::string& devtoolsRequestId,
HighResTimeStamp start,
const std::string& url,
const std::string& requestMethod,
const Headers& headers) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
auto resourceType =
jsinspector_modern::cdp::network::resourceTypeFromMimeType(
jsinspector_modern::mimeTypeFromHeaders(headers));
enqueueEvent(
PerformanceTracerResourceSendRequest{
.requestId = devtoolsRequestId,
.url = url,
.start = start,
.requestMethod = requestMethod,
.resourceType = resourceType,
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportResourceReceiveResponse(
const std::string& devtoolsRequestId,
HighResTimeStamp start,
int statusCode,
const Headers& headers,
int encodedDataLength,
folly::dynamic timingData) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerResourceReceiveResponse{
.requestId = devtoolsRequestId,
.start = start,
.encodedDataLength = encodedDataLength,
.headers = headers,
.mimeType = jsinspector_modern::mimeTypeFromHeaders(headers),
.protocol = "h2",
.statusCode = statusCode,
.timing = std::move(timingData),
.threadId = getCurrentThreadId(),
});
}
void PerformanceTracer::reportResourceFinish(
const std::string& devtoolsRequestId,
HighResTimeStamp start,
int encodedDataLength,
int decodedBodyLength) {
if (!tracingAtomic_) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (!tracingAtomic_) {
return;
}
enqueueEvent(
PerformanceTracerResourceFinish{
.requestId = devtoolsRequestId,
.start = start,
.encodedDataLength = encodedDataLength,
.decodedBodyLength = decodedBodyLength,
.threadId = getCurrentThreadId(),
});
}
/* static */ TraceEvent PerformanceTracer::constructRuntimeProfileTraceEvent(
RuntimeProfileId profileId,
ProcessId processId,
ThreadId threadId,
HighResTimeStamp profileTimestamp) {
// CDT prioritizes event timestamp over startTime metadata field.
// https://fburl.com/lo764pf4
return TraceEvent{
.id = profileId,
.name = "Profile",
.cat = "disabled-by-default-v8.cpu_profiler",
.ph = 'P',
.ts = profileTimestamp,
.pid = processId,
.tid = threadId,
.args = folly::dynamic::object(
"data",
folly::dynamic::object(
"startTime",
highResTimeStampToTracingClockTimeStamp(profileTimestamp))),
};
}
/* static */ TraceEvent
PerformanceTracer::constructRuntimeProfileChunkTraceEvent(
RuntimeProfileId profileId,
ProcessId processId,
ProcessId threadId,
HighResTimeStamp chunkTimestamp,
TraceEventProfileChunk&& traceEventProfileChunk) {
return TraceEvent{
.id = profileId,
.name = "ProfileChunk",
.cat = "disabled-by-default-v8.cpu_profiler",
.ph = 'P',
.ts = chunkTimestamp,
.pid = processId,
.tid = threadId,
.args = folly::dynamic::object(
"data",
TraceEventSerializer::serializeProfileChunk(
std::move(traceEventProfileChunk))),
};
}
#pragma mark - Tracing window methods
/**
* If a `maxDuration` value is set when starting a trace, we use 2 buffers
* for events. Each buffer can contain entries for a range up to `maxDuration`.
* When the current buffer is full, we clear the previous one and we start
* collecting events in a new buffer, which becomes current.
*
* Example:
* - Start:
* previousBuffer: null, currentBuffer: []
* - As entries are added:
* previousBuffer: null, currentBuffer: [a, b, c...]
* - When now - currentBuffer start time > maxDuration:
* previousBuffer: [a, b, c...], currentBuffer: []
* - As entries are added:
* previousbuffer: [a, b, c...], currentBuffer: [x, y, z...]
* - When now - currentBuffer start time > maxDuration:
* previousBuffer: [x, y, z...], currentBuffer: []
* - When the trace finishes:
* We collect all events in both buffers that are still within
* `maxDuration`.
*
* This way, we ensure we keep all events in the `maxDuration` window, and
* clearing expired events is trivial (just clearing a vector).
*/
std::vector<TraceEvent> PerformanceTracer::collectEventsAndClearBuffers(
HighResTimeStamp currentTraceEndTime) {
std::vector<TraceEvent> events;
if (currentTraceMaxDuration_) {
// Collect non-expired entries from the previous buffer
if (previousBuffer_ != nullptr) {
collectEventsAndClearBuffer(
events, *previousBuffer_, currentTraceEndTime);
}
collectEventsAndClearBuffer(events, *currentBuffer_, currentTraceEndTime);
// Reset state.
currentBuffer_ = &buffer_;
previousBuffer_ = nullptr;
} else {
// Trivial case.
events.reserve(currentBuffer_->size());
for (auto&& event : *currentBuffer_) {
enqueueTraceEventsFromPerformanceTracerEvent(events, std::move(event));
}
currentBuffer_->clear();
}
return events;
}
void PerformanceTracer::collectEventsAndClearBuffer(
std::vector<TraceEvent>& events,
std::vector<PerformanceTracerEvent>& buffer,
HighResTimeStamp currentTraceEndTime) {
for (auto&& event : buffer) {
if (isInTracingWindow(currentTraceEndTime, getCreatedAt(event))) {
enqueueTraceEventsFromPerformanceTracerEvent(events, std::move(event));
}
}
buffer.clear();
buffer.shrink_to_fit();
}
bool PerformanceTracer::isInTracingWindow(
HighResTimeStamp now,
HighResTimeStamp timeStampToCheck) const {
if (!currentTraceMaxDuration_) {
return true;
}
return timeStampToCheck > now - *currentTraceMaxDuration_;
}
void PerformanceTracer::enqueueEvent(PerformanceTracerEvent&& event) {
if (currentTraceMaxDuration_) {
auto createdAt = getCreatedAt(event);
// Check if the current buffer is "full"
if (createdAt > currentBufferStartTime_ + *currentTraceMaxDuration_) {
// We moved past the current buffer. We need to switch the other buffer as
// current.
previousBuffer_ = currentBuffer_;
currentBuffer_ = currentBuffer_ == &buffer_ ? &altBuffer_ : &buffer_;
currentBuffer_->clear();
currentBufferStartTime_ = createdAt;
}
}
currentBuffer_->emplace_back(std::move(event));
}
HighResTimeStamp PerformanceTracer::getCreatedAt(
const PerformanceTracerEvent& event) const {
return std::visit(
[](const auto& variant) { return variant.createdAt; }, event);
}
namespace {
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
} // namespace
void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent(
std::vector<TraceEvent>& events,
PerformanceTracerEvent&& event) {
std::visit(
overloaded{
[&](PerformanceTracerEventEventLoopTask&& event) {
events.emplace_back(
TraceEvent{
.name = "RunTask",
.cat = "disabled-by-default-devtools.timeline",
.ph = 'X',
.ts = event.start,
.pid = processId_,
.tid = event.threadId,
.dur = event.end - event.start,
});
},
[&](PerformanceTracerEventEventLoopMicrotask&& event) {
events.emplace_back(
TraceEvent{
.name = "RunMicrotasks",
.cat = "v8.execute",
.ph = 'X',
.ts = event.start,
.pid = processId_,
.tid = event.threadId,
.dur = event.end - event.start,
});
},
[&](PerformanceTracerEventMark&& event) {
folly::dynamic eventArgs = folly::dynamic::object();
if (event.detail != nullptr) {
eventArgs = folly::dynamic::object(
"data",
folly::dynamic::object(
"detail", folly::toJson(event.detail)));
}
events.emplace_back(
TraceEvent{
.name = std::move(event.name),
.cat = "blink.user_timing",
.ph = 'I',
.ts = event.start,
.pid = processId_,
.tid = event.threadId,
.args = std::move(eventArgs),
});
},
[&](PerformanceTracerEventMeasure&& event) {
folly::dynamic beginEventArgs = folly::dynamic::object();
if (event.detail != nullptr) {
beginEventArgs =
folly::dynamic::object("detail", folly::toJson(event.detail));
}
auto eventId = ++performanceMeasureCount_;
events.emplace_back(
TraceEvent{
.id = eventId,
.name = event.name,
.cat = "blink.user_timing",
.ph = 'b',
.ts = event.start,
.pid = processId_,
.tid = event.threadId,
.args = std::move(beginEventArgs),
});
events.emplace_back(
TraceEvent{
.id = eventId,
.name = std::move(event.name),
.cat = "blink.user_timing",
.ph = 'e',
.ts = event.start + event.duration,
.pid = processId_,
.tid = event.threadId,
});
},
[&](PerformanceTracerEventTimeStamp&& event) {
folly::dynamic data = folly::dynamic::object("name", event.name)(
"message", std::move(event.name));
if (event.start) {
if (std::holds_alternative<HighResTimeStamp>(*event.start)) {
data["start"] = highResTimeStampToTracingClockTimeStamp(
std::get<HighResTimeStamp>(*event.start));
} else {
data["start"] = std::move(std::get<std::string>(*event.start));
}
}
if (event.end) {
if (std::holds_alternative<HighResTimeStamp>(*event.end)) {
data["end"] = highResTimeStampToTracingClockTimeStamp(
std::get<HighResTimeStamp>(*event.end));
} else {
data["end"] = std::move(std::get<std::string>(*event.end));
}
}
// We add a custom synthetic entry here to manually put Components
// track right under the Scheduler track. Will be removed once CDT
// Frontend preserves track ordering and we upgrade the fork.
if (!alreadyEmittedEntryForComponentsTrackOrdering_ &&
event.trackName && !event.trackGroup) {
if (*event.trackName == "Components \u269B") {
alreadyEmittedEntryForComponentsTrackOrdering_ = true;
// React is using 0.003 for Scheduler sub-tracks.
auto timestamp = highResTimeStampToTracingClockTimeStamp(
HighResTimeStamp::fromDOMHighResTimeStamp(0.004));
events.emplace_back(
TraceEvent{
.name = "TimeStamp",
.cat = "devtools.timeline",
.ph = 'I',
.ts = event.createdAt,
.pid = processId_,
.tid = event.threadId,
.args = folly::dynamic::object(
"data",
folly::dynamic::object(
"name", "ReactNative-ComponentsTrack")(
"start", timestamp)("end", timestamp)(
"track", *event.trackName)),
});
}
}
if (event.trackName) {
data["track"] = std::move(*event.trackName);
}
if (event.trackGroup) {
data["trackGroup"] = std::move(*event.trackGroup);
}
if (event.color) {
data["color"] = consoleTimeStampColorToString(*event.color);
}
if (event.detail) {
folly::dynamic devtoolsDetail = folly::dynamic::object();
for (const auto& [key, value] : event.detail->items()) {
devtoolsDetail[key] = value;
}
data["devtools"] = folly::toJson(devtoolsDetail);
}
events.emplace_back(
TraceEvent{
.name = "TimeStamp",
.cat = "devtools.timeline",
.ph = 'I',
.ts = event.createdAt,
.pid = processId_,
.tid = event.threadId,
.args = folly::dynamic::object("data", std::move(data)),
});
},
[&](PerformanceTracerResourceSendRequest&& event) {
folly::dynamic data =
folly::dynamic::object("initiator", folly::dynamic::object())(
"priority", "VeryHigh")("renderBlocking", "non_blocking")(
"requestId", std::move(event.requestId))(
"requestMethod", std::move(event.requestMethod))(
"resourceType", std::move(event.resourceType))(
"url", std::move(event.url));
events.emplace_back(
TraceEvent{
.name = "ResourceSendRequest",
.cat = "devtools.timeline",
.ph = 'I',
.ts = event.start,
.pid = processId_,
.s = 't',
.tid = event.threadId,
.args = folly::dynamic::object("data", std::move(data)),
});
},
[&](PerformanceTracerResourceReceiveResponse&& event) {
folly::dynamic headersEntries = folly::dynamic::array;
for (const auto& [key, value] : event.headers) {
headersEntries.push_back(
folly::dynamic::object("name", key)("value", value));
}
folly::dynamic data = folly::dynamic::object(
"encodedDataLength", event.encodedDataLength)(
"headers", headersEntries)("mimeType", event.mimeType)(
"protocol", event.protocol)(
"requestId", std::move(event.requestId))(
"statusCode", event.statusCode)(
"timing", std::move(event.timing));
events.emplace_back(
TraceEvent{
.name = "ResourceReceiveResponse",
.cat = "devtools.timeline",
.ph = 'I',
.ts = event.start,
.pid = processId_,
.s = 't',
.tid = event.threadId,
.args = folly::dynamic::object("data", std::move(data)),
});
},
[&](PerformanceTracerResourceFinish&& event) {
folly::dynamic data = folly::dynamic::object(
"decodedBodyLength", event.decodedBodyLength)("didFail", false)(
"encodedDataLength", event.encodedDataLength)(
"requestId", std::move(event.requestId));
events.emplace_back(
TraceEvent{
.name = "ResourceFinish",
.cat = "devtools.timeline",
.ph = 'I',
.ts = event.start,
.pid = processId_,
.s = 't',
.tid = event.threadId,
.args = folly::dynamic::object("data", std::move(data)),
});
},
},
std::move(event));
}
uint32_t PerformanceTracer::subscribeToTracingStateChanges(
TracingStateCallback callback) {
std::lock_guard lock(mutex_);
uint32_t subscriptionId = nextCallbackId_++;
tracingStateCallbacks_[subscriptionId] = std::move(callback);
return subscriptionId;
}
void PerformanceTracer::unsubscribeFromTracingStateChanges(
uint32_t subscriptionId) {
std::lock_guard lock(mutex_);
tracingStateCallbacks_.erase(subscriptionId);
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,364 @@
/*
* 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 "ConsoleTimeStamp.h"
#include "TraceEvent.h"
#include "TraceEventProfile.h"
#include <react/timing/primitives.h>
#include <folly/dynamic.h>
#include <atomic>
#include <functional>
#include <map>
#include <mutex>
#include <optional>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
// TODO: Review how this API is integrated into jsinspector_modern (singleton
// design is copied from earlier FuseboxTracer prototype).
using Headers = std::map<std::string, std::string>;
/**
* [Experimental] An interface for logging performance trace events to the
* modern debugger server.
*/
class PerformanceTracer {
public:
static PerformanceTracer &getInstance();
/**
* Starts a tracing session. Returns `false` if already tracing.
*/
bool startTracing();
/**
* Starts a tracing session with a maximum duration (events older than this
* duration are dropped). Returns `false` if already tracing.
*/
bool startTracing(HighResDuration maxDuration);
/**
* If there is a current tracing session, it stops tracing and returns all
* collected events. Otherwise, it returns empty.
*/
std::optional<std::vector<TraceEvent>> stopTracing();
/**
* Returns whether the tracer is currently tracing. This can be useful to
* avoid doing expensive work (like formatting strings) if tracing is not
* enabled.
*/
inline bool isTracing() const
{
return tracingAtomic_;
}
/**
* Record a `Performance.mark()` event - a labelled timestamp. If not
* currently tracing, this is a no-op.
*
* See https://w3c.github.io/user-timing/#mark-method.
*/
void reportMark(const std::string &name, HighResTimeStamp start, folly::dynamic &&detail = nullptr);
/**
* Record a `Performance.measure()` event - a labelled duration. If not
* currently tracing, this is a no-op.
*
* See https://w3c.github.io/user-timing/#measure-method.
*/
void reportMeasure(
const std::string &name,
HighResTimeStamp start,
HighResDuration duration,
folly::dynamic &&detail = nullptr);
/**
* Record a "TimeStamp" Trace Event - a labelled entry on Performance
* timeline. The only required argument is `name`. Optional arguments, if not
* provided, won't be recorded in the serialized Trace Event.
* @see
https://developer.chrome.com/docs/devtools/performance/extension#inject_your_data_with_consoletimestamp
*/
void reportTimeStamp(
const std::string &name,
std::optional<ConsoleTimeStampEntry> start = std::nullopt,
std::optional<ConsoleTimeStampEntry> end = std::nullopt,
std::optional<std::string> trackName = std::nullopt,
std::optional<std::string> trackGroup = std::nullopt,
std::optional<ConsoleTimeStampColor> color = std::nullopt,
std::optional<folly::dynamic> detail = std::nullopt);
/**
* Record an Event Loop tick, which will be represented as an Event Loop task
* on a timeline view and grouped with JavaScript samples.
*/
void reportEventLoopTask(HighResTimeStamp start, HighResTimeStamp end);
/**
* Record Microtasks phase of the Event Loop tick. Will be represented as a
* "Run Microtasks" block under a task.
*/
void reportEventLoopMicrotasks(HighResTimeStamp start, HighResTimeStamp end);
/**
* Record a "ResourceWillSendRequest" event. Paired with other "Resource*"
* events, renders a network request timeline in the Performance panel Network
* track.
*
* If not currently tracing, this is a no-op.
*/
void reportResourceWillSendRequest(const std::string &devtoolsRequestId, HighResTimeStamp start);
/**
* Record a "ResourceSendRequest" event. Paired with other "Resource*"
* events, renders a network request timeline in the Performance panel Network
* track.
*
* If not currently tracing, this is a no-op.
*/
void reportResourceSendRequest(
const std::string &devtoolsRequestId,
HighResTimeStamp start,
const std::string &url,
const std::string &requestMethod,
const Headers &headers);
/**
* Record a "ResourceReceiveResponse" event. Paired with other "Resource*"
* events, renders a network request timeline in the Performance panel Network
* track.
*
* If not currently tracing, this is a no-op.
*/
void reportResourceReceiveResponse(
const std::string &devtoolsRequestId,
HighResTimeStamp start,
int statusCode,
const Headers &headers,
int encodedDataLength,
folly::dynamic timingData);
/**
* Record a "ResourceFinish" event. Paired with other "Resource*" events,
* renders a network request timeline in the Performance panel Network track.
*
* If not currently tracing, this is a no-op.
*/
void reportResourceFinish(
const std::string &devtoolsRequestId,
HighResTimeStamp start,
int encodedDataLength,
int decodedBodyLength);
/**
* Creates "Profile" Trace Event.
*
* Can be serialized to JSON with TraceEventSerializer::serialize.
*/
static TraceEvent constructRuntimeProfileTraceEvent(
RuntimeProfileId profileId,
ProcessId processId,
ThreadId threadId,
HighResTimeStamp profileTimestamp);
/**
* Creates "ProfileChunk" Trace Event.
*
* Can be serialized to JSON with TraceEventSerializer::serialize.
*/
static TraceEvent constructRuntimeProfileChunkTraceEvent(
RuntimeProfileId profileId,
ProcessId processId,
ProcessId threadId,
HighResTimeStamp chunkTimestamp,
TraceEventProfileChunk &&traceEventProfileChunk);
/**
* Callback function type for tracing state changes.
* @param isTracing true if tracing has started, false if tracing has stopped
*/
using TracingStateCallback = std::function<void(bool isTracing)>;
/**
* Subscribe to tracing state changes (start/stop events).
* Tracing start state is reported after tracing has started, so callbacks can
* report events immediately.
* Tracing stop state is reported before tracing has stopped, so callbacks
* can report final events.
* @param callback Function to call when tracing starts or stops
* @return A unique identifier for the subscription that can be used to unsubscribe
*/
uint32_t subscribeToTracingStateChanges(TracingStateCallback callback);
/**
* Unsubscribe from tracing state changes.
* @param subscriptionId The identifier returned from subscribeToTracingStateChanges
*/
void unsubscribeFromTracingStateChanges(uint32_t subscriptionId);
private:
PerformanceTracer();
PerformanceTracer(const PerformanceTracer &) = delete;
PerformanceTracer &operator=(const PerformanceTracer &) = delete;
~PerformanceTracer() = default;
#pragma mark - Internal trace event types
struct PerformanceTracerEventEventLoopTask {
HighResTimeStamp start;
HighResTimeStamp end;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerEventEventLoopMicrotask {
HighResTimeStamp start;
HighResTimeStamp end;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerEventMark {
std::string name;
HighResTimeStamp start;
folly::dynamic detail;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerEventMeasure {
std::string name;
HighResTimeStamp start;
HighResDuration duration;
folly::dynamic detail;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerEventTimeStamp {
std::string name;
std::optional<ConsoleTimeStampEntry> start;
std::optional<ConsoleTimeStampEntry> end;
std::optional<std::string> trackName;
std::optional<std::string> trackGroup;
std::optional<ConsoleTimeStampColor> color;
std::optional<folly::dynamic> detail;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerResourceSendRequest {
std::string requestId;
std::string url;
HighResTimeStamp start;
std::string requestMethod;
std::string resourceType;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerResourceFinish {
std::string requestId;
HighResTimeStamp start;
int encodedDataLength;
int decodedBodyLength;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
struct PerformanceTracerResourceReceiveResponse {
std::string requestId;
HighResTimeStamp start;
int encodedDataLength;
Headers headers;
std::string mimeType;
std::string protocol;
int statusCode;
folly::dynamic timing;
ThreadId threadId;
HighResTimeStamp createdAt = HighResTimeStamp::now();
};
using PerformanceTracerEvent = std::variant<
PerformanceTracerEventTimeStamp,
PerformanceTracerEventEventLoopTask,
PerformanceTracerEventEventLoopMicrotask,
PerformanceTracerEventMark,
PerformanceTracerEventMeasure,
PerformanceTracerResourceSendRequest,
PerformanceTracerResourceReceiveResponse,
PerformanceTracerResourceFinish>;
#pragma mark - Private fields and methods
const ProcessId processId_;
/**
* The flag is atomic in order to enable any thread to read it (via
* isTracing()) without holding the mutex.
* Within this class, both reads and writes MUST be protected by the mutex to
* avoid false positives and data races.
*/
std::atomic<bool> tracingAtomic_{false};
/**
* The counter for recorded User Timing "measure" events.
* Used for generating unique IDs for each measure event inside a specific
* Trace.
* Does not need to be atomic, because it is always accessed within the mutex
* lock.
*/
uint32_t performanceMeasureCount_{0};
HighResTimeStamp currentTraceStartTime_;
std::optional<HighResDuration> currentTraceMaxDuration_;
std::vector<PerformanceTracerEvent> buffer_;
// These fields are only used when setting a max duration on the trace.
std::vector<PerformanceTracerEvent> altBuffer_;
std::vector<PerformanceTracerEvent> *currentBuffer_ = &buffer_;
std::vector<PerformanceTracerEvent> *previousBuffer_{};
HighResTimeStamp currentBufferStartTime_;
// A flag that is used to ensure we only emit one auxiliary entry for the
// ordering of Scheduler / Component tracks.
bool alreadyEmittedEntryForComponentsTrackOrdering_ = false;
/**
* Protects data members of this class for concurrent access, including
* the tracingAtomic_, in order to eliminate potential "logic" races.
*/
std::mutex mutex_;
// Callback management
std::map<uint32_t, TracingStateCallback> tracingStateCallbacks_;
uint32_t nextCallbackId_{0};
bool startTracingImpl(std::optional<HighResDuration> maxDuration = std::nullopt);
std::vector<TraceEvent> collectEventsAndClearBuffers(HighResTimeStamp currentTraceEndTime);
void collectEventsAndClearBuffer(
std::vector<TraceEvent> &events,
std::vector<PerformanceTracerEvent> &buffer,
HighResTimeStamp currentTraceEndTime);
bool isInTracingWindow(HighResTimeStamp now, HighResTimeStamp timeStampToCheck) const;
void enqueueEvent(PerformanceTracerEvent &&event);
HighResTimeStamp getCreatedAt(const PerformanceTracerEvent &event) const;
void enqueueTraceEventsFromPerformanceTracerEvent(std::vector<TraceEvent> &events, PerformanceTracerEvent &&event);
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,122 @@
/*
* 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 <utility>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
namespace facebook::react::jsinspector_modern::tracing {
/*
* Auxiliary data structure used for creating Profile tree and identifying
* identical frames.
*/
class ProfileTreeNode {
public:
/*
* For Chromium & V8 this could also be WASM, this is not the case for us.
*/
enum class CodeType {
JavaScript,
Other,
};
static constexpr uint32_t NO_PARENT = UINT32_MAX;
ProfileTreeNode(
uint32_t id,
CodeType codeType,
RuntimeSamplingProfile::SampleCallStackFrame callFrame,
uint32_t parentId = NO_PARENT)
: id_(id), codeType_(codeType), parentId_(parentId), callFrame_(std::move(callFrame))
{
}
uint32_t getId() const
{
return id_;
}
CodeType getCodeType() const
{
return codeType_;
}
inline bool hasParent() const
{
return parentId_ != NO_PARENT;
}
uint32_t getParentId() const
{
return parentId_;
}
/**
* \return call frame information that is represented by this node.
*/
const RuntimeSamplingProfile::SampleCallStackFrame &getCallFrame() const
{
return callFrame_;
}
/**
* \return a pointer if the node already contains a child with the same
* codeType and callFrame, nullptr otherwise.
*/
ProfileTreeNode *getIfAlreadyExists(
CodeType childCodeType,
const RuntimeSamplingProfile::SampleCallStackFrame &childCallFrame)
{
for (auto &existingChild : children_) {
if (existingChild.getCodeType() == childCodeType && existingChild.getCallFrame() == childCallFrame) {
return &existingChild;
}
}
return nullptr;
}
/**
* Creates a ProfileTreeNode and links it as a child to this node.
* \return a pointer to the child node.
*/
ProfileTreeNode *
addChild(uint32_t childId, CodeType childCodeType, RuntimeSamplingProfile::SampleCallStackFrame childCallFrame)
{
return &children_.emplace_back(childId, childCodeType, std::move(childCallFrame), id_);
}
private:
/**
* Unique id of the node.
*/
uint32_t id_;
/**
* Type of the code that is represented by this node. Either JavaScript or
* Other.
*/
CodeType codeType_;
/**
* Unique id of the parent node. NO_PARENT if this is root node.
*/
uint32_t parentId_;
/**
* List of children nodes, should be unique by codeType and callFrame among
* each other.
*/
std::vector<ProfileTreeNode> children_;
/**
* Information about the corresponding call frame that is represented by this
* node.
*/
RuntimeSamplingProfile::SampleCallStackFrame callFrame_;
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,57 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
header_search_paths = []
if ENV['USE_FRAMEWORKS']
header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../..\""
end
header_dir = 'jsinspector-modern/tracing'
module_name = "jsinspector_moderntracing"
Pod::Spec.new do |s|
s.name = "React-jsinspectortracing"
s.version = version
s.summary = "Experimental performance tooling for React Native DevTools"
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = podspec_sources("*.{cpp,h}", "*.h")
s.header_dir = header_dir
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"DEFINES_MODULE" => "YES"}
resolve_use_frameworks(s, header_mappings_dir: "../..", module_name: module_name)
add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork')
s.dependency "React-jsi"
s.dependency "React-oscompat"
s.dependency "React-timing"
if use_hermes()
s.dependency "hermes-engine"
end
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end

View File

@@ -0,0 +1,129 @@
/*
* 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 "TraceEvent.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
/// Opaque class to represent the original runtime profile returned by the
/// Runtime. RuntimeSamplingProfile class is designed to be agnostic to the
/// Runtime where sampling occurred.
class RawRuntimeProfile {
public:
virtual ~RawRuntimeProfile() = default;
};
/// Contains relevant information about the sampled runtime from start to
/// finish.
struct RuntimeSamplingProfile {
public:
/// Represents a single frame inside the captured sample stack.
struct SampleCallStackFrame {
public:
/// Represents type of frame inside of recorded call stack.
enum class Kind {
JSFunction, /// JavaScript function frame.
NativeFunction, /// Native built-in functions, like arrayPrototypeMap.
HostFunction, /// Native functions, defined by Host, a.k.a. Host
/// functions.
GarbageCollector, /// Garbage collection frame.
};
inline bool operator==(const SampleCallStackFrame &rhs) const noexcept = default;
/// type of the call stack frame
Kind kind;
/// id of the corresponding script in the VM.
uint32_t scriptId;
/// name of the function that represents call frame.
/// Storing a std::string_view should be considered safe here, beacause
/// the lifetime of the string contents are guaranteed as long as the raw
// Sampling Profiler object from Hermes is allocated.
std::string_view functionName;
/// source url of the corresponding script in the VM.
/// Storing a std::string_view should be considered safe here, beacause
/// the lifetime of the string contents are guaranteed as long as the raw
// Sampling Profiler object from Hermes is allocated.
std::optional<std::string_view> scriptURL = std::nullopt;
/// 0-based line number of the corresponding call frame.
std::optional<uint32_t> lineNumber = std::nullopt;
/// 0-based column number of the corresponding call frame.
std::optional<uint32_t> columnNumber = std::nullopt;
};
/// A pair of a timestamp and a snapshot of the call stack at this point in
/// time.
struct Sample {
public:
Sample(uint64_t timestamp, ThreadId threadId, std::vector<SampleCallStackFrame> callStack)
: timestamp(timestamp), threadId(threadId), callStack(std::move(callStack))
{
}
// Movable.
Sample &operator=(Sample &&) = default;
Sample(Sample &&) = default;
// Not copyable.
Sample(const Sample &) = delete;
Sample &operator=(const Sample &) = delete;
~Sample() = default;
/// When the call stack snapshot was taken (μs).
uint64_t timestamp;
/// Thread id where sample was recorded.
ThreadId threadId;
/// Snapshot of the call stack. The first element of the vector is
/// the lowest frame in the stack.
std::vector<SampleCallStackFrame> callStack;
};
RuntimeSamplingProfile(
std::string runtimeName,
ProcessId processId,
std::vector<Sample> samples,
std::unique_ptr<RawRuntimeProfile> rawRuntimeProfile)
: runtimeName(std::move(runtimeName)),
processId(processId),
samples(std::move(samples)),
rawRuntimeProfile(std::move(rawRuntimeProfile))
{
}
// Movable.
RuntimeSamplingProfile &operator=(RuntimeSamplingProfile &&) = default;
RuntimeSamplingProfile(RuntimeSamplingProfile &&) = default;
// Not copyable.
RuntimeSamplingProfile(const RuntimeSamplingProfile &) = delete;
RuntimeSamplingProfile &operator=(const RuntimeSamplingProfile &) = delete;
~RuntimeSamplingProfile() = default;
/// Name of the runtime, where sampling occurred: Hermes, V8, etc.
std::string runtimeName;
/// The ID of the OS-level process where the sampling occurred.
ProcessId processId;
/// List of recorded samples, should be chronologically sorted.
std::vector<Sample> samples;
/// A unique pointer to the original raw runtime profile, collected from the
/// runtime in RuntimeTargetDelegate. Keeping a pointer to the original
/// profile allows it to remain alive as long as RuntimeSamplingProfile is
/// alive, since it may be using the same std::string_view.
std::unique_ptr<RawRuntimeProfile> rawRuntimeProfile;
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,396 @@
/*
* 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 "RuntimeSamplingProfileTraceEventSerializer.h"
#include "PerformanceTracer.h"
#include "ProfileTreeNode.h"
#include "TraceEventSerializer.h"
#include <string_view>
#include <unordered_map>
namespace facebook::react::jsinspector_modern::tracing {
namespace {
// To capture samples timestamps Hermes is using steady_clock and returns
// them in microseconds granularity since epoch. In the future we might want to
// update Hermes to return timestamps in chrono type.
HighResTimeStamp getHighResTimeStampForSample(
const RuntimeSamplingProfile::Sample& sample) {
auto microsecondsSinceSteadyClockEpoch = sample.timestamp;
auto chronoTimePoint = std::chrono::steady_clock::time_point(
std::chrono::microseconds(microsecondsSinceSteadyClockEpoch));
return HighResTimeStamp::fromChronoSteadyClockTimePoint(chronoTimePoint);
}
/// Fallback script ID for artificial call frames, such as (root), (idle) or
/// (program). Required for emulating the payload in a format that is expected
/// by Chrome DevTools.
constexpr uint32_t FALLBACK_SCRIPT_ID = 0;
constexpr std::string_view GARBAGE_COLLECTOR_FRAME_NAME = "(garbage collector)";
constexpr std::string_view ROOT_FRAME_NAME = "(root)";
constexpr std::string_view IDLE_FRAME_NAME = "(idle)";
constexpr std::string_view PROGRAM_FRAME_NAME = "(program)";
TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(
const ProfileTreeNode& node) {
const RuntimeSamplingProfile::SampleCallStackFrame& callFrame =
node.getCallFrame();
auto traceEventCallFrame =
TraceEventProfileChunk::CPUProfile::Node::CallFrame{
.codeType =
node.getCodeType() == ProfileTreeNode::CodeType::JavaScript
? "JS"
: "other",
.scriptId = callFrame.scriptId,
.functionName = std::string(callFrame.functionName),
.url = callFrame.scriptURL
? std::optional<std::string>(std::string(*callFrame.scriptURL))
: std::nullopt,
.lineNumber = callFrame.lineNumber,
.columnNumber = callFrame.columnNumber,
};
return TraceEventProfileChunk::CPUProfile::Node{
.id = node.getId(),
.callFrame = std::move(traceEventCallFrame),
.parentId = node.hasParent() ? std::optional<uint32_t>(node.getParentId())
: std::nullopt,
};
}
RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame(
std::string_view callFrameName) {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = FALLBACK_SCRIPT_ID,
.functionName = callFrameName,
};
};
RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind =
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
.scriptId = FALLBACK_SCRIPT_ID,
.functionName = GARBAGE_COLLECTOR_FRAME_NAME,
};
};
class ProfileTreeRootNode : public ProfileTreeNode {
public:
explicit ProfileTreeRootNode(uint32_t id)
: ProfileTreeNode(
id,
CodeType::Other,
createArtificialCallFrame(ROOT_FRAME_NAME)) {}
};
struct ProfileChunk {
ProfileChunk(
uint16_t chunkSize,
ProcessId chunkProcessId,
ThreadId chunkThreadId,
HighResTimeStamp chunkTimestamp)
: size(chunkSize),
processId(chunkProcessId),
threadId(chunkThreadId),
timestamp(chunkTimestamp) {
samples.reserve(size);
timeDeltas.reserve(size);
}
inline bool isFull() const {
return samples.size() == size;
}
inline bool isEmpty() const {
return samples.empty();
}
std::vector<ProfileTreeNode> nodes;
std::vector<uint32_t> samples;
std::vector<HighResDuration> timeDeltas;
uint16_t size;
ProcessId processId;
ThreadId threadId;
HighResTimeStamp timestamp;
};
// Construct and send "Profile" Trace Event with dispatchCallback.
void sendProfileTraceEvent(
ProcessId processId,
ThreadId threadId,
RuntimeProfileId profileId,
HighResTimeStamp profileStartTimestamp,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback) {
auto traceEvent = PerformanceTracer::constructRuntimeProfileTraceEvent(
profileId, processId, threadId, profileStartTimestamp);
folly::dynamic serializedTraceEvent =
TraceEventSerializer::serialize(std::move(traceEvent));
dispatchCallback(folly::dynamic::array(std::move(serializedTraceEvent)));
}
// Add an empty sample to the chunk.
void chunkEmptySample(
ProfileChunk& chunk,
uint32_t idleNodeId,
HighResDuration samplesTimeDelta) {
chunk.samples.push_back(idleNodeId);
chunk.timeDeltas.push_back(samplesTimeDelta);
}
// Take the current local ProfileChunk, serialize it as "ProfileChunk" Trace
// Event and buffer it.
void bufferProfileChunkTraceEvent(
ProfileChunk&& chunk,
RuntimeProfileId profileId,
folly::dynamic& traceEventBuffer) {
std::vector<TraceEventProfileChunk::CPUProfile::Node> traceEventNodes;
traceEventNodes.reserve(chunk.nodes.size());
for (const auto& node : chunk.nodes) {
traceEventNodes.push_back(convertToTraceEventProfileNode(node));
}
auto traceEvent = PerformanceTracer::constructRuntimeProfileChunkTraceEvent(
profileId,
chunk.processId,
chunk.threadId,
chunk.timestamp,
TraceEventProfileChunk{
.cpuProfile =
TraceEventProfileChunk::CPUProfile{
.nodes = std::move(traceEventNodes),
.samples = std::move(chunk.samples)},
.timeDeltas = std::move(chunk.timeDeltas),
});
auto serializedTraceEvent =
TraceEventSerializer::serialize(std::move(traceEvent));
traceEventBuffer.push_back(std::move(serializedTraceEvent));
}
// Process a call stack of a single sample and add it to the chunk.
void processCallStack(
std::vector<RuntimeSamplingProfile::SampleCallStackFrame>&& callStack,
ProfileChunk& chunk,
ProfileTreeNode& rootNode,
uint32_t idleNodeId,
HighResDuration samplesTimeDelta,
IdGenerator& nodeIdGenerator) {
if (callStack.empty()) {
chunkEmptySample(chunk, idleNodeId, samplesTimeDelta);
return;
}
ProfileTreeNode* previousNode = &rootNode;
for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) {
const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = *it;
bool isGarbageCollectorFrame = callFrame.kind ==
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector;
ProfileTreeNode::CodeType childCodeType = isGarbageCollectorFrame
? ProfileTreeNode::CodeType::Other
: ProfileTreeNode::CodeType::JavaScript;
// We don't need real garbage collector call frame, we change it to
// what Chrome DevTools expects.
RuntimeSamplingProfile::SampleCallStackFrame childCallFrame =
isGarbageCollectorFrame ? createGarbageCollectorCallFrame() : callFrame;
ProfileTreeNode* maybeExistingChild =
previousNode->getIfAlreadyExists(childCodeType, childCallFrame);
if (maybeExistingChild != nullptr) {
previousNode = maybeExistingChild;
} else {
previousNode = previousNode->addChild(
nodeIdGenerator.getNext(), childCodeType, childCallFrame);
chunk.nodes.push_back(*previousNode);
}
}
chunk.samples.push_back(previousNode->getId());
chunk.timeDeltas.push_back(samplesTimeDelta);
}
// Send buffered Trace Events and reset the buffer.
void sendBufferedTraceEvents(
folly::dynamic&& traceEventBuffer,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback) {
dispatchCallback(std::move(traceEventBuffer));
}
// Auxilliary struct that represents the state of the Profile for a single
// thread. We record a single Profile for a single Thread.
struct ThreadProfileState {
// The current chunk that is being built for this thread.
ProfileChunk chunk;
// The id of the Profile that is being built for this thread.
RuntimeProfileId profileId;
ProfileTreeRootNode rootNode;
ProfileTreeNode programNode;
ProfileTreeNode idleNode;
// The timestamp of the last sample that was captured on this thread.
HighResTimeStamp lastCapturedSampleTimestamp;
// IdGenerator for this Profile.
IdGenerator nodeIdGenerator;
ThreadProfileState(
ProcessId processId,
ThreadId threadId,
RuntimeProfileId profileId,
HighResTimeStamp profileTimestamp,
uint16_t chunkSize,
IdGenerator nodeIdGenerator)
: chunk(chunkSize, processId, threadId, profileTimestamp),
profileId(profileId),
rootNode(ProfileTreeRootNode{nodeIdGenerator.getNext()}),
programNode(*rootNode.addChild(
nodeIdGenerator.getNext(),
ProfileTreeNode::CodeType::Other,
createArtificialCallFrame(PROGRAM_FRAME_NAME))),
idleNode(*rootNode.addChild(
nodeIdGenerator.getNext(),
ProfileTreeNode::CodeType::Other,
createArtificialCallFrame(IDLE_FRAME_NAME))),
lastCapturedSampleTimestamp(profileTimestamp),
nodeIdGenerator(nodeIdGenerator) {
chunk.nodes.push_back(rootNode);
chunk.nodes.push_back(programNode);
chunk.nodes.push_back(idleNode);
}
};
} // namespace
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::vector<RuntimeSamplingProfile>&& profiles,
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize,
uint16_t maxUniqueNodesPerChunk) {
for (auto&& profile : profiles) {
serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
dispatchCallback,
traceEventChunkSize,
profileChunkSize,
maxUniqueNodesPerChunk);
}
}
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
RuntimeSamplingProfile&& profile,
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize,
uint16_t maxUniqueNodesPerChunk) {
auto samples = std::move(profile.samples);
if (samples.empty()) {
return;
}
auto traceEventBuffer = folly::dynamic::array();
traceEventBuffer.reserve(traceEventChunkSize);
std::unordered_map<ThreadId, ThreadProfileState> threadProfiles;
for (auto& sample : samples) {
ThreadId currentSampleThreadId = sample.threadId;
auto currentSampleTimestamp = getHighResTimeStampForSample(sample);
auto threadProfileStateIterator =
threadProfiles.find(currentSampleThreadId);
if (threadProfileStateIterator == threadProfiles.end()) {
RuntimeProfileId nextProfileId = profileIdGenerator.getNext();
auto profileStartTime =
threadProfiles.empty() ? tracingStartTime : currentSampleTimestamp;
sendProfileTraceEvent(
profile.processId,
currentSampleThreadId,
nextProfileId,
profileStartTime,
dispatchCallback);
auto [emplacedThreadProfileStateIterator, _] = threadProfiles.emplace(
currentSampleThreadId,
ThreadProfileState{
profile.processId,
currentSampleThreadId,
nextProfileId,
profileStartTime,
profileChunkSize,
IdGenerator{}});
threadProfileStateIterator = emplacedThreadProfileStateIterator;
}
auto& threadProfileState = threadProfileStateIterator->second;
if (threadProfileState.chunk.isFull() ||
threadProfileState.chunk.nodes.size() >= maxUniqueNodesPerChunk) {
bufferProfileChunkTraceEvent(
std::move(threadProfileState.chunk),
threadProfileState.profileId,
traceEventBuffer);
threadProfileState.chunk = ProfileChunk{
profileChunkSize,
profile.processId,
currentSampleThreadId,
tracingStartTime};
}
if (traceEventBuffer.size() == traceEventChunkSize) {
sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback);
traceEventBuffer = folly::dynamic::array();
traceEventBuffer.reserve(traceEventChunkSize);
}
processCallStack(
std::move(sample.callStack),
threadProfileState.chunk,
threadProfileState.rootNode,
threadProfileState.idleNode.getId(),
currentSampleTimestamp - threadProfileState.lastCapturedSampleTimestamp,
threadProfileState.nodeIdGenerator);
threadProfileState.lastCapturedSampleTimestamp = currentSampleTimestamp;
}
for (auto& [threadId, threadState] : threadProfiles) {
if (!threadState.chunk.isEmpty()) {
bufferProfileChunkTraceEvent(
std::move(threadState.chunk),
threadState.profileId,
traceEventBuffer);
}
}
if (!traceEventBuffer.empty()) {
sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback);
}
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,79 @@
/*
* 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 "RuntimeSamplingProfile.h"
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern::tracing {
namespace {
/**
* Maximum number of samples per chunk.
*/
constexpr uint16_t PROFILE_CHUNK_SIZE = 100;
/**
* Maximum number of unique nodes per chunk.
*/
constexpr uint16_t MAX_UNIQUE_NODES_PER_CHUNK = 50;
} // namespace
struct IdGenerator {
public:
uint32_t getNext()
{
return ++counter_;
}
private:
uint32_t counter_ = 0;
};
/**
* Serializes RuntimeSamplingProfile into collection of specific Trace Events,
* which represent Profile information on a timeline.
*/
class RuntimeSamplingProfileTraceEventSerializer {
public:
/**
* \param profile What we will be serializing.
* \param profileIdGenerator A reference to an IdGenerator, which will be
* used for generating unique ids for ProfileChunks.
* \param tracingStartTime A timestamp of when tracing started, will be used
* as a starting reference point of JavaScript samples recording.
* \param dispatchCallback A reference to a callback, which is called when a
* chunk of trace events is ready to be sent.
* \param traceEventChunkSize The maximum number of ProfileChunk trace events
* that can be sent in a single CDP Tracing.dataCollected message.
* \param profileChunkSize The maximum number of ProfileChunk trace events
* that can be sent in a single ProfileChunk trace event.
*/
static void serializeAndDispatch(
RuntimeSamplingProfile &&profile,
IdGenerator &profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic &&traceEventsChunk)> &dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = PROFILE_CHUNK_SIZE,
uint16_t maxUniqueNodesPerChunk = MAX_UNIQUE_NODES_PER_CHUNK);
static void serializeAndDispatch(
std::vector<RuntimeSamplingProfile> &&profiles,
IdGenerator &profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic &&traceEventsChunk)> &dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = PROFILE_CHUNK_SIZE,
uint16_t maxUniqueNodesPerChunk = MAX_UNIQUE_NODES_PER_CHUNK);
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,34 @@
/*
* 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 "TraceRecordingState.h"
namespace facebook::react::jsinspector_modern::tracing {
/**
* An interface for a tracing agent of a target.
* Tracing Agents are only allocated during an active tracing session.
*
* Construction of a TracingAgent means that either the recording has just
* started or the target was just created during an active recording.
* Destruction of a TracingAgent means that either the recording has stopped or
* the target is about to be destroyed.
*/
class TargetTracingAgent {
public:
explicit TargetTracingAgent(TraceRecordingState &state) : state_(state)
{
(void)state_;
}
protected:
TraceRecordingState &state_;
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,38 @@
/*
* 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 <cassert>
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern::tracing {
// The Tracing Clock time origin is the steady_clock epoch. This is mostly done
// to replicate Chromium's behavior, but also saves us from aligning custom
// DOMHighResTimeStamps that can be specified in performance.mark /
// performance.measure calls: these should not extend the timeline window, this
// is the current approach in Chromium.
constexpr HighResTimeStamp TRACING_TIME_ORIGIN =
HighResTimeStamp::fromChronoSteadyClockTimePoint(std::chrono::steady_clock::time_point());
// Tracing timestamps are represented a time value in microseconds since
// arbitrary time origin (epoch) with no fractional part.
inline uint64_t highResTimeStampToTracingClockTimeStamp(HighResTimeStamp timestamp)
{
assert(timestamp >= TRACING_TIME_ORIGIN && "Provided timestamp is before time origin");
auto duration = timestamp - TRACING_TIME_ORIGIN;
return static_cast<uint64_t>(static_cast<double>(duration.toNanoseconds()) / 1e3);
}
inline int64_t highResDurationToTracingClockDuration(HighResDuration duration)
{
return static_cast<int64_t>(static_cast<double>(duration.toNanoseconds()) / 1e3);
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,77 @@
/*
* 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 <react/timing/primitives.h>
#include <folly/dynamic.h>
namespace facebook::react::jsinspector_modern::tracing {
using ProcessId = uint64_t;
using ThreadId = uint64_t;
/**
* The ID for the JavaScript Sampling Profile. There can be multiple Profiles
* during a single session, in case RuntimeTarget is re-initialized.
*/
using RuntimeProfileId = uint16_t;
/**
* A trace event to send to the debugger frontend, as defined by the Trace Event
* Format.
* https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?pli=1&tab=t.0#heading=h.yr4qxyxotyw
*/
struct TraceEvent {
/**
* Optional. Serialized as a string, usually is hexadecimal number.
* https://github.com/ChromeDevTools/devtools-frontend/blob/99a9104ae974f8caa63927e356800f6762cdbf25/front_end/models/trace/helpers/Trace.ts#L198-L201
*/
std::optional<uint32_t> id{};
/** The name of the event, as displayed in the Trace Viewer. */
std::string name;
/**
* A comma separated list of categories for the event, configuring how
* events are shown in the Trace Viewer UI.
*/
std::string cat;
/**
* The event type. This is a single character which changes depending on the
* type of event being output. See
* https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?pli=1&tab=t.0#heading=h.puwqg050lyuy
*/
char ph;
/** The tracing clock timestamp of the event, in microseconds (µs). */
HighResTimeStamp ts;
/** The ID for the process that output this event. */
ProcessId pid;
/**
* The scope of the event, either global (g), process (p), or thread (t).
* Only applicable to instant events ("ph": "i").
*/
std::optional<char> s{};
/** The ID for the thread that output this event. */
ThreadId tid;
/** Any arguments provided for the event. */
folly::dynamic args = folly::dynamic::object();
/**
* The duration of the event, in microseconds (µs). Only applicable to
* complete events ("ph": "X").
*/
std::optional<HighResDuration> dur{};
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,55 @@
/*
* 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 <react/timing/primitives.h>
#include <optional>
#include <string>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
/// Arbitrary data structure, which represents payload of the "ProfileChunk"
/// Trace Event.
struct TraceEventProfileChunk {
/// Deltas between timestamps of chronolocigally sorted samples.
/// Will be sent as part of the "ProfileChunk" trace event.
using TimeDeltas = std::vector<HighResDuration>;
/// Contains Profile information that will be emitted in this chunk: nodes and
/// sample root node ids.
struct CPUProfile {
/// Unique node in the profile tree, has unique id, call frame and
/// optionally
/// id of its parent node. Only root node has no parent.
struct Node {
/// Unique call frame in the call stack.
struct CallFrame {
std::string codeType;
uint32_t scriptId;
std::string functionName;
std::optional<std::string> url;
std::optional<uint32_t> lineNumber;
std::optional<uint32_t> columnNumber;
};
uint32_t id;
CallFrame callFrame;
std::optional<uint32_t> parentId;
};
std::vector<Node> nodes;
std::vector<uint32_t> samples;
};
CPUProfile cpuProfile;
TimeDeltas timeDeltas;
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,114 @@
/*
* 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 "TraceEventSerializer.h"
#include "Timing.h"
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern::tracing {
/* static */ folly::dynamic TraceEventSerializer::serialize(
TraceEvent&& event) {
folly::dynamic result = folly::dynamic::object;
if (event.id.has_value()) {
std::array<char, 16> buffer{};
snprintf(buffer.data(), buffer.size(), "0x%x", event.id.value());
result["id"] = buffer.data();
}
result["name"] = std::move(event.name);
result["cat"] = std::move(event.cat);
result["ph"] = std::string(1, event.ph);
result["ts"] = highResTimeStampToTracingClockTimeStamp(event.ts);
result["pid"] = event.pid;
if (event.s.has_value()) {
result["s"] = std::string(1, event.s.value());
}
result["tid"] = event.tid;
result["args"] = std::move(event.args);
if (event.dur.has_value()) {
result["dur"] = highResDurationToTracingClockDuration(event.dur.value());
}
return result;
}
/* static */ folly::dynamic TraceEventSerializer::serializeProfileChunk(
TraceEventProfileChunk&& profileChunk) {
return folly::dynamic::object(
"cpuProfile",
serializeProfileChunkCPUProfile(std::move(profileChunk.cpuProfile)))(
"timeDeltas",
serializeProfileChunkTimeDeltas(std::move(profileChunk.timeDeltas)));
}
/* static */ folly::dynamic
TraceEventSerializer::serializeProfileChunkTimeDeltas(
TraceEventProfileChunk::TimeDeltas&& deltas) {
auto value = folly::dynamic::array();
value.reserve(deltas.size());
for (auto& delta : deltas) {
value.push_back(highResDurationToTracingClockDuration(delta));
}
return value;
}
/* static */ folly::dynamic
TraceEventSerializer::serializeProfileChunkCPUProfile(
TraceEventProfileChunk::CPUProfile&& cpuProfile) {
folly::dynamic dynamicNodes = folly::dynamic::array();
dynamicNodes.reserve(cpuProfile.nodes.size());
for (auto& node : cpuProfile.nodes) {
dynamicNodes.push_back(
serializeProfileChunkCPUProfileNode(std::move(node)));
}
folly::dynamic dynamicSamples = folly::dynamic::array(
std::make_move_iterator(cpuProfile.samples.begin()),
std::make_move_iterator(cpuProfile.samples.end()));
return folly::dynamic::object("nodes", std::move(dynamicNodes))(
"samples", std::move(dynamicSamples));
}
/* static */ folly::dynamic
TraceEventSerializer::serializeProfileChunkCPUProfileNode(
TraceEventProfileChunk::CPUProfile::Node&& node) {
folly::dynamic dynamicNode = folly::dynamic::object();
dynamicNode["callFrame"] =
serializeProfileChunkCPUProfileNodeCallFrame(std::move(node.callFrame));
dynamicNode["id"] = node.id;
if (node.parentId.has_value()) {
dynamicNode["parent"] = node.parentId.value();
}
return dynamicNode;
}
/* static */ folly::dynamic
TraceEventSerializer::serializeProfileChunkCPUProfileNodeCallFrame(
TraceEventProfileChunk::CPUProfile::Node::CallFrame&& callFrame) {
folly::dynamic dynamicCallFrame = folly::dynamic::object();
dynamicCallFrame["codeType"] = std::move(callFrame.codeType);
dynamicCallFrame["scriptId"] = callFrame.scriptId;
dynamicCallFrame["functionName"] = std::move(callFrame.functionName);
if (callFrame.url.has_value()) {
dynamicCallFrame["url"] = std::move(callFrame.url.value());
}
if (callFrame.lineNumber.has_value()) {
dynamicCallFrame["lineNumber"] = callFrame.lineNumber.value();
}
if (callFrame.columnNumber.has_value()) {
dynamicCallFrame["columnNumber"] = callFrame.columnNumber.value();
}
return dynamicCallFrame;
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,89 @@
/*
* 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 "TraceEvent.h"
#include "TraceEventProfile.h"
#include <folly/dynamic.h>
namespace facebook::react::jsinspector_modern::tracing {
/**
* This class is only responsible for serializing a local TraceEvent
* representation into JSON, that should be ready to be dispatched over the
* protocol.
*/
class TraceEventSerializer {
public:
/**
* Serializes a TraceEvent to a folly::dynamic object.
*
* \param event rvalue reference to the TraceEvent object.
* \return A folly::dynamic object that represents a serialized into JSON
* Trace Event for CDP.
*/
static folly::dynamic serialize(TraceEvent &&event);
/**
* Serialize a TraceEventProfileChunk to a folly::dynamic object.
*
* \param profileChunk rvalue reference to the TraceEventProfileChunk object.
* \return A folly::dynamic object that represents a serialized into JSON
* "ProfileChunk" Trace Event for CDP.
*/
static folly::dynamic serializeProfileChunk(TraceEventProfileChunk &&profileChunk);
/**
* Serialize a TraceEventProfileChunk::TimeDeltas to a folly::dynamic
* object.
*
* \param deltas rvalue reference to the TraceEventProfileChunk::TimeDeltas
* object.
* \return A folly::dynamic object that represents a serialized "timeDeltas"
* property of "ProfileChunk" Trace Event for CDP.
*/
static folly::dynamic serializeProfileChunkTimeDeltas(TraceEventProfileChunk::TimeDeltas &&deltas);
/**
* Serialize a TraceEventProfileChunk::CPUProfile into a folly::dynamic
* object.
*
* \param cpuProfile rvalue reference to the
* TraceEventProfileChunk::CPUProfile object.
* \return A folly::dynamic object that represents a serialized "cpuProfile"
* property of "ProfileChunk" Trace Event for CDP.
*/
static folly::dynamic serializeProfileChunkCPUProfile(TraceEventProfileChunk::CPUProfile &&cpuProfile);
/**
* Serialize a TraceEventProfileChunk::CPUProfile::Node into a folly::dynamic
* object.
*
* \param node rvalue reference to the
* TraceEventProfileChunk::CPUProfile::Node object.
* \return A folly::dynamic object that represents a serialized
* "cpuProfile.nodes[i]" property of "ProfileChunk" Trace Event for CDP.
*/
static folly::dynamic serializeProfileChunkCPUProfileNode(TraceEventProfileChunk::CPUProfile::Node &&node);
/**
* Serialize a TraceEventProfileChunk::CPUProfile::Node::CallFrame into a
* folly::dynamic object.
*
* \param callFrame rvalue reference to the
* TraceEventProfileChunk::CPUProfile::Node::CallFrame object.
* \return A folly::dynamic object that represents a serialized
* "cpuProfile.nodes[i].callFrame" property of "ProfileChunk" Trace Event for
* CDP.
*/
static folly::dynamic serializeProfileChunkCPUProfileNodeCallFrame(
TraceEventProfileChunk::CPUProfile::Node::CallFrame &&callFrame);
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,39 @@
/*
* 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 "InstanceTracingProfile.h"
#include "RuntimeSamplingProfile.h"
#include "TracingMode.h"
#include <oscompat/OSCompat.h>
#include <react/timing/primitives.h>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
struct TraceRecordingState {
// The mode of this Trace Recording.
tracing::Mode mode;
// The ID of the OS-level process that this Trace Recording is associated
// with.
ProcessId processId = oscompat::getCurrentProcessId();
// The timestamp at which this Trace Recording started.
HighResTimeStamp startTime;
// All captured Runtime Sampling Profiles during this Trace Recording.
std::vector<RuntimeSamplingProfile> runtimeSamplingProfiles{};
// All captures Instance Tracing Profiles during this Trace Recording.
std::vector<InstanceTracingProfile> instanceTracingProfiles{};
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,68 @@
/*
* 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 "TraceRecordingStateSerializer.h"
#include "RuntimeSamplingProfileTraceEventSerializer.h"
#include "TraceEventSerializer.h"
namespace facebook::react::jsinspector_modern::tracing {
namespace {
folly::dynamic generateNewChunk(uint16_t chunkSize) {
folly::dynamic chunk = folly::dynamic::array();
chunk.reserve(chunkSize);
return chunk;
}
} // namespace
/* static */ void TraceRecordingStateSerializer::emitAsDataCollectedChunks(
TraceRecordingState&& recording,
const std::function<void(folly::dynamic&&)>& chunkCallback,
uint16_t performanceTraceEventsChunkSize,
uint16_t profileTraceEventsChunkSize) {
auto instancesProfiles = std::move(recording.instanceTracingProfiles);
IdGenerator profileIdGenerator;
for (auto& instanceProfile : instancesProfiles) {
emitPerformanceTraceEvents(
std::move(instanceProfile.performanceTraceEvents),
chunkCallback,
performanceTraceEventsChunkSize);
}
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(recording.runtimeSamplingProfiles),
profileIdGenerator,
recording.startTime,
chunkCallback,
profileTraceEventsChunkSize);
}
/* static */ void TraceRecordingStateSerializer::emitPerformanceTraceEvents(
std::vector<TraceEvent>&& events,
const std::function<void(folly::dynamic&&)>& chunkCallback,
uint16_t chunkSize) {
folly::dynamic chunk = generateNewChunk(chunkSize);
for (auto& event : events) {
if (chunk.size() == chunkSize) {
chunkCallback(std::move(chunk));
chunk = generateNewChunk(chunkSize);
}
chunk.push_back(TraceEventSerializer::serialize(std::move(event)));
}
if (!chunk.empty()) {
chunkCallback(std::move(chunk));
}
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,42 @@
/*
* 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 "TraceEvent.h"
#include "TraceRecordingState.h"
#include <folly/dynamic.h>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
/**
* A serializer for TraceRecordingState that can be used for tranforming the
* recording into sequence of serialized Trace Events.
*/
class TraceRecordingStateSerializer {
public:
/**
* Transforms the recording into a sequence of serialized Trace Events, which
* is split in chunks of sizes \p performanceTraceEventsChunkSize or
* \p profileTraceEventsChunkSize, depending on type, and sent with \p
* chunkCallback.
*/
static void emitAsDataCollectedChunks(
TraceRecordingState &&recording,
const std::function<void(folly::dynamic &&chunk)> &chunkCallback,
uint16_t performanceTraceEventsChunkSize,
uint16_t profileTraceEventsChunkSize);
static void emitPerformanceTraceEvents(
std::vector<TraceEvent> &&events,
const std::function<void(folly::dynamic &&chunk)> &chunkCallback,
uint16_t chunkSize);
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,17 @@
/*
* 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
namespace facebook::react::jsinspector_modern::tracing {
enum class Mode {
CDP, // Initiated by the user via Chrome DevTools Frontend.
Background, // Initiated by the host, doesn't require active CDP session.
};
}

View File

@@ -0,0 +1,24 @@
/*
* 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 <cstdint>
namespace facebook::react::jsinspector_modern::tracing {
// Keep in sync with `TracingState.kt`
enum class TracingState : int32_t {
// There is no active trace
Disabled = 0,
// Trace is currently running in background mode
EnabledInBackgroundMode = 1,
// Trace is currently running in CDP mode
EnabledInCDPMode = 2,
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,58 @@
/*
* 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 "ProfileTreeNode.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace facebook::react::jsinspector_modern::tracing {
TEST(ProfileTreeNodeTest, OnlyAddsUniqueChildren) {
auto fooCallFrame = RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = 0,
.functionName = "foo"};
auto barCallFrame = RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = 0,
.functionName = "bar"};
ProfileTreeNode parent(
1, ProfileTreeNode::CodeType::JavaScript, fooCallFrame);
ProfileTreeNode* child =
parent.addChild(2, ProfileTreeNode::CodeType::JavaScript, barCallFrame);
auto maybeAlreadyExistingChild = parent.getIfAlreadyExists(
ProfileTreeNode::CodeType::JavaScript, barCallFrame);
EXPECT_NE(maybeAlreadyExistingChild, nullptr);
auto maybeExistingChildOfChild = child->getIfAlreadyExists(
ProfileTreeNode::CodeType::JavaScript, barCallFrame);
EXPECT_EQ(maybeExistingChildOfChild, nullptr);
}
TEST(ProfileTreeNodeTest, ConsidersCodeTypeOfChild) {
auto parentCallFrame = RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = 0,
.functionName = "foo"};
auto childCallFrame = RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = 0,
.functionName = "bar"};
ProfileTreeNode parent(
1, ProfileTreeNode::CodeType::JavaScript, parentCallFrame);
parent.addChild(2, ProfileTreeNode::CodeType::JavaScript, childCallFrame);
auto maybeExistingChild = parent.getIfAlreadyExists(
ProfileTreeNode::CodeType::Other, childCallFrame);
EXPECT_EQ(maybeExistingChild, nullptr);
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,360 @@
/*
* 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 <jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h>
#include <jsinspector-modern/tracing/Timing.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <utility>
namespace facebook::react::jsinspector_modern::tracing {
class RuntimeSamplingProfileTraceEventSerializerTest : public ::testing::Test {
protected:
std::vector<folly::dynamic> notificationEvents_;
std::function<void(folly::dynamic&& traceEventsChunk)>
createNotificationCallback() {
return [this](folly::dynamic&& traceEventsChunk) {
notificationEvents_.push_back(traceEventsChunk);
};
}
RuntimeSamplingProfile::SampleCallStackFrame createJSCallFrame(
std::string_view functionName,
uint32_t scriptId = 1,
std::optional<std::string_view> url = std::nullopt,
std::optional<uint32_t> lineNumber = std::nullopt,
std::optional<uint32_t> columnNumber = std::nullopt) {
return RuntimeSamplingProfile::SampleCallStackFrame(
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
scriptId,
functionName,
url,
lineNumber,
columnNumber);
}
RuntimeSamplingProfile::SampleCallStackFrame createGCCallFrame() {
return RuntimeSamplingProfile::SampleCallStackFrame(
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
0,
"(garbage collector)");
}
RuntimeSamplingProfile::Sample createSample(
uint64_t timestamp,
ThreadId threadId,
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack) {
return {timestamp, threadId, std::move(callStack)};
}
RuntimeSamplingProfile createEmptyProfile() {
return {"TestRuntime", 1, {}, {}};
}
RuntimeSamplingProfile createProfileWithSamples(
std::vector<RuntimeSamplingProfile::Sample> samples) {
return {"TestRuntime", 1, std::move(samples), {}};
}
};
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptyProfile) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
auto profile = createEmptyProfile();
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Nothing should be reported if the profile is empty.
EXPECT_TRUE(notificationEvents_.empty());
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
SameCallFramesAreMerged) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
// [ foo ]
// [ bar ]
// [baz][(gc)]
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack1 = {
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack2 = {
createJSCallFrame("baz", 1, "other.js", 5, 1),
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack3 = {
createGCCallFrame(),
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
ThreadId threadId = 1;
uint64_t timestamp1 = 1000000;
uint64_t timestamp2 = 2000000;
uint64_t timestamp3 = 3000000;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp1, threadId, callStack1));
samples.emplace_back(createSample(timestamp2, threadId, callStack2));
samples.emplace_back(createSample(timestamp3, threadId, callStack3));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Verify
ASSERT_EQ(notificationEvents_.size(), 2);
// (root), (program), (idle), foo, bar, baz, (garbage collector)
ASSERT_EQ(
notificationEvents_[1][0]["args"]["data"]["cpuProfile"]["nodes"].size(),
7);
}
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptySample) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
// Create an empty sample (no call stack)
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> emptyCallStack;
ThreadId threadId = 1;
uint64_t timestamp = 1000000;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp, threadId, emptyCallStack));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Verify
// [["Profile"], ["ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);
// (root), (program), (idle)
ASSERT_EQ(
notificationEvents_[1][0]["args"]["data"]["cpuProfile"]["nodes"].size(),
3);
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
SamplesFromDifferentThreads) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
// Create samples with different thread IDs
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
ThreadId threadId1 = 1;
ThreadId threadId2 = 2;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp, threadId1, callStack));
samples.emplace_back(createSample(timestamp + 1000, threadId2, callStack));
samples.emplace_back(createSample(timestamp + 2000, threadId1, callStack));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
/**
* [
* ["Profile"],
* ["Profile"],
* ["ProfileChunk" for threadId1, "ProfileChunk" for threadId2]
* ]
*
* Samples from different thread should never be grouped together in the same
* chunk.
**/
ASSERT_EQ(notificationEvents_.size(), 3);
ASSERT_EQ(notificationEvents_[2].size(), 2);
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
TraceEventChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
uint16_t traceEventChunkSize = 2;
uint16_t profileChunkSize = 2;
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
ThreadId threadId = 1;
std::vector<RuntimeSamplingProfile::Sample> samples;
samples.reserve(5);
for (int i = 0; i < 5; i++) {
samples.push_back(createSample(timestamp + i * 1000, threadId, callStack));
}
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// [["Profile"], ["ProfileChunk", "ProfileChunk"], ["ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 3);
// Check that each chunk has at most traceEventChunkSize events
for (size_t i = 1; i < notificationEvents_.size(); i++) {
EXPECT_LE(notificationEvents_[i].size(), traceEventChunkSize);
}
}
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, ProfileChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
// Set a small profile chunk size to test profile chunking
uint16_t traceEventChunkSize = 10;
uint16_t profileChunkSize = 2;
double samplesCount = 5;
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
ThreadId threadId = 1;
std::vector<RuntimeSamplingProfile::Sample> samples;
samples.reserve(samplesCount);
for (int i = 0; i < samplesCount; i++) {
samples.push_back(createSample(timestamp + i * 1000, threadId, callStack));
}
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);
ASSERT_EQ(
notificationEvents_[1].size(),
std::ceil(samplesCount / profileChunkSize));
for (auto& profileChunk : notificationEvents_[1]) {
EXPECT_LE(
profileChunk["args"]["data"]["cpuProfile"]["samples"].size(),
profileChunkSize);
}
}
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, UniqueNodesThreshold) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
uint16_t traceEventChunkSize = 10;
uint16_t profileChunkSize = 10;
uint16_t maxUniqueNodesPerChunk = 3;
// Create samples with different function names to generate unique nodes
ThreadId threadId = 1;
uint64_t timestamp = 1000000;
std::vector<RuntimeSamplingProfile::Sample> samples;
// In total we would have 8 unique nodes, 5 of which are created here.
// Other 3 are (root), (program), (idle).
for (int i = 0; i < 5; i++) {
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame(
"function" + std::to_string(i), 1, "test.js", 10 + i, 5)};
samples.push_back(createSample(timestamp + i * 1000, threadId, callStack));
}
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = HighResTimeStamp::now();
// Execute
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
traceEventChunkSize,
profileChunkSize,
maxUniqueNodesPerChunk);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);
EXPECT_EQ(notificationEvents_[1].size(), 3);
// Verify that each chunk respects the unique nodes limit
for (auto& profileChunk : notificationEvents_[1]) {
auto& nodes = profileChunk["args"]["data"]["cpuProfile"]["nodes"];
EXPECT_LE(nodes.size(), maxUniqueNodesPerChunk);
}
}
} // namespace facebook::react::jsinspector_modern::tracing