Skip to content

Commit 2dd3405

Browse files
committed
Backport performance debugging improvements
Backports some CDP Performance features and stability improvements to `0.83-stable`. This includes: - Remaining implementation of Frame Timings and screenshot capture in performance traces (Android) - New support for Frame Timings and screenshot capture on iOS - Optimisations to trace chunk generation, memory usage during performance recording - `Page.captureScreenshot` (Android, iOS) All features remain gated behind feature flags (`fuseboxFrameRecordingEnabled`, `fuseboxScreenshotCaptureEnabled`). **Commits applied** - Define TracingCategory enum (#54377) bd6c6bf - Use TracingCategory in TraceEvent (#54378) ce60f27 - Simple parser for serialized tracing categories (#54379) 62275b6 - Propagate tracing categories to recording state (#54380) 3841ef0 - Re-land "Revert D85999774: [rn][android] Add FrameTiming module" (#54502) 85905ad - Add screenshot category (#54537) 5d9cb80 - Define HostTargetTracingDelegate (#54622) be0dae0 - Define TracingDelegate for Android Host (#54628) a212e95 - Use new API for tracing state on Android (#54629) 532d0be - Internalize TracingState interface (#54631) 58ec261 - Move TracingState interfaces to inspector package (#54632) 10d0d99 - Rename values of TracingState enum (#54630) be94707 - FrameTiming module to subscribe to Inspector tracing lifecycle (#54633) 04ee02b - Clarify HostTracingProfile and HostTraceRecordingState (#54677) ec92f12 - Introduce TimeWindowedBuffer (#54679) d6ee54f - Keep tracing time window on TraceRecordingState (#54673) e651563 - Define FrameTimingSequence (#54674) 5b7d92f - Define an endpoint on HostTarget to capture frame timings (#54672) f469aa3 - Extract frame trace events construction logic (#54675) 57973fa - Define serializers for frame timings as part of HostTargetTracingProfile (#54681) 587d360 - Propagate Frames data through Host (#54671) 26ff069 - Cleanup no longer used jni layer for frame timings (#54678) 9ba4ce6 - Fix screenshot typo (#54742) 52186a3 - Add optional screenshot argument to FrameTimingSequence (#54743) 57c29fc - Frame screenshot event generation (#54744) b447d26 - Frame screenshots capture implementation (#54745) f3c9a8d - BeginDrawing: INTENDED_VSYNC_TIMESTAMP -> VSYNC_TIMESTAMP (#54765) 9221772 - Add frames category (#54768) 31a0c9a - Specify correct category for SetLayerTreeId event (#54769) cd055e9 - Reduce the screenshots size (#54800) e4a5a56 - Remove Commit from FrameTimingSequence (#54779) 39f7703 - Fix potential bitmap leaks in FrameTimingsObserver (#55652) 3918dd1 - Capture initial screenshot when starting frame timing trace (#55720) 863f5c0 - Refactor FrameTimingsObserver for multi-activity support (#55740) 8230f3b - Reorder FrameTimingsObserver methods (#55743) f56b295 - Increase frame capture quality, apply scaling after DPI normalization (#55731) e5f9f5f - Fix FrameTimingsObverver to initiate PixelCopy on main thread (#55744) d552f2a - Fix bitmap reuse race condition in FrameTimingsObserver (#55745) 3924768 - Fix trailing frame capture after recording ended (#55704) 47684ca - Move base64Encode into react/utils (#55873) 422770d - Move screenshot Base64 encoding to trace serialization (#55803) dc4c36e - Restore inspector addPage listener callback (#55925) 8c763fc - Add tracing helper functions for RNDT traces in C++ and Kotlin (#55935) 4f3f536 - Introduce fuseboxFrameRecordingEnabled flag, gate existing code (#55941) 1aa7a32 - Implement Performance frames and screenshots on iOS (#56015) 64a1a10 - Add pixel diffing to RCTFrameTimingsObserver (#56043) 9d231af - Add dynamic sampling to frame screenshots (#56048) d309cda - Switch trace event chunks from event count to size based (#56080) d3b33f5 - Increase trace screenshot scale factor to 1x (#56079) 972a30d - Add Page.captureScreenshot CDP support (#56307) 77332d2 - Fix data race on PerformanceObserver entry buffer (#56352) 5eb1ca1 - Add dynamic sampling to frame screenshots (#56135) e0d1e29 - Increase trace screenshot scale factor to 1x (#56136) 90e02fa - Add missing debugger define for React-RuntimeApple (#56397) - [LOCAL] Remove stale frame event code from PerformanceTracer - [LOCAL] Update Podfile.lock
1 parent ad2f7b4 commit 2dd3405

File tree

95 files changed

+3932
-417
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+3932
-417
lines changed

packages/react-native/Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,10 @@ let reactRuntimeApple = RNTarget(
400400
name: .reactRuntimeApple,
401401
path: "ReactCommon/react/runtime/platform/ios",
402402
excludedPaths: ["ReactCommon/RCTJscInstance.mm", "ReactCommon/metainternal"],
403-
dependencies: [.reactNativeDependencies, .jsi, .reactPerfLogger, .reactCxxReact, .rctDeprecation, .yoga, .reactRuntime, .reactRCTFabric, .reactCoreModules, .reactTurboModuleCore, .hermesPrebuilt, .reactUtils]
403+
dependencies: [.reactNativeDependencies, .jsi, .reactPerfLogger, .reactCxxReact, .rctDeprecation, .yoga, .reactRuntime, .reactRCTFabric, .reactCoreModules, .reactTurboModuleCore, .hermesPrebuilt, .reactUtils],
404+
defines: [
405+
CXXSetting.define("REACT_NATIVE_DEBUGGER_ENABLED", to: "1", .when(configuration: BuildConfiguration.debug))
406+
]
404407
)
405408

406409
let publicHeadersPathForReactCore: String = BUILD_FROM_SOURCE ? "includes" : "."
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#ifdef __cplusplus
11+
#import <jsinspector-modern/tracing/FrameTimingSequence.h>
12+
13+
using RCTFrameTimingCallback = void (^)(facebook::react::jsinspector_modern::tracing::FrameTimingSequence);
14+
#endif
15+
16+
@interface RCTFrameTimingsObserver : NSObject
17+
18+
#ifdef __cplusplus
19+
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback;
20+
#endif
21+
- (void)start;
22+
- (void)stop;
23+
24+
@end
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTFrameTimingsObserver.h"
9+
10+
#import <UIKit/UIKit.h>
11+
12+
#import <mach/thread_act.h>
13+
#import <pthread.h>
14+
15+
#import <atomic>
16+
#import <chrono>
17+
#import <mutex>
18+
#import <optional>
19+
#import <vector>
20+
21+
#import <react/timing/primitives.h>
22+
23+
using namespace facebook::react;
24+
25+
static constexpr CGFloat kScreenshotScaleFactor = 1.0;
26+
static constexpr CGFloat kScreenshotJPEGQuality = 0.8;
27+
28+
namespace {
29+
30+
// Stores a captured frame screenshot and its associated metadata, used for
31+
// buffering frames during dynamic sampling.
32+
struct FrameData {
33+
UIImage *image;
34+
uint64_t frameId;
35+
jsinspector_modern::tracing::ThreadId threadId;
36+
HighResTimeStamp beginTimestamp;
37+
HighResTimeStamp endTimestamp;
38+
};
39+
40+
} // namespace
41+
42+
@implementation RCTFrameTimingsObserver {
43+
BOOL _screenshotsEnabled;
44+
RCTFrameTimingCallback _callback;
45+
CADisplayLink *_displayLink;
46+
uint64_t _frameCounter;
47+
// Serial queue for encoding work (single background thread). We limit to 1
48+
// thread to minimize the performance impact of screenshot recording.
49+
dispatch_queue_t _encodingQueue;
50+
std::atomic<bool> _running;
51+
uint64_t _lastScreenshotHash;
52+
53+
// Stores the most recently captured frame to opportunistically encode after
54+
// the current frame. Replaced frames are emitted as timings without
55+
// screenshots.
56+
std::mutex _lastFrameMutex;
57+
std::optional<FrameData> _lastFrameData;
58+
59+
std::atomic<bool> _encodingInProgress;
60+
}
61+
62+
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback
63+
{
64+
if (self = [super init]) {
65+
_screenshotsEnabled = screenshotsEnabled;
66+
_callback = [callback copy];
67+
_frameCounter = 0;
68+
_encodingQueue = dispatch_queue_create("com.facebook.react.frame-timings-observer", DISPATCH_QUEUE_SERIAL);
69+
_running.store(false);
70+
_lastScreenshotHash = 0;
71+
_encodingInProgress.store(false);
72+
}
73+
return self;
74+
}
75+
76+
- (void)start
77+
{
78+
_running.store(true, std::memory_order_relaxed);
79+
_frameCounter = 0;
80+
_lastScreenshotHash = 0;
81+
_encodingInProgress.store(false, std::memory_order_relaxed);
82+
{
83+
std::lock_guard<std::mutex> lock(_lastFrameMutex);
84+
_lastFrameData.reset();
85+
}
86+
87+
// Emit initial frame event
88+
auto now = HighResTimeStamp::now();
89+
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
90+
91+
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
92+
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
93+
}
94+
95+
- (void)stop
96+
{
97+
_running.store(false, std::memory_order_relaxed);
98+
[_displayLink invalidate];
99+
_displayLink = nil;
100+
{
101+
std::lock_guard<std::mutex> lock(_lastFrameMutex);
102+
_lastFrameData.reset();
103+
}
104+
}
105+
106+
- (void)_displayLinkTick:(CADisplayLink *)sender
107+
{
108+
// CADisplayLink.timestamp and targetTimestamp are in the same timebase as
109+
// CACurrentMediaTime() / mach_absolute_time(), which on Apple platforms maps
110+
// to CLOCK_UPTIME_RAW — the same clock backing std::chrono::steady_clock.
111+
auto beginNanos = static_cast<int64_t>(sender.timestamp * 1e9);
112+
auto endNanos = static_cast<int64_t>(sender.targetTimestamp * 1e9);
113+
114+
auto beginTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
115+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos)));
116+
auto endTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
117+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(endNanos)));
118+
119+
[self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp];
120+
}
121+
122+
- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endTimestamp:(HighResTimeStamp)endTimestamp
123+
{
124+
uint64_t frameId = _frameCounter++;
125+
auto threadId = static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
126+
127+
if (!_screenshotsEnabled) {
128+
// Screenshots disabled - emit without screenshot
129+
[self _emitFrameEventWithFrameId:frameId
130+
threadId:threadId
131+
beginTimestamp:beginTimestamp
132+
endTimestamp:endTimestamp
133+
screenshot:std::nullopt];
134+
return;
135+
}
136+
137+
UIImage *image = [self _captureScreenshot];
138+
if (image == nil) {
139+
// Failed to capture (e.g. no window, duplicate hash) - emit without screenshot
140+
[self _emitFrameEventWithFrameId:frameId
141+
threadId:threadId
142+
beginTimestamp:beginTimestamp
143+
endTimestamp:endTimestamp
144+
screenshot:std::nullopt];
145+
return;
146+
}
147+
148+
FrameData frameData{image, frameId, threadId, beginTimestamp, endTimestamp};
149+
150+
bool expected = false;
151+
if (_encodingInProgress.compare_exchange_strong(expected, true)) {
152+
// Not encoding - encode this frame immediately
153+
[self _encodeFrame:std::move(frameData)];
154+
} else {
155+
// Encoding thread busy - store current screenshot in buffer for tail-capture
156+
std::optional<FrameData> oldFrame;
157+
{
158+
std::lock_guard<std::mutex> lock(_lastFrameMutex);
159+
oldFrame = std::move(_lastFrameData);
160+
_lastFrameData = std::move(frameData);
161+
}
162+
if (oldFrame.has_value()) {
163+
// Skipped frame - emit event without screenshot
164+
[self _emitFrameEventWithFrameId:oldFrame->frameId
165+
threadId:oldFrame->threadId
166+
beginTimestamp:oldFrame->beginTimestamp
167+
endTimestamp:oldFrame->endTimestamp
168+
screenshot:std::nullopt];
169+
}
170+
}
171+
}
172+
173+
- (void)_emitFrameEventWithFrameId:(uint64_t)frameId
174+
threadId:(jsinspector_modern::tracing::ThreadId)threadId
175+
beginTimestamp:(HighResTimeStamp)beginTimestamp
176+
endTimestamp:(HighResTimeStamp)endTimestamp
177+
screenshot:(std::optional<std::vector<uint8_t>>)screenshot
178+
{
179+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
180+
if (!self->_running.load(std::memory_order_relaxed)) {
181+
return;
182+
}
183+
jsinspector_modern::tracing::FrameTimingSequence sequence{
184+
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshot)};
185+
self->_callback(std::move(sequence));
186+
});
187+
}
188+
189+
- (void)_encodeFrame:(FrameData)frameData
190+
{
191+
dispatch_async(_encodingQueue, ^{
192+
if (!self->_running.load(std::memory_order_relaxed)) {
193+
return;
194+
}
195+
196+
auto screenshot = [self _encodeScreenshot:frameData.image];
197+
[self _emitFrameEventWithFrameId:frameData.frameId
198+
threadId:frameData.threadId
199+
beginTimestamp:frameData.beginTimestamp
200+
endTimestamp:frameData.endTimestamp
201+
screenshot:std::move(screenshot)];
202+
203+
// Clear encoding flag early, allowing new frames to start fresh encoding
204+
// sessions
205+
self->_encodingInProgress.store(false, std::memory_order_release);
206+
207+
// Opportunistically encode tail frame (if present) without blocking new
208+
// frames
209+
std::optional<FrameData> tailFrame;
210+
{
211+
std::lock_guard<std::mutex> lock(self->_lastFrameMutex);
212+
tailFrame = std::move(self->_lastFrameData);
213+
self->_lastFrameData.reset();
214+
}
215+
if (tailFrame.has_value()) {
216+
if (!self->_running.load(std::memory_order_relaxed)) {
217+
return;
218+
}
219+
auto tailScreenshot = [self _encodeScreenshot:tailFrame->image];
220+
[self _emitFrameEventWithFrameId:tailFrame->frameId
221+
threadId:tailFrame->threadId
222+
beginTimestamp:tailFrame->beginTimestamp
223+
endTimestamp:tailFrame->endTimestamp
224+
screenshot:std::move(tailScreenshot)];
225+
}
226+
});
227+
}
228+
229+
// Captures a screenshot of the current window. Must be called on the main
230+
// thread. Returns nil if capture fails or if the frame content is unchanged.
231+
- (UIImage *)_captureScreenshot
232+
{
233+
UIWindow *keyWindow = [self _getKeyWindow];
234+
if (keyWindow == nil) {
235+
return nil;
236+
}
237+
238+
UIView *rootView = keyWindow.rootViewController.view ?: keyWindow;
239+
CGSize viewSize = rootView.bounds.size;
240+
CGSize scaledSize = CGSizeMake(viewSize.width * kScreenshotScaleFactor, viewSize.height * kScreenshotScaleFactor);
241+
242+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
243+
format.scale = 1.0;
244+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format];
245+
246+
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
247+
[rootView drawViewHierarchyInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height) afterScreenUpdates:NO];
248+
}];
249+
250+
// Skip duplicate frames via sampled FNV-1a pixel hash
251+
CGImageRef cgImage = image.CGImage;
252+
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
253+
uint64_t hash = 0xcbf29ce484222325ULL;
254+
const uint8_t *ptr = CFDataGetBytePtr(pixelData);
255+
CFIndex length = CFDataGetLength(pixelData);
256+
// Use prime stride to prevent row alignment on power-of-2 pixel widths
257+
for (CFIndex i = 0; i < length; i += 67) {
258+
hash ^= ptr[i];
259+
hash *= 0x100000001b3ULL;
260+
}
261+
CFRelease(pixelData);
262+
263+
if (hash == _lastScreenshotHash) {
264+
return nil;
265+
}
266+
_lastScreenshotHash = hash;
267+
268+
return image;
269+
}
270+
271+
- (std::optional<std::vector<uint8_t>>)_encodeScreenshot:(UIImage *)image
272+
{
273+
NSData *jpegData = UIImageJPEGRepresentation(image, kScreenshotJPEGQuality);
274+
if (jpegData == nil) {
275+
return std::nullopt;
276+
}
277+
278+
const auto *bytes = static_cast<const uint8_t *>(jpegData.bytes);
279+
return std::vector<uint8_t>(bytes, bytes + jpegData.length);
280+
}
281+
282+
- (UIWindow *)_getKeyWindow
283+
{
284+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
285+
if (scene.activationState == UISceneActivationStateForegroundActive &&
286+
[scene isKindOfClass:[UIWindowScene class]]) {
287+
auto windowScene = (UIWindowScene *)scene;
288+
for (UIWindow *window = nullptr in windowScene.windows) {
289+
if (window.isKeyWindow) {
290+
return window;
291+
}
292+
}
293+
}
294+
}
295+
return nil;
296+
}
297+
298+
@end

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,15 +2257,6 @@ public abstract interface class com/facebook/react/devsupport/interfaces/StackFr
22572257
public abstract fun toJSON ()Lorg/json/JSONObject;
22582258
}
22592259

2260-
public final class com/facebook/react/devsupport/interfaces/TracingState : java/lang/Enum {
2261-
public static final field DISABLED Lcom/facebook/react/devsupport/interfaces/TracingState;
2262-
public static final field ENABLEDINBACKGROUNDMODE Lcom/facebook/react/devsupport/interfaces/TracingState;
2263-
public static final field ENABLEDINCDPMODE Lcom/facebook/react/devsupport/interfaces/TracingState;
2264-
public static fun getEntries ()Lkotlin/enums/EnumEntries;
2265-
public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/devsupport/interfaces/TracingState;
2266-
public static fun values ()[Lcom/facebook/react/devsupport/interfaces/TracingState;
2267-
}
2268-
22692260
public final class com/facebook/react/fabric/ComponentFactory {
22702261
public fun <init> ()V
22712262
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ package com.facebook.react.devsupport
1010
import android.content.Context
1111
import com.facebook.react.bridge.UiThreadUtil
1212
import com.facebook.react.common.SurfaceDelegateFactory
13+
import com.facebook.react.devsupport.inspector.TracingState
1314
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
1415
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
1516
import com.facebook.react.devsupport.interfaces.DevSupportManager
1617
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
1718
import com.facebook.react.devsupport.interfaces.RedBoxHandler
18-
import com.facebook.react.devsupport.interfaces.TracingState
1919
import com.facebook.react.packagerconnection.RequestHandler
2020

2121
/**
@@ -83,6 +83,6 @@ internal class BridgelessDevSupportManager(
8383
}
8484

8585
fun tracingState(): TracingState {
86-
return TracingState.ENABLEDINCDPMODE
86+
return TracingState.ENABLED_IN_CDP_MODE
8787
}
8888
}

0 commit comments

Comments
 (0)