Skip to content

Commit 108c3ec

Browse files
committed
Add frame data to time to display spans
1 parent c680ff0 commit 108c3ec

1 file changed

Lines changed: 177 additions & 22 deletions

File tree

packages/core/src/js/tracing/timetodisplay.tsx

Lines changed: 177 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import type { Span,StartSpanOptions } from '@sentry/core';
1+
/* eslint-disable max-lines */
2+
import type { Span, StartSpanOptions } from '@sentry/core';
23
import { fill, getActiveSpan, getSpanDescendants, logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, spanToJSON, startInactiveSpan } from '@sentry/core';
34
import * as React from 'react';
45
import { useState } from 'react';
56

7+
import type { NativeFramesResponse } from '../NativeRNSentry';
68
import { isTurboModuleEnabled } from '../utils/environment';
9+
import { NATIVE } from '../wrapper';
710
import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from './origin';
811
import { getRNSentryOnDrawReporter, nativeComponentExists } from './timetodisplaynative';
912
import { setSpanDurationAsMeasurement, setSpanDurationAsMeasurementOnSpan } from './utils';
1013

1114
let nativeComponentMissingLogged = false;
1215

16+
/**
17+
* Timeout for fetching native frames
18+
*/
19+
const FETCH_FRAMES_TIMEOUT_MS = 2_000;
20+
1321
/**
1422
* Flags of active spans with manual initial display.
1523
*/
@@ -20,6 +28,13 @@ export const manualInitialDisplaySpans = new WeakMap<Span, true>();
2028
*/
2129
const fullDisplayBeforeInitialDisplay = new WeakMap<Span, true>();
2230

31+
interface FrameDataForSpan {
32+
startFrames: NativeFramesResponse | null;
33+
endFrames: NativeFramesResponse | null;
34+
}
35+
36+
const spanFrameDataMap = new Map<string, FrameDataForSpan>();
37+
2338
export type TimeToDisplayProps = {
2439
children?: React.ReactNode;
2540
record?: boolean;
@@ -106,7 +121,7 @@ export function startTimeToInitialDisplaySpan(
106121
const existingSpan = getSpanDescendants(activeSpan).find((span) => spanToJSON(span).op === 'ui.load.initial_display');
107122
if (existingSpan) {
108123
logger.debug(`[TimeToDisplay] Found existing ui.load.initial_display span.`);
109-
return existingSpan
124+
return existingSpan;
110125
}
111126

112127
const initialDisplaySpan = startInactiveSpan({
@@ -120,6 +135,10 @@ export function startTimeToInitialDisplaySpan(
120135
return undefined;
121136
}
122137

138+
captureStartFramesForSpan(initialDisplaySpan.spanContext().spanId).catch((error) => {
139+
logger.debug(`[TimeToDisplay] Failed to capture start frames for initial display span (${initialDisplaySpan.spanContext().spanId}).`, error);
140+
});
141+
123142
if (options?.isAutoInstrumented) {
124143
initialDisplaySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY);
125144
} else {
@@ -176,13 +195,26 @@ export function startTimeToFullDisplaySpan(
176195
return undefined;
177196
}
178197

198+
captureStartFramesForSpan(fullDisplaySpan.spanContext().spanId).catch((error) => {
199+
logger.debug(`[TimeToDisplay] Failed to capture start frames for full display span(${fullDisplaySpan.spanContext().spanId}).`, error);
200+
});
201+
179202
const timeout = setTimeout(() => {
180203
if (spanToJSON(fullDisplaySpan).timestamp) {
181204
return;
182205
}
183206
fullDisplaySpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' });
184-
fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp);
185-
setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan);
207+
208+
captureEndFramesAndAttachToSpan(fullDisplaySpan).then(() => {
209+
logger.debug(`[TimeToDisplay] ${fullDisplaySpan.spanContext().spanId} span updated with frame data.`);
210+
fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp);
211+
setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan);
212+
}).catch(() => {
213+
logger.warn(`[TimeToDisplay] Failed to capture end frames for full display span (${fullDisplaySpan.spanContext().spanId}).`);
214+
fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp);
215+
setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan);
216+
});
217+
186218
logger.warn(`[TimeToDisplay] Full display span deadline_exceeded.`);
187219
}, options.timeoutMs);
188220

@@ -235,17 +267,31 @@ export function updateInitialDisplaySpan(
235267
return;
236268
}
237269

238-
span.end(frameTimestampSeconds);
239-
span.setStatus({ code: SPAN_STATUS_OK });
240-
logger.debug(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp.`);
270+
captureEndFramesAndAttachToSpan(span).then(() => {
271+
span.end(frameTimestampSeconds);
272+
span.setStatus({ code: SPAN_STATUS_OK });
273+
logger.debug(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp and frame data.`);
241274

242-
if (fullDisplayBeforeInitialDisplay.has(activeSpan)) {
243-
fullDisplayBeforeInitialDisplay.delete(activeSpan);
244-
logger.debug(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`);
245-
updateFullDisplaySpan(frameTimestampSeconds, span);
246-
}
275+
if (fullDisplayBeforeInitialDisplay.has(activeSpan)) {
276+
fullDisplayBeforeInitialDisplay.delete(activeSpan);
277+
logger.debug(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`);
278+
updateFullDisplaySpan(frameTimestampSeconds, span);
279+
}
247280

248-
setSpanDurationAsMeasurementOnSpan('time_to_initial_display', span, activeSpan);
281+
setSpanDurationAsMeasurementOnSpan('time_to_initial_display', span, activeSpan);
282+
}).catch((error) => {
283+
logger.debug(`[TimeToDisplay] Failed to capture frame data for initial display span.`, error);
284+
span.end(frameTimestampSeconds);
285+
span.setStatus({ code: SPAN_STATUS_OK });
286+
287+
if (fullDisplayBeforeInitialDisplay.has(activeSpan)) {
288+
fullDisplayBeforeInitialDisplay.delete(activeSpan);
289+
logger.debug(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`);
290+
updateFullDisplaySpan(frameTimestampSeconds, span);
291+
}
292+
293+
setSpanDurationAsMeasurementOnSpan('time_to_initial_display', span, activeSpan);
294+
});
249295
}
250296

251297
function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDisplaySpan?: Span): void {
@@ -278,17 +324,26 @@ function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDispl
278324
return;
279325
}
280326

281-
if (initialDisplayEndTimestamp > frameTimestampSeconds) {
282-
logger.warn(`[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.`);
283-
span.end(initialDisplayEndTimestamp);
284-
} else {
285-
span.end(frameTimestampSeconds);
286-
}
327+
captureEndFramesAndAttachToSpan(span).then(() => {
328+
const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds;
287329

288-
span.setStatus({ code: SPAN_STATUS_OK });
289-
logger.debug(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span updated with end timestamp.`);
330+
if (initialDisplayEndTimestamp > frameTimestampSeconds) {
331+
logger.warn(`[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.`);
332+
}
290333

291-
setSpanDurationAsMeasurement('time_to_full_display', span);
334+
span.end(endTimestamp);
335+
span.setStatus({ code: SPAN_STATUS_OK });
336+
logger.debug(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span updated with end timestamp and frame data.`);
337+
338+
setSpanDurationAsMeasurement('time_to_full_display', span);
339+
}).catch((error) => {
340+
logger.debug(`[TimeToDisplay] Failed to capture frame data for full display span.`, error);
341+
const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds;
342+
343+
span.end(endTimestamp);
344+
span.setStatus({ code: SPAN_STATUS_OK });
345+
setSpanDurationAsMeasurement('time_to_full_display', span);
346+
});
292347
}
293348

294349
/**
@@ -342,3 +397,103 @@ function createTimeToDisplay({
342397
TimeToDisplayWrapper.displayName = `TimeToDisplayWrapper`;
343398
return TimeToDisplayWrapper;
344399
}
400+
401+
/**
402+
* Attaches frame data to a span's data object.
403+
*/
404+
function attachFrameDataToSpan(span: Span, startFrames: NativeFramesResponse, endFrames: NativeFramesResponse): void {
405+
const totalFrames = endFrames.totalFrames - startFrames.totalFrames;
406+
const slowFrames = endFrames.slowFrames - startFrames.slowFrames;
407+
const frozenFrames = endFrames.frozenFrames - startFrames.frozenFrames;
408+
409+
if (totalFrames <= 0 && slowFrames <= 0 && frozenFrames <= 0) {
410+
logger.warn(`[TimeToDisplay] Detected zero slow or frozen frames. Not adding measurements to span (${span.spanContext().spanId}).`);
411+
return;
412+
}
413+
span.setAttribute('frames.total', totalFrames);
414+
span.setAttribute('frames.slow', slowFrames);
415+
span.setAttribute('frames.frozen', frozenFrames);
416+
417+
logger.debug('[TimeToDisplay] Attached frame data to span.', {
418+
spanId: span.spanContext().spanId,
419+
frameData: {
420+
total: totalFrames,
421+
slow: slowFrames,
422+
frozen: frozenFrames,
423+
},
424+
});
425+
}
426+
427+
/**
428+
* Captures start frames for a time-to-display span
429+
*/
430+
async function captureStartFramesForSpan(spanId: string): Promise<void> {
431+
if (!NATIVE.enableNative) {
432+
return;
433+
}
434+
435+
try {
436+
const startFrames = await fetchNativeFramesWithTimeout();
437+
if (!spanFrameDataMap.has(spanId)) {
438+
spanFrameDataMap.set(spanId, { startFrames: null, endFrames: null });
439+
}
440+
const frameData = spanFrameDataMap.get(spanId)!;
441+
frameData.startFrames = startFrames;
442+
logger.debug(`[TimeToDisplay] Captured start frames for span ${spanId}.`, startFrames);
443+
} catch (error) {
444+
logger.debug(`[TimeToDisplay] Failed to capture start frames for span ${spanId}.`, error);
445+
}
446+
}
447+
448+
/**
449+
* Captures end frames and attaches frame data to span
450+
*/
451+
async function captureEndFramesAndAttachToSpan(span: Span): Promise<void> {
452+
if (!NATIVE.enableNative) {
453+
return;
454+
}
455+
456+
const spanId = span.spanContext().spanId;
457+
const frameData = spanFrameDataMap.get(spanId);
458+
459+
if (!frameData?.startFrames) {
460+
logger.debug(`[TimeToDisplay] No start frames found for span ${spanId}, skipping frame data collection.`);
461+
return;
462+
}
463+
464+
try {
465+
const endFrames = await fetchNativeFramesWithTimeout();
466+
frameData.endFrames = endFrames;
467+
468+
attachFrameDataToSpan(span, frameData.startFrames, endFrames);
469+
470+
logger.debug(`[TimeToDisplay] Captured and attached end frames for span ${spanId}.`, endFrames);
471+
} catch (error) {
472+
logger.debug(`[TimeToDisplay] Failed to capture end frames for span ${spanId}.`, error);
473+
} finally {
474+
spanFrameDataMap.delete(spanId);
475+
}
476+
}
477+
478+
/**
479+
* Fetches native frames with a timeout
480+
*/
481+
function fetchNativeFramesWithTimeout(): Promise<NativeFramesResponse> {
482+
return new Promise<NativeFramesResponse>((resolve, reject) => {
483+
NATIVE.fetchNativeFrames()
484+
.then(value => {
485+
if (!value) {
486+
reject('Native frames response is null.');
487+
return;
488+
}
489+
resolve(value);
490+
})
491+
.then(undefined, (error: unknown) => {
492+
reject(error);
493+
});
494+
495+
setTimeout(() => {
496+
reject('Fetching native frames took too long. Dropping frames.');
497+
}, FETCH_FRAMES_TIMEOUT_MS);
498+
});
499+
}

0 commit comments

Comments
 (0)