1414
1515#import < atomic>
1616#import < chrono>
17+ #import < mutex>
1718#import < optional>
1819#import < vector>
1920
2425static constexpr CGFloat kScreenshotScaleFactor = 1.0 ;
2526static 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