Skip to content

Commit 1e0f497

Browse files
committed
Add dynamic sampling to frame screenshots (facebook#56135)
Summary: Pull Request resolved: facebook#56135 Ports the dynamic sampling algorithm from Android (D95987488) to iOS. Uses an `encodingInProgress` atomic flag with a single encoding thread and `lastFrameData` buffer to skip screenshot encoding when the encoder is busy, while always emitting frame timing events. This prevents truncated trace data on slower devices and reduces recording overhead. Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D97131485 fbshipit-source-id: 5c30f1052d5b5a61f27f2ab90946ef47b8b6b279
1 parent 2071867 commit 1e0f497

1 file changed

Lines changed: 149 additions & 34 deletions

File tree

packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm

Lines changed: 149 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#import <atomic>
1616
#import <chrono>
17+
#import <mutex>
1718
#import <optional>
1819
#import <vector>
1920

@@ -24,14 +25,38 @@
2425
static constexpr CGFloat kScreenshotScaleFactor = 1.0;
2526
static constexpr CGFloat kScreenshotJPEGQuality = 0.8;
2627

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+
2742
@implementation RCTFrameTimingsObserver {
2843
BOOL _screenshotsEnabled;
2944
RCTFrameTimingCallback _callback;
3045
CADisplayLink *_displayLink;
3146
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.
3249
dispatch_queue_t _encodingQueue;
3350
std::atomic<bool> _running;
3451
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;
3560
}
3661

3762
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback
@@ -43,6 +68,7 @@ - (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RC
4368
_encodingQueue = dispatch_queue_create("com.facebook.react.frame-timings-observer", DISPATCH_QUEUE_SERIAL);
4469
_running.store(false);
4570
_lastScreenshotHash = 0;
71+
_encodingInProgress.store(false);
4672
}
4773
return self;
4874
}
@@ -52,9 +78,13 @@ - (void)start
5278
_running.store(true, std::memory_order_relaxed);
5379
_frameCounter = 0;
5480
_lastScreenshotHash = 0;
81+
_encodingInProgress.store(false, std::memory_order_relaxed);
82+
{
83+
std::lock_guard<std::mutex> lock(_lastFrameMutex);
84+
_lastFrameData.reset();
85+
}
5586

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.
87+
// Emit initial frame event
5888
auto now = HighResTimeStamp::now();
5989
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
6090

@@ -67,6 +97,10 @@ - (void)stop
6797
_running.store(false, std::memory_order_relaxed);
6898
[_displayLink invalidate];
6999
_displayLink = nil;
100+
{
101+
std::lock_guard<std::mutex> lock(_lastFrameMutex);
102+
_lastFrameData.reset();
103+
}
70104
}
71105

72106
- (void)_displayLinkTick:(CADisplayLink *)sender
@@ -90,32 +124,115 @@ - (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endT
90124
uint64_t frameId = _frameCounter++;
91125
auto threadId = static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
92126

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-
}];
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)];
102154
} else {
103-
dispatch_async(_encodingQueue, ^{
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()) {
104216
if (!self->_running.load(std::memory_order_relaxed)) {
105217
return;
106218
}
107-
jsinspector_modern::tracing::FrameTimingSequence sequence{frameId, threadId, beginTimestamp, endTimestamp};
108-
self->_callback(std::move(sequence));
109-
});
110-
}
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+
});
111227
}
112228

113-
- (void)_captureScreenshotWithCompletion:(void (^)(std::optional<std::vector<uint8_t>>))completion
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
114232
{
115233
UIWindow *keyWindow = [self _getKeyWindow];
116-
if (keyWindow == nullptr) {
117-
completion(std::nullopt);
118-
return;
234+
if (keyWindow == nil) {
235+
return nil;
119236
}
120237

121238
UIView *rootView = keyWindow.rootViewController.view ?: keyWindow;
@@ -144,24 +261,22 @@ - (void)_captureScreenshotWithCompletion:(void (^)(std::optional<std::vector<uin
144261
CFRelease(pixelData);
145262

146263
if (hash == _lastScreenshotHash) {
147-
return;
264+
return nil;
148265
}
149266
_lastScreenshotHash = hash;
150267

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-
}
268+
return image;
269+
}
160270

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-
});
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);
165280
}
166281

167282
- (UIWindow *)_getKeyWindow

0 commit comments

Comments
 (0)