Skip to content

Commit 4e44b3e

Browse files
huntiefacebook-github-bot
authored andcommitted
Implement Performance frames and screenshots on iOS
Summary: 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. **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] Differential Revision: D95566220
1 parent 3b8455c commit 4e44b3e

4 files changed

Lines changed: 240 additions & 1 deletion

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
#import <jsinspector-modern/tracing/FrameTimingSequence.h>
11+
12+
using RCTFrameTimingCallback = void (^)(facebook::react::jsinspector_modern::tracing::FrameTimingSequence);
13+
14+
@interface RCTFrameTimingsObserver : NSObject
15+
16+
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback;
17+
- (void)start;
18+
- (void)stop;
19+
20+
@end
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
jsinspector_modern::tracing::ThreadId threadId =
89+
static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
90+
91+
if (_screenshotsEnabled) {
92+
[self _captureScreenshotWithCompletion:^(std::optional<std::vector<uint8_t>> screenshotData) {
93+
if (!self->_running.load()) {
94+
return;
95+
}
96+
jsinspector_modern::tracing::FrameTimingSequence sequence{
97+
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshotData)};
98+
self->_callback(std::move(sequence));
99+
}];
100+
} else {
101+
dispatch_async(_encodingQueue, ^{
102+
if (!self->_running.load(std::memory_order_relaxed)) {
103+
return;
104+
}
105+
jsinspector_modern::tracing::FrameTimingSequence sequence{frameId, threadId, beginTimestamp, endTimestamp};
106+
self->_callback(std::move(sequence));
107+
});
108+
}
109+
}
110+
111+
- (void)_captureScreenshotWithCompletion:(void (^)(std::optional<std::vector<uint8_t>>))completion
112+
{
113+
UIWindow *keyWindow = [self _getKeyWindow];
114+
if (!keyWindow) {
115+
completion(std::nullopt);
116+
return;
117+
}
118+
119+
UIView *rootView = keyWindow.rootViewController.view ?: keyWindow;
120+
CGSize viewSize = rootView.bounds.size;
121+
CGSize scaledSize = CGSizeMake(viewSize.width * kScreenshotScaleFactor, viewSize.height * kScreenshotScaleFactor);
122+
123+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
124+
format.scale = 1.0;
125+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format];
126+
127+
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
128+
[rootView drawViewHierarchyInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height) afterScreenUpdates:NO];
129+
}];
130+
131+
dispatch_async(_encodingQueue, ^{
132+
if (!self->_running.load(std::memory_order_relaxed)) {
133+
return;
134+
}
135+
NSData *jpegData = UIImageJPEGRepresentation(image, kScreenshotJPEGQuality);
136+
if (jpegData == nullptr) {
137+
completion(std::nullopt);
138+
return;
139+
}
140+
141+
const uint8_t *bytes = static_cast<const uint8_t *>(jpegData.bytes);
142+
std::vector<uint8_t> screenshotBytes(bytes, bytes + jpegData.length);
143+
completion(std::move(screenshotBytes));
144+
});
145+
}
146+
147+
- (UIWindow *)_getKeyWindow
148+
{
149+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
150+
if (scene.activationState == UISceneActivationStateForegroundActive &&
151+
[scene isKindOfClass:[UIWindowScene class]]) {
152+
UIWindowScene *windowScene = (UIWindowScene *)scene;
153+
for (UIWindow *window in windowScene.windows) {
154+
if (window.isKeyWindow) {
155+
return window;
156+
}
157+
}
158+
}
159+
}
160+
return nil;
161+
}
162+
163+
@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 || !host.inspectorTarget) {
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 && strongHost.inspectorTarget) {
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)