From 2dd340523b6b7c7ff4fb9c4d072fbd3209727aba Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 9 Apr 2026 13:37:31 +0100 Subject: [PATCH] Backport performance debugging improvements Backports some CDP Performance features and stability improvements to `0.83-stable`. This includes: - Remaining implementation of Frame Timings and screenshot capture in performance traces (Android) - New support for Frame Timings and screenshot capture on iOS - Optimisations to trace chunk generation, memory usage during performance recording - `Page.captureScreenshot` (Android, iOS) All features remain gated behind feature flags (`fuseboxFrameRecordingEnabled`, `fuseboxScreenshotCaptureEnabled`). **Commits applied** - Define TracingCategory enum (#54377) bd6c6bf - Use TracingCategory in TraceEvent (#54378) ce60f27 - Simple parser for serialized tracing categories (#54379) 62275b6 - Propagate tracing categories to recording state (#54380) 3841ef0 - Re-land "Revert D85999774: [rn][android] Add FrameTiming module" (#54502) 85905ad - Add screenshot category (#54537) 5d9cb80 - Define HostTargetTracingDelegate (#54622) be0dae0 - Define TracingDelegate for Android Host (#54628) a212e95 - Use new API for tracing state on Android (#54629) 532d0be - Internalize TracingState interface (#54631) 58ec261 - Move TracingState interfaces to inspector package (#54632) 10d0d99 - Rename values of TracingState enum (#54630) be94707 - FrameTiming module to subscribe to Inspector tracing lifecycle (#54633) 04ee02b - Clarify HostTracingProfile and HostTraceRecordingState (#54677) ec92f12 - Introduce TimeWindowedBuffer (#54679) d6ee54f - Keep tracing time window on TraceRecordingState (#54673) e651563 - Define FrameTimingSequence (#54674) 5b7d92f - Define an endpoint on HostTarget to capture frame timings (#54672) f469aa3 - Extract frame trace events construction logic (#54675) 57973fa - Define serializers for frame timings as part of HostTargetTracingProfile (#54681) 587d360 - Propagate Frames data through Host (#54671) 26ff069 - Cleanup no longer used jni layer for frame timings (#54678) 9ba4ce6 - Fix screenshot typo (#54742) 52186a3 - Add optional screenshot argument to FrameTimingSequence (#54743) 57c29fc - Frame screenshot event generation (#54744) b447d26 - Frame screenshots capture implementation (#54745) f3c9a8d - BeginDrawing: INTENDED_VSYNC_TIMESTAMP -> VSYNC_TIMESTAMP (#54765) 9221772 - Add frames category (#54768) 31a0c9a - Specify correct category for SetLayerTreeId event (#54769) cd055e9 - Reduce the screenshots size (#54800) e4a5a56 - Remove Commit from FrameTimingSequence (#54779) 39f7703 - Fix potential bitmap leaks in FrameTimingsObserver (#55652) 3918dd1 - Capture initial screenshot when starting frame timing trace (#55720) 863f5c0 - Refactor FrameTimingsObserver for multi-activity support (#55740) 8230f3b - Reorder FrameTimingsObserver methods (#55743) f56b295 - Increase frame capture quality, apply scaling after DPI normalization (#55731) e5f9f5f - Fix FrameTimingsObverver to initiate PixelCopy on main thread (#55744) d552f2a - Fix bitmap reuse race condition in FrameTimingsObserver (#55745) 3924768 - Fix trailing frame capture after recording ended (#55704) 47684ca - Move base64Encode into react/utils (#55873) 422770d - Move screenshot Base64 encoding to trace serialization (#55803) dc4c36e - Restore inspector addPage listener callback (#55925) 8c763fc - Add tracing helper functions for RNDT traces in C++ and Kotlin (#55935) 4f3f536 - Introduce fuseboxFrameRecordingEnabled flag, gate existing code (#55941) 1aa7a32 - Implement Performance frames and screenshots on iOS (#56015) 64a1a10 - Add pixel diffing to RCTFrameTimingsObserver (#56043) 9d231af - Add dynamic sampling to frame screenshots (#56048) d309cda - Switch trace event chunks from event count to size based (#56080) d3b33f5 - Increase trace screenshot scale factor to 1x (#56079) 972a30d - Add Page.captureScreenshot CDP support (#56307) 77332d2 - Fix data race on PerformanceObserver entry buffer (#56352) 5eb1ca1 - Add dynamic sampling to frame screenshots (#56135) e0d1e29 - Increase trace screenshot scale factor to 1x (#56136) 90e02fa - Add missing debugger define for React-RuntimeApple (#56397) - [LOCAL] Remove stale frame event code from PerformanceTracer - [LOCAL] Update Podfile.lock --- packages/react-native/Package.swift | 5 +- .../DevSupport/RCTFrameTimingsObserver.h | 24 ++ .../DevSupport/RCTFrameTimingsObserver.mm | 298 +++++++++++++++ .../ReactAndroid/api/ReactAndroid.api | 9 - .../devsupport/BridgelessDevSupportManager.kt | 4 +- .../react/devsupport/DevSupportManagerBase.kt | 14 +- .../react/devsupport/InspectorFlags.kt | 4 + .../inspector/FrameTimingSequence.kt | 16 + .../inspector/FrameTimingsObserver.kt | 275 ++++++++++++++ .../devsupport/inspector/TracingState.kt | 17 + .../inspector/TracingStateListener.kt | 15 + .../TracingStateProvider.kt | 2 +- .../devsupport/interfaces/TracingState.kt | 19 - .../PerfMonitorInspectorTargetBinding.kt | 2 +- .../perfmonitor/PerfMonitorOverlayManager.kt | 8 +- .../perfmonitor/PerfMonitorOverlayView.kt | 6 +- .../perfmonitor/PerfMonitorUpdateListener.kt | 2 +- .../featureflags/ReactNativeFeatureFlags.kt | 14 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 22 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 6 +- .../ReactNativeFeatureFlagsDefaults.kt | 6 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 24 +- .../ReactNativeFeatureFlagsProvider.kt | 6 +- .../internal/tracing/PerformanceTracer.kt | 39 ++ .../facebook/react/runtime/ReactHostImpl.kt | 90 ++++- .../react/runtime/ReactHostImplDevHelper.kt | 6 +- .../react/runtime/ReactHostInspectorTarget.kt | 16 +- .../jni/react/devsupport/JInspectorFlags.cpp | 22 ++ .../jni/react/devsupport/JInspectorFlags.h | 2 + .../JReactNativeFeatureFlagsCxxInterop.cpp | 30 +- .../JReactNativeFeatureFlagsCxxInterop.h | 8 +- .../runtime/jni/JReactHostInspectorTarget.cpp | 213 ++++++++++- .../runtime/jni/JReactHostInspectorTarget.h | 186 ++++++++- .../main/jni/third-party/folly/CMakeLists.txt | 1 + .../jsinspector-modern/HostAgent.cpp | 55 ++- .../jsinspector-modern/HostAgent.h | 4 +- .../jsinspector-modern/HostTarget.cpp | 21 +- .../jsinspector-modern/HostTarget.h | 115 +++++- .../HostTargetTraceRecording.cpp | 47 ++- .../HostTargetTraceRecording.h | 47 ++- .../jsinspector-modern/HostTargetTracing.cpp | 75 ++-- .../jsinspector-modern/HostTargetTracing.h | 89 +++++ .../jsinspector-modern/InspectorFlags.cpp | 12 + .../jsinspector-modern/InspectorFlags.h | 12 + .../InspectorInterfaces.cpp | 10 +- .../jsinspector-modern/InstanceAgent.cpp | 13 +- .../jsinspector-modern/NetworkIOAgent.cpp | 2 +- .../jsinspector-modern/TracingAgent.cpp | 42 ++- .../jsinspector-modern/TracingAgent.h | 9 +- .../tests/HostTargetTest.cpp | 65 ++++ .../jsinspector-modern/tests/InspectorMocks.h | 25 +- .../tests/JsiIntegrationTest.cpp | 1 + .../tests/NetworkReporterTest.cpp | 1 + .../jsinspector-modern/tests/TracingTest.cpp | 335 +++++++++++++++++ .../jsinspector-modern/tests/TracingTest.h | 95 +++++ .../utils/InspectorFlagOverridesGuard.cpp | 10 + .../tests/utils/InspectorFlagOverridesGuard.h | 4 +- .../jsinspector-modern/tracing/CMakeLists.txt | 1 + .../tracing/FrameTimingSequence.h | 61 +++ .../tracing/HostTracingProfile.h | 43 +++ .../tracing/HostTracingProfileSerializer.cpp | 165 ++++++++ .../tracing/HostTracingProfileSerializer.h | 50 +++ .../tracing/PerformanceTracer.cpp | 30 +- .../tracing/PerformanceTracerSection.h | 113 ++++++ .../tracing/React-jsinspectortracing.podspec | 1 + .../tracing/TimeWindowedBuffer.h | 158 ++++++++ .../jsinspector-modern/tracing/TraceEvent.h | 3 +- .../tracing/TraceEventGenerator.cpp | 100 +++++ .../tracing/TraceEventGenerator.h | 60 +++ .../tracing/TraceEventSerializer.cpp | 45 ++- .../tracing/TraceEventSerializer.h | 7 + .../tracing/TraceRecordingState.h | 25 +- .../tracing/TraceRecordingStateSerializer.cpp | 68 ---- .../tracing/TraceRecordingStateSerializer.h | 42 --- .../tracing/TracingCategory.h | 136 +++++++ .../jsinspector-modern/tracing/TracingState.h | 24 -- .../tracing/tests/TimeWindowedBufferTest.cpp | 352 ++++++++++++++++++ .../featureflags/ReactNativeFeatureFlags.cpp | 10 +- .../featureflags/ReactNativeFeatureFlags.h | 12 +- .../ReactNativeFeatureFlagsAccessor.cpp | 94 +++-- .../ReactNativeFeatureFlagsAccessor.h | 8 +- .../ReactNativeFeatureFlagsDefaults.h | 10 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 20 +- .../ReactNativeFeatureFlagsProvider.h | 4 +- .../NativeReactNativeFeatureFlags.cpp | 12 +- .../NativeReactNativeFeatureFlags.h | 6 +- .../timeline/PerformanceObserver.cpp | 24 +- .../timeline/PerformanceObserver.h | 2 + .../platform/ios/ReactCommon/RCTHost.mm | 115 ++++++ .../utils}/Base64.h | 4 +- .../react-native/scripts/cocoapods/utils.rb | 1 + .../ReactNativeFeatureFlags.config.js | 22 ++ .../featureflags/ReactNativeFeatureFlags.js | 12 +- .../specs/NativeReactNativeFeatureFlags.js | 4 +- packages/rn-tester/Podfile.lock | 5 +- 95 files changed, 3932 insertions(+), 417 deletions(-) create mode 100644 packages/react-native/React/DevSupport/RCTFrameTimingsObserver.h create mode 100644 packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt rename packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/{interfaces => inspector}/TracingStateProvider.kt (84%) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracerSection.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h delete mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp delete mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingCategory.h delete mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp rename packages/react-native/ReactCommon/{jsinspector-modern => react/utils}/Base64.h (96%) diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 3cddb0fc82e..d9f6481c8d3 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -400,7 +400,10 @@ let reactRuntimeApple = RNTarget( name: .reactRuntimeApple, path: "ReactCommon/react/runtime/platform/ios", excludedPaths: ["ReactCommon/RCTJscInstance.mm", "ReactCommon/metainternal"], - dependencies: [.reactNativeDependencies, .jsi, .reactPerfLogger, .reactCxxReact, .rctDeprecation, .yoga, .reactRuntime, .reactRCTFabric, .reactCoreModules, .reactTurboModuleCore, .hermesPrebuilt, .reactUtils] + dependencies: [.reactNativeDependencies, .jsi, .reactPerfLogger, .reactCxxReact, .rctDeprecation, .yoga, .reactRuntime, .reactRCTFabric, .reactCoreModules, .reactTurboModuleCore, .hermesPrebuilt, .reactUtils], + defines: [ + CXXSetting.define("REACT_NATIVE_DEBUGGER_ENABLED", to: "1", .when(configuration: BuildConfiguration.debug)) + ] ) let publicHeadersPathForReactCore: String = BUILD_FROM_SOURCE ? "includes" : "." diff --git a/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.h b/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.h new file mode 100644 index 00000000000..ebf7b4bcf49 --- /dev/null +++ b/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.h @@ -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. + */ + +#import + +#ifdef __cplusplus +#import + +using RCTFrameTimingCallback = void (^)(facebook::react::jsinspector_modern::tracing::FrameTimingSequence); +#endif + +@interface RCTFrameTimingsObserver : NSObject + +#ifdef __cplusplus +- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback; +#endif +- (void)start; +- (void)stop; + +@end diff --git a/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm b/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm new file mode 100644 index 00000000000..ec9a0ae01fb --- /dev/null +++ b/packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm @@ -0,0 +1,298 @@ +/* + * 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. + */ + +#import "RCTFrameTimingsObserver.h" + +#import + +#import +#import + +#import +#import +#import +#import +#import + +#import + +using namespace facebook::react; + +static constexpr CGFloat kScreenshotScaleFactor = 1.0; +static constexpr CGFloat kScreenshotJPEGQuality = 0.8; + +namespace { + +// Stores a captured frame screenshot and its associated metadata, used for +// buffering frames during dynamic sampling. +struct FrameData { + UIImage *image; + uint64_t frameId; + jsinspector_modern::tracing::ThreadId threadId; + HighResTimeStamp beginTimestamp; + HighResTimeStamp endTimestamp; +}; + +} // namespace + +@implementation RCTFrameTimingsObserver { + BOOL _screenshotsEnabled; + RCTFrameTimingCallback _callback; + CADisplayLink *_displayLink; + uint64_t _frameCounter; + // Serial queue for encoding work (single background thread). We limit to 1 + // thread to minimize the performance impact of screenshot recording. + dispatch_queue_t _encodingQueue; + std::atomic _running; + uint64_t _lastScreenshotHash; + + // Stores the most recently captured frame to opportunistically encode after + // the current frame. Replaced frames are emitted as timings without + // screenshots. + std::mutex _lastFrameMutex; + std::optional _lastFrameData; + + std::atomic _encodingInProgress; +} + +- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback +{ + if (self = [super init]) { + _screenshotsEnabled = screenshotsEnabled; + _callback = [callback copy]; + _frameCounter = 0; + _encodingQueue = dispatch_queue_create("com.facebook.react.frame-timings-observer", DISPATCH_QUEUE_SERIAL); + _running.store(false); + _lastScreenshotHash = 0; + _encodingInProgress.store(false); + } + return self; +} + +- (void)start +{ + _running.store(true, std::memory_order_relaxed); + _frameCounter = 0; + _lastScreenshotHash = 0; + _encodingInProgress.store(false, std::memory_order_relaxed); + { + std::lock_guard lock(_lastFrameMutex); + _lastFrameData.reset(); + } + + // Emit initial frame event + auto now = HighResTimeStamp::now(); + [self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now]; + + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +- (void)stop +{ + _running.store(false, std::memory_order_relaxed); + [_displayLink invalidate]; + _displayLink = nil; + { + std::lock_guard lock(_lastFrameMutex); + _lastFrameData.reset(); + } +} + +- (void)_displayLinkTick:(CADisplayLink *)sender +{ + // CADisplayLink.timestamp and targetTimestamp are in the same timebase as + // CACurrentMediaTime() / mach_absolute_time(), which on Apple platforms maps + // to CLOCK_UPTIME_RAW — the same clock backing std::chrono::steady_clock. + auto beginNanos = static_cast(sender.timestamp * 1e9); + auto endNanos = static_cast(sender.targetTimestamp * 1e9); + + auto beginTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos))); + auto endTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(endNanos))); + + [self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp]; +} + +- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endTimestamp:(HighResTimeStamp)endTimestamp +{ + uint64_t frameId = _frameCounter++; + auto threadId = static_cast(pthread_mach_thread_np(pthread_self())); + + if (!_screenshotsEnabled) { + // Screenshots disabled - emit without screenshot + [self _emitFrameEventWithFrameId:frameId + threadId:threadId + beginTimestamp:beginTimestamp + endTimestamp:endTimestamp + screenshot:std::nullopt]; + return; + } + + UIImage *image = [self _captureScreenshot]; + if (image == nil) { + // Failed to capture (e.g. no window, duplicate hash) - emit without screenshot + [self _emitFrameEventWithFrameId:frameId + threadId:threadId + beginTimestamp:beginTimestamp + endTimestamp:endTimestamp + screenshot:std::nullopt]; + return; + } + + FrameData frameData{image, frameId, threadId, beginTimestamp, endTimestamp}; + + bool expected = false; + if (_encodingInProgress.compare_exchange_strong(expected, true)) { + // Not encoding - encode this frame immediately + [self _encodeFrame:std::move(frameData)]; + } else { + // Encoding thread busy - store current screenshot in buffer for tail-capture + std::optional oldFrame; + { + std::lock_guard lock(_lastFrameMutex); + oldFrame = std::move(_lastFrameData); + _lastFrameData = std::move(frameData); + } + if (oldFrame.has_value()) { + // Skipped frame - emit event without screenshot + [self _emitFrameEventWithFrameId:oldFrame->frameId + threadId:oldFrame->threadId + beginTimestamp:oldFrame->beginTimestamp + endTimestamp:oldFrame->endTimestamp + screenshot:std::nullopt]; + } + } +} + +- (void)_emitFrameEventWithFrameId:(uint64_t)frameId + threadId:(jsinspector_modern::tracing::ThreadId)threadId + beginTimestamp:(HighResTimeStamp)beginTimestamp + endTimestamp:(HighResTimeStamp)endTimestamp + screenshot:(std::optional>)screenshot +{ + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + if (!self->_running.load(std::memory_order_relaxed)) { + return; + } + jsinspector_modern::tracing::FrameTimingSequence sequence{ + frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshot)}; + self->_callback(std::move(sequence)); + }); +} + +- (void)_encodeFrame:(FrameData)frameData +{ + dispatch_async(_encodingQueue, ^{ + if (!self->_running.load(std::memory_order_relaxed)) { + return; + } + + auto screenshot = [self _encodeScreenshot:frameData.image]; + [self _emitFrameEventWithFrameId:frameData.frameId + threadId:frameData.threadId + beginTimestamp:frameData.beginTimestamp + endTimestamp:frameData.endTimestamp + screenshot:std::move(screenshot)]; + + // Clear encoding flag early, allowing new frames to start fresh encoding + // sessions + self->_encodingInProgress.store(false, std::memory_order_release); + + // Opportunistically encode tail frame (if present) without blocking new + // frames + std::optional tailFrame; + { + std::lock_guard lock(self->_lastFrameMutex); + tailFrame = std::move(self->_lastFrameData); + self->_lastFrameData.reset(); + } + if (tailFrame.has_value()) { + if (!self->_running.load(std::memory_order_relaxed)) { + return; + } + auto tailScreenshot = [self _encodeScreenshot:tailFrame->image]; + [self _emitFrameEventWithFrameId:tailFrame->frameId + threadId:tailFrame->threadId + beginTimestamp:tailFrame->beginTimestamp + endTimestamp:tailFrame->endTimestamp + screenshot:std::move(tailScreenshot)]; + } + }); +} + +// Captures a screenshot of the current window. Must be called on the main +// thread. Returns nil if capture fails or if the frame content is unchanged. +- (UIImage *)_captureScreenshot +{ + UIWindow *keyWindow = [self _getKeyWindow]; + if (keyWindow == nil) { + return nil; + } + + UIView *rootView = keyWindow.rootViewController.view ?: keyWindow; + CGSize viewSize = rootView.bounds.size; + CGSize scaledSize = CGSizeMake(viewSize.width * kScreenshotScaleFactor, viewSize.height * kScreenshotScaleFactor); + + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format]; + + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [rootView drawViewHierarchyInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height) afterScreenUpdates:NO]; + }]; + + // Skip duplicate frames via sampled FNV-1a pixel hash + CGImageRef cgImage = image.CGImage; + CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(cgImage)); + uint64_t hash = 0xcbf29ce484222325ULL; + const uint8_t *ptr = CFDataGetBytePtr(pixelData); + CFIndex length = CFDataGetLength(pixelData); + // Use prime stride to prevent row alignment on power-of-2 pixel widths + for (CFIndex i = 0; i < length; i += 67) { + hash ^= ptr[i]; + hash *= 0x100000001b3ULL; + } + CFRelease(pixelData); + + if (hash == _lastScreenshotHash) { + return nil; + } + _lastScreenshotHash = hash; + + return image; +} + +- (std::optional>)_encodeScreenshot:(UIImage *)image +{ + NSData *jpegData = UIImageJPEGRepresentation(image, kScreenshotJPEGQuality); + if (jpegData == nil) { + return std::nullopt; + } + + const auto *bytes = static_cast(jpegData.bytes); + return std::vector(bytes, bytes + jpegData.length); +} + +- (UIWindow *)_getKeyWindow +{ + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && + [scene isKindOfClass:[UIWindowScene class]]) { + auto windowScene = (UIWindowScene *)scene; + for (UIWindow *window = nullptr in windowScene.windows) { + if (window.isKeyWindow) { + return window; + } + } + } + } + return nil; +} + +@end diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index e7b53c1f62d..d2001c61e1a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2257,15 +2257,6 @@ public abstract interface class com/facebook/react/devsupport/interfaces/StackFr public abstract fun toJSON ()Lorg/json/JSONObject; } -public final class com/facebook/react/devsupport/interfaces/TracingState : java/lang/Enum { - public static final field DISABLED Lcom/facebook/react/devsupport/interfaces/TracingState; - public static final field ENABLEDINBACKGROUNDMODE Lcom/facebook/react/devsupport/interfaces/TracingState; - public static final field ENABLEDINCDPMODE Lcom/facebook/react/devsupport/interfaces/TracingState; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/devsupport/interfaces/TracingState; - public static fun values ()[Lcom/facebook/react/devsupport/interfaces/TracingState; -} - public final class com/facebook/react/fabric/ComponentFactory { public fun ()V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt index e0a56992ec1..9679ea67c2f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt @@ -10,12 +10,12 @@ package com.facebook.react.devsupport import android.content.Context import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.SurfaceDelegateFactory +import com.facebook.react.devsupport.inspector.TracingState import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener import com.facebook.react.devsupport.interfaces.DevLoadingViewManager import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager import com.facebook.react.devsupport.interfaces.RedBoxHandler -import com.facebook.react.devsupport.interfaces.TracingState import com.facebook.react.packagerconnection.RequestHandler /** @@ -83,6 +83,6 @@ internal class BridgelessDevSupportManager( } fun tracingState(): TracingState { - return TracingState.ENABLEDINCDPMODE + return TracingState.ENABLED_IN_CDP_MODE } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt index 40cdf48cb33..543ed13f937 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt @@ -52,6 +52,8 @@ import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled import com.facebook.react.devsupport.StackTraceHelper.convertJavaStackTrace import com.facebook.react.devsupport.StackTraceHelper.convertJsStackTrace +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateProvider import com.facebook.react.devsupport.interfaces.BundleLoadCallback import com.facebook.react.devsupport.interfaces.DebuggerFrontendPanelName import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener @@ -66,8 +68,6 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager import com.facebook.react.devsupport.interfaces.RedBoxHandler import com.facebook.react.devsupport.interfaces.StackFrame -import com.facebook.react.devsupport.interfaces.TracingState -import com.facebook.react.devsupport.interfaces.TracingStateProvider import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper import com.facebook.react.devsupport.perfmonitor.PerfMonitorOverlayManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags @@ -396,21 +396,21 @@ public abstract class DevSupportManagerBase( val analyzePerformanceItemString = when (tracingState) { - TracingState.ENABLEDINBACKGROUNDMODE -> + TracingState.ENABLED_IN_BACKGROUND_MODE -> applicationContext.getString(R.string.catalyst_performance_background) - TracingState.ENABLEDINCDPMODE -> + TracingState.ENABLED_IN_CDP_MODE -> applicationContext.getString(R.string.catalyst_performance_cdp) TracingState.DISABLED -> applicationContext.getString(R.string.catalyst_performance_disabled) } - if (!isConnected || tracingState == TracingState.ENABLEDINCDPMODE) { + if (!isConnected || tracingState == TracingState.ENABLED_IN_CDP_MODE) { disabledItemKeys.add(analyzePerformanceItemString) } options[analyzePerformanceItemString] = when (tracingState) { - TracingState.ENABLEDINBACKGROUNDMODE -> + TracingState.ENABLED_IN_BACKGROUND_MODE -> DevOptionHandler { UiThreadUtil.runOnUiThread { if (reactInstanceDevHelper is PerfMonitorDevHelper) { @@ -427,7 +427,7 @@ public abstract class DevSupportManagerBase( if (reactInstanceDevHelper is PerfMonitorDevHelper) reactInstanceDevHelper.inspectorTarget?.resumeBackgroundTrace() } - TracingState.ENABLEDINCDPMODE -> DevOptionHandler {} + TracingState.ENABLED_IN_CDP_MODE -> DevOptionHandler {} } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt index 8cb59abb19e..c7ffcff620a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt @@ -17,7 +17,11 @@ internal object InspectorFlags { SoLoader.loadLibrary("react_devsupportjni") } + @DoNotStrip @JvmStatic external fun getScreenshotCaptureEnabled(): Boolean + @DoNotStrip @JvmStatic external fun getFuseboxEnabled(): Boolean @DoNotStrip @JvmStatic external fun getIsProfilingBuild(): Boolean + + @DoNotStrip @JvmStatic external fun getFrameRecordingEnabled(): Boolean } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt new file mode 100644 index 00000000000..5eaae383eed --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt @@ -0,0 +1,16 @@ +/* + * 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. + */ + +package com.facebook.react.devsupport.inspector + +internal data class FrameTimingSequence( + val id: Int, + val threadId: Int, + val beginTimestamp: Long, + val endTimestamp: Long, + val screenshot: ByteArray? = null, +) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt new file mode 100644 index 00000000000..a6233999023 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt @@ -0,0 +1,275 @@ +/* + * 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. + */ + +package com.facebook.react.devsupport.inspector + +import android.graphics.Bitmap +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.view.FrameMetrics +import android.view.PixelCopy +import android.view.Window +import com.facebook.proguard.annotations.DoNotStripAny +import java.io.ByteArrayOutputStream +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch + +@DoNotStripAny +internal class FrameTimingsObserver( + private val screenshotsEnabled: Boolean, + private val onFrameTimingSequence: (sequence: FrameTimingSequence) -> Unit, +) { + private val isSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + private val mainHandler = Handler(Looper.getMainLooper()) + + // Serial dispatcher for encoding work (single background thread). We limit to 1 thread to + // minimize the performance impact of screenshot recording. + private val encodingDispatcher: CoroutineDispatcher = + Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + // Stores the most recently captured frame to opportunistically encode after the current frame. + // Replaced frames are emitted as timings without screenshots. + private val lastFrameBuffer = AtomicReference(null) + + private var frameCounter: Int = 0 + private val encodingInProgress = AtomicBoolean(false) + @Volatile private var isTracing: Boolean = false + @Volatile private var currentWindow: Window? = null + + private data class FrameData( + val bitmap: Bitmap, + val frameId: Int, + val threadId: Int, + val beginTimestamp: Long, + val endTimestamp: Long, + ) + + fun start() { + if (!isSupported) { + return + } + + frameCounter = 0 + encodingInProgress.set(false) + lastFrameBuffer.set(null) + isTracing = true + + // Emit initial frame event + val timestamp = System.nanoTime() + emitFrameTiming(timestamp, timestamp) + + currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler) + } + + fun stop() { + if (!isSupported) { + return + } + + isTracing = false + + currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener) + mainHandler.removeCallbacksAndMessages(null) + lastFrameBuffer.getAndSet(null)?.bitmap?.recycle() + } + + fun setCurrentWindow(window: Window?) { + if (!isSupported || currentWindow === window) { + return + } + + currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener) + currentWindow = window + if (isTracing) { + currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler) + } + } + + private val frameMetricsListener = + Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ -> + // Guard against calls after stop() + if (!isTracing) { + return@OnFrameMetricsAvailableListener + } + val beginTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP) + val endTimestamp = beginTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) + emitFrameTiming(beginTimestamp, endTimestamp) + } + + private fun emitFrameTiming(beginTimestamp: Long, endTimestamp: Long) { + val frameId = frameCounter++ + val threadId = Process.myTid() + + if (!screenshotsEnabled) { + // Screenshots disabled - emit without screenshot + emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null) + return + } + + captureScreenshot(frameId, threadId, beginTimestamp, endTimestamp) { frameData -> + if (frameData != null) { + if (encodingInProgress.compareAndSet(false, true)) { + // Not encoding - encode this frame immediately + encodeFrame(frameData) + } else { + // Encoding thread busy - store current screenshot in buffer for tail-capture + val oldFrameData = lastFrameBuffer.getAndSet(frameData) + if (oldFrameData != null) { + // Skipped frame - emit event without screenshot + emitFrameEvent( + oldFrameData.frameId, + oldFrameData.threadId, + oldFrameData.beginTimestamp, + oldFrameData.endTimestamp, + null, + ) + oldFrameData.bitmap.recycle() + } + } + } else { + // Failed to capture (e.g. timeout) - emit without screenshot + emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null) + } + } + } + + private fun emitFrameEvent( + frameId: Int, + threadId: Int, + beginTimestamp: Long, + endTimestamp: Long, + screenshot: ByteArray?, + ) { + CoroutineScope(Dispatchers.Default).launch { + onFrameTimingSequence( + FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot) + ) + } + } + + private fun encodeFrame(frameData: FrameData) { + CoroutineScope(encodingDispatcher).launch { + try { + val screenshot = encodeScreenshot(frameData.bitmap) + emitFrameEvent( + frameData.frameId, + frameData.threadId, + frameData.beginTimestamp, + frameData.endTimestamp, + screenshot, + ) + } finally { + frameData.bitmap.recycle() + } + + // Clear encoding flag early, allowing new frames to start fresh encoding sessions + encodingInProgress.set(false) + + // Opportunistically encode tail frame (if present) without blocking new frames + val tailFrame = lastFrameBuffer.getAndSet(null) + if (tailFrame != null) { + try { + val screenshot = encodeScreenshot(tailFrame.bitmap) + emitFrameEvent( + tailFrame.frameId, + tailFrame.threadId, + tailFrame.beginTimestamp, + tailFrame.endTimestamp, + screenshot, + ) + } finally { + tailFrame.bitmap.recycle() + } + } + } + } + + // Must be called from the main thread so that PixelCopy captures the current frame. + private fun captureScreenshot( + frameId: Int, + threadId: Int, + beginTimestamp: Long, + endTimestamp: Long, + callback: (FrameData?) -> Unit, + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // PixelCopy not available + callback(null) + return + } + + val window = currentWindow + if (window == null) { + // No window + callback(null) + return + } + + val decorView = window.decorView + val width = decorView.width + val height = decorView.height + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + PixelCopy.request( + window, + bitmap, + { copyResult -> + if (copyResult == PixelCopy.SUCCESS) { + callback(FrameData(bitmap, frameId, threadId, beginTimestamp, endTimestamp)) + } else { + bitmap.recycle() + callback(null) + } + }, + mainHandler, + ) + } + + private fun encodeScreenshot(bitmap: Bitmap): ByteArray? { + var scaledBitmap: Bitmap? = null + return try { + val window = currentWindow ?: return null + val width = bitmap.width + val height = bitmap.height + val density = window.context.resources.displayMetrics.density + val scaledWidth = (width / density * SCREENSHOT_SCALE_FACTOR).toInt() + val scaledHeight = (height / density * SCREENSHOT_SCALE_FACTOR).toInt() + scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true) + + val compressFormat = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY + else Bitmap.CompressFormat.JPEG + + ByteArrayOutputStream(SCREENSHOT_OUTPUT_SIZE_HINT).use { outputStream -> + scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream) + outputStream.toByteArray() + } + } catch (e: Exception) { + null + } finally { + scaledBitmap?.recycle() + } + } + + companion object { + private const val SCREENSHOT_SCALE_FACTOR = 1.0f + private const val SCREENSHOT_QUALITY = 80 + + // Capacity hint for the ByteArrayOutputStream used during bitmap + // compression. Sized slightly above typical compressed output to minimise + // internal buffer resizing. + private const val SCREENSHOT_OUTPUT_SIZE_HINT = 65536 // 64 KB + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt new file mode 100644 index 00000000000..c8f236c3552 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingState.kt @@ -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. + */ + +package com.facebook.react.devsupport.inspector + +import com.facebook.proguard.annotations.DoNotStripAny + +@DoNotStripAny +internal enum class TracingState { + DISABLED, // There is no active trace + ENABLED_IN_BACKGROUND_MODE, // Trace is currently running in background mode + ENABLED_IN_CDP_MODE, // Trace is currently running in CDP mode +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt new file mode 100644 index 00000000000..0f54753a42d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateListener.kt @@ -0,0 +1,15 @@ +/* + * 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. + */ + +package com.facebook.react.devsupport.inspector + +import com.facebook.proguard.annotations.DoNotStripAny + +@DoNotStripAny +internal fun interface TracingStateListener { + public fun onStateChanged(state: TracingState, screenshotsEnabled: Boolean) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt similarity index 84% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt index 1e9b2fead9d..b9e7e53964a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingStateProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/TracingStateProvider.kt @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.devsupport.interfaces +package com.facebook.react.devsupport.inspector internal interface TracingStateProvider { fun getTracingState(): TracingState diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt deleted file mode 100644 index 91a7b73ea23..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/TracingState.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.devsupport.interfaces - -import com.facebook.proguard.annotations.DoNotStripAny - -// Keep in sync with `TracingState.h` -// JNI wrapper for `jsinspector_modern::Tracing::TracingState`. -@DoNotStripAny -public enum class TracingState { - DISABLED, // There is no active trace - ENABLEDINBACKGROUNDMODE, // Trace is currently running in background mode - ENABLEDINCDPMODE, // Trace is currently running in CDP mode -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt index 6df3bdeda73..dcd48ffb7b2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorInspectorTargetBinding.kt @@ -7,7 +7,7 @@ package com.facebook.react.devsupport.perfmonitor -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState /** * [Experimental] Interface implemented by [com.facebook.react.runtime.ReactHostInspectorTarget] diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt index aa10fdc89a1..87fe0a92943 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayManager.kt @@ -8,7 +8,7 @@ package com.facebook.react.devsupport.perfmonitor import com.facebook.react.bridge.UiThreadUtil -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState internal class PerfMonitorOverlayManager( private val devHelper: PerfMonitorDevHelper, @@ -21,7 +21,7 @@ internal class PerfMonitorOverlayManager( get() = enabled private var view: PerfMonitorOverlayView? = null - private var tracingState: TracingState = TracingState.ENABLEDINCDPMODE + private var tracingState: TracingState = TracingState.ENABLED_IN_CDP_MODE private var perfIssueCount: Int = 0 /** Enable the Perf Monitor overlay. */ @@ -92,7 +92,7 @@ internal class PerfMonitorOverlayManager( private fun handleRecordingButtonPress() { when (tracingState) { - TracingState.ENABLEDINBACKGROUNDMODE -> { + TracingState.ENABLED_IN_BACKGROUND_MODE -> { devHelper.inspectorTarget?.let { target -> if (!target.pauseAndAnalyzeBackgroundTrace()) { onRequestOpenDevTools() @@ -102,7 +102,7 @@ internal class PerfMonitorOverlayManager( TracingState.DISABLED -> { devHelper.inspectorTarget?.resumeBackgroundTrace() } - TracingState.ENABLEDINCDPMODE -> Unit + TracingState.ENABLED_IN_CDP_MODE -> Unit } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt index 512fb852b72..5f1ce5a89c5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorOverlayView.kt @@ -21,7 +21,7 @@ import android.widget.TextView import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.facebook.react.R -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.PixelUtil @@ -50,12 +50,12 @@ internal class PerfMonitorOverlayView( } fun updateRecordingState(state: TracingState) { - if (state == TracingState.ENABLEDINCDPMODE) { + if (state == TracingState.ENABLED_IN_CDP_MODE) { dialog.hide() return } - if (state == TracingState.ENABLEDINBACKGROUNDMODE) { + if (state == TracingState.ENABLED_IN_BACKGROUND_MODE) { (statusIndicator.background as GradientDrawable).setColor(Color.RED) statusLabel.text = "Profiling Active" tooltipLabel.text = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt index 417064fbd1f..0da5d115faf 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/perfmonitor/PerfMonitorUpdateListener.kt @@ -7,7 +7,7 @@ package com.facebook.react.devsupport.perfmonitor -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.TracingState /** [Experimental] An interface for subscribing to updates for the V2 Perf Monitor. */ internal interface PerfMonitorUpdateListener { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 4545b13de9c..a39034bc983 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -372,12 +372,24 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fuseboxEnabledRelease(): Boolean = accessor.fuseboxEnabledRelease() + /** + * Enable frame timings and screenshots support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + @JvmStatic + public fun fuseboxFrameRecordingEnabled(): Boolean = accessor.fuseboxFrameRecordingEnabled() + /** * Enable network inspection support in the React Native DevTools CDP backend. Requires `enableBridgelessArchitecture`. This flag is global and should not be changed across React Host lifetimes. */ @JvmStatic public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled() + /** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + @JvmStatic + public fun fuseboxScreenshotCaptureEnabled(): Boolean = accessor.fuseboxScreenshotCaptureEnabled() + /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 509fd8779e5..33180deb585 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -77,7 +77,9 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null + private var fuseboxFrameRecordingEnabledCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null private var perfIssuesEnabledCache: Boolean? = null @@ -619,6 +621,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fuseboxFrameRecordingEnabled(): Boolean { + var cached = fuseboxFrameRecordingEnabledCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fuseboxFrameRecordingEnabled() + fuseboxFrameRecordingEnabledCache = cached + } + return cached + } + override fun fuseboxNetworkInspectionEnabled(): Boolean { var cached = fuseboxNetworkInspectionEnabledCache if (cached == null) { @@ -628,6 +639,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fuseboxScreenshotCaptureEnabled(): Boolean { + var cached = fuseboxScreenshotCaptureEnabledCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fuseboxScreenshotCaptureEnabled() + fuseboxScreenshotCaptureEnabledCache = cached + } + return cached + } + override fun hideOffscreenVirtualViewsOnIOS(): Boolean { var cached = hideOffscreenVirtualViewsOnIOSCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index d5cf7666cf4..6de1a23ce55 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -142,8 +142,12 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fuseboxEnabledRelease(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxFrameRecordingEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun fuseboxScreenshotCaptureEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun hideOffscreenVirtualViewsOnIOS(): Boolean @DoNotStrip @JvmStatic public external fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 9f3e38e089b..de038034156 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<303c55a883b4798288716d168ce82d06>> + * @generated SignedSource<> */ /** @@ -137,8 +137,12 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fuseboxEnabledRelease(): Boolean = false + override fun fuseboxFrameRecordingEnabled(): Boolean = false + override fun fuseboxNetworkInspectionEnabled(): Boolean = true + override fun fuseboxScreenshotCaptureEnabled(): Boolean = false + override fun hideOffscreenVirtualViewsOnIOS(): Boolean = false override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 659d6e343d9..f116734c24d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<98eb8f2d7036cf8a3023a8c560375f6a>> + * @generated SignedSource<<38e86d50298e5c4199d456f709ac13fc>> */ /** @@ -81,7 +81,9 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null + private var fuseboxFrameRecordingEnabledCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null private var perfIssuesEnabledCache: Boolean? = null @@ -680,6 +682,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fuseboxFrameRecordingEnabled(): Boolean { + var cached = fuseboxFrameRecordingEnabledCache + if (cached == null) { + cached = currentProvider.fuseboxFrameRecordingEnabled() + accessedFeatureFlags.add("fuseboxFrameRecordingEnabled") + fuseboxFrameRecordingEnabledCache = cached + } + return cached + } + override fun fuseboxNetworkInspectionEnabled(): Boolean { var cached = fuseboxNetworkInspectionEnabledCache if (cached == null) { @@ -690,6 +702,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fuseboxScreenshotCaptureEnabled(): Boolean { + var cached = fuseboxScreenshotCaptureEnabledCache + if (cached == null) { + cached = currentProvider.fuseboxScreenshotCaptureEnabled() + accessedFeatureFlags.add("fuseboxScreenshotCaptureEnabled") + fuseboxScreenshotCaptureEnabledCache = cached + } + return cached + } + override fun hideOffscreenVirtualViewsOnIOS(): Boolean { var cached = hideOffscreenVirtualViewsOnIOSCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 38443832013..415136b6b9c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<18744192baedc0d5fa0fc7873adf0422>> + * @generated SignedSource<> */ /** @@ -137,8 +137,12 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fuseboxEnabledRelease(): Boolean + @DoNotStrip public fun fuseboxFrameRecordingEnabled(): Boolean + @DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip public fun fuseboxScreenshotCaptureEnabled(): Boolean + @DoNotStrip public fun hideOffscreenVirtualViewsOnIOS(): Boolean @DoNotStrip public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean 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 dad904b4b1d..8bac4f57c38 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 @@ -23,6 +23,45 @@ public object PerformanceTracer { SoLoader.loadLibrary("react_performancetracerjni") } + public fun trace(name: String, block: () -> T): T { + return trace(name, null /* track */, null /* trackGroup */, null /* color */, block) + } + + public fun trace(name: String, track: String, block: () -> T): T { + return trace(name, track, null /* trackGroup */, null /* color */, block) + } + + public fun trace(name: String, track: String, trackGroup: String, block: () -> T): T { + return trace(name, track, trackGroup, null /* color */, block) + } + + public fun trace( + name: String, + track: String?, + trackGroup: String?, + color: String?, + block: () -> T, + ): T { + if (!isTracing()) { + return block() + } + + val startTimeNanos = java.lang.System.nanoTime() + try { + return block() + } finally { + val endTimeNanos = java.lang.System.nanoTime() + reportTimeStamp( + name, + startTimeNanos, + endTimeNanos, + track, + trackGroup, + color, + ) + } + } + /** Callback interface for tracing state changes. */ @DoNotStrip public interface TracingStateCallback { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 09978ecbf5f..48ae99d9335 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -12,6 +12,7 @@ import android.content.Context import android.content.Intent import android.nfc.NfcAdapter import android.os.Bundle +import androidx.core.graphics.createBitmap import com.facebook.common.logging.FLog import com.facebook.infer.annotation.Assertions import com.facebook.infer.annotation.ThreadConfined @@ -43,8 +44,11 @@ import com.facebook.react.devsupport.DevMenuConfiguration import com.facebook.react.devsupport.DevSupportManagerBase import com.facebook.react.devsupport.DevSupportManagerFactory import com.facebook.react.devsupport.InspectorFlags +import com.facebook.react.devsupport.inspector.FrameTimingsObserver import com.facebook.react.devsupport.inspector.InspectorNetworkHelper import com.facebook.react.devsupport.inspector.InspectorNetworkRequestListener +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateListener import com.facebook.react.devsupport.interfaces.BundleLoadCallback import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.devsupport.interfaces.DevSupportManager.PausedInDebuggerOverlayCommandListener @@ -147,6 +151,7 @@ public class ReactHostImpl( private val beforeDestroyListeners: MutableList<() -> Unit> = CopyOnWriteArrayList() internal var reactHostInspectorTarget: ReactHostInspectorTarget? = null + private var frameTimingsObserver: FrameTimingsObserver? = null @Volatile private var hostInvalidated = false @@ -246,6 +251,7 @@ public class ReactHostImpl( stateTracker.enterState("onHostResume(activity)") currentActivity = activity + frameTimingsObserver?.setCurrentWindow(activity?.window) maybeEnableDevSupport(true) reactLifecycleStateManager.moveToOnHostResume(currentReactContext, activity) @@ -442,6 +448,43 @@ public class ReactHostImpl( InspectorNetworkHelper.loadNetworkResource(url, listener) } + @DoNotStrip + private fun captureScreenshot(format: String, quality: Int): String? { + val activity = currentActivity ?: return null + val window = activity.window ?: return null + val decorView = window.decorView.rootView + + val width = decorView.width + val height = decorView.height + if (width <= 0 || height <= 0) { + return null + } + + val bitmap = createBitmap(width, height) + val canvas = android.graphics.Canvas(bitmap) + decorView.draw(canvas) + + val outputStream = java.io.ByteArrayOutputStream() + val compressFormat = + when (format) { + "jpeg" -> android.graphics.Bitmap.CompressFormat.JPEG + "webp" -> + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + android.graphics.Bitmap.CompressFormat.WEBP_LOSSY + } else { + @Suppress("DEPRECATION") android.graphics.Bitmap.CompressFormat.WEBP + } + else -> android.graphics.Bitmap.CompressFormat.PNG + } + val compressQuality = if (quality in 0..100) quality else 80 + + bitmap.compress(compressFormat, compressQuality, outputStream) + bitmap.recycle() + + val bytes = outputStream.toByteArray() + return android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP) + } + /** * Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until * reload is finished, before destroying. @@ -847,6 +890,7 @@ public class ReactHostImpl( private fun moveToHostDestroy(currentContext: ReactContext?) { reactLifecycleStateManager.moveToOnHostDestroy(currentContext) currentActivity = null + frameTimingsObserver?.setCurrentWindow(null) } private fun raiseSoftException( @@ -1442,8 +1486,7 @@ public class ReactHostImpl( // If the host has been invalidated, now that the current context/instance // has been unregistered, we can safely destroy the host's inspector // target. - reactHostInspectorTarget?.close() - reactHostInspectorTarget = null + destroyReactHostInspectorTarget() } // Step 1: Destroy DevSupportManager @@ -1554,13 +1597,52 @@ public class ReactHostImpl( internal fun getOrCreateReactHostInspectorTarget(): ReactHostInspectorTarget? { if (reactHostInspectorTarget == null && InspectorFlags.getFuseboxEnabled()) { - // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. - reactHostInspectorTarget = ReactHostInspectorTarget(this) + reactHostInspectorTarget = createReactHostInspectorTarget() } return reactHostInspectorTarget } + private fun createReactHostInspectorTarget(): ReactHostInspectorTarget { + // NOTE: ReactHostInspectorTarget only retains a weak reference to `this`. + val inspectorTarget = ReactHostInspectorTarget(this) + inspectorTarget.registerTracingStateListener( + TracingStateListener { state: TracingState, _screenshotsEnabled: Boolean -> + when (state) { + TracingState.ENABLED_IN_BACKGROUND_MODE, + TracingState.ENABLED_IN_CDP_MODE -> { + if (InspectorFlags.getFrameRecordingEnabled()) { + val observer = + FrameTimingsObserver( + _screenshotsEnabled, + { frameTimingsSequence -> + inspectorTarget.recordFrameTimings(frameTimingsSequence) + }, + ) + observer.setCurrentWindow(currentActivity?.window) + observer.start() + frameTimingsObserver = observer + } + } + TracingState.DISABLED -> { + frameTimingsObserver?.stop() + frameTimingsObserver = null + } + } + } + ) + + return inspectorTarget + } + + private fun destroyReactHostInspectorTarget() { + frameTimingsObserver?.stop() + frameTimingsObserver = null + + reactHostInspectorTarget?.close() + reactHostInspectorTarget = null + } + @ThreadConfined(ThreadConfined.UI) internal fun unregisterInstanceFromInspector(reactInstance: ReactInstance?) { if (reactInstance != null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt index cc4598e7c98..322bbb81376 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImplDevHelper.kt @@ -16,8 +16,8 @@ import com.facebook.react.bridge.ReactContext import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.devsupport.ReactInstanceDevHelper -import com.facebook.react.devsupport.interfaces.TracingState -import com.facebook.react.devsupport.interfaces.TracingStateProvider +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateProvider import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTarget import com.facebook.react.interfaces.TaskInterface @@ -81,6 +81,6 @@ internal class ReactHostImplDevHelper(private val delegate: ReactHostImpl) : delegate.loadBundle(bundleLoader) override fun getTracingState(): TracingState { - return delegate.reactHostInspectorTarget?.getTracingState() ?: TracingState.ENABLEDINCDPMODE + return delegate.reactHostInspectorTarget?.getTracingState() ?: TracingState.ENABLED_IN_CDP_MODE } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt index 07397b85cf0..68fb8a8945d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostInspectorTarget.kt @@ -12,7 +12,9 @@ import com.facebook.proguard.annotations.DoNotStripAny import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI -import com.facebook.react.devsupport.interfaces.TracingState +import com.facebook.react.devsupport.inspector.FrameTimingSequence +import com.facebook.react.devsupport.inspector.TracingState +import com.facebook.react.devsupport.inspector.TracingStateListener import com.facebook.react.devsupport.perfmonitor.PerfMonitorInspectorTarget import com.facebook.react.devsupport.perfmonitor.PerfMonitorUpdateListener import com.facebook.soloader.SoLoader @@ -41,11 +43,13 @@ internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) : external fun stopAndDiscardBackgroundTrace() - external fun tracingStateAsInt(): Int + external override fun getTracingState(): TracingState - override fun getTracingState(): TracingState { - return TracingState.entries[tracingStateAsInt()] - } + external fun registerTracingStateListener(listener: TracingStateListener): Long + + external fun unregisterTracingStateListener(subscriptionId: Long) + + external fun recordFrameTimings(frameTimingSequence: FrameTimingSequence) override fun addPerfMonitorListener(listener: PerfMonitorUpdateListener) { perfMonitorListeners.add(listener) @@ -63,7 +67,7 @@ internal class ReactHostInspectorTarget(reactHostImpl: ReactHostImpl) : override fun resumeBackgroundTrace() { startBackgroundTrace() perfMonitorListeners.forEach { listener -> - listener.onRecordingStateChanged(TracingState.ENABLEDINBACKGROUNDMODE) + listener.onRecordingStateChanged(TracingState.ENABLED_IN_BACKGROUND_MODE) } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp index dbc58c41a91..95cf9636f43 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp @@ -11,6 +11,12 @@ namespace facebook::react::jsinspector_modern { +bool JInspectorFlags::getScreenshotCaptureEnabled( + jni::alias_ref /*unused*/) { + auto& inspectorFlags = InspectorFlags::getInstance(); + return inspectorFlags.getScreenshotCaptureEnabled(); +} + bool JInspectorFlags::getFuseboxEnabled(jni::alias_ref /*unused*/) { auto& inspectorFlags = InspectorFlags::getInstance(); return inspectorFlags.getFuseboxEnabled(); @@ -21,7 +27,18 @@ bool JInspectorFlags::getIsProfilingBuild(jni::alias_ref /*unused*/) { return inspectorFlags.getIsProfilingBuild(); } +bool JInspectorFlags::getFrameRecordingEnabled( + jni::alias_ref /*unused*/) { + auto& inspectorFlags = InspectorFlags::getInstance(); + return inspectorFlags.getFrameRecordingEnabled(); +} + void JInspectorFlags::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod( + "getScreenshotCaptureEnabled", + JInspectorFlags::getScreenshotCaptureEnabled), + }); javaClassLocal()->registerNatives({ makeNativeMethod("getFuseboxEnabled", JInspectorFlags::getFuseboxEnabled), }); @@ -29,6 +46,11 @@ void JInspectorFlags::registerNatives() { makeNativeMethod( "getIsProfilingBuild", JInspectorFlags::getIsProfilingBuild), }); + javaClassLocal()->registerNatives({ + makeNativeMethod( + "getFrameRecordingEnabled", + JInspectorFlags::getFrameRecordingEnabled), + }); } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h index 4541665e2ca..1056132c7c6 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h @@ -18,8 +18,10 @@ class JInspectorFlags : public jni::JavaClass { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/InspectorFlags;"; + static bool getScreenshotCaptureEnabled(jni::alias_ref /*unused*/); static bool getFuseboxEnabled(jni::alias_ref /*unused*/); static bool getIsProfilingBuild(jni::alias_ref /*unused*/); + static bool getFrameRecordingEnabled(jni::alias_ref /*unused*/); static void registerNatives(); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index ec2463f855d..c13b3e0a783 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -381,12 +381,24 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fuseboxFrameRecordingEnabled() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxFrameRecordingEnabled"); + return method(javaProvider_); + } + bool fuseboxNetworkInspectionEnabled() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxNetworkInspectionEnabled"); return method(javaProvider_); } + bool fuseboxScreenshotCaptureEnabled() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fuseboxScreenshotCaptureEnabled"); + return method(javaProvider_); + } + bool hideOffscreenVirtualViewsOnIOS() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("hideOffscreenVirtualViewsOnIOS"); @@ -838,11 +850,21 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxEnabledRelease( return ReactNativeFeatureFlags::fuseboxEnabledRelease(); } +bool JReactNativeFeatureFlagsCxxInterop::fuseboxFrameRecordingEnabled( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fuseboxFrameRecordingEnabled(); +} + bool JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +bool JReactNativeFeatureFlagsCxxInterop::fuseboxScreenshotCaptureEnabled( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(); +} + bool JReactNativeFeatureFlagsCxxInterop::hideOffscreenVirtualViewsOnIOS( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS(); @@ -1180,9 +1202,15 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fuseboxEnabledRelease", JReactNativeFeatureFlagsCxxInterop::fuseboxEnabledRelease), + makeNativeMethod( + "fuseboxFrameRecordingEnabled", + JReactNativeFeatureFlagsCxxInterop::fuseboxFrameRecordingEnabled), makeNativeMethod( "fuseboxNetworkInspectionEnabled", JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled), + makeNativeMethod( + "fuseboxScreenshotCaptureEnabled", + JReactNativeFeatureFlagsCxxInterop::fuseboxScreenshotCaptureEnabled), makeNativeMethod( "hideOffscreenVirtualViewsOnIOS", JReactNativeFeatureFlagsCxxInterop::hideOffscreenVirtualViewsOnIOS), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index dd31d2416db..6764c07f6ac 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9853cf162b179249b2c6af120a662cf3>> + * @generated SignedSource<<554bd02cd328ba462d4e703b653d6cdd>> */ /** @@ -201,9 +201,15 @@ class JReactNativeFeatureFlagsCxxInterop static bool fuseboxEnabledRelease( facebook::jni::alias_ref); + static bool fuseboxFrameRecordingEnabled( + facebook::jni::alias_ref); + static bool fuseboxNetworkInspectionEnabled( facebook::jni::alias_ref); + static bool fuseboxScreenshotCaptureEnabled( + facebook::jni::alias_ref); + static bool hideOffscreenVirtualViewsOnIOS( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp index 4ad1f7bd0c8..eeafc2085f5 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.cpp @@ -16,6 +16,36 @@ using namespace facebook::jni; using namespace facebook::react::jsinspector_modern; namespace facebook::react { + +namespace { +jni::local_ref convertCPPTracingStateToJava( + TracingState tracingState) { + auto tracingStateClass = jni::findClassLocal( + "com/facebook/react/devsupport/inspector/TracingState"); + auto valueOfMethod = + tracingStateClass->getStaticMethod("valueOf"); + + switch (tracingState) { + case TracingState::Disabled: + return valueOfMethod( + tracingStateClass, jni::make_jstring("DISABLED").get()); + + case TracingState::EnabledInBackgroundMode: + return valueOfMethod( + tracingStateClass, + jni::make_jstring("ENABLED_IN_BACKGROUND_MODE").get()); + + case TracingState::EnabledInCDPMode: + return valueOfMethod( + tracingStateClass, jni::make_jstring("ENABLED_IN_CDP_MODE").get()); + + default: + jni::throwNewJavaException( + "java/lang/IllegalStateException", "Unexpected new TracingState."); + } +} +} // namespace + JReactHostInspectorTarget::JReactHostInspectorTarget( alias_ref jobj, alias_ref reactHostImpl, @@ -29,7 +59,8 @@ JReactHostInspectorTarget::JReactHostInspectorTarget( std::function&& callback) mutable { auto jrunnable = JNativeRunnable::newObjectCxxArgs(std::move(callback)); javaExecutor->execute(jrunnable); - }) { + }), + tracingDelegate_(std::make_unique()) { auto& inspectorFlags = InspectorFlags::getInstance(); if (inspectorFlags.getFuseboxEnabled()) { inspectorTarget_ = HostTarget::create(*this, inspectorExecutor_); @@ -140,13 +171,34 @@ void JReactHostInspectorTarget::loadNetworkResource( } } +std::optional JReactHostInspectorTarget::captureScreenshot( + const jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest& + request) { + if (auto javaReactHostImplStrong = javaReactHostImpl_->get()) { + std::string format = request.format.value_or("png"); + int quality = request.quality.value_or(-1); + auto result = javaReactHostImplStrong->captureScreenshot(format, quality); + if (result) { + return result->toStdString(); + } + } + return std::nullopt; +} + HostTarget* JReactHostInspectorTarget::getInspectorTarget() { return inspectorTarget_ ? inspectorTarget_.get() : nullptr; } bool JReactHostInspectorTarget::startBackgroundTrace() { if (inspectorTarget_) { - return inspectorTarget_->startTracing(tracing::Mode::Background); + return inspectorTarget_->startTracing( + tracing::Mode::Background, + { + tracing::Category::HiddenTimeline, + tracing::Category::RuntimeExecution, + tracing::Category::Timeline, + tracing::Category::UserTiming, + }); } else { jni::throwNewJavaException( "java/lang/IllegalStateException", @@ -154,7 +206,7 @@ bool JReactHostInspectorTarget::startBackgroundTrace() { } } -tracing::TraceRecordingState JReactHostInspectorTarget::stopTracing() { +tracing::HostTracingProfile JReactHostInspectorTarget::stopTracing() { if (inspectorTarget_) { return inspectorTarget_->stopTracing(); } else { @@ -167,12 +219,12 @@ tracing::TraceRecordingState JReactHostInspectorTarget::stopTracing() { jboolean JReactHostInspectorTarget::stopAndMaybeEmitBackgroundTrace() { auto capturedTrace = inspectorTarget_->stopTracing(); if (inspectorTarget_->hasActiveSessionWithFuseboxClient()) { - inspectorTarget_->emitTraceRecordingForFirstFuseboxClient( + inspectorTarget_->emitTracingProfileForFirstFuseboxClient( std::move(capturedTrace)); return jboolean(true); } - stashTraceRecordingState(std::move(capturedTrace)); + stashTracingProfile(std::move(capturedTrace)); return jboolean(false); } @@ -180,16 +232,16 @@ void JReactHostInspectorTarget::stopAndDiscardBackgroundTrace() { inspectorTarget_->stopTracing(); } -void JReactHostInspectorTarget::stashTraceRecordingState( - tracing::TraceRecordingState&& state) { - stashedTraceRecordingState_ = std::move(state); +void JReactHostInspectorTarget::stashTracingProfile( + tracing::HostTracingProfile&& hostTracingProfile) { + stashedTracingProfile_ = std::move(hostTracingProfile); } -std::optional JReactHostInspectorTarget:: - unstable_getTraceRecordingThatWillBeEmittedOnInitialization() { - auto state = std::move(stashedTraceRecordingState_); - stashedTraceRecordingState_.reset(); - return state; +std::optional JReactHostInspectorTarget:: + unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() { + auto tracingProfile = std::move(stashedTracingProfile_); + stashedTracingProfile_.reset(); + return tracingProfile; } void JReactHostInspectorTarget::registerNatives() { @@ -208,13 +260,140 @@ void JReactHostInspectorTarget::registerNatives() { "stopAndDiscardBackgroundTrace", JReactHostInspectorTarget::stopAndDiscardBackgroundTrace), makeNativeMethod( - "tracingStateAsInt", JReactHostInspectorTarget::tracingState), + "getTracingState", JReactHostInspectorTarget::getTracingState), + makeNativeMethod( + "registerTracingStateListener", + JReactHostInspectorTarget::registerTracingStateListener), + makeNativeMethod( + "unregisterTracingStateListener", + JReactHostInspectorTarget::unregisterTracingStateListener), + makeNativeMethod( + "recordFrameTimings", JReactHostInspectorTarget::recordFrameTimings), + }); +} + +jni::local_ref +JReactHostInspectorTarget::getTracingState() { + return convertCPPTracingStateToJava(tracingDelegate_->getTracingState()); +} + +jlong JReactHostInspectorTarget::registerTracingStateListener( + jni::alias_ref listener) { + auto cppListener = [globalRef = make_global(listener)]( + TracingState state, bool screenshotsEnabled) { + static auto method = + globalRef->getClass() + ->getMethod, jboolean)>( + "onStateChanged"); + + method( + globalRef, + convertCPPTracingStateToJava(state), + static_cast(screenshotsEnabled)); + }; + + return static_cast( + tracingDelegate_->registerTracingStateListener(std::move(cppListener))); +} + +void JReactHostInspectorTarget::unregisterTracingStateListener( + jlong subscriptionId) { + tracingDelegate_->unregisterTracingStateListener(subscriptionId); +} + +HostTargetTracingDelegate* JReactHostInspectorTarget::getTracingDelegate() { + return tracingDelegate_.get(); +} + +void JReactHostInspectorTarget::recordFrameTimings( + jni::alias_ref frameTimingSequence) { + inspectorTarget_->recordFrameTimings({ + frameTimingSequence->getId(), + frameTimingSequence->getThreadId(), + frameTimingSequence->getBeginTimestamp(), + frameTimingSequence->getEndTimestamp(), + frameTimingSequence->getScreenshot(), }); } -jint JReactHostInspectorTarget::tracingState() { - auto state = inspectorTarget_->tracingState(); - return static_cast(state); +void TracingDelegate::onTracingStarted( + tracing::Mode tracingMode, + bool screenshotsCategoryEnabled) { + TracingState nextState = TracingState::Disabled; + switch (tracingMode) { + case tracing::Mode::CDP: + nextState = TracingState::EnabledInCDPMode; + break; + case tracing::Mode::Background: + nextState = TracingState::EnabledInBackgroundMode; + break; + default: + throw std::logic_error("Unexpected new Tracing Mode"); + } + + std::vector listeners; + { + std::lock_guard lock(mutex_); + + tracingState_ = nextState; + listeners = copySubscribedListeners(); + } + + notifyListeners(listeners, nextState, screenshotsCategoryEnabled); +} + +void TracingDelegate::onTracingStopped() { + std::vector listeners; + { + std::lock_guard lock(mutex_); + + tracingState_ = TracingState::Disabled; + listeners = copySubscribedListeners(); + } + + notifyListeners(listeners, TracingState::Disabled, false); +} + +TracingState TracingDelegate::getTracingState() { + std::lock_guard lock(mutex_); + + return tracingState_; +} + +size_t TracingDelegate::registerTracingStateListener( + TracingStateListener listener) { + std::lock_guard lock(mutex_); + + auto id = nextSubscriptionId_++; + subscriptions_[id] = std::move(listener); + return id; +} + +void TracingDelegate::unregisterTracingStateListener(size_t subscriptionId) { + std::lock_guard lock(mutex_); + + subscriptions_.erase(subscriptionId); +} + +std::vector TracingDelegate::copySubscribedListeners() { + std::vector listeners; + listeners.reserve(subscriptions_.size()); + + for (auto& [_, listener] : subscriptions_) { + listeners.push_back(listener); + } + + return listeners; +} + +void TracingDelegate::notifyListeners( + const std::vector& listeners, + TracingState state, + bool screenshotsCategoryEnabled) { + for (const auto& listener : listeners) { + listener(state, screenshotsCategoryEnabled); + } } } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h index 1807509a460..6b0beae1e86 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h @@ -7,11 +7,17 @@ #pragma once +#include #include + #include #include #include + +#include +#include #include +#include namespace facebook::react { @@ -20,7 +26,56 @@ struct JTaskInterface : public jni::JavaClass { }; struct JTracingState : public jni::JavaClass { - static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/TracingState;"; + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/TracingState;"; +}; + +struct JTracingStateListener : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/TracingStateListener;"; +}; + +struct JFrameTimingSequence : public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/inspector/FrameTimingSequence;"; + + uint64_t getId() const + { + auto field = javaClassStatic()->getField("id"); + return static_cast(getFieldValue(field)); + } + + uint64_t getThreadId() const + { + auto field = javaClassStatic()->getField("threadId"); + return static_cast(getFieldValue(field)); + } + + HighResTimeStamp getBeginTimestamp() const + { + auto field = javaClassStatic()->getField("beginTimestamp"); + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); + } + + HighResTimeStamp getEndTimestamp() const + { + auto field = javaClassStatic()->getField("endTimestamp"); + return HighResTimeStamp::fromChronoSteadyClockTimePoint( + std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); + } + + std::optional> getScreenshot() const + { + auto field = javaClassStatic()->getField("screenshot"); + auto javaScreenshot = getFieldValue(field); + if (javaScreenshot) { + auto size = static_cast(javaScreenshot->size()); + if (size > 0) { + std::vector result(size); + javaScreenshot->getRegion(0, javaScreenshot->size(), reinterpret_cast(result.data())); + return result; + } + } + return std::nullopt; + } }; struct JReactHostImpl : public jni::JavaClass { @@ -53,6 +108,78 @@ struct JReactHostImpl : public jni::JavaClass { "loadNetworkResource"); return method(self(), jni::make_jstring(url), listener); } + + jni::local_ref captureScreenshot(const std::string &format, int quality) const + { + auto method = javaClassStatic()->getMethod(jni::local_ref, jint)>( + "captureScreenshot"); + return method(self(), jni::make_jstring(format), static_cast(quality)); + } +}; + +enum class TracingState { + Disabled, + EnabledInBackgroundMode, + EnabledInCDPMode, +}; + +/** + * A callback that will be invoked when tracing state has changed. + */ +using TracingStateListener = std::function; + +class TracingDelegate : public jsinspector_modern::HostTargetTracingDelegate { + public: + void onTracingStarted(jsinspector_modern::tracing::Mode tracingMode, bool screenshotsCategoryEnabled) override; + void onTracingStopped() override; + + /** + * A synchronous way to get the current tracing state. + * Could be called from any thread. + */ + TracingState getTracingState(); + /** + * Register a listener that will be notified when the tracing state changes. + * Could be called from any thread. + */ + size_t registerTracingStateListener(TracingStateListener listener); + /** + * Unregister previously registered listener with the id returned from + * TracingDelegate::registerTracingStateListener(). + */ + void unregisterTracingStateListener(size_t subscriptionId); + + private: + /** + * Covers read / write operations on tracingState_ and subscriptions_. + */ + std::mutex mutex_; + /** + * Since HostInspectorTarget creates HostTarget, the default value is Disabled. + * However, the TracingDelegate is subscribed at the construction of HostTarget, so it will be notified as early as + * possible. + */ + TracingState tracingState_ = TracingState::Disabled; + /** + * Map of subscription ID to listener. + */ + std::unordered_map subscriptions_; + /** + * A counter for generating unique subscription IDs. + */ + uint64_t nextSubscriptionId_ = 0; + /** + * Returns a collection of listeners that are subscribed at the time of the call. + * Expected to be only called with mutex_ locked. + */ + std::vector copySubscribedListeners(); + /** + * Notifies specified listeners about the state change. + */ + void notifyListeners( + const std::vector &listeners, + TracingState state, + bool screenshotsCategoryEnabled); }; class JReactHostInspectorTarget : public jni::HybridClass, @@ -70,14 +197,6 @@ class JReactHostInspectorTarget : public jni::HybridClass getTracingState(); + + /** + * Register a listener that will be notified when the tracing state changes. + * Could be called from any thread. + * + * \return A unique subscription ID to use for unregistering the listener. + */ + jlong registerTracingStateListener(jni::alias_ref listener); + + /** + * Unregister a previously registered tracing state listener. + * + * \param subscriptionId The subscription ID returned from JReactHostInspectorTarget::registerTracingStateListener. + */ + void unregisterTracingStateListener(jlong subscriptionId); + + /** + * Propagate frame timings information to the Inspector's Tracing subsystem. + */ + void recordFrameTimings(jni::alias_ref frameTimingSequence); + // HostTargetDelegate methods jsinspector_modern::HostTargetMetadata getMetadata() override; void onReload(const PageReloadRequest &request) override; @@ -107,8 +251,11 @@ class JReactHostInspectorTarget : public jni::HybridClass executor) override; - std::optional - unstable_getTraceRecordingThatWillBeEmittedOnInitialization() override; + std::optional captureScreenshot( + const jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest &request) override; + std::optional + unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() override; + jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override; private: JReactHostInspectorTarget( @@ -126,20 +273,23 @@ class JReactHostInspectorTarget : public jni::HybridClass inspectorPageId_; /** - * Stops previously started trace recording and returns the captured trace. + * Stops previously started trace recording and returns the captured HostTracingProfile. */ - jsinspector_modern::tracing::TraceRecordingState stopTracing(); + jsinspector_modern::tracing::HostTracingProfile stopTracing(); /** - * Stashes previously recorded trace recording state that will be emitted when + * Stashes previously recorded HostTracingProfile that will be emitted when * CDP session is created. Once emitted, the value will be cleared from this * instance. */ - void stashTraceRecordingState(jsinspector_modern::tracing::TraceRecordingState &&state); + void stashTracingProfile(jsinspector_modern::tracing::HostTracingProfile &&hostTracingProfile); + /** + * Previously recorded HostTracingProfile that will be emitted when CDP session is created. + */ + std::optional stashedTracingProfile_; /** - * Previously recorded trace recording state that will be emitted when - * CDP session is created. + * Encapsulates the logic around tracing for this HostInspectorTarget. */ - std::optional stashedTraceRecordingState_; + std::unique_ptr tracingDelegate_; friend HybridBase; }; diff --git a/packages/react-native/ReactAndroid/src/main/jni/third-party/folly/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/third-party/folly/CMakeLists.txt index f86871b32f6..1ebf1f1b6f0 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/third-party/folly/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/third-party/folly/CMakeLists.txt @@ -43,6 +43,7 @@ SET(folly_runtime_SRC folly/json/json_pointer.cpp folly/json/json.cpp folly/lang/CString.cpp + folly/lang/Exception.cpp folly/lang/SafeAssert.cpp folly/lang/ToAscii.cpp folly/memory/detail/MallocImpl.cpp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 95b86ad8cfb..d535b7496d1 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -198,6 +198,42 @@ class HostAgent::Impl final { .shouldSendOKResponse = true, }; } + if (InspectorFlags::getInstance().getScreenshotCaptureEnabled()) { + if (req.method == "Page.captureScreenshot") { + std::optional format; + std::optional quality; + + if (req.params.isObject()) { + if (req.params.count("format") != 0u) { + format = req.params.at("format").asString(); + } + if (req.params.count("quality") != 0u) { + quality = static_cast(req.params.at("quality").asInt()); + } + } + + auto base64Data = targetController_.getDelegate().captureScreenshot( + {.format = format, .quality = quality}); + + if (base64Data.has_value()) { + frontendChannel_( + cdp::jsonResult( + req.id, + folly::dynamic::object("data", std::move(*base64Data)))); + } else { + frontendChannel_( + cdp::jsonError( + req.id, + cdp::ErrorCode::InternalError, + "Failed to capture screenshot")); + } + + return { + .isFinishedHandlingRequest = true, + .shouldSendOKResponse = false, + }; + } + } if (req.method == "Overlay.setPausedInDebuggerMessage") { auto message = req.params.isObject() && (req.params.count("message") != 0u) @@ -238,9 +274,9 @@ class HostAgent::Impl final { auto stashedTraceRecording = targetController_.getDelegate() - .unstable_getTraceRecordingThatWillBeEmittedOnInitialization(); + .unstable_getHostTracingProfileThatWillBeEmittedOnInitialization(); if (stashedTraceRecording.has_value()) { - tracingAgent_.emitExternalTraceRecording( + tracingAgent_.emitExternalHostTracingProfile( std::move(stashedTraceRecording.value())); } @@ -385,12 +421,12 @@ class HostAgent::Impl final { return fuseboxClientType_ == FuseboxClientType::Fusebox; } - void emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { + void emitExternalTracingProfile( + tracing::HostTracingProfile tracingProfile) const { assert( hasFuseboxClientConnected() && "Attempted to emit a trace recording to a non-Fusebox client"); - tracingAgent_.emitExternalTraceRecording(std::move(traceRecording)); + tracingAgent_.emitExternalHostTracingProfile(std::move(tracingProfile)); } void emitSystemStateChanged(bool isSingleHost) { @@ -506,8 +542,7 @@ class HostAgent::Impl final { bool hasFuseboxClientConnected() const { return false; } - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) { - } + void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) {} void emitSystemStateChanged(bool isSingleHost) {} }; @@ -543,9 +578,9 @@ bool HostAgent::hasFuseboxClientConnected() const { return impl_->hasFuseboxClientConnected(); } -void HostAgent::emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { - impl_->emitExternalTraceRecording(std::move(traceRecording)); +void HostAgent::emitExternalTracingProfile( + tracing::HostTracingProfile tracingProfile) const { + impl_->emitExternalTracingProfile(std::move(tracingProfile)); } void HostAgent::emitSystemStateChanged(bool isSingleHost) const { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h index ab447881656..8789cb5d32c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h @@ -74,10 +74,10 @@ class HostAgent final { bool hasFuseboxClientConnected() const; /** - * Emits the trace recording that was captured externally, not via the + * Emits the HostTracingProfile that was captured externally, not via the * CDP-initiated request. */ - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitExternalTracingProfile(tracing::HostTracingProfile tracingProfile) const; /** * Emits a system state changed event when the number of ReactHost instances diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 6639614b822..26c7e1ca849 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -111,8 +111,9 @@ class HostTargetSession { return hostAgent_.hasFuseboxClientConnected(); } - void emitTraceRecording(tracing::TraceRecordingState traceRecording) const { - hostAgent_.emitExternalTraceRecording(std::move(traceRecording)); + void emitHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { + hostAgent_.emitExternalTracingProfile(std::move(tracingProfile)); } private: @@ -328,6 +329,7 @@ namespace { struct StaticHostTargetMetadata { std::optional isProfilingBuild; std::optional networkInspectionEnabled; + std::optional frameRecordingEnabled; }; StaticHostTargetMetadata getStaticHostMetadata() { @@ -335,7 +337,8 @@ StaticHostTargetMetadata getStaticHostMetadata() { return { .isProfilingBuild = inspectorFlags.getIsProfilingBuild(), - .networkInspectionEnabled = inspectorFlags.getNetworkInspectionEnabled()}; + .networkInspectionEnabled = inspectorFlags.getNetworkInspectionEnabled(), + .frameRecordingEnabled = inspectorFlags.getFrameRecordingEnabled()}; } } // namespace @@ -370,6 +373,10 @@ folly::dynamic createHostMetadataPayload(const HostTargetMetadata& metadata) { result["unstable_networkInspectionEnabled"] = staticMetadata.networkInspectionEnabled.value(); } + if (staticMetadata.frameRecordingEnabled) { + result["unstable_frameRecordingEnabled"] = + staticMetadata.frameRecordingEnabled.value(); + } return result; } @@ -382,13 +389,13 @@ bool HostTarget::hasActiveSessionWithFuseboxClient() const { return hasActiveFuseboxSession; } -void HostTarget::emitTraceRecordingForFirstFuseboxClient( - tracing::TraceRecordingState traceRecording) const { +void HostTarget::emitTracingProfileForFirstFuseboxClient( + tracing::HostTracingProfile tracingProfile) const { bool emitted = false; sessions_.forEach([&](HostTargetSession& session) { if (emitted) { /** - * TraceRecordingState object is not copiable for performance reasons, + * HostTracingProfile object is not copiable for performance reasons, * because it could contain large Runtime sampling profile object. * * This approach would not work with multi-client debugger setup. @@ -396,7 +403,7 @@ void HostTarget::emitTraceRecordingForFirstFuseboxClient( return; } if (session.hasFuseboxClient()) { - session.emitTraceRecording(std::move(traceRecording)); + session.emitHostTracingProfile(std::move(tracingProfile)); emitted = true; } }); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index c64413b6f35..798fc6d8dc0 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -16,11 +16,16 @@ #include "ScopedExecutor.h" #include "WeakList.h" +#include #include +#include #include +#include +#include +#include +#include #include -#include #ifndef JSINSPECTOR_EXPORT #ifdef _MSC_VER @@ -53,6 +58,36 @@ struct HostTargetMetadata { std::optional reactNativeVersion{}; }; +/** + * Receives any performance-related events from a HostTarget: could be Tracing, Performance Monitor, etc. + */ +class HostTargetTracingDelegate { + public: + HostTargetTracingDelegate() = default; + virtual ~HostTargetTracingDelegate() = default; + + /** + * Fired when the corresponding HostTarget started recording a tracing session. + * The tracing state is expected to be initialized at this point and the delegate should be able to record events + * through HostTarget. + */ + virtual void onTracingStarted(tracing::Mode /* tracingMode */, bool /* screenshotsCategoryEnabled */) {} + + /** + * Fired when the corresponding HostTarget is about to end recording a tracing session. + * The tracing state is expected to be still initialized during the call and the delegate should be able to record + * events through HostTarget. + * + * Any attempts to record events after this callback is finished will fail. + */ + virtual void onTracingStopped() {} + + HostTargetTracingDelegate(const HostTargetTracingDelegate &) = delete; + HostTargetTracingDelegate(HostTargetTracingDelegate &&) = delete; + HostTargetTracingDelegate &operator=(const HostTargetTracingDelegate &) = delete; + HostTargetTracingDelegate &operator=(HostTargetTracingDelegate &&) = delete; +}; + /** * Receives events from a HostTarget. This is a shared interface that each * React Native platform needs to implement in order to integrate with the @@ -100,6 +135,19 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { } }; + struct PageCaptureScreenshotRequest { + /** + * Image compression format. Defaults to "png". + * Allowed values: "jpeg", "png", "webp". + */ + std::optional format; + + /** + * Compression quality from range [0..100] (jpeg only). + */ + std::optional quality; + }; + virtual ~HostTargetDelegate() override; /** @@ -147,18 +195,37 @@ class HostTargetDelegate : public LoadNetworkResourceDelegate { "LoadNetworkResourceDelegate.loadNetworkResource is not implemented by this host target delegate."); } + /** + * Called when the debugger requests a screenshot of the current page via + * @cdp Page.captureScreenshot. The delegate should capture the current + * view, encode it to the requested format, and return base64-encoded + * image data. Return std::nullopt on failure. + */ + virtual std::optional captureScreenshot(const PageCaptureScreenshotRequest & /*request*/) + { + return std::nullopt; + } + /** * [Experimental] Will be called at the CDP session initialization to get the * trace recording that may have been stashed by the Host from the previous * background session. * - * \return the trace recording state if there is one that needs to be + * \return the HostTracingProfile if there is one that needs to be * displayed, otherwise std::nullopt. */ - virtual std::optional unstable_getTraceRecordingThatWillBeEmittedOnInitialization() + virtual std::optional unstable_getHostTracingProfileThatWillBeEmittedOnInitialization() { return std::nullopt; } + + /** + * An optional delegate that will be used by HostTarget to notify about tracing-related events. + */ + virtual HostTargetTracingDelegate *getTracingDelegate() + { + return nullptr; + } }; /** @@ -203,14 +270,16 @@ class HostTargetController final { * Starts trace recording for this HostTarget. * * \param mode In which mode to start the trace recording. + * \param enabledCategories The set of categories to enable. + * * \return false if already tracing, true otherwise. */ - bool startTracing(tracing::Mode mode); + bool startTracing(tracing::Mode mode, std::set enabledCategories); /** * Stops previously started trace recording. */ - tracing::TraceRecordingState stopTracing(); + tracing::HostTracingProfile stopTracing(); private: HostTarget &target_; @@ -226,12 +295,15 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis public: /** * Constructs a new HostTarget. + * * \param delegate The HostTargetDelegate that will * receive events from this HostTarget. The caller is responsible for ensuring * that the HostTargetDelegate outlives this object. + * * \param executor An executor that may be used to call methods on this * HostTarget while it exists. \c create additionally guarantees that the * executor will not be called after the HostTarget is destroyed. + * * \note Copies of the provided executor may be destroyed on arbitrary * threads, including after the HostTarget is destroyed. Callers must ensure * that such destructor calls are safe - e.g. if using a lambda as the @@ -279,6 +351,7 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis */ void sendCommand(HostCommand command); +#pragma region Tracing /** * Creates a new HostTracingAgent. * This Agent is not owned by the HostTarget. The Agent will be destroyed at @@ -292,19 +365,16 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis * Starts trace recording for this HostTarget. * * \param mode In which mode to start the trace recording. + * \param enabledCategories The set of categories to enable. + * * \return false if already tracing, true otherwise. */ - bool startTracing(tracing::Mode mode); + bool startTracing(tracing::Mode mode, std::set enabledCategories); /** * Stops previously started trace recording. */ - tracing::TraceRecordingState stopTracing(); - - /** - * Returns the state of the background trace, running, stopped, or disabled - */ - tracing::TracingState tracingState() const; + tracing::HostTracingProfile stopTracing(); /** * Returns whether there is an active session with the Fusebox client, i.e. @@ -313,22 +383,30 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis bool hasActiveSessionWithFuseboxClient() const; /** - * Emits the trace recording for the first active session with the Fusebox + * Emits the HostTracingProfile for the first active session with the Fusebox * client. * * @see \c hasActiveFrontendSession */ - void emitTraceRecordingForFirstFuseboxClient(tracing::TraceRecordingState traceRecording) const; + void emitTracingProfileForFirstFuseboxClient(tracing::HostTracingProfile tracingProfile) const; /** * Emits a system state changed event to all active sessions. */ void emitSystemStateChanged(bool isSingleHost) const; + /** + * An endpoint for the Host to report frame timings that will be recorded if and only if there is currently an active + * tracing session. + */ + void recordFrameTimings(tracing::FrameTimingSequence frameTimingSequence); +#pragma endregion + private: /** * Constructs a new HostTarget. * The caller must call setExecutor immediately afterwards. + * * \param delegate The HostTargetDelegate that will * receive events from this HostTarget. The caller is responsible for ensuring * that the HostTargetDelegate outlives this object. @@ -347,6 +425,7 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis std::unique_ptr perfMonitorUpdateHandler_; std::unique_ptr perfMetricsBinding_; +#pragma region Tracing /** * Current pending trace recording, which encapsulates the configuration of * the tracing session and the state. @@ -354,6 +433,14 @@ class JSINSPECTOR_EXPORT HostTarget : public EnableExecutorFromThis * Should only be allocated when there is an active tracing session. */ std::unique_ptr traceRecording_{nullptr}; + /** + * Protects the state inside traceRecording_. + * + * Calls to tracing subsystem could happen from different threads, depending on the mode (Background or CDP) and + * the method: the Host could report frame timings from any arbitrary thread. + */ + std::mutex tracingMutex_; +#pragma endregion inline HostTargetDelegate &getDelegate() { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp index b763cbdafd7..a6e2d5da63f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.cpp @@ -8,12 +8,26 @@ #include "HostTargetTraceRecording.h" #include "HostTarget.h" +#include + namespace facebook::react::jsinspector_modern { HostTargetTraceRecording::HostTargetTraceRecording( + HostTarget& hostTarget, tracing::Mode tracingMode, - HostTarget& hostTarget) - : tracingMode_(tracingMode), hostTarget_(hostTarget) {} + std::set enabledCategories, + std::optional windowSize) + : hostTarget_(hostTarget), + tracingMode_(tracingMode), + enabledCategories_(std::move(enabledCategories)), + windowSize_(windowSize) { + if (windowSize) { + frameTimings_ = tracing::TimeWindowedBuffer( + [](auto& sequence) { return sequence.beginTimestamp; }, *windowSize); + } else { + frameTimings_ = tracing::TimeWindowedBuffer(); + }; +} void HostTargetTraceRecording::setTracedInstance( InstanceTarget* instanceTarget) { @@ -29,14 +43,13 @@ void HostTargetTraceRecording::start() { hostTracingAgent_ == nullptr && "Tracing Agent for the HostTarget was already initialized."); - state_ = tracing::TraceRecordingState{ - .mode = tracingMode_, - .startTime = HighResTimeStamp::now(), - }; + startTime_ = HighResTimeStamp::now(); + state_ = tracing::TraceRecordingState( + tracingMode_, enabledCategories_, windowSize_); hostTracingAgent_ = hostTarget_.createTracingAgent(*state_); } -tracing::TraceRecordingState HostTargetTraceRecording::stop() { +tracing::HostTracingProfile HostTargetTraceRecording::stop() { assert( hostTracingAgent_ != nullptr && "TracingAgent for the HostTarget has not been initialized."); @@ -48,7 +61,25 @@ tracing::TraceRecordingState HostTargetTraceRecording::stop() { auto state = std::move(*state_); state_.reset(); - return state; + auto startTime = *startTime_; + startTime_.reset(); + + return tracing::HostTracingProfile{ + .processId = oscompat::getCurrentProcessId(), + .startTime = startTime, + .frameTimings = frameTimings_.pruneExpiredAndExtract(), + .instanceTracingProfiles = std::move(state.instanceTracingProfiles), + .runtimeSamplingProfiles = std::move(state.runtimeSamplingProfiles), + }; +} + +void HostTargetTraceRecording::recordFrameTimings( + tracing::FrameTimingSequence frameTimingSequence) { + assert( + state_.has_value() && + "The state for this tracing session has not been initialized."); + + frameTimings_.push(frameTimingSequence); } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h index 18f42e027e8..193d9e2aa97 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTraceRecording.h @@ -11,9 +11,15 @@ #include "HostTarget.h" #include "InstanceTarget.h" +#include +#include +#include #include +#include +#include #include +#include namespace facebook::react::jsinspector_modern { @@ -28,7 +34,11 @@ namespace facebook::react::jsinspector_modern { */ class HostTargetTraceRecording { public: - explicit HostTargetTraceRecording(tracing::Mode tracingMode, HostTarget &hostTarget); + HostTargetTraceRecording( + HostTarget &hostTarget, + tracing::Mode tracingMode, + std::set enabledCategories, + std::optional windowSize = std::nullopt); inline bool isBackgroundInitiated() const { @@ -53,22 +63,34 @@ class HostTargetTraceRecording { void start(); /** - * Stops the recording and drops the recording state. + * Stops the recording and returns the recorded HostTracingProfile. * * Will deallocate all Tracing Agents. */ - tracing::TraceRecordingState stop(); + tracing::HostTracingProfile stop(); + + /** + * Adds the frame timing sequence to the current state of this trace recording. + * + * The caller guarantees the protection from data races. This is protected by the tracing mutex in HostTarget. + */ + void recordFrameTimings(tracing::FrameTimingSequence frameTimingSequence); private: + /** + * The Host for which this Trace Recording is going to happen. + */ + HostTarget &hostTarget_; + /** * The mode in which this trace recording was initialized. */ tracing::Mode tracingMode_; /** - * The Host for which this Trace Recording is going to happen. + * The timestamp at which this Trace Recording started. */ - HostTarget &hostTarget_; + std::optional startTime_; /** * The state of the current Trace Recording. @@ -81,6 +103,21 @@ class HostTargetTraceRecording { * Only allocated if the recording is enabled. */ std::shared_ptr hostTracingAgent_; + + /** + * The list of categories that are enabled for this recording. + */ + std::set enabledCategories_; + + /** + * The size of the time window for this recording. + */ + std::optional windowSize_; + + /** + * Frame timings captured on the Host side. + */ + tracing::TimeWindowedBuffer frameTimings_; }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp index 5eeea6cc2f1..7d41971122c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp @@ -5,18 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -#include - #include "HostTarget.h" #include "HostTargetTraceRecording.h" namespace facebook::react::jsinspector_modern { -bool HostTargetController::startTracing(tracing::Mode tracingMode) { - return target_.startTracing(tracingMode); +namespace { + +// The size of the timeline for the trace recording that happened in the +// background. +constexpr HighResDuration kBackgroundTraceWindowSize = + HighResDuration::fromMilliseconds(20000); + +} // namespace + +bool HostTargetController::startTracing( + tracing::Mode tracingMode, + std::set enabledCategories) { + return target_.startTracing(tracingMode, std::move(enabledCategories)); } -tracing::TraceRecordingState HostTargetController::stopTracing() { +tracing::HostTracingProfile HostTargetController::stopTracing() { return target_.stopTracing(); } @@ -27,45 +36,69 @@ std::shared_ptr HostTarget::createTracingAgent( return agent; } -bool HostTarget::startTracing(tracing::Mode tracingMode) { +bool HostTarget::startTracing( + tracing::Mode tracingMode, + std::set enabledCategories) { + std::lock_guard lock(tracingMutex_); + if (traceRecording_ != nullptr) { if (traceRecording_->isBackgroundInitiated() && tracingMode == tracing::Mode::CDP) { + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStopped(); + } + + traceRecording_->stop(); traceRecording_.reset(); } else { return false; } } - traceRecording_ = - std::make_unique(tracingMode, *this); + auto timeWindow = tracingMode == tracing::Mode::Background + ? std::make_optional(kBackgroundTraceWindowSize) + : std::nullopt; + auto screenshotsCategoryEnabled = + enabledCategories.contains(tracing::Category::Screenshot); + + traceRecording_ = std::make_unique( + *this, tracingMode, std::move(enabledCategories), timeWindow); traceRecording_->setTracedInstance(currentInstance_.get()); traceRecording_->start(); + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStarted(tracingMode, screenshotsCategoryEnabled); + } + return true; } -tracing::TraceRecordingState HostTarget::stopTracing() { +tracing::HostTracingProfile HostTarget::stopTracing() { + std::lock_guard lock(tracingMutex_); + assert(traceRecording_ != nullptr && "No tracing in progress"); - auto state = traceRecording_->stop(); + if (auto tracingDelegate = delegate_.getTracingDelegate()) { + tracingDelegate->onTracingStopped(); + } + + auto profile = traceRecording_->stop(); traceRecording_.reset(); - return state; + return profile; } -tracing::TracingState HostTarget::tracingState() const { - if (traceRecording_ == nullptr) { - return tracing::TracingState::Disabled; - } +void HostTarget::recordFrameTimings( + tracing::FrameTimingSequence frameTimingSequence) { + std::lock_guard lock(tracingMutex_); - if (traceRecording_->isBackgroundInitiated()) { - return tracing::TracingState::EnabledInBackgroundMode; + if (traceRecording_) { + traceRecording_->recordFrameTimings(std::move(frameTimingSequence)); + } else { + assert( + false && + "The HostTarget is not being profiled. Did you call recordFrameTimings() from the native Host side when there is no tracing in progress?"); } - - // This means we have a traceRecording_, but not running in the background. - // CDP initiated this trace so we should report as disabled. - return tracing::TracingState::EnabledInCDPMode; } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h new file mode 100644 index 00000000000..c007212445d --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h @@ -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 "InspectorInterfaces.h" + +#include "cdp/CdpJson.h" +#include "tracing/HostTracingProfile.h" +#include "tracing/HostTracingProfileSerializer.h" + +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Emits a captured HostTracingProfile in a series of + * Tracing.dataCollected events, followed by a Tracing.tracingComplete event, to zero or more + * FrontendChannels. If \p isBackgroundTrace is true, a ReactNativeApplication.traceRequested + * notification is sent to each FrontendChannel before the trace events are emitted. + */ +template +void emitNotificationsForTracingProfile( + tracing::HostTracingProfile &&hostTracingProfile, + const ChannelsRange &channels, + bool isBackgroundTrace) + requires std::ranges::range && + std::convertible_to, FrontendChannel> +{ + /** + * Maximum serialized byte size of a Trace Event chunk before it is flushed + * with a Tracing.dataCollected event. + */ + static constexpr size_t TRACE_EVENT_CHUNK_MAX_BYTES = 10 * 1024 * 1024; // 10 MiB + + /** + * The maximum number of ProfileChunk trace events + * that will be sent in a single CDP Tracing.dataCollected message. + */ + static constexpr uint16_t PROFILE_TRACE_EVENT_CHUNK_SIZE = 10; + + if (std::ranges::empty(channels)) { + return; + } + + if (isBackgroundTrace) { + for (auto &frontendChannel : channels) { + frontendChannel(cdp::jsonNotification("ReactNativeApplication.traceRequested")); + } + } + + // Serialize each chunk once and send it to all eligible sessions. + tracing::HostTracingProfileSerializer::emitAsDataCollectedChunks( + std::move(hostTracingProfile), + [&](folly::dynamic &&serializedChunk) { + for (auto &frontendChannel : channels) { + frontendChannel( + cdp::jsonNotification("Tracing.dataCollected", folly::dynamic::object("value", serializedChunk))); + } + }, + TRACE_EVENT_CHUNK_MAX_BYTES, + PROFILE_TRACE_EVENT_CHUNK_SIZE); + + for (auto &frontendChannel : channels) { + frontendChannel( + cdp::jsonNotification("Tracing.tracingComplete", folly::dynamic::object("dataLossOccurred", false))); + } +} + +/** + * Convenience overload of emitNotificationsForTracingProfile() for a single FrontendChannel. + */ +inline void emitNotificationsForTracingProfile( + tracing::HostTracingProfile &&hostTracingProfile, + const FrontendChannel &channel, + bool isBackgroundTrace) +{ + std::array channels{channel}; + emitNotificationsForTracingProfile(std::move(hostTracingProfile), channels, isBackgroundTrace); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp index 010813a2426..c2c31ee13d2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp @@ -21,6 +21,14 @@ bool InspectorFlags::getAssertSingleHostState() const { return loadFlagsAndAssertUnchanged().assertSingleHostState; } +bool InspectorFlags::getScreenshotCaptureEnabled() const { + return loadFlagsAndAssertUnchanged().screenshotCaptureEnabled; +} + +bool InspectorFlags::getFrameRecordingEnabled() const { + return loadFlagsAndAssertUnchanged().frameRecordingEnabled; +} + bool InspectorFlags::getFuseboxEnabled() const { if (fuseboxDisabledForTest_) { return false; @@ -54,6 +62,10 @@ const InspectorFlags::Values& InspectorFlags::loadFlagsAndAssertUnchanged() InspectorFlags::Values newValues = { .assertSingleHostState = ReactNativeFeatureFlags::fuseboxAssertSingleHostState(), + .screenshotCaptureEnabled = + ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(), + .frameRecordingEnabled = + ReactNativeFeatureFlags::fuseboxFrameRecordingEnabled(), .fuseboxEnabled = #if defined(REACT_NATIVE_DEBUGGER_ENABLED) true, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h index 5e245d482ee..f1f1353e5e9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.h @@ -36,6 +36,16 @@ class InspectorFlags { */ bool getIsProfilingBuild() const; + /** + * Flag determining if Page.captureScreenshot CDP method is enabled. + */ + bool getScreenshotCaptureEnabled() const; + + /** + * Flag determining if frame recording (timings + screenshots) is enabled. + */ + bool getFrameRecordingEnabled() const; + /** * Flag determining if network inspection is enabled. */ @@ -61,6 +71,8 @@ class InspectorFlags { private: struct Values { bool assertSingleHostState; + bool screenshotCaptureEnabled; + bool frameRecordingEnabled; bool fuseboxEnabled; bool isProfilingBuild; bool networkInspectionEnabled; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp index 065dd0e53c3..ab5a973f12c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.cpp @@ -143,13 +143,9 @@ int InspectorImpl::addPage( pageId, Page{pageId, description, vm, std::move(connectFunc), capabilities}); - // Strong assumption: If prefersFuseboxFrontend is set, the page added is a - // HostTarget and not a legacy Hermes runtime target. - if (capabilities.prefersFuseboxFrontend) { - for (const auto& listenerWeak : listeners_) { - if (auto listener = listenerWeak.lock()) { - listener->unstable_onHostTargetAdded(); - } + for (const auto& listenerWeak : listeners_) { + if (auto listener = listenerWeak.lock()) { + listener->unstable_onHostTargetAdded(); } } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp index d868160c07b..f1eae37bb82 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp @@ -15,15 +15,6 @@ namespace facebook::react::jsinspector_modern { -namespace { - -// The size of the timeline for the trace recording that happened in the -// background. -constexpr HighResDuration kBackgroundTracePerformanceTracerWindowSize = - HighResDuration::fromMilliseconds(20000); - -} // namespace - InstanceAgent::InstanceAgent( FrontendChannel frontendChannel, InstanceTarget& target, @@ -171,8 +162,8 @@ void InstanceAgent::maybeSendPendingConsoleMessages() { InstanceTracingAgent::InstanceTracingAgent(tracing::TraceRecordingState& state) : tracing::TargetTracingAgent(state) { auto& performanceTracer = tracing::PerformanceTracer::getInstance(); - if (state.mode == tracing::Mode::Background) { - performanceTracer.startTracing(kBackgroundTracePerformanceTracerWindowSize); + if (state.windowSize) { + performanceTracer.startTracing(*state.windowSize); } else { performanceTracer.startTracing(); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp index b914998e96c..53dcc0a3469 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp @@ -8,10 +8,10 @@ #include "NetworkIOAgent.h" #include "InspectorFlags.h" -#include "Base64.h" #include "Utf8.h" #include +#include #include #include diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp index b8cc1cc1ab1..2c8b2030a7a 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp @@ -7,10 +7,10 @@ #include "TracingAgent.h" +#include #include #include #include -#include #include namespace facebook::react::jsinspector_modern { @@ -74,8 +74,25 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) { return true; } - bool didNotHaveAlreadyRunningRecording = - hostTargetController_.startTracing(tracing::Mode::CDP); + /** + * This logic has to be updated with the next upgrade of Chrome + * DevTools Frotnend fork. + * + * At the moment of writing this, our fork uses categories field, which is + * marked as depreacted in CDP spec. + * + * Latest versions of Chrome DevTools in stable channel of Chromium are + * already using traceConfig field. + */ + std::set enabledCategories; + if (req.params.isObject() && req.params.count("categories") != 0 && + req.params["categories"].isString()) { + enabledCategories = tracing::parseSerializedTracingCategories( + req.params["categories"].getString()); + } + + bool didNotHaveAlreadyRunningRecording = hostTargetController_.startTracing( + tracing::Mode::CDP, std::move(enabledCategories)); if (!didNotHaveAlreadyRunningRecording) { frontendChannel_( cdp::jsonError( @@ -91,37 +108,36 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) { return true; } else if (req.method == "Tracing.end") { - // @cdp Tracing.end support is experimental. - auto state = hostTargetController_.stopTracing(); + auto tracingProfile = hostTargetController_.stopTracing(); sessionState_.hasPendingTraceRecording = false; // Send response to Tracing.end request. frontendChannel_(cdp::jsonResult(req.id)); - emitTraceRecording(std::move(state)); + emitHostTracingProfile(std::move(tracingProfile)); return true; } return false; } -void TracingAgent::emitExternalTraceRecording( - tracing::TraceRecordingState traceRecording) const { +void TracingAgent::emitExternalHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { frontendChannel_( cdp::jsonNotification("ReactNativeApplication.traceRequested")); - emitTraceRecording(std::move(traceRecording)); + emitHostTracingProfile(std::move(tracingProfile)); } -void TracingAgent::emitTraceRecording( - tracing::TraceRecordingState traceRecording) const { +void TracingAgent::emitHostTracingProfile( + tracing::HostTracingProfile tracingProfile) const { auto dataCollectedCallback = [this](folly::dynamic&& eventsChunk) { frontendChannel_( cdp::jsonNotification( "Tracing.dataCollected", folly::dynamic::object("value", std::move(eventsChunk)))); }; - tracing::TraceRecordingStateSerializer::emitAsDataCollectedChunks( - std::move(traceRecording), + tracing::HostTracingProfileSerializer::emitAsDataCollectedChunks( + std::move(tracingProfile), dataCollectedCallback, TRACE_EVENT_CHUNK_SIZE, PROFILE_TRACE_EVENT_CHUNK_SIZE); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h index 9a27dcbe718..d0f474f1234 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.h @@ -11,6 +11,7 @@ #include "InspectorInterfaces.h" #include +#include #include #include @@ -41,9 +42,9 @@ class TracingAgent { bool handleRequest(const cdp::PreparsedRequest &req); /** - * Emits the Trace Recording that was stashed externally by the HostTarget. + * Emits the HostTracingProfile that was stashed externally by the HostTarget. */ - void emitExternalTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitExternalHostTracingProfile(tracing::HostTracingProfile tracingProfile) const; private: /** @@ -56,10 +57,10 @@ class TracingAgent { HostTargetController &hostTargetController_; /** - * Emits the captured Trace Recording state in a series of + * Emits captured HostTracingProfile in a series of * Tracing.dataCollected events, followed by a Tracing.tracingComplete event. */ - void emitTraceRecording(tracing::TraceRecordingState traceRecording) const; + void emitHostTracingProfile(tracing::HostTracingProfile tracingProfile) const; }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp index 88fef17ce04..89eb1ed6acb 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp @@ -1526,4 +1526,69 @@ TEST_F(HostTargetTest, IOReadSizeValidation) { })"); } +TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnCDPRequest) { + connect(); + InSequence s; + + EXPECT_CALL( + hostTargetDelegate_.getTracingDelegateMock(), + onTracingStarted(Eq(tracing::Mode::CDP), Eq(false))) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 1, + "result": {} + })"))); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Tracing.start" + })"); + + EXPECT_CALL(hostTargetDelegate_.getTracingDelegateMock(), onTracingStopped()) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 1, + "result": {} + })"))); + EXPECT_CALL( + fromPage(), + onMessage(JsonParsed( + testing::AllOf( + AtJsonPtr("/method", "Tracing.tracingComplete"), + AtJsonPtr("/params/dataLossOccurred", false))))); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Tracing.end" + })"); +} + +TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnDirectTracingCall) { + connect(); + + EXPECT_CALL( + hostTargetDelegate_.getTracingDelegateMock(), + onTracingStarted(Eq(tracing::Mode::Background), Eq(false))) + .Times(1) + .RetiresOnSaturation(); + page_->startTracing(tracing::Mode::Background, {}); + + EXPECT_CALL(hostTargetDelegate_.getTracingDelegateMock(), onTracingStopped()) + .Times(1) + .RetiresOnSaturation(); + page_->stopTracing(); +} + +TEST_F(HostTargetProtocolTest, CaptureScreenshotNotSupportedWhenFlagDisabled) { + EXPECT_CALL( + fromPage(), + onMessage(JsonParsed(AllOf( + AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1)))))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Page.captureScreenshot" + })"); +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h index d83ed861884..b50582b771f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h @@ -87,13 +87,13 @@ class MockInspectorPackagerConnectionDelegate : public InspectorPackagerConnecti explicit MockInspectorPackagerConnectionDelegate(folly::Executor &executor) : executor_(executor) { using namespace testing; - ON_CALL(*this, scheduleCallback(_, _)).WillByDefault(Invoke<>([this](auto callback, auto delay) { + ON_CALL(*this, scheduleCallback(_, _)).WillByDefault([this](auto callback, auto delay) { if (auto scheduledExecutor = dynamic_cast(&executor_)) { scheduledExecutor->scheduleAt(callback, scheduledExecutor->now() + delay); } else { executor_.add(callback); } - })); + }); EXPECT_CALL(*this, scheduleCallback(_, _)).Times(AnyNumber()); } @@ -113,6 +113,12 @@ class MockInspectorPackagerConnectionDelegate : public InspectorPackagerConnecti folly::Executor &executor_; }; +class MockHostTargetTracingDelegate : public HostTargetTracingDelegate { + public: + MOCK_METHOD(void, onTracingStarted, (tracing::Mode tracingMode, bool screenshotsCategoryEnabled), (override)); + MOCK_METHOD(void, onTracingStopped, (), (override)); +}; + class MockHostTargetDelegate : public HostTargetDelegate { public: // HostTargetDelegate methods @@ -131,6 +137,21 @@ class MockHostTargetDelegate : public HostTargetDelegate { loadNetworkResource, (const LoadNetworkResourceRequest ¶ms, ScopedExecutor executor), (override)); + MOCK_METHOD(std::optional, captureScreenshot, (const PageCaptureScreenshotRequest &request), (override)); + + HostTargetTracingDelegate *getTracingDelegate() override + { + return mockTracingDelegate_.get(); + } + + MockHostTargetTracingDelegate &getTracingDelegateMock() + { + return *mockTracingDelegate_; + } + + private: + std::unique_ptr mockTracingDelegate_ = + std::make_unique(); }; class MockInstanceTargetDelegate : public InstanceTargetDelegate {}; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp index 3df2f0b2fce..c0f729723d3 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp @@ -358,6 +358,7 @@ TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationEnable) { "method": "ReactNativeApplication.metadataUpdated", "params": { "integrationName": "JsiIntegrationTest", + "unstable_frameRecordingEnabled": false, "unstable_isProfilingBuild": false, "unstable_networkInspectionEnabled": false } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp index 21ba36af0bd..00885fdd17c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp @@ -42,6 +42,7 @@ class NetworkReporterTestBase : public JsiIntegrationPortableTestBase< .enableNetworkEventReporting = WithParamInterface::GetParam() .enableNetworkEventReporting, + .networkInspectionEnabled = true, }) {} void SetUp() override { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp new file mode 100644 index 00000000000..c371e76349e --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp @@ -0,0 +1,335 @@ +/* + * 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 "TracingTest.h" +#include "engines/JsiIntegrationTestHermesEngineAdapter.h" + +#include +#include +#include +#include + +using namespace ::testing; + +namespace facebook::react::jsinspector_modern { + +class TracingTest : public TracingTestBase< + JsiIntegrationTestHermesEngineAdapter, + folly::QueuedImmediateExecutor> { + protected: + TracingTest() : TracingTestBase() {} + + void SetUp() override { + JsiIntegrationPortableTestBase::SetUp(); + connect(); + EXPECT_CALL( + fromPage(), + onMessage( + JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed"))))) + .Times(AnyNumber()); + } +}; + +TEST_F(TracingTest, EnablesSamplingProfilerOnlyCategoryIsSpecified) { + InSequence s; + + startTracing({}); + auto allTraceEvents = endTracingAndCollectEvents(); + + EXPECT_THAT( + allTraceEvents, + Not(Contains(AllOf( + AtJsonPtr("/name", "Profile"), + AtJsonPtr("/cat", "disabled-by-default-v8.cpu_profiler"))))); + + startTracing({tracing::Category::JavaScriptSampling}); + allTraceEvents = endTracingAndCollectEvents(); + + EXPECT_THAT( + allTraceEvents, + Contains(AllOf( + AtJsonPtr("/name", "Profile"), + AtJsonPtr("/cat", "disabled-by-default-v8.cpu_profiler")))); +} + +TEST_F(TracingTest, RecordsFrameTimings) { + InSequence s; + + page_->startTracing(tracing::Mode::Background, {tracing::Category::Timeline}); + + auto now = HighResTimeStamp::now(); + auto frameTimingSequence = tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(50)); + + page_->recordFrameTimings(frameTimingSequence); + + auto tracingProfile = page_->stopTracing(); + EXPECT_EQ(tracingProfile.frameTimings.size(), 1u); + EXPECT_EQ(tracingProfile.frameTimings[0].id, frameTimingSequence.id); +} + +TEST_F(TracingTest, EmitsRecordedFrameTimingSequences) { + InSequence s; + + startTracing(); + auto now = HighResTimeStamp::now(); + page_->recordFrameTimings( + tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(50))); + + auto allTraceEvents = endTracingAndCollectEvents(); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "BeginFrame"))); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "DrawFrame"))); +} + +TEST_F(TracingTest, EmitsScreenshotEventWhenScreenshotValuePassed) { + InSequence s; + + startTracing({tracing::Category::Screenshot}); + auto now = HighResTimeStamp::now(); + page_->recordFrameTimings( + tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(50), + std::vector{})); + + auto allTraceEvents = endTracingAndCollectEvents(); + EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "Screenshot"))); +} + +TEST_F( + TracingTest, + SecondSessionTracingStartIsRejectedWhileFirstSessionIsTracing) { + auto secondary = connectSecondary(); + InSequence s; + + // Session 1 starts tracing successfully + startTracing(); + + // Session 2 tries to start tracing - should get error + EXPECT_CALL( + secondary.fromPage(), + onMessage(JsonParsed(AllOf( + AtJsonPtr("/id", 2), + AtJsonPtr("/error/message", "Tracing has already been started"))))); + secondary.toPage().sendMessage(R"({"id": 2, "method": "Tracing.start"})"); + + // Session 1 ends tracing normally + endTracingAndCollectEvents(); + + // Now Session 2 can start tracing + EXPECT_CALL( + secondary.fromPage(), onMessage(JsonEq(R"({"id": 3, "result": {}})"))); + secondary.toPage().sendMessage(R"({"id": 3, "method": "Tracing.start"})"); + + // Clean up - end secondary's tracing + EXPECT_CALL( + secondary.fromPage(), onMessage(JsonEq(R"({"id": 4, "result": {}})"))); + EXPECT_CALL( + secondary.fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected")))) + .Times(AtLeast(1)); + EXPECT_CALL( + secondary.fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete")))); + secondary.toPage().sendMessage(R"({"id": 4, "method": "Tracing.end"})"); +} + +TEST_F(TracingTest, CDPTracingPreemptsBackgroundTracing) { + InSequence s; + + // Start background tracing directly + page_->startTracing(tracing::Mode::Background, {}); + + // CDP Tracing.start should preempt background (succeed, not fail) + startTracing(); + + // End tracing normally + endTracingAndCollectEvents(); +} + +TEST_F(TracingTest, BackgroundTracingIsRejectedWhileCDPTracingIsRunning) { + InSequence s; + + // Start CDP tracing + startTracing(); + + // Background tracing should be rejected + bool started = page_->startTracing(tracing::Mode::Background, {}); + EXPECT_FALSE(started); + + // End CDP tracing + endTracingAndCollectEvents(); + + // Now background tracing should succeed + started = page_->startTracing(tracing::Mode::Background, {}); + EXPECT_TRUE(started); + + // Clean up + page_->stopTracing(); +} + +TEST_F(TracingTest, EmitsToAllSessionsWithReactNativeApplicationDomainEnabled) { + auto secondaryFusebox = this->connectSecondary(); + auto secondaryNonFusebox = this->connectSecondary(); + + // Enable ReactNativeApplication domain on primary and secondaryFusebox + // sessions (but NOT on secondaryNonFusebox) + { + InSequence s; + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.metadataUpdated")))); + EXPECT_CALL( + this->fromPage(), onMessage(JsonEq(R"({"id": 1, "result": {}})"))); + } + this->toPage_->sendMessage( + R"({"id": 1, "method": "ReactNativeApplication.enable"})"); + + { + InSequence s; + EXPECT_CALL( + secondaryFusebox.fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.metadataUpdated")))); + EXPECT_CALL( + secondaryFusebox.fromPage(), + onMessage(JsonEq(R"({"id": 1, "result": {}})"))); + } + secondaryFusebox.toPage().sendMessage( + R"({"id": 1, "method": "ReactNativeApplication.enable"})"); + + // Start background tracing + this->page_->startTracing( + tracing::Mode::Background, {tracing::Category::Timeline}); + + // Record some frame timings + auto now = HighResTimeStamp::now(); + this->page_->recordFrameTimings( + tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(50))); + + // Primary and secondaryFusebox sessions should receive the trace. + // Events within each session are ordered, but order between sessions is + // arbitrary. + Sequence primarySeq; + Sequence secondarySeq; + + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.traceRequested")))) + .InSequence(primarySeq); + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected")))) + .Times(AtLeast(1)) + .InSequence(primarySeq); + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete")))) + .InSequence(primarySeq); + + EXPECT_CALL( + secondaryFusebox.fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.traceRequested")))) + .InSequence(secondarySeq); + EXPECT_CALL( + secondaryFusebox.fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected")))) + .Times(AtLeast(1)) + .InSequence(secondarySeq); + EXPECT_CALL( + secondaryFusebox.fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete")))) + .InSequence(secondarySeq); + + // secondaryNonFusebox should NOT receive anything (it did not enable the + // domain) + EXPECT_CALL(secondaryNonFusebox.fromPage(), onMessage(_)).Times(0); + + // Stop tracing and emit to all eligible sessions + EXPECT_TRUE(this->page_->stopAndMaybeEmitBackgroundTrace()); +} + +TEST_F(TracingTest, StashedTraceIsEmittedOnlyToFirstEligibleSession) { + // Start background tracing with no sessions having ReactNativeApplication + // enabled + this->page_->startTracing( + tracing::Mode::Background, {tracing::Category::Timeline}); + + // Record some frame timings + auto now = HighResTimeStamp::now(); + this->page_->recordFrameTimings( + tracing::FrameTimingSequence( + 1, // id + 11, // threadId + now, + now + HighResDuration::fromNanoseconds(50))); + + // Stop tracing - no eligible sessions exist, so the trace is stashed + EXPECT_FALSE(this->page_->stopAndMaybeEmitBackgroundTrace()); + + // Now the primary session enables ReactNativeApplication - it should receive + // the stashed trace. Events within a session are ordered. + Sequence primarySeq; + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.metadataUpdated")))) + .InSequence(primarySeq); + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.traceRequested")))) + .InSequence(primarySeq); + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected")))) + .Times(AtLeast(1)) + .InSequence(primarySeq); + EXPECT_CALL( + this->fromPage(), + onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete")))) + .InSequence(primarySeq); + EXPECT_CALL(this->fromPage(), onMessage(JsonEq(R"({"id": 1, "result": {}})"))) + .InSequence(primarySeq); + this->toPage_->sendMessage( + R"({"id": 1, "method": "ReactNativeApplication.enable"})"); + + // Connect a secondary session and enable ReactNativeApplication - it should + // NOT receive the already-emitted stashed trace + auto secondary = this->connectSecondary(); + Sequence secondarySeq; + EXPECT_CALL( + secondary.fromPage(), + onMessage(JsonParsed( + AtJsonPtr("/method", "ReactNativeApplication.metadataUpdated")))) + .InSequence(secondarySeq); + // No traceRequested, dataCollected, or tracingComplete expected for + // secondary + EXPECT_CALL( + secondary.fromPage(), onMessage(JsonEq(R"({"id": 1, "result": {}})"))) + .InSequence(secondarySeq); + secondary.toPage().sendMessage( + R"({"id": 1, "method": "ReactNativeApplication.enable"})"); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.h new file mode 100644 index 00000000000..bc8e271d316 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.h @@ -0,0 +1,95 @@ +/* + * 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 "FollyDynamicMatchers.h" +#include "JsiIntegrationTest.h" + +#include +#include +#include +#include + +#include +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Base test class providing tracing-related test utilities for tests. + */ +template +class TracingTestBase : public JsiIntegrationPortableTestBase { + protected: + using JsiIntegrationPortableTestBase::JsiIntegrationPortableTestBase; + + /** + * Helper method to start tracing via Tracing.start CDP command. + */ + void startTracing( + const std::set &enabledCategories = { + tracing::Category::HiddenTimeline, + tracing::Category::JavaScriptSampling, + tracing::Category::RuntimeExecution, + tracing::Category::Timeline, + tracing::Category::UserTiming, + }) + { + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + + this->toPage_->sendMessage( + fmt::format( + R"({{ + "id": 1, + "method": "Tracing.start", + "params": {{ "categories": "{0}" }} + }})", + tracing::serializeTracingCategories(enabledCategories))); + } + + /** + * Helper method to end tracing and collect all trace events from potentially + * multiple chunked Tracing.dataCollected messages. + * \returns A vector containing all collected trace events + */ + std::vector endTracingAndCollectEvents() + { + testing::InSequence s; + + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + + std::vector allTraceEvents; + + EXPECT_CALL(this->fromPage(), onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected")))) + .Times(testing::AtLeast(1)) + .WillRepeatedly(testing::Invoke([&allTraceEvents](const std::string &message) { + auto parsedMessage = folly::parseJson(message); + auto &events = parsedMessage.at("params").at("value"); + allTraceEvents.insert( + allTraceEvents.end(), std::make_move_iterator(events.begin()), std::make_move_iterator(events.end())); + })); + + this->expectMessageFromPage(JsonParsed( + testing::AllOf(AtJsonPtr("/method", "Tracing.tracingComplete"), AtJsonPtr("/params/dataLossOccurred", false)))); + + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Tracing.end" + })"); + + return allTraceEvents; + } +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp index 837301c934a..17a83c99413 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp @@ -26,11 +26,21 @@ class ReactNativeFeatureFlagsOverrides const InspectorFlagOverrides& overrides) : overrides_(overrides) {} + bool fuseboxScreenshotCaptureEnabled() override { + return overrides_.screenshotCaptureEnabled.value_or( + ReactNativeFeatureFlagsDefaults::fuseboxScreenshotCaptureEnabled()); + } + bool fuseboxEnabledRelease() override { return overrides_.fuseboxEnabledRelease.value_or( ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease()); } + bool fuseboxFrameRecordingEnabled() override { + return overrides_.frameRecordingEnabled.value_or( + ReactNativeFeatureFlagsDefaults::fuseboxFrameRecordingEnabled()); + } + bool fuseboxNetworkInspectionEnabled() override { return overrides_.networkInspectionEnabled.value_or( ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled()); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h index 84401fd1703..12122b3f2d7 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h @@ -19,9 +19,11 @@ namespace facebook::react::jsinspector_modern { struct InspectorFlagOverrides { // NOTE: Keep these entries in sync with ReactNativeFeatureFlagsOverrides in // the implementation file. + std::optional screenshotCaptureEnabled; + std::optional enableNetworkEventReporting; + std::optional frameRecordingEnabled; std::optional fuseboxEnabledRelease; std::optional networkInspectionEnabled; - std::optional enableNetworkEventReporting; }; /** diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/CMakeLists.txt b/packages/react-native/ReactCommon/jsinspector-modern/tracing/CMakeLists.txt index cfd3ba84b28..f0c7cd063c4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/CMakeLists.txt +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/CMakeLists.txt @@ -22,6 +22,7 @@ target_link_libraries(jsinspector_tracing jsinspector_network oscompat react_timing + react_utils ) target_compile_reactnative_options(jsinspector_tracing PRIVATE) target_compile_options(jsinspector_tracing PRIVATE -Wpedantic) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h new file mode 100644 index 00000000000..6aa0db5d174 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h @@ -0,0 +1,61 @@ +/* + * 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 + +#include +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +using FrameSequenceId = uint64_t; + +/** + * A struct representing a sequence of frame timings that happened on the Host side. + */ +struct FrameTimingSequence { + FrameTimingSequence() = delete; + + FrameTimingSequence( + FrameSequenceId id, + ThreadId threadId, + HighResTimeStamp beginTimestamp, + HighResTimeStamp endTimestamp, + std::optional> screenshot = std::nullopt) + : id(id), + threadId(threadId), + beginTimestamp(beginTimestamp), + endTimestamp(endTimestamp), + screenshot(std::move(screenshot)) + { + } + + /** + * Unique ID of the sequence, used by Chrome DevTools Frontend to identify the events that form one sequence. + */ + FrameSequenceId id; + + /** + * The ID of the native thread that is associated with the frame. + */ + ThreadId threadId; + + HighResTimeStamp beginTimestamp; + HighResTimeStamp endTimestamp; + + /** + * Optional screenshot data captured during the frame. + */ + std::optional> screenshot; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h new file mode 100644 index 00000000000..fd32cb3c225 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfile.h @@ -0,0 +1,43 @@ +/* + * 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 "FrameTimingSequence.h" +#include "InstanceTracingProfile.h" +#include "RuntimeSamplingProfile.h" + +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * The final tracing profile for the given HostTarget. + * Contains all necessary information that is required to be be emitted as a series of Tracing.dataCollected CDP + * messages. + */ +struct HostTracingProfile { + // The ID of the OS-level process that this Trace Recording is associated + // with. + ProcessId processId; + + // The timestamp at which this Trace Recording started. + HighResTimeStamp startTime; + + // Frame timings captured on the Host side. + std::vector frameTimings; + + // All captured Instance Tracing Profiles during this Trace Recording. + std::vector instanceTracingProfiles; + + // All captured Runtime Sampling Profiles during this Trace Recording. + std::vector runtimeSamplingProfiles; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp new file mode 100644 index 00000000000..1053790a353 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp @@ -0,0 +1,165 @@ +/* + * 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 "HostTracingProfileSerializer.h" +#include "RuntimeSamplingProfileTraceEventSerializer.h" +#include "TraceEventGenerator.h" +#include "TraceEventSerializer.h" + +namespace facebook::react::jsinspector_modern::tracing { + +namespace { + +/** + * Hardcoded layer tree ID for all recorded frames. + * https://chromedevtools.github.io/devtools-protocol/tot/LayerTree/ + */ +constexpr int FALLBACK_LAYER_TREE_ID = 1; + +} // namespace + +/* static */ void HostTracingProfileSerializer::emitAsDataCollectedChunks( + HostTracingProfile&& hostTracingProfile, + const std::function& chunkCallback, + size_t maxChunkBytes, + uint16_t profileTraceEventsChunkSize) { + emitFrameTimings( + std::move(hostTracingProfile.frameTimings), + hostTracingProfile.processId, + hostTracingProfile.startTime, + chunkCallback, + maxChunkBytes); + + auto instancesProfiles = + std::move(hostTracingProfile.instanceTracingProfiles); + IdGenerator profileIdGenerator; + + for (auto& instanceProfile : instancesProfiles) { + emitPerformanceTraceEvents( + std::move(instanceProfile.performanceTraceEvents), + chunkCallback, + maxChunkBytes); + } + + RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( + std::move(hostTracingProfile.runtimeSamplingProfiles), + profileIdGenerator, + hostTracingProfile.startTime, + chunkCallback, + profileTraceEventsChunkSize); +} + +/* static */ void HostTracingProfileSerializer::emitPerformanceTraceEvents( + std::vector&& events, + const std::function& chunkCallback, + size_t maxChunkBytes) { + folly::dynamic chunk = folly::dynamic::array(); + size_t currentChunkBytes = 0; + + for (auto& event : events) { + auto serializedEvent = TraceEventSerializer::serialize(std::move(event)); + size_t eventBytes = TraceEventSerializer::estimateJsonSize(serializedEvent); + + if (currentChunkBytes + eventBytes > maxChunkBytes && !chunk.empty()) { + chunkCallback(std::move(chunk)); + chunk = folly::dynamic::array(); + currentChunkBytes = 0; + } + + chunk.push_back(std::move(serializedEvent)); + currentChunkBytes += eventBytes; + } + + if (!chunk.empty()) { + chunkCallback(std::move(chunk)); + } +} + +/* static */ void HostTracingProfileSerializer::emitFrameTimings( + std::vector&& frameTimings, + ProcessId processId, + HighResTimeStamp recordingStartTimestamp, + const std::function& chunkCallback, + size_t maxChunkBytes) { + if (frameTimings.empty()) { + return; + } + + folly::dynamic chunk = folly::dynamic::array(); + size_t currentChunkBytes = 0; + + auto setLayerTreeIdEvent = TraceEventGenerator::createSetLayerTreeIdEvent( + "", // Hardcoded frame name for the default (and only) layer. + FALLBACK_LAYER_TREE_ID, + processId, + frameTimings.front().threadId, + recordingStartTimestamp); + auto serializedSetLayerTreeId = + TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent)); + currentChunkBytes += + TraceEventSerializer::estimateJsonSize(serializedSetLayerTreeId); + chunk.push_back(std::move(serializedSetLayerTreeId)); + + for (auto&& frameTimingSequence : frameTimings) { + // Serialize all events for this frame. + folly::dynamic frameEvents = folly::dynamic::array(); + size_t totalFrameBytes = 0; + + auto [beginDrawingEvent, endDrawingEvent] = + TraceEventGenerator::createFrameTimingsEvents( + frameTimingSequence.id, + FALLBACK_LAYER_TREE_ID, + frameTimingSequence.beginTimestamp, + frameTimingSequence.endTimestamp, + processId, + frameTimingSequence.threadId); + + auto serializedBegin = + TraceEventSerializer::serialize(std::move(beginDrawingEvent)); + totalFrameBytes += TraceEventSerializer::estimateJsonSize(serializedBegin); + frameEvents.push_back(std::move(serializedBegin)); + + auto serializedEnd = + TraceEventSerializer::serialize(std::move(endDrawingEvent)); + totalFrameBytes += TraceEventSerializer::estimateJsonSize(serializedEnd); + frameEvents.push_back(std::move(serializedEnd)); + + if (frameTimingSequence.screenshot.has_value()) { + auto screenshotEvent = TraceEventGenerator::createScreenshotEvent( + frameTimingSequence.id, + FALLBACK_LAYER_TREE_ID, + std::move(frameTimingSequence.screenshot.value()), + frameTimingSequence.endTimestamp, + processId, + frameTimingSequence.threadId); + + auto serializedScreenshot = + TraceEventSerializer::serialize(std::move(screenshotEvent)); + totalFrameBytes += + TraceEventSerializer::estimateJsonSize(serializedScreenshot); + frameEvents.push_back(std::move(serializedScreenshot)); + } + + // Flush current chunk if adding this frame would exceed the limit. + if (currentChunkBytes + totalFrameBytes > maxChunkBytes && !chunk.empty()) { + chunkCallback(std::move(chunk)); + chunk = folly::dynamic::array(); + currentChunkBytes = 0; + } + + for (auto& frameEvent : frameEvents) { + chunk.push_back(std::move(frameEvent)); + } + currentChunkBytes += totalFrameBytes; + } + + if (!chunk.empty()) { + chunkCallback(std::move(chunk)); + } +} + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h new file mode 100644 index 00000000000..ceb8ac4c072 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h @@ -0,0 +1,50 @@ +/* + * 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 "FrameTimingSequence.h" +#include "HostTracingProfile.h" +#include "TraceEvent.h" + +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * A serializer for HostTracingProfile that can be used for transforming the + * profile into sequence of serialized Trace Events. + */ +class HostTracingProfileSerializer { + public: + /** + * Transforms the profile into a sequence of serialized Trace Events, which + * is split in chunks of at most \p maxChunkBytes serialized bytes or + * \p profileTraceEventsChunkSize events, depending on type, and sent with + * \p chunkCallback. + */ + static void emitAsDataCollectedChunks( + HostTracingProfile &&hostTracingProfile, + const std::function &chunkCallback, + size_t maxChunkBytes, + uint16_t profileTraceEventsChunkSize); + + static void emitPerformanceTraceEvents( + std::vector &&events, + const std::function &chunkCallback, + size_t maxChunkBytes); + + static void emitFrameTimings( + std::vector &&frameTimings, + ProcessId processId, + HighResTimeStamp recordingStartTimestamp, + const std::function &chunkCallback, + size_t maxChunkBytes); +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp index 66f1b484447..5769ef2f60c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp @@ -7,7 +7,9 @@ #include "PerformanceTracer.h" #include "Timing.h" +#include "TraceEventGenerator.h" #include "TraceEventSerializer.h" +#include "TracingCategory.h" #include #include @@ -138,7 +140,7 @@ std::optional> PerformanceTracer::stopTracing() { events.emplace_back( TraceEvent{ .name = "TracingStartedInPage", - .cat = "disabled-by-default-devtools.timeline", + .cat = {Category::HiddenTimeline}, .ph = 'I', .ts = currentTraceStartTime, .pid = processId_, @@ -149,7 +151,7 @@ std::optional> PerformanceTracer::stopTracing() { events.emplace_back( TraceEvent{ .name = "ReactNative-TracingStopped", - .cat = "disabled-by-default-devtools.timeline", + .cat = {Category::HiddenTimeline}, .ph = 'I', .ts = currentTraceEndTime, .pid = processId_, @@ -370,7 +372,7 @@ void PerformanceTracer::reportResourceFinish( return TraceEvent{ .id = profileId, .name = "Profile", - .cat = "disabled-by-default-v8.cpu_profiler", + .cat = {Category::JavaScriptSampling}, .ph = 'P', .ts = profileTimestamp, .pid = processId, @@ -393,7 +395,7 @@ PerformanceTracer::constructRuntimeProfileChunkTraceEvent( return TraceEvent{ .id = profileId, .name = "ProfileChunk", - .cat = "disabled-by-default-v8.cpu_profiler", + .cat = {Category::JavaScriptSampling}, .ph = 'P', .ts = chunkTimestamp, .pid = processId, @@ -525,7 +527,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "RunTask", - .cat = "disabled-by-default-devtools.timeline", + .cat = {Category::HiddenTimeline}, .ph = 'X', .ts = event.start, .pid = processId_, @@ -537,7 +539,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "RunMicrotasks", - .cat = "v8.execute", + .cat = {Category::RuntimeExecution}, .ph = 'X', .ts = event.start, .pid = processId_, @@ -557,7 +559,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = std::move(event.name), - .cat = "blink.user_timing", + .cat = {Category::UserTiming}, .ph = 'I', .ts = event.start, .pid = processId_, @@ -578,7 +580,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( TraceEvent{ .id = eventId, .name = event.name, - .cat = "blink.user_timing", + .cat = {Category::UserTiming}, .ph = 'b', .ts = event.start, .pid = processId_, @@ -589,7 +591,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( TraceEvent{ .id = eventId, .name = std::move(event.name), - .cat = "blink.user_timing", + .cat = {Category::UserTiming}, .ph = 'e', .ts = event.start + event.duration, .pid = processId_, @@ -629,7 +631,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "TimeStamp", - .cat = "devtools.timeline", + .cat = {Category::Timeline}, .ph = 'I', .ts = event.createdAt, .pid = processId_, @@ -664,7 +666,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "TimeStamp", - .cat = "devtools.timeline", + .cat = {Category::Timeline}, .ph = 'I', .ts = event.createdAt, .pid = processId_, @@ -684,7 +686,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "ResourceSendRequest", - .cat = "devtools.timeline", + .cat = {Category::Timeline}, .ph = 'I', .ts = event.start, .pid = processId_, @@ -710,7 +712,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "ResourceReceiveResponse", - .cat = "devtools.timeline", + .cat = {Category::Timeline}, .ph = 'I', .ts = event.start, .pid = processId_, @@ -728,7 +730,7 @@ void PerformanceTracer::enqueueTraceEventsFromPerformanceTracerEvent( events.emplace_back( TraceEvent{ .name = "ResourceFinish", - .cat = "devtools.timeline", + .cat = {Category::Timeline}, .ph = 'I', .ts = event.start, .pid = processId_, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracerSection.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracerSection.h new file mode 100644 index 00000000000..59545a2b623 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracerSection.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * This is a RAII class that reports a timeStamp block to the React Native + * Performance Tracer. + * + * @example + * { + * PerformanceTracerSection s("name", "track", "track group"); + * // do something + * } + */ +template +class PerformanceTracerSection { + public: + explicit PerformanceTracerSection( + const char *name, + const char *track = nullptr, + const char *trackGroup = nullptr, + const char *color = nullptr, + Args... args) noexcept + : name_(name), track_(track), trackGroup_(trackGroup), color_(color), args_(std::move(args)...) + { + static_assert( + sizeof...(Args) % 2 == 0, + "PerformanceTracerSection expects an even number of variadic args representing [name, value] pairs."); + } + + // Non-movable + PerformanceTracerSection(const PerformanceTracerSection &) = delete; + PerformanceTracerSection(PerformanceTracerSection &&) = delete; + + // Non-copyable + PerformanceTracerSection &operator=(const PerformanceTracerSection &) = delete; + PerformanceTracerSection &operator=(PerformanceTracerSection &&) = delete; + + ~PerformanceTracerSection() noexcept + { + auto &tracer = PerformanceTracer::getInstance(); + if (!tracer.isTracing()) { + return; + } + + auto endTime = HighResTimeStamp::now(); + + // Slow path when passing properties + if constexpr (sizeof...(Args) > 0) { + auto properties = folly::dynamic::array(); + std::apply( + [&](const auto &...elems) { + size_t idx = 0; + (((idx % 2 == 0) ? properties.push_back(folly::dynamic::array(elems)) + : properties[properties.size() - 1].push_back(elems), + ++idx), + ...); + }, + args_); + + folly::dynamic devtools = folly::dynamic::object(); + devtools["properties"] = std::move(properties); + + if (track_ != nullptr) { + devtools["track"] = track_; + } + + if (trackGroup_ != nullptr) { + devtools["trackGroup"] = trackGroup_; + } + + if (color_ != nullptr) { + devtools["color"] = color_; + } + + folly::dynamic detail = folly::dynamic::object(); + detail["devtools"] = std::move(devtools); + + tracer.reportMeasure(std::string(name_), startTime_, endTime - startTime_, std::move(detail)); + } else { + tracer.reportTimeStamp( + std::string(name_), + startTime_, + endTime, + track_ != nullptr ? std::optional{track_} : std::nullopt, + trackGroup_ != nullptr ? std::optional{trackGroup_} : std::nullopt, + color_ != nullptr ? getConsoleTimeStampColorFromString(color_) : std::nullopt); + } + } + + private: + HighResTimeStamp startTime_{HighResTimeStamp::now()}; + std::string_view name_; + const char *track_; + const char *trackGroup_; + const char *color_; + std::tuple args_; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec b/packages/react-native/ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec index 38675d36751..1b988ab698c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec @@ -47,6 +47,7 @@ Pod::Spec.new do |s| s.dependency "React-jsi" s.dependency "React-oscompat" s.dependency "React-timing" + add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"]) if use_hermes() s.dependency "hermes-engine" diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h new file mode 100644 index 00000000000..6951c815f48 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TimeWindowedBuffer.h @@ -0,0 +1,158 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * The currentBufferStartTime_ is initialized once first element is pushed. + */ +constexpr HighResTimeStamp kCurrentBufferStartTimeUninitialized = HighResTimeStamp::min(); + +template +class TimeWindowedBuffer { + public: + using TimestampAccessor = std::function; + + TimeWindowedBuffer() : timestampAccessor_(std::nullopt), windowSize_(std::nullopt) {} + + TimeWindowedBuffer(TimestampAccessor timestampAccessor, HighResDuration windowSize) + : timestampAccessor_(std::move(timestampAccessor)), windowSize_(windowSize) + { + } + + void push(const T &element) + { + if (timestampAccessor_) { + auto timestamp = (*timestampAccessor_)(element); + enqueueElement(element, timestamp); + } else { + enqueueElement(element, HighResTimeStamp::now()); + } + } + + void push(T &&element) + { + if (timestampAccessor_) { + auto timestamp = (*timestampAccessor_)(element); + enqueueElement(std::move(element), timestamp); + } else { + enqueueElement(std::move(element), HighResTimeStamp::now()); + } + } + + void clear() + { + primaryBuffer_.clear(); + alternativeBuffer_.clear(); + currentBufferIndex_ = BufferIndex::Primary; + currentBufferStartTime_ = kCurrentBufferStartTimeUninitialized; + } + + /** + * Forces immediate removal of elements that are outside the time window. + * The right boundary of the window is the reference timestamp passed as an argument. + */ + std::vector pruneExpiredAndExtract(HighResTimeStamp windowRightBoundary = HighResTimeStamp::now()) + { + std::vector result; + + for (auto &wrappedElement : getPreviousBuffer()) { + if (isInsideTimeWindow(wrappedElement, windowRightBoundary)) { + result.push_back(std::move(wrappedElement.element)); + } + } + + for (auto &wrappedElement : getCurrentBuffer()) { + if (isInsideTimeWindow(wrappedElement, windowRightBoundary)) { + result.push_back(std::move(wrappedElement.element)); + } + } + + clear(); + return result; + } + + private: + enum class BufferIndex { Primary, Alternative }; + + struct TimestampedElement { + T element; + HighResTimeStamp timestamp; + }; + + std::vector &getCurrentBuffer() + { + return currentBufferIndex_ == BufferIndex::Primary ? primaryBuffer_ : alternativeBuffer_; + } + + std::vector &getPreviousBuffer() + { + return currentBufferIndex_ == BufferIndex::Primary ? alternativeBuffer_ : primaryBuffer_; + } + + void enqueueElement(const T &element, HighResTimeStamp timestamp) + { + if (windowSize_) { + if (currentBufferStartTime_ == kCurrentBufferStartTimeUninitialized) { + currentBufferStartTime_ = timestamp; + } else if (timestamp > currentBufferStartTime_ + *windowSize_) { + // We moved past the current buffer. We need to switch the other buffer as current. + currentBufferIndex_ = + currentBufferIndex_ == BufferIndex::Primary ? BufferIndex::Alternative : BufferIndex::Primary; + getCurrentBuffer().clear(); + currentBufferStartTime_ = timestamp; + } + } + + getCurrentBuffer().push_back({element, timestamp}); + } + + void enqueueElement(T &&element, HighResTimeStamp timestamp) + { + if (windowSize_) { + if (currentBufferStartTime_ == kCurrentBufferStartTimeUninitialized) { + currentBufferStartTime_ = timestamp; + } else if (timestamp > currentBufferStartTime_ + *windowSize_) { + // We moved past the current buffer. We need to switch the other buffer as current. + currentBufferIndex_ = + currentBufferIndex_ == BufferIndex::Primary ? BufferIndex::Alternative : BufferIndex::Primary; + getCurrentBuffer().clear(); + currentBufferStartTime_ = timestamp; + } + } + + getCurrentBuffer().push_back({std::move(element), timestamp}); + } + + bool isInsideTimeWindow(const TimestampedElement &element, HighResTimeStamp windowRightBoundary) const + { + if (!windowSize_) { + return true; + } + + return element.timestamp >= windowRightBoundary - *windowSize_ && element.timestamp <= windowRightBoundary; + } + + std::optional timestampAccessor_; + std::optional windowSize_; + + std::vector primaryBuffer_; + std::vector alternativeBuffer_; + BufferIndex currentBufferIndex_ = BufferIndex::Primary; + HighResTimeStamp currentBufferStartTime_{kCurrentBufferStartTimeUninitialized}; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEvent.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEvent.h index aa8e2222e57..ba94a7af0e2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEvent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEvent.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -40,7 +41,7 @@ struct TraceEvent { * A comma separated list of categories for the event, configuring how * events are shown in the Trace Viewer UI. */ - std::string cat; + Categories cat; /** * The event type. This is a single character which changes depending on the diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp new file mode 100644 index 00000000000..215c25e1bd8 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp @@ -0,0 +1,100 @@ +/* + * 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 "TraceEventGenerator.h" +#include "Timing.h" +#include "TracingCategory.h" + +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/* static */ TraceEvent TraceEventGenerator::createSetLayerTreeIdEvent( + std::string frame, + int layerTreeId, + ProcessId processId, + ThreadId threadId, + HighResTimeStamp timestamp) { + folly::dynamic data = folly::dynamic::object("frame", std::move(frame))( + "layerTreeId", layerTreeId); + + return TraceEvent{ + .name = "SetLayerTreeId", + .cat = {Category::HiddenTimeline}, + .ph = 'I', + .ts = timestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = folly::dynamic::object("data", std::move(data)), + }; +} + +/* static */ std::pair +TraceEventGenerator::createFrameTimingsEvents( + uint64_t sequenceId, + int layerTreeId, + HighResTimeStamp beginTimestamp, + HighResTimeStamp endTimestamp, + ProcessId processId, + ThreadId threadId) { + folly::dynamic args = folly::dynamic::object("frameSeqId", sequenceId)( + "layerTreeId", layerTreeId); + + auto beginEvent = TraceEvent{ + .name = "BeginFrame", + .cat = {Category::Frame}, + .ph = 'I', + .ts = beginTimestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = args, + }; + auto drawEvent = TraceEvent{ + .name = "DrawFrame", + .cat = {Category::Frame}, + .ph = 'I', + .ts = endTimestamp, + .pid = processId, + .s = 't', + .tid = threadId, + .args = args, + }; + + return {std::move(beginEvent), std::move(drawEvent)}; +} + +/* static */ TraceEvent TraceEventGenerator::createScreenshotEvent( + FrameSequenceId frameSequenceId, + int sourceId, + std::vector&& snapshot, + HighResTimeStamp expectedDisplayTime, + ProcessId processId, + ThreadId threadId) { + // Convert binary data to string for Base64 encoding + std::string snapshotBytes(snapshot.begin(), snapshot.end()); + std::string base64Snapshot = base64Encode(snapshotBytes); + + folly::dynamic args = + folly::dynamic::object("snapshot", std::move(base64Snapshot))( + "source_id", sourceId)("frame_sequence", frameSequenceId)( + "expected_display_time", + highResTimeStampToTracingClockTimeStamp(expectedDisplayTime)); + + return TraceEvent{ + .name = "Screenshot", + .cat = {Category::Screenshot}, + .ph = 'O', + .ts = expectedDisplayTime, + .pid = processId, + .tid = threadId, + .args = std::move(args), + }; +} + +}; // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h new file mode 100644 index 00000000000..898fba6ef72 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h @@ -0,0 +1,60 @@ +/* + * 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 +#include + +#include +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * This class encapsulates the logic for generating canonical trace events that will be serialized and sent as part of + * Tracing.dataCollected CDP message. + */ +class TraceEventGenerator { + public: + /** + * Creates canonical "SetLayerTreeId" trace event. + */ + static TraceEvent createSetLayerTreeIdEvent( + std::string frame, + int layerTreeId, + ProcessId processId, + ThreadId threadId, + HighResTimeStamp timestamp); + + /** + * Creates canonical "BeginFrame", "Commit", "DrawFrame" trace events. + */ + static std::pair createFrameTimingsEvents( + FrameSequenceId sequenceId, + int layerTreeId, + HighResTimeStamp beginTimestamp, + HighResTimeStamp endTimestamp, + ProcessId processId, + ThreadId threadId); + + /** + * Creates canonical "Screenshot" trace event. + */ + static TraceEvent createScreenshotEvent( + FrameSequenceId frameSequenceId, + int sourceId, + std::vector &&snapshot, + HighResTimeStamp expectedDisplayTime, + ProcessId processId, + ThreadId threadId); +}; + +}; // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp index f8181cf3902..46497c8240f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp @@ -7,6 +7,7 @@ #include "TraceEventSerializer.h" #include "Timing.h" +#include "TracingCategory.h" #include @@ -22,7 +23,7 @@ namespace facebook::react::jsinspector_modern::tracing { result["id"] = buffer.data(); } result["name"] = std::move(event.name); - result["cat"] = std::move(event.cat); + result["cat"] = serializeTracingCategories(event.cat); result["ph"] = std::string(1, event.ph); result["ts"] = highResTimeStampToTracingClockTimeStamp(event.ts); result["pid"] = event.pid; @@ -111,4 +112,46 @@ TraceEventSerializer::serializeProfileChunkCPUProfileNodeCallFrame( return dynamicCallFrame; } +/* static */ size_t TraceEventSerializer::estimateJsonSize( + const folly::dynamic& value) { + switch (value.type()) { + case folly::dynamic::Type::NULLT: + return 4; // null + case folly::dynamic::Type::BOOL: + return 5; // false + case folly::dynamic::Type::INT64: + case folly::dynamic::Type::DOUBLE: + return 20; // conservative max for numeric values + case folly::dynamic::Type::STRING: + return value.stringPiece().size() + 2; // quotes + case folly::dynamic::Type::ARRAY: { + size_t size = 2; // [] + bool first = true; + for (const auto& element : value) { + if (!first) { + size += 1; // comma + } + first = false; + size += estimateJsonSize(element); + } + return size; + } + case folly::dynamic::Type::OBJECT: { + size_t size = 2; // {} + bool first = true; + for (const auto& [key, val] : value.items()) { + if (!first) { + size += 1; // comma + } + first = false; + // key size + quotes + colon + size += key.stringPiece().size() + 3; + size += estimateJsonSize(val); + } + return size; + } + } + return 0; +} + } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.h index 38d8eb6f620..0240e2c5d2c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.h @@ -84,6 +84,13 @@ class TraceEventSerializer { */ static folly::dynamic serializeProfileChunkCPUProfileNodeCallFrame( TraceEventProfileChunk::CPUProfile::Node::CallFrame &&callFrame); + + /** + * Estimates the JSON-serialized byte size of a folly::dynamic value. + * This is a rough but conservative estimate to avoid the cost of + * double-serialization (once to measure, once to emit). + */ + static size_t estimateJsonSize(const folly::dynamic &value); }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h index df3087fa562..db0acc40b76 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingState.h @@ -18,22 +18,33 @@ namespace facebook::react::jsinspector_modern::tracing { +/** + * The global state for the given Trace Recording. + * Shared with Tracing Agents, which could use it to stash the corresponding target profiles during reloads. + */ struct TraceRecordingState { + TraceRecordingState( + tracing::Mode tracingMode, + std::set enabledCategories, + std::optional windowSize = std::nullopt) + : mode(tracingMode), enabledCategories(std::move(enabledCategories)), windowSize(windowSize) + { + } + // 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 runtimeSamplingProfiles{}; // All captures Instance Tracing Profiles during this Trace Recording. std::vector instanceTracingProfiles{}; + + // The list of categories that are enabled for this recording. + std::set enabledCategories; + + // The size of the time window for this recording. + std::optional windowSize; }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp deleted file mode 100644 index 09fe7368404..00000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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& 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&& events, - const std::function& 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 diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h deleted file mode 100644 index 3936eab663b..00000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceRecordingStateSerializer.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 -#include - -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 &chunkCallback, - uint16_t performanceTraceEventsChunkSize, - uint16_t profileTraceEventsChunkSize); - - static void emitPerformanceTraceEvents( - std::vector &&events, - const std::function &chunkCallback, - uint16_t chunkSize); -}; - -} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingCategory.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingCategory.h new file mode 100644 index 00000000000..011c05fd1c0 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingCategory.h @@ -0,0 +1,136 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +enum class Category { + HiddenTimeline, /* disabled-by-default-devtools.timeline */ + JavaScriptSampling, /* disabled-by-default-v8.cpu_profiler */ + RuntimeExecution, /* v8.execute */ + Timeline, /* devtools.timeline */ + UserTiming, /* blink.user_timing */ + Frame, /* disabled-by-default-devtools.timeline.frame */ + Screenshot, /* disabled-by-default-devtools.screenshot */ +}; + +inline std::string tracingCategoryToString(const Category &category) +{ + switch (category) { + case Category::Timeline: + return "devtools.timeline"; + case Category::HiddenTimeline: + return "disabled-by-default-devtools.timeline"; + case Category::UserTiming: + return "blink.user_timing"; + case Category::JavaScriptSampling: + return "disabled-by-default-v8.cpu_profiler"; + case Category::RuntimeExecution: + return "v8.execute"; + case Category::Frame: + return "disabled-by-default-devtools.timeline.frame"; + case Category::Screenshot: + return "disabled-by-default-devtools.screenshot"; + } +} + +inline std::optional getTracingCategoryFromString(const std::string &str) +{ + if (str == "blink.user_timing") { + return Category::UserTiming; + } else if (str == "devtools.timeline") { + return Category::Timeline; + } else if (str == "disabled-by-default-devtools.timeline") { + return Category::HiddenTimeline; + } else if (str == "disabled-by-default-v8.cpu_profiler") { + return Category::JavaScriptSampling; + } else if (str == "v8.execute") { + return Category::RuntimeExecution; + } else if (str == "disabled-by-default-devtools.timeline.frame") { + return Category::Frame; + } else if (str == "disabled-by-default-devtools.screenshot") { + return Category::Screenshot; + } else { + return std::nullopt; + } +} + +/** + * The Trace Event could have multiple categories, but this is extremely rare case. + */ +using Categories = folly::small_vector; + +// { Timeline, UserTiming } => "devtools.timeline,blink.user_timing" +inline std::string serializeTracingCategories(const Categories &categories) +{ + if (categories.size() == 1) { + return tracingCategoryToString(categories.front()); + } + + std::string serializedValue; + for (size_t i = 0; i < categories.size(); ++i) { + serializedValue += tracingCategoryToString(categories[i]); + if (i < categories.size() - 1) { + serializedValue += ","; + } + } + return serializedValue; +} + +// { Timeline, UserTiming } => "devtools.timeline,blink.user_timing" +inline std::string serializeTracingCategories(const std::set &categories) +{ + std::string serializedValue; + + auto current = categories.begin(); + while (current != categories.end()) { + serializedValue += tracingCategoryToString(*current); + + ++current; + if (current != categories.end()) { + serializedValue += ","; + } + } + + return serializedValue; +} + +// "devtools.timeline,blink.user_timing" => { Timeline, UserTiming } +inline std::set parseSerializedTracingCategories(const std::string &serializedCategories) +{ + std::set categories; + if (serializedCategories.empty()) { + return categories; + } + + size_t start = 0; + size_t end = serializedCategories.find(','); + while (end != std::string::npos) { + std::string token = serializedCategories.substr(start, end - start); + if (auto category = getTracingCategoryFromString(token)) { + categories.insert(*category); + } + start = end + 1; + end = serializedCategories.find(',', start); + } + + std::string lastToken = serializedCategories.substr(start); + if (auto category = getTracingCategoryFromString(lastToken)) { + categories.insert(*category); + } + + return categories; +} + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h deleted file mode 100644 index 5914de8c5a4..00000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TracingState.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 - -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 diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp new file mode 100644 index 00000000000..03bed9a9830 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/TimeWindowedBufferTest.cpp @@ -0,0 +1,352 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +// Test structure with timestamp field +struct TestEvent { + int value; + HighResTimeStamp timestamp; + + bool operator==(const TestEvent& other) const { + return value == other.value; + } +}; + +// ============================================================================ +// Tests for unbounded buffer (no timestamp accessor) +// ============================================================================ + +TEST(TimeWindowedBufferTest, DefaultConstructorCreatesEmptyBuffer) { + TimeWindowedBuffer buffer; + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, PushAddsElementsToUnboundedBuffer) { + TimeWindowedBuffer buffer; + buffer.push(1); + buffer.push(2); + buffer.push(3); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], 1); + EXPECT_EQ(result[1], 2); + EXPECT_EQ(result[2], 3); +} + +TEST(TimeWindowedBufferTest, UnboundedBufferPreservesAllElements) { + TimeWindowedBuffer buffer; + for (int i = 0; i < 100; ++i) { + buffer.push(i); + } + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 100u); + for (int i = 0; i < 100; ++i) { + EXPECT_EQ(result[i], i); + } +} + +TEST(TimeWindowedBufferTest, ClearEmptiesBuffer) { + TimeWindowedBuffer buffer; + buffer.push(1); + buffer.push(2); + buffer.push(3); + + buffer.clear(); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, PushRvalueReference) { + TimeWindowedBuffer buffer; + std::string str = "test"; + buffer.push(std::move(str)); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], "test"); +} + +// ============================================================================ +// Tests for time-windowed buffer (with timestamp accessor) +// ============================================================================ + +TEST(TimeWindowedBufferTest, TimeWindowedBufferCreation) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(TimeWindowedBufferTest, TimeWindowedBufferAddsElements) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + + auto result = buffer.pruneExpiredAndExtract(baseTime + windowSize); + EXPECT_EQ(result.size(), 2u); + EXPECT_EQ(result[0].value, 1); + EXPECT_EQ(result[1].value, 2); +} + +TEST(TimeWindowedBufferTest, ElementsWithinWindowArePreserved) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(500); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + buffer.push( + TestEvent{ + .value = 4, + .timestamp = baseTime + HighResDuration::fromMilliseconds(300)}); + + // Extract with window [300ms, 800ms] - only event at 300ms should be included + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(800)); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 4); +} + +TEST(TimeWindowedBufferTest, BufferSwitchingWhenWindowExceeded) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events within first window + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(50)}); + + // Add event that exceeds the window - should trigger buffer switch + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(150)}); + + // Extract events within the window using reference point at 250ms + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(250)); + + // Events from 150ms should be in the window (250 - 100 = 150) + EXPECT_GE(result.size(), 1u); +} + +TEST(TimeWindowedBufferTest, PruneExpiredFiltersOldElements) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events in first window + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(50)}); + + // Move to second window + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(150)}); + + // Move to third window + buffer.push( + TestEvent{ + .value = 4, + .timestamp = baseTime + HighResDuration::fromMilliseconds(300)}); + + // Extract with reference at 300ms: window is [200ms, 300ms] + // Only event 4 at 300ms should be within window + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(300)); + + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 4); +} + +TEST(TimeWindowedBufferTest, OutOfOrderTimestampsAreHandled) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(10000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add events out of order (by timestamp) + buffer.push( + TestEvent{ + .value = 1, + .timestamp = baseTime + HighResDuration::fromMilliseconds(100)}); + buffer.push( + TestEvent{.value = 2, .timestamp = baseTime}); // Earlier timestamp + buffer.push( + TestEvent{ + .value = 3, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + + // Extract with window [200ms, 10200ms] - only event at 200ms should be + // included + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(10200)); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].value, 3); +} + +TEST(TimeWindowedBufferTest, ClearResetsTimeWindowedBuffer) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(100); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(200)}); + + buffer.clear(); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 0u); +} + +// ============================================================================ +// Tests for edge cases +// ============================================================================ + +TEST(TimeWindowedBufferTest, SingleElementBuffer) { + TimeWindowedBuffer buffer; + buffer.push(42); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 42); +} + +TEST(TimeWindowedBufferTest, LargeNumberOfElements) { + TimeWindowedBuffer buffer; + + const int count = 10000; + for (int i = 0; i < count; ++i) { + buffer.push(i); + } + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), static_cast(count)); + EXPECT_EQ(result[0], 0); + EXPECT_EQ(result[count - 1], count - 1); +} + +TEST(TimeWindowedBufferTest, VerySmallTimeWindow) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromNanoseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push(TestEvent{.value = 1, .timestamp = baseTime}); + + // Next event with significant time difference should trigger switch + buffer.push( + TestEvent{ + .value = 2, + .timestamp = baseTime + HighResDuration::fromMilliseconds(1)}); + + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(1)); + EXPECT_GE(result.size(), 1u); +} + +TEST(TimeWindowedBufferTest, VeryLargeTimeWindow) { + auto timestampAccessor = [](const TestEvent& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(3600000); // 1 hour + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + + // Add many events spread over time + for (int i = 0; i < 100; ++i) { + buffer.push( + TestEvent{ + .value = i, + .timestamp = + baseTime + HighResDuration::fromMilliseconds(i * 10000)}); + } + + // All events should still be in the window + auto result = buffer.pruneExpiredAndExtract( + baseTime + HighResDuration::fromMilliseconds(100 * 10000)); + EXPECT_EQ(result.size(), 100u); +} + +// ============================================================================ +// Tests for complex types +// ============================================================================ + +TEST(TimeWindowedBufferTest, WorksWithComplexTypes) { + struct ComplexType { + std::string name; + std::vector data; + HighResTimeStamp timestamp; + }; + + auto timestampAccessor = [](const ComplexType& e) { return e.timestamp; }; + auto windowSize = HighResDuration::fromMilliseconds(1000); + + TimeWindowedBuffer buffer(timestampAccessor, windowSize); + + auto baseTime = HighResTimeStamp::now(); + buffer.push( + ComplexType{.name = "test", .data = {1, 2, 3}, .timestamp = baseTime}); + + auto result = buffer.pruneExpiredAndExtract(); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].name, "test"); + EXPECT_EQ(result[0].data.size(), 3u); +} + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index d7b162a5df8..733c9708c31 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6728f8cada1d0d9d21800b4fefe76b77>> + * @generated SignedSource<<20973c91d9d7e791598083c0cee5b934>> */ /** @@ -254,10 +254,18 @@ bool ReactNativeFeatureFlags::fuseboxEnabledRelease() { return getAccessor().fuseboxEnabledRelease(); } +bool ReactNativeFeatureFlags::fuseboxFrameRecordingEnabled() { + return getAccessor().fuseboxFrameRecordingEnabled(); +} + bool ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled() { return getAccessor().fuseboxNetworkInspectionEnabled(); } +bool ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled() { + return getAccessor().fuseboxScreenshotCaptureEnabled(); +} + bool ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS() { return getAccessor().hideOffscreenVirtualViewsOnIOS(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 52eddb33111..5bbb0dd5ebc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4b8574d0682b5e9644affc89559393ac>> + * @generated SignedSource<> */ /** @@ -324,11 +324,21 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fuseboxEnabledRelease(); + /** + * Enable frame timings and screenshots support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + RN_EXPORT static bool fuseboxFrameRecordingEnabled(); + /** * Enable network inspection support in the React Native DevTools CDP backend. Requires `enableBridgelessArchitecture`. This flag is global and should not be changed across React Host lifetimes. */ RN_EXPORT static bool fuseboxNetworkInspectionEnabled(); + /** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ + RN_EXPORT static bool fuseboxScreenshotCaptureEnabled(); + /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 893d6010279..8c89226787a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<3c510c97c279768dbf3bfd5c2b2c1903>> */ /** @@ -1055,6 +1055,24 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { + auto flagValue = fuseboxFrameRecordingEnabled_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(57, "fuseboxFrameRecordingEnabled"); + + flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); + fuseboxFrameRecordingEnabled_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { auto flagValue = fuseboxNetworkInspectionEnabled_.load(); @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(58, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -1073,6 +1091,24 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { + auto flagValue = fuseboxScreenshotCaptureEnabled_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(59, "fuseboxScreenshotCaptureEnabled"); + + flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); + fuseboxScreenshotCaptureEnabled_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { auto flagValue = hideOffscreenVirtualViewsOnIOS_.load(); @@ -1082,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(60, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1100,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(61, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1118,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "perfIssuesEnabled"); + markFlagAsAccessed(62, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1136,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "perfMonitorV2Enabled"); + markFlagAsAccessed(63, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1154,7 +1190,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "preparedTextCacheSize"); + markFlagAsAccessed(64, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1172,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(65, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1190,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(66, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1208,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(67, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1226,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(68, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1244,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::sweepActiveTouchOnChildNativeGesturesAndro // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "sweepActiveTouchOnChildNativeGesturesAndroid"); + markFlagAsAccessed(69, "sweepActiveTouchOnChildNativeGesturesAndroid"); flagValue = currentProvider_->sweepActiveTouchOnChildNativeGesturesAndroid(); sweepActiveTouchOnChildNativeGesturesAndroid_ = flagValue; @@ -1262,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(70, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1280,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(71, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1298,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(72, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1316,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "useFabricInterop"); + markFlagAsAccessed(73, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1334,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeEqualsInNativeReadableArrayAndroi // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "useNativeEqualsInNativeReadableArrayAndroid"); + markFlagAsAccessed(74, "useNativeEqualsInNativeReadableArrayAndroid"); flagValue = currentProvider_->useNativeEqualsInNativeReadableArrayAndroid(); useNativeEqualsInNativeReadableArrayAndroid_ = flagValue; @@ -1352,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeTransformHelperAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "useNativeTransformHelperAndroid"); + markFlagAsAccessed(75, "useNativeTransformHelperAndroid"); flagValue = currentProvider_->useNativeTransformHelperAndroid(); useNativeTransformHelperAndroid_ = flagValue; @@ -1370,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(76, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1388,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(77, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -1406,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useRawPropsJsiValue"); + markFlagAsAccessed(78, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -1424,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useShadowNodeStateOnClone"); + markFlagAsAccessed(79, "useShadowNodeStateOnClone"); flagValue = currentProvider_->useShadowNodeStateOnClone(); useShadowNodeStateOnClone_ = flagValue; @@ -1442,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useSharedAnimatedBackend"); + markFlagAsAccessed(80, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1460,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(81, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1478,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useTurboModuleInterop"); + markFlagAsAccessed(82, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1496,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useTurboModules"); + markFlagAsAccessed(83, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1514,7 +1550,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "viewCullingOutsetRatio"); + markFlagAsAccessed(84, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1532,7 +1568,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewHysteresisRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "virtualViewHysteresisRatio"); + markFlagAsAccessed(85, "virtualViewHysteresisRatio"); flagValue = currentProvider_->virtualViewHysteresisRatio(); virtualViewHysteresisRatio_ = flagValue; @@ -1550,7 +1586,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "virtualViewPrerenderRatio"); + markFlagAsAccessed(86, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 121a013433c..38ba562f5a0 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -89,7 +89,9 @@ class ReactNativeFeatureFlagsAccessor { bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); bool fuseboxAssertSingleHostState(); bool fuseboxEnabledRelease(); + bool fuseboxFrameRecordingEnabled(); bool fuseboxNetworkInspectionEnabled(); + bool fuseboxScreenshotCaptureEnabled(); bool hideOffscreenVirtualViewsOnIOS(); bool overrideBySynchronousMountPropsAtMountingAndroid(); bool perfIssuesEnabled(); @@ -128,7 +130,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 85> accessedFeatureFlags_; + std::array, 87> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -187,7 +189,9 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; std::atomic> fuseboxAssertSingleHostState_; std::atomic> fuseboxEnabledRelease_; + std::atomic> fuseboxFrameRecordingEnabled_; std::atomic> fuseboxNetworkInspectionEnabled_; + std::atomic> fuseboxScreenshotCaptureEnabled_; std::atomic> hideOffscreenVirtualViewsOnIOS_; std::atomic> overrideBySynchronousMountPropsAtMountingAndroid_; std::atomic> perfIssuesEnabled_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 8e1c0080101..b4924ba5601 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -255,10 +255,18 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fuseboxFrameRecordingEnabled() override { + return false; + } + bool fuseboxNetworkInspectionEnabled() override { return true; } + bool fuseboxScreenshotCaptureEnabled() override { + return false; + } + bool hideOffscreenVirtualViewsOnIOS() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 084452c4f3e..22046ddd4b1 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6e8731d9d7642d65b9b707741ef71873>> */ /** @@ -558,6 +558,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fuseboxEnabledRelease(); } + bool fuseboxFrameRecordingEnabled() override { + auto value = values_["fuseboxFrameRecordingEnabled"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxFrameRecordingEnabled(); + } + bool fuseboxNetworkInspectionEnabled() override { auto value = values_["fuseboxNetworkInspectionEnabled"]; if (!value.isNull()) { @@ -567,6 +576,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled(); } + bool fuseboxScreenshotCaptureEnabled() override { + auto value = values_["fuseboxScreenshotCaptureEnabled"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fuseboxScreenshotCaptureEnabled(); + } + bool hideOffscreenVirtualViewsOnIOS() override { auto value = values_["hideOffscreenVirtualViewsOnIOS"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index acce771423a..8dd58d390fc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<06c6740fe7d613f235ae791da750bd52>> + * @generated SignedSource<<886710691ddc524f90129b6921cb2c2a>> */ /** @@ -82,7 +82,9 @@ class ReactNativeFeatureFlagsProvider { virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; virtual bool fuseboxAssertSingleHostState() = 0; virtual bool fuseboxEnabledRelease() = 0; + virtual bool fuseboxFrameRecordingEnabled() = 0; virtual bool fuseboxNetworkInspectionEnabled() = 0; + virtual bool fuseboxScreenshotCaptureEnabled() = 0; virtual bool hideOffscreenVirtualViewsOnIOS() = 0; virtual bool overrideBySynchronousMountPropsAtMountingAndroid() = 0; virtual bool perfIssuesEnabled() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 28a1eda002b..de0b91d4a8d 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3f6cc9604905bb29a9524a97eaa294bd>> + * @generated SignedSource<<884b5382bd771ceb400f15ab4069bc75>> */ /** @@ -329,11 +329,21 @@ bool NativeReactNativeFeatureFlags::fuseboxEnabledRelease( return ReactNativeFeatureFlags::fuseboxEnabledRelease(); } +bool NativeReactNativeFeatureFlags::fuseboxFrameRecordingEnabled( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fuseboxFrameRecordingEnabled(); +} + bool NativeReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +bool NativeReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fuseboxScreenshotCaptureEnabled(); +} + bool NativeReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 2cbe1b923c9..d09128160d4 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<41e6644bf2ceca861bfb1e491c912c1d>> */ /** @@ -150,8 +150,12 @@ class NativeReactNativeFeatureFlags bool fuseboxEnabledRelease(jsi::Runtime& runtime); + bool fuseboxFrameRecordingEnabled(jsi::Runtime& runtime); + bool fuseboxNetworkInspectionEnabled(jsi::Runtime& runtime); + bool fuseboxScreenshotCaptureEnabled(jsi::Runtime& runtime); + bool hideOffscreenVirtualViewsOnIOS(jsi::Runtime& runtime); bool overrideBySynchronousMountPropsAtMountingAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp index fbdf02a31ff..b2244b8c3ed 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.cpp @@ -25,16 +25,21 @@ void PerformanceObserver::handleEntry(const PerformanceEntry& entry) { return; } - buffer_.push_back(entry); + { + std::lock_guard lock(bufferMutex_); + buffer_.push_back(entry); + } scheduleFlushBuffer(); } } std::vector PerformanceObserver::takeRecords() { std::vector result; - buffer_.swap(result); - - didScheduleFlushBuffer_ = false; + { + std::lock_guard lock(bufferMutex_); + buffer_.swap(result); + didScheduleFlushBuffer_ = false; + } return result; } @@ -87,9 +92,16 @@ void PerformanceObserver::disconnect() noexcept { } void PerformanceObserver::scheduleFlushBuffer() { - if (!didScheduleFlushBuffer_) { - didScheduleFlushBuffer_ = true; + bool shouldSchedule = false; + { + std::lock_guard lock(bufferMutex_); + if (!didScheduleFlushBuffer_) { + didScheduleFlushBuffer_ = true; + shouldSchedule = true; + } + } + if (shouldSchedule) { callback_(); } } diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h index c397bf14ae3..a8335aea3a9 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceObserver.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -126,6 +127,7 @@ class PerformanceObserver : public std::enable_shared_from_this buffer_; bool didScheduleFlushBuffer_ = false; bool requiresDroppedEntries_ = false; diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index 5c7bf6b986a..207e602260b 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -13,6 +13,7 @@ #import #import #import +#import #import #import #import @@ -37,12 +38,52 @@ @interface RCTHost () @property (nonatomic, readonly) jsinspector_modern::HostTarget *inspectorTarget; @end +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) +class RCTHostTracingDelegate : public jsinspector_modern::HostTargetTracingDelegate { + public: + explicit RCTHostTracingDelegate(RCTHost *host) : host_(host) {} + + void onTracingStarted(jsinspector_modern::tracing::Mode /*tracingMode*/, bool screenshotsCategoryEnabled) override + { + RCTHost *host = host_; + if (host == nil || host.inspectorTarget == nullptr) { + return; + } + __weak RCTHost *weakHost = host; + + observer_ = [[RCTFrameTimingsObserver alloc] + initWithScreenshotsEnabled:screenshotsCategoryEnabled + callback:^(jsinspector_modern::tracing::FrameTimingSequence sequence) { + RCTHost *strongHost = weakHost; + if (strongHost != nil && strongHost.inspectorTarget != nullptr) { + strongHost.inspectorTarget->recordFrameTimings(std::move(sequence)); + } + }]; + [observer_ start]; + } + + void onTracingStopped() override + { + [observer_ stop]; + observer_ = nil; + } + + private: + __weak RCTHost *host_; + RCTFrameTimingsObserver *observer_{nil}; +}; +#endif + class RCTHostHostTargetDelegate : public facebook::react::jsinspector_modern::HostTargetDelegate { public: RCTHostHostTargetDelegate(RCTHost *host) : host_(host), pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]), networkHelper_([[RCTInspectorNetworkHelper alloc] init]) +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) + , + tracingDelegate_(host) +#endif { } @@ -100,10 +141,84 @@ void loadNetworkResource(const RCTInspectorLoadNetworkResourceRequest ¶ms, R [networkHelper_ loadNetworkResourceWithParams:params executor:executor]; } +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) + std::optional captureScreenshot(const PageCaptureScreenshotRequest &request) override + { + UIWindow *keyWindow = nil; + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && + [scene isKindOfClass:[UIWindowScene class]]) { + auto *windowScene = (UIWindowScene *)scene; + for (UIWindow *win in windowScene.windows) { + if (win.isKeyWindow) { + keyWindow = win; + break; + } + } + } + if (keyWindow != nil) { + break; + } + } + + if (keyWindow == nil) { + return std::nullopt; + } + + UIView *rootView = keyWindow.rootViewController.view != nil ? keyWindow.rootViewController.view : keyWindow; + CGSize viewSize = rootView.bounds.size; + + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:viewSize format:format]; + + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [rootView drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:NO]; + }]; + + if (image == nil) { + return std::nullopt; + } + + NSData *encodedData = nil; + std::string formatStr = request.format.value_or("png"); + + if (formatStr == "jpeg") { + CGFloat quality = request.quality.has_value() ? (*request.quality / 100.0) : 0.8; + encodedData = UIImageJPEGRepresentation(image, quality); + } else { + // Default to PNG for "png" and "webp" (WebP encoding not available via UIKit) + encodedData = UIImagePNGRepresentation(image); + } + + if (encodedData == nil) { + return std::nullopt; + } + + NSString *base64String = [encodedData base64EncodedStringWithOptions:0]; + return std::string([base64String UTF8String]); + } +#endif + +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) + jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override + { + auto &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance(); + if (!inspectorFlags.getFrameRecordingEnabled()) { + return nullptr; + } + + return &tracingDelegate_; + } +#endif + private: __weak RCTHost *host_; RCTPausedInDebuggerOverlayController *pauseOverlayController_; RCTInspectorNetworkHelper *networkHelper_; +#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED) + RCTHostTracingDelegate tracingDelegate_; +#endif }; @implementation RCTHost { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/Base64.h b/packages/react-native/ReactCommon/react/utils/Base64.h similarity index 96% rename from packages/react-native/ReactCommon/jsinspector-modern/Base64.h rename to packages/react-native/ReactCommon/react/utils/Base64.h index 0057445de0a..ec879c2b62b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/Base64.h +++ b/packages/react-native/ReactCommon/react/utils/Base64.h @@ -10,7 +10,7 @@ #include #include -namespace facebook::react::jsinspector_modern { +namespace facebook::react { namespace { // Vendored from Folly @@ -96,4 +96,4 @@ inline std::string base64Encode(const std::string_view s) return res; } -} // namespace facebook::react::jsinspector_modern +} // namespace facebook::react diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index a24489637f2..03ce89ecadb 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -61,6 +61,7 @@ def self.set_gcc_preprocessor_definition_for_debugger(installer) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED=1", "React-jsinspectornetwork", :debug) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED=1", "React-RCTNetwork", :debug) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED=1", "React-networking", :debug) + self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED=1", "React-RuntimeApple", :debug) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED_DEVONLY=1", "React-jsinspector", :debug) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED_DEVONLY=1", "React-jsinspectornetwork", :debug) self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "REACT_NATIVE_DEBUGGER_ENABLED_DEVONLY=1", "React-RCTNetwork", :debug) diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 0db4359e14c..b4d2ccb9d13 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -653,6 +653,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fuseboxFrameRecordingEnabled: { + defaultValue: false, + metadata: { + dateAdded: '2026-03-05', + description: + 'Enable frame timings and screenshots support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, fuseboxNetworkInspectionEnabled: { defaultValue: true, metadata: { @@ -664,6 +675,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'stable', }, + fuseboxScreenshotCaptureEnabled: { + defaultValue: false, + metadata: { + dateAdded: '2026-04-01', + description: + 'Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, hideOffscreenVirtualViewsOnIOS: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 668290f21a6..69a23d5f985 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<474b28e49cdb8a46a361a32df3c92eed>> + * @generated SignedSource<<075c9fd7af1eab0ffdde75a05daa8cac>> * @flow strict * @noformat */ @@ -107,7 +107,9 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, fuseboxAssertSingleHostState: Getter, fuseboxEnabledRelease: Getter, + fuseboxFrameRecordingEnabled: Getter, fuseboxNetworkInspectionEnabled: Getter, + fuseboxScreenshotCaptureEnabled: Getter, hideOffscreenVirtualViewsOnIOS: Getter, overrideBySynchronousMountPropsAtMountingAndroid: Getter, perfIssuesEnabled: Getter, @@ -444,10 +446,18 @@ export const fuseboxAssertSingleHostState: Getter = createNativeFlagGet * Flag determining if the React Native DevTools (Fusebox) CDP backend should be enabled in release builds. This flag is global and should not be changed across React Host lifetimes. */ export const fuseboxEnabledRelease: Getter = createNativeFlagGetter('fuseboxEnabledRelease', false); +/** + * Enable frame timings and screenshots support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ +export const fuseboxFrameRecordingEnabled: Getter = createNativeFlagGetter('fuseboxFrameRecordingEnabled', false); /** * Enable network inspection support in the React Native DevTools CDP backend. Requires `enableBridgelessArchitecture`. This flag is global and should not be changed across React Host lifetimes. */ export const fuseboxNetworkInspectionEnabled: Getter = createNativeFlagGetter('fuseboxNetworkInspectionEnabled', true); +/** + * Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes. + */ +export const fuseboxScreenshotCaptureEnabled: Getter = createNativeFlagGetter('fuseboxScreenshotCaptureEnabled', false); /** * Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 1fb807a4e4c..9295169694d 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -82,7 +82,9 @@ export interface Spec extends TurboModule { +fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; +fuseboxAssertSingleHostState?: () => boolean; +fuseboxEnabledRelease?: () => boolean; + +fuseboxFrameRecordingEnabled?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; + +fuseboxScreenshotCaptureEnabled?: () => boolean; +hideOffscreenVirtualViewsOnIOS?: () => boolean; +overrideBySynchronousMountPropsAtMountingAndroid?: () => boolean; +perfIssuesEnabled?: () => boolean; diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 9079776ac95..22ccac31416 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -1921,6 +1921,7 @@ PODS: - React-jsinspectornetwork - React-oscompat - React-timing + - React-utils - SocketRocket - React-jsitooling (0.83.4): - boost @@ -2867,7 +2868,7 @@ SPEC CHECKSUMS: FBLazyVector: 82d1d7996af4c5850242966eb81e73f9a6dfab1e fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 420e3616ff59e380aefea760efb12026686b963a + hermes-engine: 02b1ea6892513a5b09716362f85b2511e4f9b754 MyNativeView: 1af823608512b912ff60718a6d48bacbece9c1f0 NativeCxxModuleExample: ed1893117fdd164b44e67b2a1d7f4620610a8081 OCMock: 589f2c84dacb1f5aaf6e4cec1f292551fe748e74 @@ -2901,7 +2902,7 @@ SPEC CHECKSUMS: React-jsinspector: 5f756f86c8263f3e0e462f4b12b8da3b677686a4 React-jsinspectorcdp: d6bcfdb732d99f6240e3ed6b82da58f7391a4ce2 React-jsinspectornetwork: 9e2a9df177614e7e4a058c37ae2d7cefe59a7d8d - React-jsinspectortracing: 106ef2423c9c90c88d01f7e9b86cc86668d06bb4 + React-jsinspectortracing: 3bd4664b3eef90bb9a1c61a012f904f162d2c721 React-jsitooling: 5c7a6e98c27452fa0043c112ae53a7b499d08d30 React-jsitracing: d68eea24f3feea58726ae44fab02d571b9011f36 React-logger: 993e4b9793768764e0fdd379ad1d6582f7905463