getsentry / getsentry/sentry-dotnet
Implement Slow and Frozen Frames on Android
- 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
Assessment
This issue has not been assessed yet.