Skip to content

Commit 55b30ce

Browse files
hoxyqhuntie
authored andcommitted
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 - [LOCAL] Remove stale frame event code from PerformanceTracer - [LOCAL] Update Podfile.lock
1 parent ad2f7b4 commit 55b30ce

File tree

94 files changed

+3824
-416
lines changed

Some content is hidden

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

94 files changed

+3824
-416
lines changed

.claude/settings.local.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(gh log-list:*)",
5+
"Bash(for f:*)",
6+
"Bash(do)",
7+
"Bash(echo \"=== $f ===\")",
8+
"Read(//Users/huntie/Development/forks/facebook/react-native/**)",
9+
"Bash(done)"
10+
]
11+
}
12+
}
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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 <optional>
18+
#import <vector>
19+
20+
#import <react/timing/primitives.h>
21+
22+
using namespace facebook::react;
23+
24+
static constexpr CGFloat kScreenshotScaleFactor = 0.75;
25+
static constexpr CGFloat kScreenshotJPEGQuality = 0.8;
26+
27+
@implementation RCTFrameTimingsObserver {
28+
BOOL _screenshotsEnabled;
29+
RCTFrameTimingCallback _callback;
30+
CADisplayLink *_displayLink;
31+
uint64_t _frameCounter;
32+
dispatch_queue_t _encodingQueue;
33+
std::atomic<bool> _running;
34+
uint64_t _lastScreenshotHash;
35+
}
36+
37+
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback
38+
{
39+
if (self = [super init]) {
40+
_screenshotsEnabled = screenshotsEnabled;
41+
_callback = [callback copy];
42+
_frameCounter = 0;
43+
_encodingQueue = dispatch_queue_create("com.facebook.react.frame-timings-observer", DISPATCH_QUEUE_SERIAL);
44+
_running.store(false);
45+
_lastScreenshotHash = 0;
46+
}
47+
return self;
48+
}
49+
50+
- (void)start
51+
{
52+
_running.store(true, std::memory_order_relaxed);
53+
_frameCounter = 0;
54+
_lastScreenshotHash = 0;
55+
56+
// Emit an initial frame timing to ensure at least one frame is captured at the
57+
// start of tracing, even if no UI changes occur.
58+
auto now = HighResTimeStamp::now();
59+
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
60+
61+
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
62+
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
63+
}
64+
65+
- (void)stop
66+
{
67+
_running.store(false, std::memory_order_relaxed);
68+
[_displayLink invalidate];
69+
_displayLink = nil;
70+
}
71+
72+
- (void)_displayLinkTick:(CADisplayLink *)sender
73+
{
74+
// CADisplayLink.timestamp and targetTimestamp are in the same timebase as
75+
// CACurrentMediaTime() / mach_absolute_time(), which on Apple platforms maps
76+
// to CLOCK_UPTIME_RAW — the same clock backing std::chrono::steady_clock.
77+
auto beginNanos = static_cast<int64_t>(sender.timestamp * 1e9);
78+
auto endNanos = static_cast<int64_t>(sender.targetTimestamp * 1e9);
79+
80+
auto beginTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
81+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos)));
82+
auto endTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
83+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(endNanos)));
84+
85+
[self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp];
86+
}
87+
88+
- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endTimestamp:(HighResTimeStamp)endTimestamp
89+
{
90+
uint64_t frameId = _frameCounter++;
91+
auto threadId = static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
92+
93+
if (_screenshotsEnabled) {
94+
[self _captureScreenshotWithCompletion:^(std::optional<std::vector<uint8_t>> screenshotData) {
95+
if (!self->_running.load()) {
96+
return;
97+
}
98+
jsinspector_modern::tracing::FrameTimingSequence sequence{
99+
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshotData)};
100+
self->_callback(std::move(sequence));
101+
}];
102+
} else {
103+
dispatch_async(_encodingQueue, ^{
104+
if (!self->_running.load(std::memory_order_relaxed)) {
105+
return;
106+
}
107+
jsinspector_modern::tracing::FrameTimingSequence sequence{frameId, threadId, beginTimestamp, endTimestamp};
108+
self->_callback(std::move(sequence));
109+
});
110+
}
111+
}
112+
113+
- (void)_captureScreenshotWithCompletion:(void (^)(std::optional<std::vector<uint8_t>>))completion
114+
{
115+
UIWindow *keyWindow = [self _getKeyWindow];
116+
if (keyWindow == nullptr) {
117+
completion(std::nullopt);
118+
return;
119+
}
120+
121+
UIView *rootView = keyWindow.rootViewController.view ?: keyWindow;
122+
CGSize viewSize = rootView.bounds.size;
123+
CGSize scaledSize = CGSizeMake(viewSize.width * kScreenshotScaleFactor, viewSize.height * kScreenshotScaleFactor);
124+
125+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
126+
format.scale = 1.0;
127+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format];
128+
129+
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
130+
[rootView drawViewHierarchyInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height) afterScreenUpdates:NO];
131+
}];
132+
133+
// Skip duplicate frames via sampled FNV-1a pixel hash
134+
CGImageRef cgImage = image.CGImage;
135+
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
136+
uint64_t hash = 0xcbf29ce484222325ULL;
137+
const uint8_t *ptr = CFDataGetBytePtr(pixelData);
138+
CFIndex length = CFDataGetLength(pixelData);
139+
// Use prime stride to prevent row alignment on power-of-2 pixel widths
140+
for (CFIndex i = 0; i < length; i += 67) {
141+
hash ^= ptr[i];
142+
hash *= 0x100000001b3ULL;
143+
}
144+
CFRelease(pixelData);
145+
146+
if (hash == _lastScreenshotHash) {
147+
return;
148+
}
149+
_lastScreenshotHash = hash;
150+
151+
dispatch_async(_encodingQueue, ^{
152+
if (!self->_running.load(std::memory_order_relaxed)) {
153+
return;
154+
}
155+
NSData *jpegData = UIImageJPEGRepresentation(image, kScreenshotJPEGQuality);
156+
if (jpegData == nullptr) {
157+
completion(std::nullopt);
158+
return;
159+
}
160+
161+
const auto *bytes = static_cast<const uint8_t *>(jpegData.bytes);
162+
std::vector<uint8_t> screenshotBytes(bytes, bytes + jpegData.length);
163+
completion(std::move(screenshotBytes));
164+
});
165+
}
166+
167+
- (UIWindow *)_getKeyWindow
168+
{
169+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
170+
if (scene.activationState == UISceneActivationStateForegroundActive &&
171+
[scene isKindOfClass:[UIWindowScene class]]) {
172+
auto windowScene = (UIWindowScene *)scene;
173+
for (UIWindow *window = nullptr in windowScene.windows) {
174+
if (window.isKeyWindow) {
175+
return window;
176+
}
177+
}
178+
}
179+
}
180+
return nil;
181+
}
182+
183+
@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
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener
5252
import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled
5353
import com.facebook.react.devsupport.StackTraceHelper.convertJavaStackTrace
5454
import com.facebook.react.devsupport.StackTraceHelper.convertJsStackTrace
55+
import com.facebook.react.devsupport.inspector.TracingState
56+
import com.facebook.react.devsupport.inspector.TracingStateProvider
5557
import com.facebook.react.devsupport.interfaces.BundleLoadCallback
5658
import com.facebook.react.devsupport.interfaces.DebuggerFrontendPanelName
5759
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
@@ -66,8 +68,6 @@ import com.facebook.react.devsupport.interfaces.PackagerStatusCallback
6668
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
6769
import com.facebook.react.devsupport.interfaces.RedBoxHandler
6870
import com.facebook.react.devsupport.interfaces.StackFrame
69-
import com.facebook.react.devsupport.interfaces.TracingState
70-
import com.facebook.react.devsupport.interfaces.TracingStateProvider
7171
import com.facebook.react.devsupport.perfmonitor.PerfMonitorDevHelper
7272
import com.facebook.react.devsupport.perfmonitor.PerfMonitorOverlayManager
7373
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
@@ -396,21 +396,21 @@ public abstract class DevSupportManagerBase(
396396

397397
val analyzePerformanceItemString =
398398
when (tracingState) {
399-
TracingState.ENABLEDINBACKGROUNDMODE ->
399+
TracingState.ENABLED_IN_BACKGROUND_MODE ->
400400
applicationContext.getString(R.string.catalyst_performance_background)
401-
TracingState.ENABLEDINCDPMODE ->
401+
TracingState.ENABLED_IN_CDP_MODE ->
402402
applicationContext.getString(R.string.catalyst_performance_cdp)
403403
TracingState.DISABLED ->
404404
applicationContext.getString(R.string.catalyst_performance_disabled)
405405
}
406406

407-
if (!isConnected || tracingState == TracingState.ENABLEDINCDPMODE) {
407+
if (!isConnected || tracingState == TracingState.ENABLED_IN_CDP_MODE) {
408408
disabledItemKeys.add(analyzePerformanceItemString)
409409
}
410410

411411
options[analyzePerformanceItemString] =
412412
when (tracingState) {
413-
TracingState.ENABLEDINBACKGROUNDMODE ->
413+
TracingState.ENABLED_IN_BACKGROUND_MODE ->
414414
DevOptionHandler {
415415
UiThreadUtil.runOnUiThread {
416416
if (reactInstanceDevHelper is PerfMonitorDevHelper) {
@@ -427,7 +427,7 @@ public abstract class DevSupportManagerBase(
427427
if (reactInstanceDevHelper is PerfMonitorDevHelper)
428428
reactInstanceDevHelper.inspectorTarget?.resumeBackgroundTrace()
429429
}
430-
TracingState.ENABLEDINCDPMODE -> DevOptionHandler {}
430+
TracingState.ENABLED_IN_CDP_MODE -> DevOptionHandler {}
431431
}
432432
}
433433

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ internal object InspectorFlags {
1717
SoLoader.loadLibrary("react_devsupportjni")
1818
}
1919

20+
@DoNotStrip @JvmStatic external fun getScreenshotCaptureEnabled(): Boolean
21+
2022
@DoNotStrip @JvmStatic external fun getFuseboxEnabled(): Boolean
2123

2224
@DoNotStrip @JvmStatic external fun getIsProfilingBuild(): Boolean
25+
26+
@DoNotStrip @JvmStatic external fun getFrameRecordingEnabled(): Boolean
2327
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
package com.facebook.react.devsupport.inspector
9+
10+
internal data class FrameTimingSequence(
11+
val id: Int,
12+
val threadId: Int,
13+
val beginTimestamp: Long,
14+
val endTimestamp: Long,
15+
val screenshot: ByteArray? = null,
16+
)

0 commit comments

Comments
 (0)