diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index ef1eea145f46..5a6908d9c9e1 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -398,7 +398,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 000000000000..ebf7b4bcf499 --- /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 000000000000..ec9a0ae01fba --- /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/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorFlags.kt index 8cb59abb19ee..c7ffcff620ad 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 index e6aa485371c0..5eaae383eed3 100644 --- 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 @@ -12,5 +12,5 @@ internal data class FrameTimingSequence( val threadId: Int, val beginTimestamp: Long, val endTimestamp: Long, - val screenshot: String? = null, + 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 index bf352f9b8f17..a62339990234 100644 --- 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 @@ -12,14 +12,18 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.Process -import android.util.Base64 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 @@ -30,20 +34,39 @@ internal class FrameTimingsObserver( 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 - // Capture initial screenshot to ensure there's always at least one frame - // recorded at the start of tracing, even if no UI changes occur + // Emit initial frame event val timestamp = System.nanoTime() emitFrameTiming(timestamp, timestamp) @@ -59,6 +82,7 @@ internal class FrameTimingsObserver( currentWindow?.removeOnFrameMetricsAvailableListener(frameMetricsListener) mainHandler.removeCallbacksAndMessages(null) + lastFrameBuffer.getAndSet(null)?.bitmap?.recycle() } fun setCurrentWindow(window: Window?) { @@ -75,8 +99,7 @@ internal class FrameTimingsObserver( private val frameMetricsListener = Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ -> - // Guard against calls arriving after stop() has ended tracing. Async work scheduled from - // previous frames will still finish. + // Guard against calls after stop() if (!isTracing) { return@OnFrameMetricsAvailableListener } @@ -89,34 +112,107 @@ internal class FrameTimingsObserver( val frameId = frameCounter++ val threadId = Process.myTid() - if (screenshotsEnabled) { - // Initiate PixelCopy immediately on the main thread, while still in the current frame, - // then process and emit asynchronously once the copy is complete. - captureScreenshot { screenshot -> - CoroutineScope(Dispatchers.Default).launch { - onFrameTimingSequence( - FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot) - ) + 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) } - } else { - CoroutineScope(Dispatchers.Default).launch { - onFrameTimingSequence( - FrameTimingSequence(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(callback: (String?) -> Unit) { + 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 } @@ -131,9 +227,7 @@ internal class FrameTimingsObserver( bitmap, { copyResult -> if (copyResult == PixelCopy.SUCCESS) { - CoroutineScope(Dispatchers.Default).launch { - callback(encodeScreenshot(window, bitmap, width, height)) - } + callback(FrameData(bitmap, frameId, threadId, beginTimestamp, endTimestamp)) } else { bitmap.recycle() callback(null) @@ -143,9 +237,12 @@ internal class FrameTimingsObserver( ) } - private fun encodeScreenshot(window: Window, bitmap: Bitmap, width: Int, height: Int): String? { + 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() @@ -155,20 +252,24 @@ internal class FrameTimingsObserver( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY else Bitmap.CompressFormat.JPEG - ByteArrayOutputStream().use { outputStream -> + ByteArrayOutputStream(SCREENSHOT_OUTPUT_SIZE_HINT).use { outputStream -> scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream) - Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + outputStream.toByteArray() } } catch (e: Exception) { null } finally { scaledBitmap?.recycle() - bitmap.recycle() } } companion object { - private const val SCREENSHOT_SCALE_FACTOR = 0.75f + 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/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index a2295ba531ec..ecd23baf2ff9 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<<156d4f5f35037184b6fc61ff1d856028>> + * @generated SignedSource<> */ /** @@ -384,12 +384,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 df54de170654..98a9b43be0f6 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<<0875d5e54d884a26d37bb4eb2acc57d5>> + * @generated SignedSource<> */ /** @@ -79,7 +79,9 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var fixTextClippingAndroid15useBoundsForWidthCache: 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 @@ -637,6 +639,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) { @@ -646,6 +657,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 a9e7550867b7..56240c84bc87 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<<948a9beebe2ff00791a03455eb774eee>> + * @generated SignedSource<> */ /** @@ -146,8 +146,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 b7e15d740909..8d828f79696d 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<<89c61520177334f93c65ff92c2fc74a6>> + * @generated SignedSource<<9c0f3543f620cbed7dc0646e804e99c6>> */ /** @@ -141,8 +141,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 48cb60dc5894..631f5a31cd88 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<<669708c311abe9ffc8f7783219e2baad>> + * @generated SignedSource<<1a04fbbdeb04ad45889eb36e871a5307>> */ /** @@ -83,7 +83,9 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var fixTextClippingAndroid15useBoundsForWidthCache: 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 @@ -700,6 +702,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) { @@ -710,6 +722,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 e0b05b9dde3c..7a63fcd48508 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<> + * @generated SignedSource<> */ /** @@ -141,8 +141,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 dad904b4b1d5..8bac4f57c382 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 d3c8224f3e0b..f857e44754f1 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 @@ -447,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. @@ -1565,16 +1603,18 @@ public class ReactHostImpl( when (state) { TracingState.ENABLED_IN_BACKGROUND_MODE, TracingState.ENABLED_IN_CDP_MODE -> { - val observer = - FrameTimingsObserver( - _screenshotsEnabled, - { frameTimingsSequence -> - inspectorTarget.recordFrameTimings(frameTimingsSequence) - }, - ) - observer.setCurrentWindow(currentActivity?.window) - observer.start() - frameTimingsObserver = observer + if (InspectorFlags.getFrameRecordingEnabled()) { + val observer = + FrameTimingsObserver( + _screenshotsEnabled, + { frameTimingsSequence -> + inspectorTarget.recordFrameTimings(frameTimingsSequence) + }, + ) + observer.setCurrentWindow(currentActivity?.window) + observer.start() + frameTimingsObserver = observer + } } TracingState.DISABLED -> { frameTimingsObserver?.stop() 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 dbc58c41a917..95cf9636f43f 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 4541665e2ca7..1056132c7c68 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 7a60a11c7279..080be266fc64 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<<0b95d68522d63d51d3e524aeecff246a>> + * @generated SignedSource<> */ /** @@ -393,12 +393,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"); @@ -848,11 +860,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(); @@ -1186,9 +1208,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 67889af8a636..d52602f4172b 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<<6b7e2af51ba9d64ae4e474dfa104a7c3>> + * @generated SignedSource<<9a0ad2e34163e0efed56ca1662d68c35>> */ /** @@ -207,9 +207,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 709b5a0dd7b2..d8498aeea988 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 @@ -145,6 +145,20 @@ 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; } 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 5f69f05829d6..d2389940308b 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,6 +7,7 @@ #pragma once +#include #include #include @@ -98,13 +99,17 @@ struct JFrameTimingSequence : public jni::JavaClass { std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field)))); } - std::optional getScreenshot() const + std::optional> getScreenshot() const { - auto field = javaClassStatic()->getField("screenshot"); + auto field = javaClassStatic()->getField("screenshot"); auto javaScreenshot = getFieldValue(field); if (javaScreenshot) { - auto jstring = jni::static_ref_cast(javaScreenshot); - return jstring->toStdString(); + 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; } @@ -140,6 +145,13 @@ 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)); + } }; /** @@ -270,6 +282,8 @@ class JReactHostInspectorTarget : public jni::HybridClass executor) override; + std::optional captureScreenshot( + const jsinspector_modern::HostTargetDelegate::PageCaptureScreenshotRequest &request) override; jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override; private: diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 8fd200024d3a..d1ef2ab54f05 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) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 3d61df29b8c2..637198d2a57e 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -328,6 +328,7 @@ namespace { struct StaticHostTargetMetadata { std::optional isProfilingBuild; std::optional networkInspectionEnabled; + std::optional frameRecordingEnabled; }; StaticHostTargetMetadata getStaticHostMetadata() { @@ -335,7 +336,8 @@ StaticHostTargetMetadata getStaticHostMetadata() { return { .isProfilingBuild = inspectorFlags.getIsProfilingBuild(), - .networkInspectionEnabled = inspectorFlags.getNetworkInspectionEnabled()}; + .networkInspectionEnabled = inspectorFlags.getNetworkInspectionEnabled(), + .frameRecordingEnabled = inspectorFlags.getFrameRecordingEnabled()}; } } // namespace @@ -370,6 +372,10 @@ folly::dynamic createHostMetadataPayload(const HostTargetMetadata& metadata) { result["unstable_networkInspectionEnabled"] = staticMetadata.networkInspectionEnabled.value(); } + if (staticMetadata.frameRecordingEnabled) { + result["unstable_frameRecordingEnabled"] = + staticMetadata.frameRecordingEnabled.value(); + } return result; } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h index c7b8281bb89d..1030b90879f9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.h @@ -16,6 +16,7 @@ #include "ScopedExecutor.h" #include "WeakList.h" +#include #include #include #include @@ -134,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; /** @@ -181,6 +195,17 @@ 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; + } + /** * An optional delegate that will be used by HostTarget to notify about tracing-related events. */ diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp index 9fba17e555f2..7d41971122cc 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp @@ -93,7 +93,7 @@ void HostTarget::recordFrameTimings( std::lock_guard lock(tracingMutex_); if (traceRecording_) { - traceRecording_->recordFrameTimings(frameTimingSequence); + traceRecording_->recordFrameTimings(std::move(frameTimingSequence)); } else { assert( false && diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h index e5a676d5195a..c007212445d4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.h @@ -35,10 +35,10 @@ void emitNotificationsForTracingProfile( std::convertible_to, FrontendChannel> { /** - * Threshold for the size Trace Event chunk, that will be flushed out with - * Tracing.dataCollected event. + * Maximum serialized byte size of a Trace Event chunk before it is flushed + * with a Tracing.dataCollected event. */ - static constexpr uint16_t TRACE_EVENT_CHUNK_SIZE = 1000; + static constexpr size_t TRACE_EVENT_CHUNK_MAX_BYTES = 10 * 1024 * 1024; // 10 MiB /** * The maximum number of ProfileChunk trace events @@ -65,7 +65,7 @@ void emitNotificationsForTracingProfile( cdp::jsonNotification("Tracing.dataCollected", folly::dynamic::object("value", serializedChunk))); } }, - TRACE_EVENT_CHUNK_SIZE, + TRACE_EVENT_CHUNK_MAX_BYTES, PROFILE_TRACE_EVENT_CHUNK_SIZE); for (auto &frontendChannel : channels) { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InspectorFlags.cpp index 010813a24264..c2c31ee13d28 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 5e245d482eed..f1f1353e5e95 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/NetworkIOAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp index c18795b7e529..ba7447baa230 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/tests/HostTargetTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp index 38175d4110c6..a4bca34e82b7 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp @@ -1579,4 +1579,16 @@ TEST_F(HostTargetTest, TracingDelegateIsNotifiedOnDirectTracingCall) { 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 122b233f24a3..b50582b771f9 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()); } @@ -137,6 +137,7 @@ class MockHostTargetDelegate : public HostTargetDelegate { loadNetworkResource, (const LoadNetworkResourceRequest ¶ms, ScopedExecutor executor), (override)); + MOCK_METHOD(std::optional, captureScreenshot, (const PageCaptureScreenshotRequest &request), (override)); HostTargetTracingDelegate *getTracingDelegate() override { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp index a88593eca563..11871ddbf2c1 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 6b77a0493567..678a8c164533 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp @@ -38,10 +38,10 @@ class NetworkReporterTestBase : public TracingTestBase< protected: NetworkReporterTestBase() : TracingTestBase({ - .networkInspectionEnabled = true, .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 index ffd3d4a05bf5..c371e76349e6 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp @@ -103,7 +103,7 @@ TEST_F(TracingTest, EmitsScreenshotEventWhenScreenshotValuePassed) { 11, // threadId now, now + HighResDuration::fromNanoseconds(50), - "base64EncodedScreenshotData")); + std::vector{})); auto allTraceEvents = endTracingAndCollectEvents(); EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "Screenshot"))); 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 837301c934a0..17a83c994139 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 84401fd17032..12122b3f2d72 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 cfd3ba84b284..f0c7cd063c41 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 index 65d492276aeb..6aa0db5d1744 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h @@ -11,6 +11,10 @@ #include +#include +#include +#include + namespace facebook::react::jsinspector_modern::tracing { using FrameSequenceId = uint64_t; @@ -26,7 +30,7 @@ struct FrameTimingSequence { ThreadId threadId, HighResTimeStamp beginTimestamp, HighResTimeStamp endTimestamp, - std::optional screenshot = std::nullopt) + std::optional> screenshot = std::nullopt) : id(id), threadId(threadId), beginTimestamp(beginTimestamp), @@ -49,9 +53,9 @@ struct FrameTimingSequence { HighResTimeStamp endTimestamp; /** - * Optional screenshot data (base64 encoded) captured during the frame. + * Optional screenshot data captured during the frame. */ - std::optional screenshot; + std::optional> screenshot; }; } // 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 index 68bd53f180fe..1053790a3532 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp @@ -14,13 +14,6 @@ namespace facebook::react::jsinspector_modern::tracing { namespace { -folly::dynamic generateNewChunk(uint16_t chunkSize) { - folly::dynamic chunk = folly::dynamic::array(); - chunk.reserve(chunkSize); - - return chunk; -} - /** * Hardcoded layer tree ID for all recorded frames. * https://chromedevtools.github.io/devtools-protocol/tot/LayerTree/ @@ -32,14 +25,14 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; /* static */ void HostTracingProfileSerializer::emitAsDataCollectedChunks( HostTracingProfile&& hostTracingProfile, const std::function& chunkCallback, - uint16_t traceEventsChunkSize, + size_t maxChunkBytes, uint16_t profileTraceEventsChunkSize) { emitFrameTimings( std::move(hostTracingProfile.frameTimings), hostTracingProfile.processId, hostTracingProfile.startTime, chunkCallback, - traceEventsChunkSize); + maxChunkBytes); auto instancesProfiles = std::move(hostTracingProfile.instanceTracingProfiles); @@ -49,7 +42,7 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; emitPerformanceTraceEvents( std::move(instanceProfile.performanceTraceEvents), chunkCallback, - traceEventsChunkSize); + maxChunkBytes); } RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( @@ -63,16 +56,22 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; /* static */ void HostTracingProfileSerializer::emitPerformanceTraceEvents( std::vector&& events, const std::function& chunkCallback, - uint16_t chunkSize) { - folly::dynamic chunk = generateNewChunk(chunkSize); + size_t maxChunkBytes) { + folly::dynamic chunk = folly::dynamic::array(); + size_t currentChunkBytes = 0; for (auto& event : events) { - if (chunk.size() == chunkSize) { + auto serializedEvent = TraceEventSerializer::serialize(std::move(event)); + size_t eventBytes = TraceEventSerializer::estimateJsonSize(serializedEvent); + + if (currentChunkBytes + eventBytes > maxChunkBytes && !chunk.empty()) { chunkCallback(std::move(chunk)); - chunk = generateNewChunk(chunkSize); + chunk = folly::dynamic::array(); + currentChunkBytes = 0; } - chunk.push_back(TraceEventSerializer::serialize(std::move(event))); + chunk.push_back(std::move(serializedEvent)); + currentChunkBytes += eventBytes; } if (!chunk.empty()) { @@ -85,26 +84,30 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; ProcessId processId, HighResTimeStamp recordingStartTimestamp, const std::function& chunkCallback, - uint16_t chunkSize) { + size_t maxChunkBytes) { if (frameTimings.empty()) { return; } - folly::dynamic chunk = generateNewChunk(chunkSize); + 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); - chunk.push_back( - TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent))); + auto serializedSetLayerTreeId = + TraceEventSerializer::serialize(std::move(setLayerTreeIdEvent)); + currentChunkBytes += + TraceEventSerializer::estimateJsonSize(serializedSetLayerTreeId); + chunk.push_back(std::move(serializedSetLayerTreeId)); for (auto&& frameTimingSequence : frameTimings) { - if (chunk.size() >= chunkSize) { - chunkCallback(std::move(chunk)); - chunk = generateNewChunk(chunkSize); - } + // Serialize all events for this frame. + folly::dynamic frameEvents = folly::dynamic::array(); + size_t totalFrameBytes = 0; auto [beginDrawingEvent, endDrawingEvent] = TraceEventGenerator::createFrameTimingsEvents( @@ -115,10 +118,15 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; processId, frameTimingSequence.threadId); - chunk.push_back( - TraceEventSerializer::serialize(std::move(beginDrawingEvent))); - chunk.push_back( - TraceEventSerializer::serialize(std::move(endDrawingEvent))); + 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( @@ -129,9 +137,24 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1; processId, frameTimingSequence.threadId); - chunk.push_back( - TraceEventSerializer::serialize(std::move(screenshotEvent))); + 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()) { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h index 1007898ff912..ceb8ac4c072c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.h @@ -24,27 +24,27 @@ class HostTracingProfileSerializer { public: /** * Transforms the profile into a sequence of serialized Trace Events, which - * is split in chunks of sizes \p traceEventsChunkSize or - * \p profileTraceEventsChunkSize, depending on type, and sent with \p - * chunkCallback. + * 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, - uint16_t traceEventsChunkSize, + size_t maxChunkBytes, uint16_t profileTraceEventsChunkSize); static void emitPerformanceTraceEvents( std::vector &&events, const std::function &chunkCallback, - uint16_t chunkSize); + size_t maxChunkBytes); static void emitFrameTimings( std::vector &&frameTimings, ProcessId processId, HighResTimeStamp recordingStartTimestamp, const std::function &chunkCallback, - uint16_t chunkSize); + size_t maxChunkBytes); }; } // namespace facebook::react::jsinspector_modern::tracing 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 000000000000..59545a2b6237 --- /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 38675d367517..1b988ab698c7 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/TraceEventGenerator.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp index 0c94276d76ee..215c25e1bd8a 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp @@ -9,6 +9,8 @@ #include "Timing.h" #include "TracingCategory.h" +#include + namespace facebook::react::jsinspector_modern::tracing { /* static */ TraceEvent TraceEventGenerator::createSetLayerTreeIdEvent( @@ -70,14 +72,19 @@ TraceEventGenerator::createFrameTimingsEvents( /* static */ TraceEvent TraceEventGenerator::createScreenshotEvent( FrameSequenceId frameSequenceId, int sourceId, - std::string&& snapshot, + std::vector&& snapshot, HighResTimeStamp expectedDisplayTime, ProcessId processId, ThreadId threadId) { - folly::dynamic args = folly::dynamic::object("snapshot", std::move(snapshot))( - "source_id", sourceId)("frame_sequence", frameSequenceId)( - "expected_display_time", - highResTimeStampToTracingClockTimeStamp(expectedDisplayTime)); + // 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", diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h index 26db3d499682..898fba6ef729 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h @@ -12,7 +12,9 @@ #include #include +#include #include +#include namespace facebook::react::jsinspector_modern::tracing { @@ -49,7 +51,7 @@ class TraceEventGenerator { static TraceEvent createScreenshotEvent( FrameSequenceId frameSequenceId, int sourceId, - std::string &&snapshot, + std::vector &&snapshot, HighResTimeStamp expectedDisplayTime, ProcessId processId, ThreadId threadId); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp index ad3c52d97493..46497c8240f4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventSerializer.cpp @@ -112,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 38d8eb6f6201..0240e2c5d2c2 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/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 0c045bcf92d2..efa82d77cd82 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<<08a361f2ffac6a0496adac1d4c3e4726>> + * @generated SignedSource<> */ /** @@ -262,10 +262,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 af8e15b1e6ed..ada4a01f3dd0 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<<0bbe4d41581432dfad7adbc2db133d00>> + * @generated SignedSource<<674bb7aa293aca110f6980f5e7fa619e>> */ /** @@ -334,11 +334,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 de6c64c0be12..dec5a7b015e5 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<<3dd9492ca660ad6350ce6ee4a9b5e310>> + * @generated SignedSource<> */ /** @@ -1091,6 +1091,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(59, "fuseboxFrameRecordingEnabled"); + + flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); + fuseboxFrameRecordingEnabled_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { auto flagValue = fuseboxNetworkInspectionEnabled_.load(); @@ -1100,7 +1118,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(59, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(60, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -1109,6 +1127,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(61, "fuseboxScreenshotCaptureEnabled"); + + flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); + fuseboxScreenshotCaptureEnabled_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::hideOffscreenVirtualViewsOnIOS() { auto flagValue = hideOffscreenVirtualViewsOnIOS_.load(); @@ -1118,7 +1154,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(60, "hideOffscreenVirtualViewsOnIOS"); + markFlagAsAccessed(62, "hideOffscreenVirtualViewsOnIOS"); flagValue = currentProvider_->hideOffscreenVirtualViewsOnIOS(); hideOffscreenVirtualViewsOnIOS_ = flagValue; @@ -1136,7 +1172,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(61, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(63, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1154,7 +1190,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(62, "perfIssuesEnabled"); + markFlagAsAccessed(64, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1172,7 +1208,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(63, "perfMonitorV2Enabled"); + markFlagAsAccessed(65, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1190,7 +1226,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(64, "preparedTextCacheSize"); + markFlagAsAccessed(66, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1208,7 +1244,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(65, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(67, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1226,7 +1262,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(66, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(68, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1244,7 +1280,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(67, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(69, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1262,7 +1298,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(68, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(70, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1280,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipToPaddingWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "syncAndroidClipToPaddingWithOverflow"); + markFlagAsAccessed(71, "syncAndroidClipToPaddingWithOverflow"); flagValue = currentProvider_->syncAndroidClipToPaddingWithOverflow(); syncAndroidClipToPaddingWithOverflow_ = flagValue; @@ -1298,7 +1334,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(70, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(72, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1316,7 +1352,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(71, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(73, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1334,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(74, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1352,7 +1388,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(73, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(75, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1370,7 +1406,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(74, "useFabricInterop"); + markFlagAsAccessed(76, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1388,7 +1424,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(75, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(77, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1406,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useNestedScrollViewAndroid"); + markFlagAsAccessed(78, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1424,7 +1460,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(77, "useSharedAnimatedBackend"); + markFlagAsAccessed(79, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1442,7 +1478,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(78, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(80, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1460,7 +1496,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(79, "useTurboModuleInterop"); + markFlagAsAccessed(81, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1478,7 +1514,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(80, "useTurboModules"); + markFlagAsAccessed(82, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1496,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(83, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = 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 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "viewTransitionEnabled"); + markFlagAsAccessed(85, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = 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 b3784c81fe63..451b622a8ba9 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<<2cb2c124f044468f96262086bfac5aad>> + * @generated SignedSource<> */ /** @@ -91,7 +91,9 @@ class ReactNativeFeatureFlagsAccessor { bool fixTextClippingAndroid15useBoundsForWidth(); 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_; @@ -189,7 +191,9 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> fixTextClippingAndroid15useBoundsForWidth_; 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 75ba8e143e40..73242e5af8cb 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<<7f1c4037925fc37dcdcba51df968d503>> + * @generated SignedSource<> */ /** @@ -263,10 +263,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 62a9247ac77d..ed5be8626d00 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<<932505a847d2290ab5940471b8a52891>> */ /** @@ -576,6 +576,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()) { @@ -585,6 +594,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 f1cff605711a..ccee1b2ffcd1 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<<43e301c5028a2ed195e2a3d0a4cd0ab3>> + * @generated SignedSource<> */ /** @@ -84,7 +84,9 @@ class ReactNativeFeatureFlagsProvider { virtual bool fixTextClippingAndroid15useBoundsForWidth() = 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 34101508393f..fa056c0b6739 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<<006c43555032f785207c6c013f1974f5>> + * @generated SignedSource<> */ /** @@ -339,11 +339,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 6675417c0e51..93d062972739 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<<525d64b15e1b72440743363280116c6a>> + * @generated SignedSource<> */ /** @@ -154,8 +154,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 fbdf02a31ffc..b2244b8c3ed4 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 c397bf14ae36..a8335aea3a96 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 5136c3420472..5ff41abd1484 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 0057445de0ac..ec879c2b62b3 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 a903495d8458..c31332b9e6c4 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 5e8c5a908923..f3de697074d7 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -678,6 +678,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: { @@ -689,6 +700,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + 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 a5d41dc0b7dc..2aa1507b31c2 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<<03688450419694f6d3f4fc709df4de9a>> + * @generated SignedSource<<2e4ed5f283ff3e54c82fca69bf3976bd>> * @flow strict * @noformat */ @@ -107,7 +107,9 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ fixTextClippingAndroid15useBoundsForWidth: Getter, fuseboxAssertSingleHostState: Getter, fuseboxEnabledRelease: Getter, + fuseboxFrameRecordingEnabled: Getter, fuseboxNetworkInspectionEnabled: Getter, + fuseboxScreenshotCaptureEnabled: Getter, hideOffscreenVirtualViewsOnIOS: Getter, overrideBySynchronousMountPropsAtMountingAndroid: Getter, perfIssuesEnabled: Getter, @@ -440,10 +442,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 4b47cc872f5c..63e74b4b5728 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<<2955ab3f744af8b5cdf587312ba423d7>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -84,7 +84,9 @@ export interface Spec extends TurboModule { +fixTextClippingAndroid15useBoundsForWidth?: () => boolean; +fuseboxAssertSingleHostState?: () => boolean; +fuseboxEnabledRelease?: () => boolean; + +fuseboxFrameRecordingEnabled?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; + +fuseboxScreenshotCaptureEnabled?: () => boolean; +hideOffscreenVirtualViewsOnIOS?: () => boolean; +overrideBySynchronousMountPropsAtMountingAndroid?: () => boolean; +perfIssuesEnabled?: () => boolean;