Skip to content

Commit ac9d145

Browse files
huntiefacebook-github-bot
authored andcommitted
Add idle frame support to Performance timeline
Summary: Implement Idle frame spans in Chrome DevTools, by emitting synthetic `NeedsBeginFrameChanged` + `BeginFrame` + `DrawFrame` events. An idle frame = a vsync where no new rendering occurred. Changelog: [Internal] Differential Revision: D97502569
1 parent 5dc27af commit ac9d145

5 files changed

Lines changed: 175 additions & 16 deletions

File tree

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ @implementation RCTFrameTimingsObserver {
5757
std::optional<FrameData> _lastFrameData;
5858

5959
std::atomic<bool> _encodingInProgress;
60+
61+
// Offset (in nanoseconds) from mach_absolute_time (CLOCK_UPTIME_RAW) to
62+
// steady_clock (CLOCK_MONOTONIC_RAW). CADisplayLink timestamps use
63+
// mach_absolute_time which excludes sleep time, while HighResTimeStamp
64+
// uses steady_clock which includes it. Without this correction, all
65+
// CADisplayLink timestamps are shifted into the past by cumulative device
66+
// sleep time.
67+
int64_t _clockOffsetNanos;
6068
}
6169

6270
- (instancetype)initWithScreenshotsEnabled:(BOOL)screenshotsEnabled callback:(RCTFrameTimingCallback)callback
@@ -79,14 +87,25 @@ - (void)start
7987
_frameCounter = 0;
8088
_lastScreenshotHash = 0;
8189
_encodingInProgress.store(false, std::memory_order_relaxed);
90+
91+
// Compute the offset between steady_clock and mach_absolute_time once at
92+
// start, so we can correctly convert CADisplayLink timestamps.
93+
{
94+
auto steadyNow = std::chrono::steady_clock::now();
95+
CFTimeInterval machNow = CACurrentMediaTime();
96+
auto steadyNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(steadyNow.time_since_epoch()).count();
97+
_clockOffsetNanos = steadyNanos - static_cast<int64_t>(machNow * 1e9);
98+
}
99+
82100
{
83101
std::lock_guard<std::mutex> lock(_lastFrameMutex);
84102
_lastFrameData.reset();
85103
}
86104

87-
// Emit initial frame event
105+
// Emit initial render frame with screenshot
88106
auto now = HighResTimeStamp::now();
89-
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:now];
107+
auto initialFrameEnd = now + HighResDuration::fromNanoseconds(16'600'000);
108+
[self _emitFrameTimingWithBeginTimestamp:now endTimestamp:initialFrameEnd];
90109

91110
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick:)];
92111
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
@@ -105,11 +124,12 @@ - (void)stop
105124

106125
- (void)_displayLinkTick:(CADisplayLink *)sender
107126
{
108-
// CADisplayLink.timestamp and targetTimestamp are in the same timebase as
109-
// CACurrentMediaTime() / mach_absolute_time(), which on Apple platforms maps
110-
// to CLOCK_UPTIME_RAW — the same clock backing std::chrono::steady_clock.
111-
auto beginNanos = static_cast<int64_t>(sender.timestamp * 1e9);
112-
auto endNanos = static_cast<int64_t>(sender.targetTimestamp * 1e9);
127+
// CADisplayLink.timestamp and targetTimestamp use CACurrentMediaTime() /
128+
// mach_absolute_time() (CLOCK_UPTIME_RAW), which excludes system sleep time.
129+
// Apply the offset computed at start to convert to the steady_clock
130+
// (CLOCK_MONOTONIC_RAW) timebase used by HighResTimeStamp.
131+
auto beginNanos = static_cast<int64_t>(sender.timestamp * 1e9) + _clockOffsetNanos;
132+
auto endNanos = static_cast<int64_t>(sender.targetTimestamp * 1e9) + _clockOffsetNanos;
113133

114134
auto beginTimestamp = HighResTimeStamp::fromChronoSteadyClockTimePoint(
115135
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(beginNanos)));
@@ -136,12 +156,10 @@ - (void)_emitFrameTimingWithBeginTimestamp:(HighResTimeStamp)beginTimestamp endT
136156

137157
UIImage *image = [self _captureScreenshot];
138158
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];
159+
// Screenshot unchanged (duplicate hash) or capture failed — don't emit
160+
// a frame event. The serializer will fill the resulting gap with an idle
161+
// frame, matching Chrome's native behavior where idle = vsync with no
162+
// new rendering.
145163
return;
146164
}
147165

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ internal class FrameTimingsObserver(
6666
lastFrameBuffer.set(null)
6767
isTracing = true
6868

69-
// Emit initial frame event
69+
// Emit initial render frame with screenshot
7070
val timestamp = System.nanoTime()
71-
emitFrameTiming(timestamp, timestamp)
71+
emitFrameTiming(timestamp, timestamp + INITIAL_FRAME_DURATION_NS)
7272

7373
currentWindow?.addOnFrameMetricsAvailableListener(frameMetricsListener, mainHandler)
7474
}
@@ -263,6 +263,9 @@ internal class FrameTimingsObserver(
263263
}
264264

265265
companion object {
266+
// Spans ~one vsync at 60Hz.
267+
private const val INITIAL_FRAME_DURATION_NS = 16_600_000L
268+
266269
private const val SCREENSHOT_SCALE_FACTOR = 1.0f
267270
private const val SCREENSHOT_QUALITY = 80
268271

packages/react-native/ReactCommon/jsinspector-modern/tracing/HostTracingProfileSerializer.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#include "TraceEventGenerator.h"
1111
#include "TraceEventSerializer.h"
1212

13+
#include <algorithm>
14+
1315
namespace facebook::react::jsinspector_modern::tracing {
1416

1517
namespace {
@@ -104,11 +106,82 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
104106
TraceEventSerializer::estimateJsonSize(serializedSetLayerTreeId);
105107
chunk.push_back(std::move(serializedSetLayerTreeId));
106108

109+
// Filter out frames that started before recording began. On Android,
110+
// FrameMetrics may deliver frames from app startup that predate the recording
111+
// session; on iOS the first CADisplayLink callback reports sender.timestamp
112+
// (the previous vsync) which can be before the recording start. These would
113+
// otherwise appear as large pre-recording render frames in the timeline.
114+
frameTimings.erase(
115+
std::remove_if(
116+
frameTimings.begin(),
117+
frameTimings.end(),
118+
[&recordingStartTimestamp](const FrameTimingSequence& ft) {
119+
return ft.beginTimestamp < recordingStartTimestamp;
120+
}),
121+
frameTimings.end());
122+
123+
if (frameTimings.empty()) {
124+
chunkCallback(std::move(chunk));
125+
return;
126+
}
127+
128+
// Sort frames by beginTimestamp to handle out-of-order arrivals caused by
129+
// async screenshot encoding. The initial synthetic frame may arrive in the
130+
// buffer after real frames because its screenshot encoding takes longer than
131+
// one vsync (~16ms). Sorting ensures the idle-gap detection loop below sees
132+
// frames in chronological order.
133+
std::sort(
134+
frameTimings.begin(),
135+
frameTimings.end(),
136+
[](const FrameTimingSequence& a, const FrameTimingSequence& b) {
137+
return a.beginTimestamp < b.beginTimestamp;
138+
});
139+
140+
// Compute the next available sequence ID for synthetic idle frames.
141+
FrameSequenceId nextIdleSeqId = 0;
142+
for (const auto& ft : frameTimings) {
143+
nextIdleSeqId = std::max(nextIdleSeqId, ft.id + 1);
144+
}
145+
146+
std::optional<HighResTimeStamp> prevEndTimestamp;
147+
107148
for (auto&& frameTimingSequence : frameTimings) {
108149
// Serialize all events for this frame.
109150
folly::dynamic frameEvents = folly::dynamic::array();
110151
size_t totalFrameBytes = 0;
111152

153+
// Detect idle period: gap between previous frame's end and this frame's
154+
// begin. Emit NeedsBeginFrameChanged + BeginFrame to fill the gap.
155+
// Chrome DevTools renders a BeginFrame without a corresponding DrawFrame
156+
// as an "Idle frame" in the Frames track.
157+
if (prevEndTimestamp.has_value() &&
158+
frameTimingSequence.beginTimestamp > *prevEndTimestamp) {
159+
auto needsBeginFrameEvent =
160+
TraceEventGenerator::createNeedsBeginFrameChangedEvent(
161+
FALLBACK_LAYER_TREE_ID,
162+
*prevEndTimestamp,
163+
processId,
164+
frameTimingSequence.threadId);
165+
auto serializedNeedsBeginFrame =
166+
TraceEventSerializer::serialize(std::move(needsBeginFrameEvent));
167+
totalFrameBytes +=
168+
TraceEventSerializer::estimateJsonSize(serializedNeedsBeginFrame);
169+
frameEvents.push_back(std::move(serializedNeedsBeginFrame));
170+
171+
auto idleBeginEvent = TraceEventGenerator::createIdleBeginFrameEvent(
172+
nextIdleSeqId++,
173+
FALLBACK_LAYER_TREE_ID,
174+
*prevEndTimestamp,
175+
processId,
176+
frameTimingSequence.threadId);
177+
178+
auto serializedIdleBegin =
179+
TraceEventSerializer::serialize(std::move(idleBeginEvent));
180+
totalFrameBytes +=
181+
TraceEventSerializer::estimateJsonSize(serializedIdleBegin);
182+
frameEvents.push_back(std::move(serializedIdleBegin));
183+
}
184+
112185
auto [beginDrawingEvent, endDrawingEvent] =
113186
TraceEventGenerator::createFrameTimingsEvents(
114187
frameTimingSequence.id,
@@ -155,6 +228,8 @@ constexpr int FALLBACK_LAYER_TREE_ID = 1;
155228
chunk.push_back(std::move(frameEvent));
156229
}
157230
currentChunkBytes += totalFrameBytes;
231+
232+
prevEndTimestamp = frameTimingSequence.endTimestamp;
158233
}
159234

160235
if (!chunk.empty()) {

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,47 @@ namespace facebook::react::jsinspector_modern::tracing {
3434
};
3535
}
3636

37+
/* static */ TraceEvent TraceEventGenerator::createNeedsBeginFrameChangedEvent(
38+
int layerTreeId,
39+
HighResTimeStamp timestamp,
40+
ProcessId processId,
41+
ThreadId threadId) {
42+
folly::dynamic data = folly::dynamic::object("needsBeginFrame", 1);
43+
44+
return TraceEvent{
45+
.name = "NeedsBeginFrameChanged",
46+
.cat = {Category::Frame},
47+
.ph = 'I',
48+
.ts = timestamp,
49+
.pid = processId,
50+
.s = 't',
51+
.tid = threadId,
52+
.args = folly::dynamic::object("layerTreeId", layerTreeId)(
53+
"data", std::move(data)),
54+
};
55+
}
56+
57+
/* static */ TraceEvent TraceEventGenerator::createIdleBeginFrameEvent(
58+
FrameSequenceId sequenceId,
59+
int layerTreeId,
60+
HighResTimeStamp timestamp,
61+
ProcessId processId,
62+
ThreadId threadId) {
63+
folly::dynamic args = folly::dynamic::object("frameSeqId", sequenceId)(
64+
"layerTreeId", layerTreeId);
65+
66+
return TraceEvent{
67+
.name = "BeginFrame",
68+
.cat = {Category::Frame},
69+
.ph = 'I',
70+
.ts = timestamp,
71+
.pid = processId,
72+
.s = 't',
73+
.tid = threadId,
74+
.args = std::move(args),
75+
};
76+
}
77+
3778
/* static */ std::pair<TraceEvent, TraceEvent>
3879
TraceEventGenerator::createFrameTimingsEvents(
3980
uint64_t sequenceId,

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,29 @@ class TraceEventGenerator {
3535
HighResTimeStamp timestamp);
3636

3737
/**
38-
* Creates canonical "BeginFrame", "Commit", "DrawFrame" trace events.
38+
* Creates a "NeedsBeginFrameChanged" trace event to mark the start of an
39+
* idle frame period.
40+
*/
41+
static TraceEvent createNeedsBeginFrameChangedEvent(
42+
int layerTreeId,
43+
HighResTimeStamp timestamp,
44+
ProcessId processId,
45+
ThreadId threadId);
46+
47+
/**
48+
* Creates a single "BeginFrame" trace event for an idle frame (no
49+
* DrawFrame). Chrome DevTools renders a BeginFrame without a corresponding
50+
* DrawFrame as an "Idle frame" in the Frames track.
51+
*/
52+
static TraceEvent createIdleBeginFrameEvent(
53+
FrameSequenceId sequenceId,
54+
int layerTreeId,
55+
HighResTimeStamp timestamp,
56+
ProcessId processId,
57+
ThreadId threadId);
58+
59+
/**
60+
* Creates canonical "BeginFrame", "DrawFrame" trace events.
3961
*/
4062
static std::pair<TraceEvent, TraceEvent> createFrameTimingsEvents(
4163
FrameSequenceId sequenceId,

0 commit comments

Comments
 (0)