From ad6076f68fd568480aa820ad07dc54ef9dfbfe25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 23 Mar 2026 09:55:02 -0700 Subject: [PATCH] Add performance track for React Native renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Changelog: [General][Added] Add new custom track for React Native Renderer operations in React Native DevTools performance traces This diff adds performance tracking instrumentation to the React Native renderer to provide visibility into rendering operations in React Native Developer Tools traces. On Android (Kotlin), the MountItemDispatcher is updated to wrap key mounting operations (view commands, premount, and mount) with PerformanceTracer.trace calls. These traces appear on the "Renderer" track within the "⚛ Native" track group. On C++ (shared renderer), the ShadowTree::tryCommit method is instrumented with PerformanceTracerSection to track commit and layout operations. The commit trace includes metadata about the source of the commit (e.g., React). This is a reland of D95982530, which was reverted in D96159765. Differential Revision: D97766334 --- packages/react-native/Package.swift | 2 +- .../ReactAndroid/api/ReactAndroid.api | 1 + .../fabric/mounting/MountItemDispatcher.kt | 161 +++++++++++------- .../internal/tracing/PerformanceTracer.kt | 2 +- .../soloader/OpenSourceMergedSoMapping.kt | 4 + .../ReactAndroid/src/main/jni/CMakeLists.txt | 3 + .../src/main/jni/react/tracing/CMakeLists.txt | 28 +++ .../ReactCommon/React-Fabric.podspec | 1 + .../react/renderer/mounting/CMakeLists.txt | 1 + .../react/renderer/mounting/ShadowTree.cpp | 27 ++- 10 files changed, 165 insertions(+), 65 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/tracing/CMakeLists.txt diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index be3ead700dc0..fd34d2dfb0f0 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -463,7 +463,7 @@ let reactFabric = RNTarget( "components/virtualview", "components/root/tests", ], - dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga], + dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga, .reactJsInspectorTracing], sources: ["animated", "animationbackend", "animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/scrollview/platform/ios", "components/legacyviewmanagerinterop", "components/legacyviewmanagerinterop/platform/ios", "dom", "scheduler", "mounting", "observers/events", "observers/intersection", "observers/mutation", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency", "viewtransition"] ) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 98883b24dc94..8871af16dbc1 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3114,6 +3114,7 @@ public final class com/facebook/react/soloader/OpenSourceMergedSoMapping : com/f public final fun libreact_devsupportjni_so ()I public final fun libreact_featureflagsjni_so ()I public final fun libreact_newarchdefaults_so ()I + public final fun libreact_tracingjni_so ()I public final fun libreactnative_so ()I public final fun libreactnativeblob_so ()I public final fun libreactnativejni_common_so ()I diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.kt index 9d372648c81b..984ff9e75074 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.kt @@ -21,6 +21,7 @@ import com.facebook.react.fabric.FabricUIManager import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem import com.facebook.react.fabric.mounting.mountitems.MountItem import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.tracing.PerformanceTracer import com.facebook.systrace.Systrace import java.util.Queue import java.util.concurrent.ConcurrentLinkedQueue @@ -201,9 +202,16 @@ internal class MountItemDispatcher( "MountItemDispatcher::mountViews viewCommandMountItems", ) - for (command in commands) { - dispatchViewCommand(command) - } + PerformanceTracer.trace( + "view commands", + "Renderer", + "\u269b Native", + { -> + for (command in commands) { + dispatchViewCommand(command) + } + }, + ) Systrace.endSection(Systrace.TRACE_TAG_REACT) } @@ -215,12 +223,21 @@ internal class MountItemDispatcher( Systrace.TRACE_TAG_REACT, "MountItemDispatcher::mountViews preMountItems", ) - for (preMountItem in preMountItems) { - if (ReactNativeFeatureFlags.enableFabricLogs()) { - printMountItem(preMountItem, "dispatchMountItems: Executing preMountItem") - } - executeOrEnqueue(preMountItem) - } + + PerformanceTracer.trace( + "premount", + "Renderer", + "\u269b Native", + { -> + for (preMountItem in preMountItems) { + if (ReactNativeFeatureFlags.enableFabricLogs()) { + printMountItem(preMountItem, "dispatchMountItems: Executing preMountItem") + } + executeOrEnqueue(preMountItem) + } + }, + ) + Systrace.endSection(Systrace.TRACE_TAG_REACT) } @@ -229,46 +246,58 @@ internal class MountItemDispatcher( Systrace.TRACE_TAG_REACT, "MountItemDispatcher::mountViews mountItems to execute", ) - val batchedExecutionStartTime = SystemClock.uptimeMillis() - for (mountItem in items) { - if (ReactNativeFeatureFlags.enableFabricLogs()) { - printMountItem(mountItem, "dispatchMountItems: Executing mountItem") - } + PerformanceTracer.trace( + "mount", + "Renderer", + "\u269b Native", + { -> + val batchedExecutionStartTime = SystemClock.uptimeMillis() - val command = mountItem as? DispatchCommandMountItem - if (command != null) { - dispatchViewCommand(command) - continue - } + for (mountItem in items) { + if (ReactNativeFeatureFlags.enableFabricLogs()) { + printMountItem(mountItem, "dispatchMountItems: Executing mountItem") + } - try { - executeOrEnqueue(mountItem) - } catch (e: Throwable) { - // If there's an exception, we want to log diagnostics in prod and rethrow. - FLog.e(TAG, "dispatchMountItems: caught exception, displaying mount state", e) - if (ReactNativeFeatureFlags.enableFabricLogs()) { - for (m in items) { - if (m === mountItem) { - // We want to mark the mount item that caused exception - FLog.e(TAG, "dispatchMountItems: mountItem: next mountItem triggered exception!") + val command = mountItem as? DispatchCommandMountItem + if (command != null) { + dispatchViewCommand(command) + continue } - printMountItem(m, "dispatchMountItems: mountItem") - } - } - if (mountItem.getSurfaceId() != View.NO_ID) { - mountingManager.getSurfaceManager(mountItem.getSurfaceId())?.printSurfaceState() - } + try { + executeOrEnqueue(mountItem) + } catch (e: Throwable) { + // If there's an exception, we want to log diagnostics in prod and rethrow. + FLog.e(TAG, "dispatchMountItems: caught exception, displaying mount state", e) + if (ReactNativeFeatureFlags.enableFabricLogs()) { + for (m in items) { + if (m === mountItem) { + // We want to mark the mount item that caused exception + FLog.e( + TAG, + "dispatchMountItems: mountItem: next mountItem triggered exception!", + ) + } + printMountItem(m, "dispatchMountItems: mountItem") + } + } + + if (mountItem.getSurfaceId() != View.NO_ID) { + mountingManager.getSurfaceManager(mountItem.getSurfaceId())?.printSurfaceState() + } + + if (ReactIgnorableMountingException.isIgnorable(e)) { + ReactSoftExceptionLogger.logSoftException(TAG, e) + } else { + throw e + } + } + } + batchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime + }, + ) - if (ReactIgnorableMountingException.isIgnorable(e)) { - ReactSoftExceptionLogger.logSoftException(TAG, e) - } else { - throw e - } - } - } - batchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime Systrace.endSection(Systrace.TRACE_TAG_REACT) } @@ -299,26 +328,34 @@ internal class MountItemDispatcher( private fun dispatchPreMountItemsImpl(deadline: Long) { Systrace.beginSection(Systrace.TRACE_TAG_REACT, "MountItemDispatcher::premountViews") - // dispatchPreMountItems cannot be reentrant, but we want to prevent dispatchMountItems from - // reentering during dispatchPreMountItems - inDispatch = true - - try { - while (true) { - if (System.nanoTime() > deadline) { - break - } + PerformanceTracer.trace( + "premount", + "Renderer", + "\u269b Native", + { -> + // dispatchPreMountItems cannot be reentrant, but we want to prevent dispatchMountItems + // from + // reentering during dispatchPreMountItems + inDispatch = true + + try { + while (true) { + if (System.nanoTime() > deadline) { + break + } - // If list is empty, `poll` will return null, or var will never be set - val preMountItemToDispatch = preMountItems.poll() ?: break - if (ReactNativeFeatureFlags.enableFabricLogs()) { - printMountItem(preMountItemToDispatch, "dispatchPreMountItems") - } - executeOrEnqueue(preMountItemToDispatch) - } - } finally { - inDispatch = false - } + // If list is empty, `poll` will return null, or var will never be set + val preMountItemToDispatch = preMountItems.poll() ?: break + if (ReactNativeFeatureFlags.enableFabricLogs()) { + printMountItem(preMountItemToDispatch, "dispatchPreMountItems") + } + executeOrEnqueue(preMountItemToDispatch) + } + } finally { + inDispatch = false + } + }, + ) Systrace.endSection(Systrace.TRACE_TAG_REACT) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/tracing/PerformanceTracer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/tracing/PerformanceTracer.kt index 8bac4f57c382..c8abc7bb3850 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/tracing/PerformanceTracer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/tracing/PerformanceTracer.kt @@ -20,7 +20,7 @@ import com.facebook.soloader.SoLoader @DoNotStrip public object PerformanceTracer { init { - SoLoader.loadLibrary("react_performancetracerjni") + SoLoader.loadLibrary("react_tracingjni") } public fun trace(name: String, block: () -> T): T { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/soloader/OpenSourceMergedSoMapping.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/soloader/OpenSourceMergedSoMapping.kt index c8ba2b2190c4..b81427f3ae6f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/soloader/OpenSourceMergedSoMapping.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/soloader/OpenSourceMergedSoMapping.kt @@ -28,6 +28,7 @@ public object OpenSourceMergedSoMapping : ExternalSoMapping { "react_devsupportjni", "react_featureflagsjni", "react_newarchdefaults", + "react_tracingjni", "reactnativeblob", "reactnativejni", "reactnativejni_common", @@ -57,6 +58,7 @@ public object OpenSourceMergedSoMapping : ExternalSoMapping { "react_devsupportjni" -> libreact_devsupportjni_so() "react_featureflagsjni" -> libreact_featureflagsjni_so() "react_newarchdefaults" -> libreact_newarchdefaults_so() + "react_tracingjni" -> libreact_tracingjni_so() "reactnative" -> libreactnative_so() "reactnativeblob" -> libreactnativeblob_so() "reactnativejni" -> libreactnativejni_so() @@ -88,6 +90,8 @@ public object OpenSourceMergedSoMapping : ExternalSoMapping { public external fun libreact_newarchdefaults_so(): Int + public external fun libreact_tracingjni_so(): Int + public external fun libreactnative_so(): Int public external fun libreactnativeblob_so(): Int diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index de4106d57321..6d245b2239cb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -159,6 +159,7 @@ add_react_android_subdir(src/main/jni/react/hermes/instrumentation/) add_react_android_subdir(src/main/jni/react/runtime/cxxreactpackage) add_react_android_subdir(src/main/jni/react/runtime/jni) add_react_android_subdir(src/main/jni/react/runtime/hermes/jni) +add_react_android_subdir(src/main/jni/react/tracing) add_react_android_subdir(src/main/jni/react/devsupport) # SoMerging Utils @@ -231,6 +232,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -331,6 +333,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/tracing/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/tracing/CMakeLists.txt new file mode 100644 index 000000000000..ee65da316089 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/tracing/CMakeLists.txt @@ -0,0 +1,28 @@ +# 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 react_tracingjni_SRC CONFIGURE_DEPENDS *.cpp) + +add_library(react_tracingjni OBJECT ${react_tracingjni_SRC}) + +target_merge_so(react_tracingjni) + +target_include_directories(react_tracingjni PUBLIC .) + +target_link_libraries(react_tracingjni + fbjni + folly_runtime + jsinspector + jsinspector_tracing + react_timing + reactnativejni) + +target_compile_reactnative_options(react_tracingjni PRIVATE) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 327baa5da5b7..bc7f02073d01 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -174,6 +174,7 @@ Pod::Spec.new do |s| end s.subspec "mounting" do |ss| + ss.dependency "React-jsinspectortracing" ss.source_files = podspec_sources("react/renderer/mounting/**/*.{m,mm,cpp,h}", "react/renderer/mounting/**/*.h") ss.exclude_files = "react/renderer/mounting/tests" ss.header_dir = "react/renderer/mounting" diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/mounting/CMakeLists.txt index eefd67c9f21a..895801657e24 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/mounting/CMakeLists.txt @@ -22,6 +22,7 @@ target_link_libraries(react_renderer_mounting glog glog_init jsi + jsinspector_tracing react_debug react_renderer_core react_renderer_debug diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 9dbeee58107f..9666041e47fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -8,6 +8,7 @@ #include "ShadowTree.h" #include +#include #include #include #include @@ -24,6 +25,19 @@ namespace facebook::react { namespace { const int MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING = 3; + +std::string getShadowTreeCommitSourceName(ShadowTreeCommitSource source) { + switch (source) { + case ShadowTreeCommitSource::Unknown: + return "Unknown"; + case ShadowTreeCommitSource::React: + return "React"; + case ShadowTreeCommitSource::AnimationEndSync: + return "AnimationEndSync"; + case ShadowTreeCommitSource::ReactRevisionMerge: + return "ReactRevisionMerge"; + } +} } // namespace using CommitStatus = ShadowTree::CommitStatus; @@ -316,6 +330,13 @@ CommitStatus ShadowTree::tryCommit( const ShadowTreeCommitTransaction& transaction, const CommitOptions& commitOptions) const { TraceSection s("ShadowTree::commit"); + jsinspector_modern::tracing::PerformanceTracerSection s1( + "commit", + "Renderer", + "\u269b Native", + nullptr, + "source", + getShadowTreeCommitSourceName(commitOptions.source)); auto isReactBranch = ReactNativeFeatureFlags::enableFabricCommitBranching() && commitOptions.source == CommitSource::React; @@ -382,7 +403,11 @@ CommitStatus ShadowTree::tryCommit( telemetry.willLayout(); telemetry.setAsThreadLocal(); - newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes); + { + jsinspector_modern::tracing::PerformanceTracerSection s2( + "layout", "Renderer", "\u269b Native"); + newRootShadowNode->layoutIfNeeded(&affectedLayoutableNodes); + } telemetry.unsetAsThreadLocal(); telemetry.didLayout(static_cast(affectedLayoutableNodes.size()));