Skip to content

Commit 023ab28

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Implement Performance frames and screenshots on iOS (facebook#56015)
Summary: Pull Request resolved: facebook#56015 Implements CDP support for the Chrome DevTools Frames track on iOS during a performance trace, including screenshot capture. This initial version matches the corresponding implementation for Android: - Captures the key window. - Always emits an initial frame. - Resizes screenshots at 0.75 scale factor (normalized for device screen DPI) and 80% JPEG compression. - Uses a background thread queue for image resizing. **To dos** - [ ] Emit frames only when there is a visual update. - [ ] Dynamic frame sampling on slower devices (planned for Android). **Limitations** - Not a true screen recording, uses `UIGraphicsImageRenderer`. - Requires no permission prompt ✅ - This can mean empty data (a black screen) is sent during states such as system alert dialogs being presented. - Like Android, if the background queue builds (on slower hardware), screenshot data can be lost when the recording is ended. This feature is gated behind the `fuseboxFrameRecordingEnabled` flag. Changelog: [Internal] Reviewed By: sbuggay Differential Revision: D95566220
1 parent 8a8f1b8 commit 023ab28

File tree

4 files changed

+243
-1
lines changed

4 files changed

+243
-1
lines changed
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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
}
35+
36+
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback
37+
{
38+
if (self = [super init]) {
39+
_screenshotsEnabled = screenshotsEnabled;
40+
_callback = [callback copy];
41+
_frameCounter = 0;
42+
_encodingQueue = dispatch_queue_create("com.facebook.react.frame-timings-observer", DISPATCH_QUEUE_SERIAL);
43+
_running.store(false);
44+
}
45+
return self;
46+
}
47+
48+
- (void)start
49+
{
50+
_running.store(true, std::memory_order_relaxed);
51+
_frameCounter = 0;
52+
53+
// Emit an initial frame timing to ensure at least one frame is captured at the
54+
// start of tracing, even if no UI changes occur.
55+
auto now = HighResTimeStamp::now();
56+
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
57+
58+
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
59+
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
60+
}
61+
62+
- (void)stop
63+
{
64+
_running.store(false, std::memory_order_relaxed);
65+
[_displayLink invalidate];
66+
_displayLink = nil;
67+
}
68+
69+
- (void)_displayLinkTick:(CADisplayLink *)sender
70+
{
71+
// CADisplayLink.timestamp and targetTimestamp are in the same timebase as
72+
// CACurrentMediaTime() / mach_absolute_time(), which on Apple platforms maps
73+
// to CLOCK_UPTIME_RAW — the same clock backing std::chrono::steady_clock.
74+
auto beginNanos = static_cast<int64_t>(sender.timestamp * 1e9);
75+
auto endNanos = static_cast<int64_t>(sender.targetTimestamp * 1e9);
76+
77+
auto beginTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
78+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos)));
79+
auto endTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
80+
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(endNanos)));
81+
82+
[self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp];
83+
}
84+
85+
- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endTimestamp:(HighResTimeStamp)endTimestamp
86+
{
87+
uint64_t frameId = _frameCounter++;
88+
auto threadId = static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
89+
90+
if (_screenshotsEnabled) {
91+
[self _captureScreenshotWithCompletion:^(std::optional<std::vector<uint8_t>> screenshotData) {
92+
if (!self->_running.load()) {
93+
return;
94+
}
95+
jsinspector_modern::tracing::FrameTimingSequence sequence{
96+
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshotData)};
97+
self->_callback(std::move(sequence));
98+
}];
99+
} else {
100+
dispatch_async(_encodingQueue, ^{
101+
if (!self->_running.load(std::memory_order_relaxed)) {
102+
return;
103+
}
104+
jsinspector_modern::tracing::FrameTimingSequence sequence{frameId, threadId, beginTimestamp, endTimestamp};
105+
self->_callback(std::move(sequence));
106+
});
107+
}
108+
}
109+
110+
- (void)_captureScreenshotWithCompletion:(void (^)(std::optional<std::vector<uint8_t>>))completion
111+
{
112+
UIWindow *keyWindow = [self _getKeyWindow];
113+
if (keyWindow == nullptr) {
114+
completion(std::nullopt);
115+
return;
116+
}
117+
118+
UIView *rootView = (keyWindow.rootViewController.view != nullptr) ?: keyWindow;
119+
CGSize viewSize = rootView.bounds.size;
120+
CGSize scaledSize = CGSizeMake(viewSize.width * kScreenshotScaleFactor, viewSize.height * kScreenshotScaleFactor);
121+
122+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
123+
format.scale = 1.0;
124+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format];
125+
126+
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
127+
[rootView drawViewHierarchyInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height) afterScreenUpdates:NO];
128+
}];
129+
130+
dispatch_async(_encodingQueue, ^{
131+
if (!self->_running.load(std::memory_order_relaxed)) {
132+
return;
133+
}
134+
NSData *jpegData = UIImageJPEGRepresentation(image, kScreenshotJPEGQuality);
135+
if (jpegData == nullptr) {
136+
completion(std::nullopt);
137+
return;
138+
}
139+
140+
const auto *bytes = static_cast<const uint8_t *>(jpegData.bytes);
141+
std::vector<uint8_t> screenshotBytes(bytes, bytes + jpegData.length);
142+
completion(std::move(screenshotBytes));
143+
});
144+
}
145+
146+
- (UIWindow *)_getKeyWindow
147+
{
148+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
149+
if (scene.activationState == UISceneActivationStateForegroundActive &&
150+
[scene isKindOfClass:[UIWindowScene class]]) {
151+
auto windowScene = (UIWindowScene *)scene;
152+
for (UIWindow *window = nullptr in windowScene.windows) {
153+
if (window.isKeyWindow) {
154+
return window;
155+
}
156+
}
157+
}
158+
}
159+
return nil;
160+
}
161+
162+
@end

packages/react-native/ReactCommon/jsinspector-modern/HostTargetTracing.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void HostTarget::recordFrameTimings(
9393
std::lock_guard lock(tracingMutex_);
9494

9595
if (traceRecording_) {
96-
traceRecording_->recordFrameTimings(frameTimingSequence);
96+
traceRecording_->recordFrameTimings(std::move(frameTimingSequence));
9797
} else {
9898
assert(
9999
false &&

packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import <React/RCTConvert.h>
1414
#import <React/RCTDevMenu.h>
1515
#import <React/RCTFabricSurface.h>
16+
#import <React/RCTFrameTimingsObserver.h>
1617
#import <React/RCTInspectorDevServerHelper.h>
1718
#import <React/RCTInspectorNetworkHelper.h>
1819
#import <React/RCTInspectorUtils.h>
@@ -37,12 +38,52 @@ @interface RCTHost () <RCTReloadListener, RCTInstanceDelegate>
3738
@property (nonatomic, readonly) jsinspector_modern::HostTarget *inspectorTarget;
3839
@end
3940

41+
#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED)
42+
class RCTHostTracingDelegate : public jsinspector_modern::HostTargetTracingDelegate {
43+
public:
44+
explicit RCTHostTracingDelegate(RCTHost *host) : host_(host) {}
45+
46+
void onTracingStarted(jsinspector_modern::tracing::Mode /*tracingMode*/, bool screenshotsCategoryEnabled) override
47+
{
48+
RCTHost *host = host_;
49+
if (host == nil || host.inspectorTarget == nullptr) {
50+
return;
51+
}
52+
__weak RCTHost *weakHost = host;
53+
54+
observer_ = [[RCTFrameTimingsObserver alloc]
55+
initWithScreenshotsEnabled:screenshotsCategoryEnabled
56+
callback:^(jsinspector_modern::tracing::FrameTimingSequence sequence) {
57+
RCTHost *strongHost = weakHost;
58+
if (strongHost != nil && strongHost.inspectorTarget != nullptr) {
59+
strongHost.inspectorTarget->recordFrameTimings(std::move(sequence));
60+
}
61+
}];
62+
[observer_ start];
63+
}
64+
65+
void onTracingStopped() override
66+
{
67+
[observer_ stop];
68+
observer_ = nil;
69+
}
70+
71+
private:
72+
__weak RCTHost *host_;
73+
RCTFrameTimingsObserver *observer_{nil};
74+
};
75+
#endif
76+
4077
class RCTHostHostTargetDelegate : public facebook::react::jsinspector_modern::HostTargetDelegate {
4178
public:
4279
RCTHostHostTargetDelegate(RCTHost *host)
4380
: host_(host),
4481
pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]),
4582
networkHelper_([[RCTInspectorNetworkHelper alloc] init])
83+
#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED)
84+
,
85+
tracingDelegate_(host)
86+
#endif
4687
{
4788
}
4889

@@ -100,10 +141,25 @@ void loadNetworkResource(const RCTInspectorLoadNetworkResourceRequest &params, R
100141
[networkHelper_ loadNetworkResourceWithParams:params executor:executor];
101142
}
102143

144+
#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED)
145+
jsinspector_modern::HostTargetTracingDelegate *getTracingDelegate() override
146+
{
147+
auto &inspectorFlags = jsinspector_modern::InspectorFlags::getInstance();
148+
if (!inspectorFlags.getFrameRecordingEnabled()) {
149+
return nullptr;
150+
}
151+
152+
return &tracingDelegate_;
153+
}
154+
#endif
155+
103156
private:
104157
__weak RCTHost *host_;
105158
RCTPausedInDebuggerOverlayController *pauseOverlayController_;
106159
RCTInspectorNetworkHelper *networkHelper_;
160+
#if TARGET_OS_IPHONE && defined(REACT_NATIVE_DEBUGGER_ENABLED)
161+
RCTHostTracingDelegate tracingDelegate_;
162+
#endif
107163
};
108164

109165
@implementation RCTHost {

0 commit comments

Comments
 (0)