Skip to content

Commit

Permalink
Merge 281eaa6 into 7eccfdb
Browse files Browse the repository at this point in the history
  • Loading branch information
markushi authored Dec 14, 2023
2 parents 7eccfdb + 281eaa6 commit faad870
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,17 @@ public AndroidProfiler(
frameMetricsCollectorId =
frameMetricsCollector.startCollection(
new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
final long nanosInSecond = TimeUnit.SECONDS.toNanos(1);
final long frozenFrameThresholdNanos = TimeUnit.MILLISECONDS.toNanos(700);
float lastRefreshRate = 0;

@Override
public void onFrameMetricCollected(
final long frameEndNanos, final long durationNanos, float refreshRate) {
final long frameStartNanos,
final long frameEndNanos,
final long durationNanos,
final long delayNanos,
final boolean isSlow,
final boolean isFrozen,
final float refreshRate) {
// transactionStartNanos is calculated through SystemClock.elapsedRealtimeNanos(),
// but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp
// relative to transactionStartNanos
Expand All @@ -160,22 +164,17 @@ public void onFrameMetricCollected(
if (frameTimestampRelativeNanos < 0) {
return;
}
// Most frames take just a few nanoseconds longer than the optimal calculated
// duration.
// Therefore we subtract one, because otherwise almost all frames would be slow.
boolean isSlow = durationNanos > nanosInSecond / (refreshRate - 1);
float newRefreshRate = (int) (refreshRate * 100) / 100F;
if (durationNanos > frozenFrameThresholdNanos) {
if (isFrozen) {
frozenFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
} else if (isSlow) {
slowFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
}
if (newRefreshRate != lastRefreshRate) {
lastRefreshRate = newRefreshRate;
if (refreshRate != lastRefreshRate) {
lastRefreshRate = refreshRate;
screenFrameRateMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, newRefreshRate));
new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate));
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class SentryFrameMetricsCollector implements Application.ActivityLifecycleCallbacks {
private static final long oneSecondInNanos = TimeUnit.SECONDS.toNanos(1);
private static final long frozenFrameThresholdNanos = TimeUnit.MILLISECONDS.toNanos(700);

private final @NotNull BuildInfoProvider buildInfoProvider;
private final @NotNull Set<Window> trackedWindows = new CopyOnWriteArraySet<>();
private final @NotNull ILogger logger;
Expand Down Expand Up @@ -132,6 +136,7 @@ public SentryFrameMetricsCollector(
logger.log(
SentryLevel.ERROR, "Unable to get the frame timestamp from the choreographer: ", e);
}

frameMetricsAvailableListener =
(window, frameMetrics, dropCountSinceLastInvocation) -> {
final long now = System.nanoTime();
Expand All @@ -140,6 +145,13 @@ public SentryFrameMetricsCollector(
? window.getContext().getDisplay().getRefreshRate()
: window.getWindowManager().getDefaultDisplay().getRefreshRate();

final long expectedFrameDuration = (long) (oneSecondInNanos / refreshRate);
final long totalFrameDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);

// if totalDurationNanos is smaller than expectedFrameTimeNanos,
// it means that the frame was drawn within it's time budget, thus 0 delay
final long delayNanos = Math.max(0, totalFrameDuration - expectedFrameDuration);

final long cpuDuration = getFrameCpuDuration(frameMetrics);
long startTime = getFrameStartTimestamp(frameMetrics);
// If we couldn't get the timestamp through reflection, we use current time
Expand All @@ -155,8 +167,21 @@ public SentryFrameMetricsCollector(
lastFrameStartNanos = startTime;
lastFrameEndNanos = startTime + cpuDuration;

// Most frames take just a few nanoseconds longer than the optimal calculated
// duration.
// Therefore we subtract one, because otherwise almost all frames would be slow.
final boolean isSlow = cpuDuration > oneSecondInNanos / (refreshRate - 1);
final boolean isFrozen = isSlow && cpuDuration > frozenFrameThresholdNanos;

for (FrameMetricsCollectorListener l : listenerMap.values()) {
l.onFrameMetricCollected(lastFrameEndNanos, cpuDuration, refreshRate);
l.onFrameMetricCollected(
startTime,
lastFrameEndNanos,
cpuDuration,
delayNanos,
isSlow,
isFrozen,
refreshRate);
}
};
}
Expand Down Expand Up @@ -299,13 +324,26 @@ public interface FrameMetricsCollectorListener {
/**
* Called when a frame is collected.
*
* @param frameStartNanos Start timestamp of a frame in nanoseconds relative to
* System.nanotime().
* @param frameEndNanos End timestamp of a frame in nanoseconds relative to System.nanotime().
* @param durationNanos Duration in nanoseconds of the time spent from the cpu on the main
* thread to create the frame.
* @param refreshRate Refresh rate of the screen.
* @param delayNanos the frame delay, in nanoseconds.
* @param isSlow True if the frame is considered slow, rendering taking longer than the
* refresh-rate based budget, false otherwise.
* @param isFrozen True if the frame is considered frozen, rendering taking longer than 700ms,
* false otherwise.
* @param refreshRate the last known refresh rate when the frame was rendered.
*/
void onFrameMetricCollected(
final long frameEndNanos, final long durationNanos, final float refreshRate);
final long frameStartNanos,
final long frameEndNanos,
final long durationNanos,
final long delayNanos,
final boolean isSlow,
final boolean isFrozen,
final float refreshRate);
}

@ApiStatus.Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ import org.mockito.kotlin.whenever
import org.robolectric.Shadows
import java.lang.ref.WeakReference
import java.lang.reflect.Field
import java.util.concurrent.TimeUnit
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class SentryFrameMetricsCollectorTest {
Expand Down Expand Up @@ -280,7 +283,10 @@ class SentryFrameMetricsCollectorTest {
val frameMetrics = createMockFrameMetrics()

var timesCalled = 0
collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
collector.startCollection { frameStartNanos, frameEndNanos,
durationNanos, delayNanos,
isSlow, isFrozen,
refreshRate ->
// The frame end is 100 (Choreographer.mLastFrameTimeNanos) plus frame duration
assertEquals(100 + durationNanos, frameEndNanos)
timesCalled++
Expand All @@ -297,12 +303,14 @@ class SentryFrameMetricsCollectorTest {
}
val collector = fixture.getSut(context, buildInfo)
val listener = collector.getProperty<Window.OnFrameMetricsAvailableListener>("frameMetricsAvailableListener")
// FrameMetrics with cpu time of 21 nanoseconds and INTENDED_VSYNC_TIMESTAMP of 50 nanoseconds
val frameMetrics = createMockFrameMetrics()
// We don't inject the choreographer field

var timesCalled = 0
collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
collector.startCollection { frameStartNanos, frameEndNanos,
durationNanos, delayNanos,
isSlow, isFrozen,
refreshRate ->
assertEquals(50 + durationNanos, frameEndNanos)
timesCalled++
}
Expand All @@ -322,7 +330,10 @@ class SentryFrameMetricsCollectorTest {
val frameMetrics = createMockFrameMetrics()

var timesCalled = 0
collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
collector.startCollection { frameStartNanos, frameEndNanos,
durationNanos, delayNanos,
isSlow, isFrozen,
refreshRate ->
assertEquals(21, durationNanos)
timesCalled++
}
Expand All @@ -342,7 +353,10 @@ class SentryFrameMetricsCollectorTest {
whenever(frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)).thenReturn(50)
var previousEnd = 0L
var timesCalled = 0
collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
collector.startCollection { frameStartNanos, frameEndNanos,
durationNanos, delayNanos,
isSlow, isFrozen,
refreshRate ->
// The second time the listener is called, the frame start is shifted to be equal to the previous frame end
if (timesCalled > 0) {
assertEquals(previousEnd + durationNanos, frameEndNanos)
Expand All @@ -356,25 +370,136 @@ class SentryFrameMetricsCollectorTest {
assertEquals(2, timesCalled)
}

private fun createMockWindow(): Window {
@Test
fun `collector properly reports slow and frozen flags`() {
val buildInfo = mock<BuildInfoProvider> {
whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.O)
}
val collector = fixture.getSut(context, buildInfo)
val listener = collector.getProperty<Window.OnFrameMetricsAvailableListener>("frameMetricsAvailableListener")

var timesCalled = 0
var lastIsSlow = false
var lastIsFrozen = false

// when a frame takes less than 16ms, it's not considered slow or frozen
collector.startCollection { _, _,
_, _,
isSlow, isFrozen,
_ ->

lastIsSlow = isSlow
lastIsFrozen = isFrozen
timesCalled++
}
listener.onFrameMetricsAvailable(createMockWindow(), createMockFrameMetrics(), 0)
assertFalse(lastIsSlow)
assertFalse(lastIsFrozen)

// when a frame takes more than 16ms, it's considered slow but not frozen
listener.onFrameMetricsAvailable(
createMockWindow(),
createMockFrameMetrics(
unknownDelayDuration = 1 + TimeUnit.MILLISECONDS.toNanos(100)
),
0
)
assertTrue(lastIsSlow)
assertFalse(lastIsFrozen)

// when a frame takes more than 700ms, it's considered slow and frozen
listener.onFrameMetricsAvailable(
createMockWindow(),
createMockFrameMetrics(
unknownDelayDuration = 1 + TimeUnit.MILLISECONDS.toNanos(1000)
),
0
)
assertTrue(lastIsSlow)
assertTrue(lastIsFrozen)

// Assert the callbacks were called
assertEquals(3, timesCalled)
}

@Test
fun `collector properly reports frame delay`() {
val buildInfo = mock<BuildInfoProvider> {
whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.O)
}
val collector = fixture.getSut(context, buildInfo)
val listener = collector.getProperty<Window.OnFrameMetricsAvailableListener>("frameMetricsAvailableListener")

var lastDelay = 0L

// when a frame takes less than 16ms, it's not considered slow or frozen
collector.startCollection { _, _,
_, delayNanos,
isSlow, isFrozen,
_ ->
lastDelay = delayNanos
}
// at 60hz, when the total duration is 10ms, the delay is 0
listener.onFrameMetricsAvailable(
createMockWindow(),
createMockFrameMetrics(
totalDuration = TimeUnit.MILLISECONDS.toNanos(16)
),
0
)
assertEquals(0, lastDelay)

// at 60hz, when the total duration is 20ms, the delay is considered ~4ms
listener.onFrameMetricsAvailable(
createMockWindow(),
createMockFrameMetrics(
totalDuration = TimeUnit.MILLISECONDS.toNanos(20)
),
0
)
assertEquals(
// 20ms - 1/60 (~16.6ms) = 4ms
TimeUnit.MILLISECONDS.toNanos(20) - (TimeUnit.SECONDS.toNanos(1) / 60.0f).toLong(),
lastDelay
)

// at 120hz, when the total duration is 20ms, the delay is considered ~8ms
listener.onFrameMetricsAvailable(
createMockWindow(120.0f),
createMockFrameMetrics(
totalDuration = TimeUnit.MILLISECONDS.toNanos(20)
),
0
)
assertEquals(
// 20ms - 1/120 (~8.33ms) = 8ms
TimeUnit.MILLISECONDS.toNanos(20) - (TimeUnit.SECONDS.toNanos(1) / 120.0f).toLong(),
lastDelay
)
}

private fun createMockWindow(refreshRate: Float = 60F): Window {
val mockWindow = mock<Window>()
val mockDisplay = mock<Display>()
val mockWindowManager = mock<WindowManager>()
whenever(mockWindow.windowManager).thenReturn(mockWindowManager)
whenever(mockWindowManager.defaultDisplay).thenReturn(mockDisplay)
whenever(mockDisplay.refreshRate).thenReturn(60F)
whenever(mockDisplay.refreshRate).thenReturn(refreshRate)
return mockWindow
}

private fun createMockFrameMetrics(): FrameMetrics {
/**
* FrameMetrics with default cpu time of 21 nanoseconds and INTENDED_VSYNC_TIMESTAMP of 50 nanoseconds
*/
private fun createMockFrameMetrics(unknownDelayDuration: Long = 1, totalDuration: Long = 60): FrameMetrics {
val frameMetrics = mock<FrameMetrics>()
whenever(frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)).thenReturn(1)
whenever(frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)).thenReturn(unknownDelayDuration)
whenever(frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)).thenReturn(2)
whenever(frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)).thenReturn(3)
whenever(frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)).thenReturn(4)
whenever(frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)).thenReturn(5)
whenever(frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)).thenReturn(6)
whenever(frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)).thenReturn(60)
whenever(frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)).thenReturn(totalDuration)
whenever(frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP)).thenReturn(50)
return frameMetrics
}
Expand Down

0 comments on commit faad870

Please sign in to comment.