25
25
import java .util .UUID ;
26
26
import java .util .concurrent .ConcurrentHashMap ;
27
27
import java .util .concurrent .CopyOnWriteArraySet ;
28
+ import java .util .concurrent .TimeUnit ;
28
29
import org .jetbrains .annotations .ApiStatus ;
29
30
import org .jetbrains .annotations .NotNull ;
30
31
import org .jetbrains .annotations .Nullable ;
31
32
32
33
@ ApiStatus .Internal
33
34
public final class SentryFrameMetricsCollector implements Application .ActivityLifecycleCallbacks {
35
+ private static final long oneSecondInNanos = TimeUnit .SECONDS .toNanos (1 );
36
+ private static final long frozenFrameThresholdNanos = TimeUnit .MILLISECONDS .toNanos (700 );
37
+
34
38
private final @ NotNull BuildInfoProvider buildInfoProvider ;
35
39
private final @ NotNull Set <Window > trackedWindows = new CopyOnWriteArraySet <>();
36
40
private final @ NotNull ILogger logger ;
@@ -132,6 +136,7 @@ public SentryFrameMetricsCollector(
132
136
logger .log (
133
137
SentryLevel .ERROR , "Unable to get the frame timestamp from the choreographer: " , e );
134
138
}
139
+
135
140
frameMetricsAvailableListener =
136
141
(window , frameMetrics , dropCountSinceLastInvocation ) -> {
137
142
final long now = System .nanoTime ();
@@ -140,7 +145,14 @@ public SentryFrameMetricsCollector(
140
145
? window .getContext ().getDisplay ().getRefreshRate ()
141
146
: window .getWindowManager ().getDefaultDisplay ().getRefreshRate ();
142
147
148
+ final long expectedFrameDuration = (long ) (oneSecondInNanos / refreshRate );
149
+
143
150
final long cpuDuration = getFrameCpuDuration (frameMetrics );
151
+
152
+ // if totalDurationNanos is smaller than expectedFrameTimeNanos,
153
+ // it means that the frame was drawn within it's time budget, thus 0 delay
154
+ final long delayNanos = Math .max (0 , cpuDuration - expectedFrameDuration );
155
+
144
156
long startTime = getFrameStartTimestamp (frameMetrics );
145
157
// If we couldn't get the timestamp through reflection, we use current time
146
158
if (startTime < 0 ) {
@@ -155,8 +167,21 @@ public SentryFrameMetricsCollector(
155
167
lastFrameStartNanos = startTime ;
156
168
lastFrameEndNanos = startTime + cpuDuration ;
157
169
170
+ // Most frames take just a few nanoseconds longer than the optimal calculated
171
+ // duration.
172
+ // Therefore we subtract one, because otherwise almost all frames would be slow.
173
+ final boolean isSlow = cpuDuration > oneSecondInNanos / (refreshRate - 1 );
174
+ final boolean isFrozen = isSlow && cpuDuration > frozenFrameThresholdNanos ;
175
+
158
176
for (FrameMetricsCollectorListener l : listenerMap .values ()) {
159
- l .onFrameMetricCollected (lastFrameEndNanos , cpuDuration , refreshRate );
177
+ l .onFrameMetricCollected (
178
+ startTime ,
179
+ lastFrameEndNanos ,
180
+ cpuDuration ,
181
+ delayNanos ,
182
+ isSlow ,
183
+ isFrozen ,
184
+ refreshRate );
160
185
}
161
186
};
162
187
}
@@ -193,6 +218,8 @@ private long getFrameStartTimestamp(final @NotNull FrameMetrics frameMetrics) {
193
218
*/
194
219
@ RequiresApi (api = Build .VERSION_CODES .N )
195
220
private long getFrameCpuDuration (final @ NotNull FrameMetrics frameMetrics ) {
221
+ // Inspired by JankStats
222
+ // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt;l=74-79;drc=1de6215c6bd9e887e3d94556e9ac55cfb7b8c797
196
223
return frameMetrics .getMetric (FrameMetrics .UNKNOWN_DELAY_DURATION )
197
224
+ frameMetrics .getMetric (FrameMetrics .INPUT_HANDLING_DURATION )
198
225
+ frameMetrics .getMetric (FrameMetrics .ANIMATION_DURATION )
@@ -299,13 +326,26 @@ public interface FrameMetricsCollectorListener {
299
326
/**
300
327
* Called when a frame is collected.
301
328
*
329
+ * @param frameStartNanos Start timestamp of a frame in nanoseconds relative to
330
+ * System.nanotime().
302
331
* @param frameEndNanos End timestamp of a frame in nanoseconds relative to System.nanotime().
303
332
* @param durationNanos Duration in nanoseconds of the time spent from the cpu on the main
304
333
* thread to create the frame.
305
- * @param refreshRate Refresh rate of the screen.
334
+ * @param delayNanos the frame delay, in nanoseconds.
335
+ * @param isSlow True if the frame is considered slow, rendering taking longer than the
336
+ * refresh-rate based budget, false otherwise.
337
+ * @param isFrozen True if the frame is considered frozen, rendering taking longer than 700ms,
338
+ * false otherwise.
339
+ * @param refreshRate the last known refresh rate when the frame was rendered.
306
340
*/
307
341
void onFrameMetricCollected (
308
- final long frameEndNanos , final long durationNanos , final float refreshRate );
342
+ final long frameStartNanos ,
343
+ final long frameEndNanos ,
344
+ final long durationNanos ,
345
+ final long delayNanos ,
346
+ final boolean isSlow ,
347
+ final boolean isFrozen ,
348
+ final float refreshRate );
309
349
}
310
350
311
351
@ ApiStatus .Internal
0 commit comments