Desrciption
Part of
See also:
What Is TTID?
TTID measures the time from when a new screen begins loading until the screen draws its first frame. It gives a more accurate picture of screen creation time than raw transaction timing.
- Span op:
ui.load.initial_display
- Measurement key:
time_to_initial_display (milliseconds)
- The span is a child of the
ui.load transaction (not a separate transaction, so it doesn't count against quota)
- Status must be
ok for the measurement to be recorded — if the span finishes with any other status the measurement is omitted
- The span starts when the screen begins loading. During app startup this is adjusted back to the native app start timestamp (same alignment as App Starts)
- The span ends automatically when the first frame is drawn — no manual intervention needed
References
How React Native Implements TTID on Android
RN has two parallel native paths for detecting first draw, plus a JS fallback. All paths ultimately store a timestamp in a shared static map (RNSentryTimeToDisplay) keyed by span ID. The JS layer reads this map after the transaction ends and constructs the TTID span.
Path A — View-based (RNSentryOnDrawReporter)
A special view component (<RNSentryOnDrawReporter initialDisplay parentSpanId={...} />) is embedded in the screen's component tree. When rendered with initialDisplay=true:
- Calls
FirstDrawDoneListener.registerForNextDraw(activity, callback, buildInfo) from the Sentry Android SDK
FirstDrawDoneListener is an OnDrawListener on the Activity's ViewTreeObserver
- On the next draw pass it captures
SentryAndroidDateProvider.now().nanoTimestamp() / 1e9
- Stores the timestamp in the map under
"ttid-{parentSpanId}"
Path B — Fragment lifecycle (RNSentryReactFragmentLifecycleTracer)
Activated when enableTimeToInitialDisplay=true in the React Navigation integration. Requires react-native-screens (specifically ScreenStackFragment):
- JS calls
NATIVE.setActiveSpanId(spanId) to register the current span on the native side
- A
FragmentLifecycleCallbacks listener watches for ScreenStackFragment view creation
- On
ScreenAppearEvent (screen visible), calls FirstDrawDoneListener.registerForNextDraw(...)
- Stores timestamp under
"ttid-navigation-{activeSpanId}"
Fallback — JS Choreographer bridge (getNewScreenTimeToDisplay)
If neither native path produces a timestamp in time, the JS side calls NATIVE.getNewScreenTimeToDisplay() which:
- Posts to the main
Looper
- Uses
Choreographer.getInstance().postFrameCallback(...) to capture the timestamp of the next rendered frame in nanoseconds
- Resolves the promise with
nanoTimestamp / 1e9 (seconds)
JS-Side Span Construction
After the transaction ends, in processEvent:
- Calls
NATIVE.popTimeToDisplayFor("ttid-{rootSpanId}") (manual) or "ttid-navigation-{rootSpanId}" (automatic navigation path), with the Choreographer fallback as last resort
- Constructs a
SpanJSON with op: "ui.load.initial_display", start_timestamp: transactionStartTimestamp, timestamp: ttidTimestamp
- Sets
time_to_initial_display measurement as (timestamp - start_timestamp) * 1000 ms
App Start Timestamp Alignment
When processing an app-start transaction, both the transaction's start_timestamp and the TTID span's start_timestamp are overwritten with the native app start timestamp. The measurement is recalculated after this adjustment.
Key Android API: FirstDrawDoneListener
This class from the Sentry Android SDK is the core mechanism on both native paths. It wraps ViewTreeObserver.addOnDrawListener and fires exactly once on the next draw pass.
Whether .NET/MAUI can use this directly (since the Android SDK is already embedded) is worth investigating — it may avoid needing to implement our own OnDrawListener.
Quirks and Limitations
- The two native paths (view-based and fragment-based) can run in parallel. The map stores results by span ID so they don't collide
activeSpanId is a single global on the native side — if navigation happens faster than the frame callback, a span ID mismatch is possible (the keyed map mitigates this)
- The fragment lifecycle path silently skips if
react-native-screens is absent or the screen is not a ScreenStackFragment
- Back navigation transactions explicitly do not receive TTID spans/measurements by default
- A 30-second timeout in
timeToDisplayIntegration.ts bounds the window for receiving a native timestamp; if the idle transaction timeout fires first, the TTID span is simply absent
Implications for .NET/MAUI (Android)
- The core mechanism is
ViewTreeObserver.OnDrawListener (or equivalently FirstDrawDoneListener from the Android Sentry SDK). This is a standard Android API accessible from .NET
- The Sentry Android SDK's
FirstDrawDoneListener may be directly usable from the managed layer since the Android SDK is already embedded — worth checking before re-implementing
- The timing of span creation needs to align with MAUI's navigation model (not
react-native-screens / ScreenStackFragment) — MAUI pages and their lifecycle events would be the equivalent hook points
- The start timestamp must be adjusted to the app start timestamp for the first screen, matching the spec requirement
Desrciption
Part of
See also:
What Is TTID?
TTID measures the time from when a new screen begins loading until the screen draws its first frame. It gives a more accurate picture of screen creation time than raw transaction timing.
ui.load.initial_displaytime_to_initial_display(milliseconds)ui.loadtransaction (not a separate transaction, so it doesn't count against quota)okfor the measurement to be recorded — if the span finishes with any other status the measurement is omittedReferences
How React Native Implements TTID on Android
RN has two parallel native paths for detecting first draw, plus a JS fallback. All paths ultimately store a timestamp in a shared static map (
RNSentryTimeToDisplay) keyed by span ID. The JS layer reads this map after the transaction ends and constructs the TTID span.Path A — View-based (
RNSentryOnDrawReporter)A special view component (
<RNSentryOnDrawReporter initialDisplay parentSpanId={...} />) is embedded in the screen's component tree. When rendered withinitialDisplay=true:FirstDrawDoneListener.registerForNextDraw(activity, callback, buildInfo)from the Sentry Android SDKFirstDrawDoneListeneris anOnDrawListeneron the Activity'sViewTreeObserverSentryAndroidDateProvider.now().nanoTimestamp() / 1e9"ttid-{parentSpanId}"Path B — Fragment lifecycle (
RNSentryReactFragmentLifecycleTracer)Activated when
enableTimeToInitialDisplay=truein the React Navigation integration. Requiresreact-native-screens(specificallyScreenStackFragment):NATIVE.setActiveSpanId(spanId)to register the current span on the native sideFragmentLifecycleCallbackslistener watches forScreenStackFragmentview creationScreenAppearEvent(screen visible), callsFirstDrawDoneListener.registerForNextDraw(...)"ttid-navigation-{activeSpanId}"Fallback — JS Choreographer bridge (
getNewScreenTimeToDisplay)If neither native path produces a timestamp in time, the JS side calls
NATIVE.getNewScreenTimeToDisplay()which:LooperChoreographer.getInstance().postFrameCallback(...)to capture the timestamp of the next rendered frame in nanosecondsnanoTimestamp / 1e9(seconds)JS-Side Span Construction
After the transaction ends, in
processEvent:NATIVE.popTimeToDisplayFor("ttid-{rootSpanId}")(manual) or"ttid-navigation-{rootSpanId}"(automatic navigation path), with the Choreographer fallback as last resortSpanJSONwithop: "ui.load.initial_display",start_timestamp: transactionStartTimestamp,timestamp: ttidTimestamptime_to_initial_displaymeasurement as(timestamp - start_timestamp) * 1000msApp Start Timestamp Alignment
When processing an app-start transaction, both the transaction's
start_timestampand the TTID span'sstart_timestampare overwritten with the native app start timestamp. The measurement is recalculated after this adjustment.Key Android API:
FirstDrawDoneListenerThis class from the Sentry Android SDK is the core mechanism on both native paths. It wraps
ViewTreeObserver.addOnDrawListenerand fires exactly once on the next draw pass.Whether
.NET/MAUIcan use this directly (since the Android SDK is already embedded) is worth investigating — it may avoid needing to implement our ownOnDrawListener.Quirks and Limitations
activeSpanIdis a single global on the native side — if navigation happens faster than the frame callback, a span ID mismatch is possible (the keyed map mitigates this)react-native-screensis absent or the screen is not aScreenStackFragmenttimeToDisplayIntegration.tsbounds the window for receiving a native timestamp; if the idle transaction timeout fires first, the TTID span is simply absentImplications for .NET/MAUI (Android)
ViewTreeObserver.OnDrawListener(or equivalentlyFirstDrawDoneListenerfrom the Android Sentry SDK). This is a standard Android API accessible from .NETFirstDrawDoneListenermay be directly usable from the managed layer since the Android SDK is already embedded — worth checking before re-implementingreact-native-screens/ScreenStackFragment) — MAUI pages and their lifecycle events would be the equivalent hook points