getsentry / getsentry/sentry-dotnet

Implement Slow and Frozen Frames on Android

Open
#5,099 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

# Desrciption

Part of

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

> \[!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.

---

## Android Implementation

**Mechanism**: [`FrameMetricsAggregator`]() from AndroidX.

RN's Java module (`RNSentryModuleImpl.java`) manages this directly:

* **Start tracking**: `frameMetricsAggregator.add(currentActivity)`
* **Stop tracking**: `frameMetricsAggregator.stop()` (called on JS reload, since native modules persist)
* **Fetch frames**: reads cumulative metrics from `getMetrics()[TOTAL_INDEX]`, where keys are frame durations in ms and values are frame counts. Classification is done in Java:

```java
if (frameTime > 700) frozenFrames += numFrames;
else if (frameTime > 16) slowFrames += numFrames;
totalFrames += numFrames;
```

Tracking must be explicitly started and stopped. It is tied to the current Activity — a new call to `add()` is needed if the Activity changes.

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

* `FrameMetricsAggregator` is an AndroidX API. It should be accessible from .NET via the Android bindings, but needs verification.
* Alternatively, the Android Sentry SDK's `ActivityFramesTracker` may already be doing this — worth checking whether that data can be read from the managed layer rather than reimplementing frame collection from scratch.

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.