Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions packages/react-native/React/DevSupport/RCTFrameTimingsObserver.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
jsinspector_modern::tracing::ThreadId threadId;
HighResTimeStamp beginTimestamp;
HighResTimeStamp endTimestamp;
HighResDuration vsyncInterval;
};

} // namespace
Expand Down Expand Up @@ -84,9 +85,11 @@ - (void)start
_lastFrameData.reset();
}

// Emit initial frame event
// Emit an initial render frame. Keep it zero-duration: the frontend derives
// each frame's rendered length from the gap to the next frame, so a synthetic
// end time here can overlap the first real frame and corrupt the frames track.
auto now = HighResTimeStamp::now();
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now vsyncInterval:HighResDuration::zero()];

_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
Expand Down Expand Up @@ -115,11 +118,14 @@ - (void)_displayLinkTick:(CADisplayLink *)sender
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos)));
auto endTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(endNanos)));
auto vsyncInterval = HighResDuration::fromNanoseconds(static_cast<int64_t>(sender.duration * 1e9));

[self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp];
[self _emitFrameTimingWithBeginTimestamp:beginTimestamp endTimestamp:endTimestamp vsyncInterval:vsyncInterval];
}

- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endTimestamp:(HighResTimeStamp)endTimestamp
- (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp
endTimestamp:(HighResTimeStamp)endTimestamp
vsyncInterval:(HighResDuration)vsyncInterval
{
uint64_t frameId = _frameCounter++;
auto threadId = static_cast<jsinspector_modern::tracing::ThreadId>(pthread_mach_thread_np(pthread_self()));
Expand All @@ -130,22 +136,21 @@ - (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endT
threadId:threadId
beginTimestamp:beginTimestamp
endTimestamp:endTimestamp
screenshot:std::nullopt];
screenshot:std::nullopt
vsyncInterval:vsyncInterval];
return;
}

UIImage *image = [self _captureScreenshot];
if (image == nil) {
// Failed to capture (e.g. no window, duplicate hash) - emit without screenshot
[self _emitFrameEventWithFrameId:frameId
threadId:threadId
beginTimestamp:beginTimestamp
endTimestamp:endTimestamp
screenshot:std::nullopt];
// Screenshot unchanged (duplicate hash) or capture failed — don't emit
// a frame event. The serializer will fill the resulting gap with an idle
// frame, matching Chrome's native behavior where idle = vsync with no
// new rendering.
return;
}

FrameData frameData{image, frameId, threadId, beginTimestamp, endTimestamp};
FrameData frameData{image, frameId, threadId, beginTimestamp, endTimestamp, vsyncInterval};

bool expected = false;
if (_encodingInProgress.compare_exchange_strong(expected, true)) {
Expand All @@ -165,7 +170,8 @@ - (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endT
threadId:oldFrame->threadId
beginTimestamp:oldFrame->beginTimestamp
endTimestamp:oldFrame->endTimestamp
screenshot:std::nullopt];
screenshot:std::nullopt
vsyncInterval:oldFrame->vsyncInterval];
}
}
}
Expand All @@ -175,13 +181,14 @@ - (void)_emitFrameEventWithFrameId:(uint64_t)frameId
beginTimestamp:(HighResTimeStamp)beginTimestamp
endTimestamp:(HighResTimeStamp)endTimestamp
screenshot:(std::optional<std::vector<uint8_t>>)screenshot
vsyncInterval:(HighResDuration)vsyncInterval
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
if (!self->_running.load(std::memory_order_relaxed)) {
return;
}
jsinspector_modern::tracing::FrameTimingSequence sequence{
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshot)};
frameId, threadId, beginTimestamp, endTimestamp, std::move(screenshot), vsyncInterval};
self->_callback(std::move(sequence));
});
}
Expand All @@ -198,7 +205,8 @@ - (void)_encodeFrame:(FrameData)frameData
threadId:frameData.threadId
beginTimestamp:frameData.beginTimestamp
endTimestamp:frameData.endTimestamp
screenshot:std::move(screenshot)];
screenshot:std::move(screenshot)
vsyncInterval:frameData.vsyncInterval];

// Clear encoding flag early, allowing new frames to start fresh encoding
// sessions
Expand All @@ -221,7 +229,8 @@ - (void)_encodeFrame:(FrameData)frameData
threadId:tailFrame->threadId
beginTimestamp:tailFrame->beginTimestamp
endTimestamp:tailFrame->endTimestamp
screenshot:std::move(tailScreenshot)];
screenshot:std::move(tailScreenshot)
vsyncInterval:tailFrame->vsyncInterval];
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ internal data class FrameTimingSequence(
val beginTimestamp: Long,
val endTimestamp: Long,
val screenshot: ByteArray? = null,
val vsyncIntervalNanos: Long = 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal class FrameTimingsObserver(
val threadId: Int,
val beginTimestamp: Long,
val endTimestamp: Long,
val vsyncIntervalNanos: Long,
)

fun start() {
Expand All @@ -66,9 +67,11 @@ internal class FrameTimingsObserver(
lastFrameBuffer.set(null)
isTracing = true

// Emit initial frame event
// Emit an initial render frame. Keep it zero-duration: the frontend derives
// each frame's rendered length from the gap to the next frame, so a synthetic
// end time here can overlap the first real frame and corrupt the frames track.
val timestamp = System.nanoTime()
emitFrameTiming(timestamp, timestamp)
emitFrameTiming(timestamp, timestamp, vsyncIntervalNanos = 0)

currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler)
}
Expand Down Expand Up @@ -97,27 +100,35 @@ internal class FrameTimingsObserver(
}
}

private val frameMetricsListener = Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ ->
// Guard against calls after stop()
if (!isTracing) {
return@OnFrameMetricsAvailableListener
}
val beginTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
val endTimestamp = beginTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
emitFrameTiming(beginTimestamp, endTimestamp)
}
private val frameMetricsListener =
Window.OnFrameMetricsAvailableListener { window, frameMetrics, _ ->
// Guard against calls after stop()
if (!isTracing) {
return@OnFrameMetricsAvailableListener
}
val beginTimestamp = frameMetrics.getMetric(FrameMetrics.VSYNC_TIMESTAMP)
val endTimestamp = beginTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
val refreshRate = window.decorView.display?.refreshRate ?: 60f
val vsyncIntervalNanos = (1_000_000_000L / refreshRate).toLong()
emitFrameTiming(beginTimestamp, endTimestamp, vsyncIntervalNanos)
}

private fun emitFrameTiming(beginTimestamp: Long, endTimestamp: Long) {
private fun emitFrameTiming(
beginTimestamp: Long,
endTimestamp: Long,
vsyncIntervalNanos: Long = 0,
) {
val frameId = frameCounter++
val threadId = Process.myTid()

if (!screenshotsEnabled) {
// Screenshots disabled - emit without screenshot
emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null)
emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null, vsyncIntervalNanos)
return
}

captureScreenshot(frameId, threadId, beginTimestamp, endTimestamp) { frameData ->
captureScreenshot(frameId, threadId, beginTimestamp, endTimestamp, vsyncIntervalNanos) {
frameData ->
if (frameData != null) {
if (encodingInProgress.compareAndSet(false, true)) {
// Not encoding - encode this frame immediately
Expand All @@ -133,13 +144,14 @@ internal class FrameTimingsObserver(
oldFrameData.beginTimestamp,
oldFrameData.endTimestamp,
null,
oldFrameData.vsyncIntervalNanos,
)
oldFrameData.bitmap.recycle()
}
}
} else {
// Failed to capture (e.g. timeout) - emit without screenshot
emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null)
emitFrameEvent(frameId, threadId, beginTimestamp, endTimestamp, null, vsyncIntervalNanos)
}
}
}
Expand All @@ -150,10 +162,18 @@ internal class FrameTimingsObserver(
beginTimestamp: Long,
endTimestamp: Long,
screenshot: ByteArray?,
vsyncIntervalNanos: Long = 0,
) {
CoroutineScope(Dispatchers.Default).launch {
onFrameTimingSequence(
FrameTimingSequence(frameId, threadId, beginTimestamp, endTimestamp, screenshot),
FrameTimingSequence(
frameId,
threadId,
beginTimestamp,
endTimestamp,
screenshot,
vsyncIntervalNanos,
),
)
}
}
Expand All @@ -168,6 +188,7 @@ internal class FrameTimingsObserver(
frameData.beginTimestamp,
frameData.endTimestamp,
screenshot,
frameData.vsyncIntervalNanos,
)
} finally {
frameData.bitmap.recycle()
Expand All @@ -187,6 +208,7 @@ internal class FrameTimingsObserver(
tailFrame.beginTimestamp,
tailFrame.endTimestamp,
screenshot,
tailFrame.vsyncIntervalNanos,
)
} finally {
tailFrame.bitmap.recycle()
Expand All @@ -201,6 +223,7 @@ internal class FrameTimingsObserver(
threadId: Int,
beginTimestamp: Long,
endTimestamp: Long,
vsyncIntervalNanos: Long,
callback: (FrameData?) -> Unit,
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Expand All @@ -226,7 +249,16 @@ internal class FrameTimingsObserver(
bitmap,
{ copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
callback(FrameData(bitmap, frameId, threadId, beginTimestamp, endTimestamp))
callback(
FrameData(
bitmap,
frameId,
threadId,
beginTimestamp,
endTimestamp,
vsyncIntervalNanos,
),
)
} else {
bitmap.recycle()
callback(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<3335ab1242e91e4b85f5514a39a65002>>
* @generated SignedSource<<22ab94c6b0cc8f7ee39d546d9b93540a>>
*/

/**
Expand All @@ -31,6 +31,8 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android : ReactN

override fun enableSwiftUIBasedFilters(): Boolean = true

override fun fuseboxFrameRecordingEnabled(): Boolean = true

override fun fuseboxWebSocketEventsEnabled(): Boolean = true

override fun useNativeViewConfigsInBridgelessMode(): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<41f83a9921de88f659ddc22172344226>>
* @generated SignedSource<<7103d9c2fa3e2f152ce60b5dff12c0e4>>
*/

/**
Expand All @@ -29,8 +29,6 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android :

override fun enableSchedulerDelegateInvalidation(): Boolean = true

override fun fuseboxFrameRecordingEnabled(): Boolean = true

override fun preventShadowTreeCommitExhaustion(): Boolean = true

override fun useSharedAnimatedBackend(): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ void JReactHostInspectorTarget::recordFrameTimings(
frameTimingSequence->getBeginTimestamp(),
frameTimingSequence->getEndTimestamp(),
frameTimingSequence->getScreenshot(),
frameTimingSequence->getVsyncInterval(),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ struct JFrameTimingSequence : public jni::JavaClass<JFrameTimingSequence> {
}
return std::nullopt;
}

HighResDuration getVsyncInterval() const
{
auto field = javaClassStatic()->getField<jlong>("vsyncIntervalNanos");
return HighResDuration::fromNanoseconds(static_cast<int64_t>(getFieldValue(field)));
}
};

struct JReactHostImpl : public jni::JavaClass<JReactHostImpl> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ struct FrameTimingSequence {
ThreadId threadId,
HighResTimeStamp beginTimestamp,
HighResTimeStamp endTimestamp,
std::optional<std::vector<uint8_t>> screenshot = std::nullopt)
std::optional<std::vector<uint8_t>> screenshot = std::nullopt,
HighResDuration vsyncInterval = HighResDuration::zero())
: id(id),
threadId(threadId),
beginTimestamp(beginTimestamp),
endTimestamp(endTimestamp),
screenshot(std::move(screenshot))
screenshot(std::move(screenshot)),
vsyncInterval(vsyncInterval)
{
}

Expand All @@ -56,6 +58,12 @@ struct FrameTimingSequence {
* Optional screenshot data captured during the frame.
*/
std::optional<std::vector<uint8_t>> screenshot;

/**
* Duration of one vsync interval from the device's display refresh rate.
* Zero when unknown (e.g. the initial synthetic frame).
*/
HighResDuration vsyncInterval = HighResDuration::zero();
};

} // namespace facebook::react::jsinspector_modern::tracing
Loading
Loading