Skip to content

Implement Time to Initial Display on iOS #5102

@jamescrosswell

Description

@jamescrosswell

Desrciption

Part of

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 iOS

Like Android, RN has two parallel native paths plus a JS fallback. All paths 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.

The iOS implementation delegates entirely to the Cocoa SDK's SentryFramesTracker (backed by CADisplayLink) rather than implementing its own frame detection.

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:

  1. A RNSentryFramesTrackerListener is attached to SentryFramesTracker from the Cocoa SDK
  2. SentryFramesTracker uses a CADisplayLink internally. On the next frame callback it calls framesTrackerHasNewFrame:(NSDate *)newFrameDate
  3. The listener removes itself immediately (one-shot), then stores [newFrameDate timeIntervalSince1970] in the map under "ttid-{parentSpanId}"

Path B — Navigation integration (swizzleViewDidAppear + RNSentryFramesTrackerListener)

Activated when enableTimeToInitialDisplay=true in the React Navigation integration:

  1. At init time, [RNSentryRNSScreen swizzleViewDidAppear] is called on the main thread — this swizzles viewDidAppear on react-native-screens screen classes
  2. A RNSentryFramesTrackerListener is created with a callback that calls [RNSentryTimeToDisplay putTimeToInitialDisplayForActiveSpan:timestamp]
  3. JS calls NATIVE.setActiveSpanId(spanId) to register the current span
  4. On viewDidAppear, the listener starts listening on SentryFramesTracker
  5. On the next frame, the timestamp is stored under "ttid-navigation-{activeSpanId}"

Fallback — CADisplayLink bridge (getNewScreenTimeToDisplay)

If neither native path produces a timestamp in time:

  1. Creates a one-shot CADisplayLink targeting a handleDisplayLink: selector
  2. Added to NSRunLoop.mainRunLoop with NSRunLoopCommonModes
  3. On the next display link callback, captures [NSDate date] timeIntervalSince1970 and resolves the promise, then invalidates the CADisplayLink
  4. Only available on TARGET_OS_IOS — returns empty on macOS / Catalyst

JS-Side Span Construction

Identical to Android — after the transaction ends, processEvent calls NATIVE.popTimeToDisplayFor(...), constructs the span, and sets the time_to_initial_display measurement. See the Android notes for the shared JS flow.

App Start Timestamp Alignment

Same as Android — both the transaction's start_timestamp and the TTID span's start_timestamp are overwritten with the native app start timestamp, and the measurement is recalculated.


Key iOS API: SentryFramesTracker / CADisplayLink

The Cocoa SDK's SentryFramesTracker is the frame detection mechanism on iOS. It runs a CADisplayLink that fires on every rendered frame. RN taps into this via PrivateSentrySDKOnly — the same internal API used for slow/frozen frames and App Start data.

SentryFramesTracker must be running for any of this to work. In the Cocoa SDK this is controlled by enableAutoPerformanceTracing.


Quirks and Limitations

  • SentryFramesTracker must be active — if enableAutoPerformanceTracing is not set in the Cocoa SDK, all paths fail silently and no TTID is reported
  • The CADisplayLink fallback is iOS-only (TARGET_OS_IOS) — it returns empty on Mac Catalyst
  • swizzleViewDidAppear requires react-native-screens — without it the navigation integration path is unavailable
  • activeSpanId is a single global — same potential span ID mismatch risk as Android (mitigated by keyed map)
  • A 30-second timeout in the JS layer bounds how long it waits for a native timestamp

Implications for .NET/MAUI (iOS)

  • The core mechanism is SentryFramesTracker / CADisplayLink via PrivateSentrySDKOnly. The Cocoa bindings need to expose the relevant API — specifically SentryFramesTracker or a listener protocol (SentryFramesTrackerListener)
  • PrivateSentrySDKOnly.isFramesTrackingRunning and currentScreenFrames already appear in ApiDefinitions.cs (used for slow/frozen frames), but the listener protocol for one-shot frame callbacks likely needs adding
  • The equivalent of viewDidAppear in MAUI is a page's OnAppearing lifecycle event — this is a natural hook point for starting the frame listener
  • The Cocoa binding additions would need to go through scripts/patch-cocoa-bindings.cs since ApiDefinitions.cs is auto-generated
  • enableAutoPerformanceTracing must be set on the Cocoa SDK options for SentryFramesTracker to run

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .net codeFeatureNew feature or requestSpans
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions