Skip to content
Open
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
42 changes: 26 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,12 @@ - (void)start
_lastFrameData.reset();
}

// Emit initial frame event
// Emit initial render frame
auto now = HighResTimeStamp::now();
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
auto vsyncDuration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(1)) /
UIScreen.mainScreen.maximumFramesPerSecond;
auto initialFrameEnd = now + HighResDuration::fromNanoseconds(vsyncDuration.count());
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:initialFrameEnd vsyncInterval:HighResDuration::zero()];

_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
Expand Down Expand Up @@ -115,11 +119,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 +137,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 +171,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 +182,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 +206,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 +230,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 @@ -18,6 +18,7 @@ import android.view.Window
import com.facebook.proguard.annotations.DoNotStripAny
import java.io.ByteArrayOutputStream
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.CoroutineDispatcher
Expand Down Expand Up @@ -54,6 +55,7 @@ internal class FrameTimingsObserver(
val threadId: Int,
val beginTimestamp: Long,
val endTimestamp: Long,
val vsyncIntervalNanos: Long,
)

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

// Emit initial frame event
// Emit initial render frame
val timestamp = System.nanoTime()
emitFrameTiming(timestamp, timestamp)
val fps = currentWindow?.decorView?.display?.refreshRate ?: 60f
val vsyncNanos = (TimeUnit.SECONDS.toNanos(1) / fps).toLong()
emitFrameTiming(timestamp, timestamp + vsyncNanos, vsyncIntervalNanos = 0)

currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler)
}
Expand Down Expand Up @@ -97,27 +101,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 +145,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 +163,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 +189,7 @@ internal class FrameTimingsObserver(
frameData.beginTimestamp,
frameData.endTimestamp,
screenshot,
frameData.vsyncIntervalNanos,
)
} finally {
frameData.bitmap.recycle()
Expand All @@ -187,6 +209,7 @@ internal class FrameTimingsObserver(
tailFrame.beginTimestamp,
tailFrame.endTimestamp,
screenshot,
tailFrame.vsyncIntervalNanos,
)
} finally {
tailFrame.bitmap.recycle()
Expand All @@ -201,6 +224,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 +250,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 @@ -241,6 +241,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