Skip to content

Commit 8d62770

Browse files
authored
Move slow+frozen frame calculation, as well as frame delay inside SentryFrameMetricsCollector (#3100)
1 parent 7eccfdb commit 8d62770

File tree

4 files changed

+266
-52
lines changed

4 files changed

+266
-52
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Attaches spans for Application, ContentProvider, and Activities to app-start timings
1010
- Uses Process.startUptimeMillis to calculate app-start timings
1111
- To enable this feature set `options.isEnablePerformanceV2 = true`
12+
- Move slow+frozen frame calculation, as well as frame delay inside SentryFrameMetricsCollector ([#3100](https://github.com/getsentry/sentry-java/pull/3100))
1213

1314
### Fixes
1415

sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java

+11-12
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,17 @@ public AndroidProfiler(
139139
frameMetricsCollectorId =
140140
frameMetricsCollector.startCollection(
141141
new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
142-
final long nanosInSecond = TimeUnit.SECONDS.toNanos(1);
143-
final long frozenFrameThresholdNanos = TimeUnit.MILLISECONDS.toNanos(700);
144142
float lastRefreshRate = 0;
145143

146144
@Override
147145
public void onFrameMetricCollected(
148-
final long frameEndNanos, final long durationNanos, float refreshRate) {
146+
final long frameStartNanos,
147+
final long frameEndNanos,
148+
final long durationNanos,
149+
final long delayNanos,
150+
final boolean isSlow,
151+
final boolean isFrozen,
152+
final float refreshRate) {
149153
// transactionStartNanos is calculated through SystemClock.elapsedRealtimeNanos(),
150154
// but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp
151155
// relative to transactionStartNanos
@@ -160,22 +164,17 @@ public void onFrameMetricCollected(
160164
if (frameTimestampRelativeNanos < 0) {
161165
return;
162166
}
163-
// Most frames take just a few nanoseconds longer than the optimal calculated
164-
// duration.
165-
// Therefore we subtract one, because otherwise almost all frames would be slow.
166-
boolean isSlow = durationNanos > nanosInSecond / (refreshRate - 1);
167-
float newRefreshRate = (int) (refreshRate * 100) / 100F;
168-
if (durationNanos > frozenFrameThresholdNanos) {
167+
if (isFrozen) {
169168
frozenFrameRenderMeasurements.addLast(
170169
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
171170
} else if (isSlow) {
172171
slowFrameRenderMeasurements.addLast(
173172
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
174173
}
175-
if (newRefreshRate != lastRefreshRate) {
176-
lastRefreshRate = newRefreshRate;
174+
if (refreshRate != lastRefreshRate) {
175+
lastRefreshRate = refreshRate;
177176
screenFrameRateMeasurements.addLast(
178-
new ProfileMeasurementValue(frameTimestampRelativeNanos, newRefreshRate));
177+
new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate));
179178
}
180179
}
181180
});

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java

+43-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@
2525
import java.util.UUID;
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.CopyOnWriteArraySet;
28+
import java.util.concurrent.TimeUnit;
2829
import org.jetbrains.annotations.ApiStatus;
2930
import org.jetbrains.annotations.NotNull;
3031
import org.jetbrains.annotations.Nullable;
3132

3233
@ApiStatus.Internal
3334
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+
3438
private final @NotNull BuildInfoProvider buildInfoProvider;
3539
private final @NotNull Set<Window> trackedWindows = new CopyOnWriteArraySet<>();
3640
private final @NotNull ILogger logger;
@@ -132,6 +136,7 @@ public SentryFrameMetricsCollector(
132136
logger.log(
133137
SentryLevel.ERROR, "Unable to get the frame timestamp from the choreographer: ", e);
134138
}
139+
135140
frameMetricsAvailableListener =
136141
(window, frameMetrics, dropCountSinceLastInvocation) -> {
137142
final long now = System.nanoTime();
@@ -140,7 +145,14 @@ public SentryFrameMetricsCollector(
140145
? window.getContext().getDisplay().getRefreshRate()
141146
: window.getWindowManager().getDefaultDisplay().getRefreshRate();
142147

148+
final long expectedFrameDuration = (long) (oneSecondInNanos / refreshRate);
149+
143150
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+
144156
long startTime = getFrameStartTimestamp(frameMetrics);
145157
// If we couldn't get the timestamp through reflection, we use current time
146158
if (startTime < 0) {
@@ -155,8 +167,21 @@ public SentryFrameMetricsCollector(
155167
lastFrameStartNanos = startTime;
156168
lastFrameEndNanos = startTime + cpuDuration;
157169

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+
158176
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);
160185
}
161186
};
162187
}
@@ -193,6 +218,8 @@ private long getFrameStartTimestamp(final @NotNull FrameMetrics frameMetrics) {
193218
*/
194219
@RequiresApi(api = Build.VERSION_CODES.N)
195220
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
196223
return frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)
197224
+ frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)
198225
+ frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)
@@ -299,13 +326,26 @@ public interface FrameMetricsCollectorListener {
299326
/**
300327
* Called when a frame is collected.
301328
*
329+
* @param frameStartNanos Start timestamp of a frame in nanoseconds relative to
330+
* System.nanotime().
302331
* @param frameEndNanos End timestamp of a frame in nanoseconds relative to System.nanotime().
303332
* @param durationNanos Duration in nanoseconds of the time spent from the cpu on the main
304333
* 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.
306340
*/
307341
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);
309349
}
310350

311351
@ApiStatus.Internal

0 commit comments

Comments
 (0)