getsentry / getsentry/sentry-dotnet

Implement Slow and Frozen Frames on iOS

Open
#5,100 1 comment 0 reactions 0 assignees View on GitHub
.NET Feature Spans
Dominant language
C#
Stars
770
Forks
248
Avg merge
3d 4h
Merged PRs (30d)
49

Description

### Description

# Desrciption

Part of

* [NET-185](https://linear.app/getsentry/issue/NET-185/mobile-vitals)

See also

* [NET-640](https://linear.app/getsentry/issue/NET-640/implement-slow-and-frozen-frames-on-android)

> \[!WARNING\]
Frame delay is the future: The spec defines frames.delay (seconds, not counts) as the preferred metric because raw counts are misleading. RN hasn't implemented it yet — just the counts. Potentially for .NET we could implement Frame delay and skip Frozen Frames (which will likely be deprecated eventually)

# Slow & Frozen Frames: Research Notes

## What Are They?

Slow and frozen frames measure rendering performance during a transaction. A frame that takes too long to render results in a visible stutter or freeze for the user.

| Metric | Threshold |
| -- | -- |
| **Slow frame** | 16.67ms at 60fps (> 8.33ms at 120fps)

|
| **Frozen frame** | 700ms

|

The expected frame duration depends on the active refresh rate (16.67ms at 60fps, 8.33ms at 120fps) and can change dynamically — SDKs must check per-frame rather than assuming a fixed rate.

### References

* [Frames Delay spec]()
* [Mobile Vitals product docs]()
* [sentry-dotnet tracking issue]()

---

## Why Frame Counts Alone Are Insufficient

Raw counts are misleading for prioritization. Consider:

* **Scenario A**: Five 500ms frames → 5 slow frames, \~2,417ms total delay
* **Scenario B**: Ten 20ms frames → 10 slow frames, \~33ms total delay
* **Scenario C**: One 800ms frame → 1 frozen frame, \~783ms delay

Scenario B has more slow frames than A, but far less user impact. This is why the spec introduces **frame delay** — the sum of `(actual duration - expected duration)` for each slow/frozen frame — as the preferred metric for prioritization.

---

## What Gets Reported

### Transaction-level measurements (`event.measurements`)

| Key | Unit | Description |
| -- | -- | -- |
| `frames_total` | `none` (count) | Total frames rendered during the transaction |
| `frames_slow` | `none` (count) | Count of slow frames |
| `frames_frozen` | `none` (count) | Count of frozen frames |

### Span data attributes (on every span)

| Key | Unit | Description |
| -- | -- | -- |
| `frames.total` | — | Total frames during the span's lifetime |
| `frames.slow` | — | Slow frames during the span's lifetime |
| `frames.frozen` | — | Frozen frames during the span's lifetime |
| `frames.delay` | seconds | Sum of delayed frame durations (the preferred metric per spec) |

Note: `frames.delay` is specified but **not yet implemented** in the React Native SDK — it currently only tracks the three counts.

---

## How React Native Implements This

The `NativeFrames` integration hooks into the span lifecycle in JS:

* **On span start**: calls `NATIVE.fetchNativeFrames()` and stores the result (cumulative counts since tracking started) keyed by span ID, with a 60s TTL.
* **On span end**: calls `NATIVE.fetchNativeFrames()` again and computes deltas:

```
totalFrames = end.total - start.total
slowFrames = end.slow - start.slow
frozenFrames = end.frozen - start.frozen
```

Sets `frames.total`, `frames.slow`, `frames.frozen` as span attributes on **every** span.
* **On transaction**: also emits the root span's deltas as `frames_total`, `frames_slow`, `frames_frozen` measurements on the event envelope.

The native layer returns **cumulative** counts; the JS layer is responsible for computing per-span deltas.

---

## iOS Implementation

**Mechanism**: Delegates entirely to the Cocoa SDK's internal `SentryFramesTracker` via `PrivateSentrySDKOnly`.

* **Enable/disable tracking**: **no-ops** on iOS — the Cocoa SDK manages frame tracking internally when `enableAutoPerformanceTracing` is set.
* **Fetch frames**:

```objc
if (PrivateSentrySDKOnly.isFramesTrackingRunning) {
resolve(@{
@"totalFrames": [SentryScreenFramesWrapper totalFrames],
@"frozenFrames": [SentryScreenFramesWrapper frozenFrames],
@"slowFrames": [SentryScreenFramesWrapper slowFrames],
});
} else {
resolve(nil);
}
```

`SentryScreenFramesWrapper` reads from `PrivateSentrySDKOnly.currentScreenFrames`, a `SentryScreenFrames` object with `.total`, `.frozen`, `.slow` properties.

Only supported on iOS and Mac Catalyst — not macOS proper.

### Implications for .NET/MAUI (iOS)

* `PrivateSentrySDKOnly.currentScreenFrames` / `isFramesTrackingRunning` already appear in `ApiDefinitions.cs` (line 1566), so the Cocoa binding may already expose what's needed.
* No explicit enable/disable needed — the Cocoa SDK handles it, provided `enableAutoPerformanceTracing` is set.
* The same cumulative-count-with-delta pattern would apply.

---

## Platform Differences Summary

| Aspect | Android | iOS |
| -- | -- | -- |
| Frame collection | `FrameMetricsAggregator` (AndroidX, RN-managed) | `PrivateSentrySDKOnly.currentScreenFrames` (Cocoa SDK internal) |
| Threshold classification | Done in RN/Java layer | Done inside Cocoa SDK |
| Enable/disable | Explicit — tied to Activity lifecycle | No-op — Cocoa SDK manages it |
| Availability check | `isFrameMetricsAggregatorAvailable()` | `isFramesTrackingRunning` |
| Platform guard | None (AndroidX availability check) | iOS / Mac Catalyst only |
| Count type | Cumulative since `add(activity)`, reset on `stop()` | Cumulative since Cocoa SDK init |

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.