From 5ec5d1b0f2f6720312a8bc2af89e5651fd8d5a6c Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 22 Jul 2025 05:41:38 -0700 Subject: [PATCH 1/2] refactor: make Targets traceable Summary: # Changelog: [Internal] The current design for Tracing is flawed. Right now the logic is mostly scattered around CDP Agents, lifetime of which is tied to CDP session. This diff introduces a new approach: Targets will expose an API to start / stop tracing. This allows us to record Tracing Profiles even if there is no active CDP session. Differential Revision: D78517818 --- .../jsinspector-modern/HostTarget.cpp | 10 +++ .../jsinspector-modern/HostTarget.h | 61 ++++++++++++++ .../jsinspector-modern/HostTargetTracing.cpp | 82 +++++++++++++++++++ .../jsinspector-modern/InstanceTarget.cpp | 13 ++- .../jsinspector-modern/InstanceTarget.h | 45 ++++++++++ .../InstanceTargetTracing.cpp | 71 ++++++++++++++++ .../tracing/HostTargetTracingProfile.h | 41 ++++++++++ .../tracing/InstanceTargetTracingProfile.h | 40 +++++++++ .../tracing/PerformanceTracer.h | 14 +++- 9 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/InstanceTargetTracing.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTargetTracingProfile.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/InstanceTargetTracingProfile.h diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 29b2a7608a4b16..a788205f2c980f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -186,6 +186,11 @@ InstanceTarget& HostTarget::registerInstance(InstanceTargetDelegate& delegate) { assert(!currentInstance_ && "Only one instance allowed"); currentInstance_ = InstanceTarget::create( executionContextManager_, delegate, makeVoidExecutor(executorFromThis())); + + if (isTracing_) { + startTracingInstanceTarget(currentInstance_.get()); + } + sessions_.forEach( [currentInstance = &*currentInstance_](HostTargetSession& session) { session.setCurrentInstance(currentInstance); @@ -197,6 +202,11 @@ void HostTarget::unregisterInstance(InstanceTarget& instance) { assert( currentInstance_ && currentInstance_.get() == &instance && "Invalid unregistration"); + + if (isTracing_) { + stopTracingInstanceTarget(&instance); + } + sessions_.forEach( [](HostTargetSession& session) { session.setCurrentInstance(nullptr); }); currentInstance_.reset(); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index 938271857198a5..2e0c5ec8b125c4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -15,6 +15,8 @@ #include "ScopedExecutor.h" #include "WeakList.h" +#include + #include #include @@ -166,6 +168,21 @@ class HostTargetController final { */ bool decrementPauseOverlayCounter(); + /** + * Start tracing the corresponding HostTarget. Can throw, if already tracing. + */ + void startTracing(); + + /** + * Stop tracing the corresponding HostTarget. Can throw, if wasn't tracing. + */ + void stopTracing(); + + /** + * Returns the full tracing profile for the last tracing session. + */ + tracing::HostTargetTracingProfile collectTracingProfile(); + private: HostTarget& target_; size_t pauseOverlayCounter_{0}; @@ -237,6 +254,21 @@ class JSINSPECTOR_EXPORT HostTarget */ void sendCommand(HostCommand command); + /** + * Start tracing this HostTarget. Can throw, if already tracing. + */ + void startTracing(); + + /** + * Stop tracing this HostTarget. Can throw, if wasn't tracing. + */ + void stopTracing(); + + /** + * Returns the full tracing profile for the last tracing session. + */ + tracing::HostTargetTracingProfile collectTracingProfile(); + private: /** * Constructs a new HostTarget. @@ -265,6 +297,35 @@ class JSINSPECTOR_EXPORT HostTarget return currentInstance_ != nullptr; } + /** + * Whether this HostTarget is currently being traced. + * Keeping a local state is necessary to start tracing InstanceTarget right + * after its reinitialization. + */ + bool isTracing_{false}; + + /** + * A container for tracing profiles of all InstanceTargets that were + * registered during this tracing session. + */ + std::vector + storedInstanceTargetTracingProfiles_; + + /** + * Start tracing already registered InstanceTarget, or the one that is about + * to be registered, in case of reinitialization. + */ + void startTracingInstanceTarget(InstanceTarget* instanceTarget); + + /** + * Stop tracing already registered InstanceTarget, of the one that is about to + * be unregistered, in case of reinitialization. + * + * Collects the tracing profile and stores it in + * \c storedInstanceTargetTracingProfiles_. + */ + void stopTracingInstanceTarget(InstanceTarget* instanceTarget); + // Necessary to allow HostAgent to access HostTarget's internals in a // controlled way (i.e. only HostTargetController gets friend access, while // HostAgent itself doesn't). diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp new file mode 100644 index 00000000000000..0188300691800b --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp @@ -0,0 +1,82 @@ +/* + * 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 "HostTarget.h" + +namespace facebook::react::jsinspector_modern { + +void HostTargetController::startTracing() { + return target_.startTracing(); +} + +void HostTargetController::stopTracing() { + return target_.stopTracing(); +} + +tracing::HostTargetTracingProfile +HostTargetController::collectTracingProfile() { + return target_.collectTracingProfile(); +} + +void HostTarget::startTracing() { + if (isTracing_) { + throw std::runtime_error( + "HostTarget already has a pending tracing session."); + } + + isTracing_ = true; + /** + * In case if \c HostTarget::collectTracingProfile was never called. + */ + storedInstanceTargetTracingProfiles_.clear(); + + if (currentInstance_) { + return startTracingInstanceTarget(currentInstance_.get()); + } +} + +void HostTarget::stopTracing() { + if (!isTracing_) { + throw std::runtime_error("HostTarget has no pending tracing session."); + } + + isTracing_ = false; + if (currentInstance_) { + return stopTracingInstanceTarget(currentInstance_.get()); + } +} + +tracing::HostTargetTracingProfile HostTarget::collectTracingProfile() { + if (isTracing_) { + throw std::runtime_error( + "Attempted to collect HostTargetTracingProfile before stopping the tracing session."); + } + + auto profiles = std::move(storedInstanceTargetTracingProfiles_); + storedInstanceTargetTracingProfiles_.clear(); + + return tracing::HostTargetTracingProfile{std::move(profiles)}; +} + +void HostTarget::startTracingInstanceTarget(InstanceTarget* instanceTarget) { + assert( + instanceTarget != nullptr && + "Attempted to start tracing a null InstanceTarget"); + return instanceTarget->startTracing(); +} + +void HostTarget::stopTracingInstanceTarget(InstanceTarget* instanceTarget) { + assert( + instanceTarget != nullptr && + "Attempted to stop tracing a null InstanceTarget"); + + instanceTarget->stopTracing(); + storedInstanceTargetTracingProfiles_.emplace_back( + instanceTarget->collectTracingProfile()); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.cpp index 53d898d4681706..5604bc551853f7 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.cpp @@ -64,16 +64,25 @@ RuntimeTarget& InstanceTarget::registerRuntime( jsExecutor, makeVoidExecutor(executorFromThis())); + if (isTracing_) { + startTracingRuntimeTarget(currentRuntime_.get()); + } + agents_.forEach([currentRuntime = &*currentRuntime_](InstanceAgent& agent) { agent.setCurrentRuntime(currentRuntime); }); return *currentRuntime_; } -void InstanceTarget::unregisterRuntime(RuntimeTarget& Runtime) { +void InstanceTarget::unregisterRuntime(RuntimeTarget& runtimeTarget) { assert( - currentRuntime_ && currentRuntime_.get() == &Runtime && + currentRuntime_ && currentRuntime_.get() == &runtimeTarget && "Invalid unregistration"); + + if (isTracing_) { + stopTracingRuntimeTarget(&runtimeTarget); + } + agents_.forEach( [](InstanceAgent& agent) { agent.setCurrentRuntime(nullptr); }); currentRuntime_.reset(); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.h index c1cdeb96f67105..201c592beb8af1 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTarget.h @@ -15,6 +15,7 @@ #include #include +#include #include @@ -86,6 +87,21 @@ class InstanceTarget : public EnableExecutorFromThis { */ void unregisterRuntime(RuntimeTarget& runtime); + /** + * Start tracing this InstanceTarget. Can throw, if already tracing. + */ + void startTracing(); + + /** + * Stop tracing this InstanceTarget. Can throw, if wasn't tracing. + */ + void stopTracing(); + + /** + * Returns the full tracing profile for the last tracing session. + */ + tracing::InstanceTargetTracingProfile collectTracingProfile(); + private: /** * Constructs a new InstanceTarget. The caller must call setExecutor @@ -103,6 +119,35 @@ class InstanceTarget : public EnableExecutorFromThis { std::shared_ptr currentRuntime_{nullptr}; WeakList agents_; std::shared_ptr executionContextManager_; + + /** + * Whether this InstanceTarget is currently being traced. + * Keeping a local state is necessary to start tracing RuntimeTarget right + * after its reinitialization. + */ + bool isTracing_{false}; + + /** + * A container for tracing profiles of all RuntimeTargets that were + * registered during this tracing session. + */ + std::vector + storedRuntimeTargetSamplingProfiles_; + + /** + * Start tracing already registered RuntimeTarget, or the one that is about + * to be registered, in case of reinitialization. + */ + void startTracingRuntimeTarget(RuntimeTarget* runtimeTarget); + + /** + * Stop tracing already registered RuntimeTarget, of the one that is about to + * be unregistered, in case of reinitialization. + * + * Collects the tracing profile and stores it in + * \c storedRuntimeTargetSamplingProfiles_. + */ + void stopTracingRuntimeTarget(RuntimeTarget* runtimeTarget); }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTargetTracing.cpp new file mode 100644 index 00000000000000..6cea61bf6841e6 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceTargetTracing.cpp @@ -0,0 +1,71 @@ +/* + * 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 "InstanceTarget.h" + +namespace facebook::react::jsinspector_modern { + +void InstanceTarget::startTracing() { + if (isTracing_) { + throw std::runtime_error( + "HostTarget already has a pending tracing session."); + } + + isTracing_ = true; + /** + * In case if \c InstanceTarget::collectTracingProfile was never called. + */ + storedRuntimeTargetSamplingProfiles_.clear(); + + if (currentRuntime_) { + return startTracingRuntimeTarget(currentRuntime_.get()); + } +} + +void InstanceTarget::stopTracing() { + if (!isTracing_) { + throw std::runtime_error("HostTarget has no pending tracing session."); + } + + isTracing_ = false; + if (currentRuntime_) { + return stopTracingRuntimeTarget(currentRuntime_.get()); + } +} + +tracing::InstanceTargetTracingProfile InstanceTarget::collectTracingProfile() { + if (isTracing_) { + throw std::runtime_error( + "Attempted to collect InstanceTargetTracingProfile before stopping the tracing session."); + } + + auto profiles = std::move(storedRuntimeTargetSamplingProfiles_); + storedRuntimeTargetSamplingProfiles_.clear(); + + return tracing::InstanceTargetTracingProfile{std::move(profiles)}; +} + +void InstanceTarget::startTracingRuntimeTarget(RuntimeTarget* runtimeTarget) { + assert( + runtimeTarget != nullptr && + "Attempted to start tracing a null RuntimeTarget"); + + runtimeTarget->registerForTracing(); + return runtimeTarget->enableSamplingProfiler(); +} + +void InstanceTarget::stopTracingRuntimeTarget(RuntimeTarget* runtimeTarget) { + assert( + runtimeTarget != nullptr && + "Attempted to stop tracing a null RuntimeTarget"); + + runtimeTarget->disableSamplingProfiler(); + storedRuntimeTargetSamplingProfiles_.emplace_back( + runtimeTarget->collectSamplingProfile()); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTargetTracingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTargetTracingProfile.h new file mode 100644 index 00000000000000..da88e3960cd0b5 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTargetTracingProfile.h @@ -0,0 +1,41 @@ +/* + * 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 "InstanceTargetTracingProfile.h" + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * A tracing profile for a single instance of an HostTarget. + * + * Captures Tracing Profiles for all InstanceTargets associated with the owning + * HostTarget during the tracing session. + */ +struct HostTargetTracingProfile { + public: + explicit HostTargetTracingProfile( + std::vector instanceTargetTracingProfiles) + : instanceTargetTracingProfiles_( + std::move(instanceTargetTracingProfiles)) {} + + const std::vector& + getInstanceTargetTracingProfiles() const { + return instanceTargetTracingProfiles_; + } + + private: + /** + * All captured InstanceTarget Tracing Profiles during the tracing session. + */ + std::vector instanceTargetTracingProfiles_; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/InstanceTargetTracingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/InstanceTargetTracingProfile.h new file mode 100644 index 00000000000000..f9719e9ce84a23 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/InstanceTargetTracingProfile.h @@ -0,0 +1,40 @@ +/* + * 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 + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * A tracing profile for a single instance of an InstanceTarget. + * + * Captures Sampling Profiles for all RuntimeTargets associated with the owning + * InstanceTarget during the tracing session. + */ +struct InstanceTargetTracingProfile { + public: + explicit InstanceTargetTracingProfile( + std::vector runtimeSamplingProfiles) + : runtimeSamplingProfiles_(std::move(runtimeSamplingProfiles)) {} + + const std::vector& getRuntimeSamplingProfiles() + const { + return runtimeSamplingProfiles_; + } + + private: + /** + * All captured Sampling Profiles during the tracing session. + */ + std::vector runtimeSamplingProfiles_; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h index d2673d7e43a1d4..dec54a0679de7e 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h @@ -22,6 +22,16 @@ namespace facebook::react::jsinspector_modern::tracing { +namespace { + +/** + * Threshold for the size Trace Event chunk, that will be flushed out with + * Tracing.dataCollected event. + */ +const uint16_t TRACE_EVENT_CHUNK_SIZE = 1000; + +} // namespace + // TODO: Review how this API is integrated into jsinspector_modern (singleton // design is copied from earlier FuseboxTracer prototype). @@ -58,13 +68,13 @@ class PerformanceTracer { void collectEvents( const std::function& resultCallback, - uint16_t chunkSize); + uint16_t chunkSize = TRACE_EVENT_CHUNK_SIZE); /** * Flush out buffered CDP Trace Events into a folly::dynamic collection of * chunks, which can be sent over CDP later. */ - folly::dynamic collectEvents(uint16_t chunkSize); + folly::dynamic collectEvents(uint16_t chunkSize = TRACE_EVENT_CHUNK_SIZE); /** * Record a `Performance.mark()` event - a labelled timestamp. If not From a2818becc724c23edbbb2dde4e052081825d26cb Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 22 Jul 2025 05:41:38 -0700 Subject: [PATCH 2/2] Extract RuntimeTarget tracing methods Summary: # Changelog: [Internal] No functional changes yet, just following the convetion like with other Targets. Differential Revision: D78576975 --- .../jsinspector-modern/RuntimeTarget.cpp | 36 -------------- .../RuntimeTargetTracing.cpp | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracing.cpp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index fb0f425a63bd59..4302a793e6cd6e 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -8,7 +8,6 @@ #include "SessionState.h" #include -#include using namespace facebook::jsi; @@ -160,39 +159,4 @@ void RuntimeTargetController::notifyDebuggerSessionDestroyed() { target_.emitDebuggerSessionDestroyed(); } -void RuntimeTargetController::registerForTracing() { - target_.registerForTracing(); -} - -void RuntimeTargetController::enableSamplingProfiler() { - target_.enableSamplingProfiler(); -} - -void RuntimeTargetController::disableSamplingProfiler() { - target_.disableSamplingProfiler(); -} - -tracing::RuntimeSamplingProfile -RuntimeTargetController::collectSamplingProfile() { - return target_.collectSamplingProfile(); -} - -void RuntimeTarget::registerForTracing() { - jsExecutor_([](auto& /*runtime*/) { - tracing::PerformanceTracer::getInstance().reportJavaScriptThread(); - }); -} - -void RuntimeTarget::enableSamplingProfiler() { - delegate_.enableSamplingProfiler(); -} - -void RuntimeTarget::disableSamplingProfiler() { - delegate_.disableSamplingProfiler(); -} - -tracing::RuntimeSamplingProfile RuntimeTarget::collectSamplingProfile() { - return delegate_.collectSamplingProfile(); -} - } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracing.cpp new file mode 100644 index 00000000000000..a085257ff406fb --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetTracing.cpp @@ -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 "RuntimeTarget.h" + +#include + +namespace facebook::react::jsinspector_modern { + +void RuntimeTargetController::registerForTracing() { + return target_.registerForTracing(); +} + +void RuntimeTargetController::enableSamplingProfiler() { + return target_.enableSamplingProfiler(); +} + +void RuntimeTargetController::disableSamplingProfiler() { + return target_.disableSamplingProfiler(); +} + +tracing::RuntimeSamplingProfile +RuntimeTargetController::collectSamplingProfile() { + return target_.collectSamplingProfile(); +} + +void RuntimeTarget::registerForTracing() { + jsExecutor_([](auto& /*runtime*/) { + tracing::PerformanceTracer::getInstance().reportJavaScriptThread(); + }); +} + +void RuntimeTarget::enableSamplingProfiler() { + delegate_.enableSamplingProfiler(); +} + +void RuntimeTarget::disableSamplingProfiler() { + delegate_.disableSamplingProfiler(); +} + +tracing::RuntimeSamplingProfile RuntimeTarget::collectSamplingProfile() { + return delegate_.collectSamplingProfile(); +} + +} // namespace facebook::react::jsinspector_modern