@@ -25,12 +25,15 @@ import org.mockito.kotlin.whenever
25
25
import org.robolectric.Shadows
26
26
import java.lang.ref.WeakReference
27
27
import java.lang.reflect.Field
28
+ import java.util.concurrent.TimeUnit
28
29
import kotlin.test.BeforeTest
29
30
import kotlin.test.Test
30
31
import kotlin.test.assertEquals
31
32
import kotlin.test.assertFailsWith
33
+ import kotlin.test.assertFalse
32
34
import kotlin.test.assertNotNull
33
35
import kotlin.test.assertNull
36
+ import kotlin.test.assertTrue
34
37
35
38
@RunWith(AndroidJUnit4 ::class )
36
39
class SentryFrameMetricsCollectorTest {
@@ -280,7 +283,10 @@ class SentryFrameMetricsCollectorTest {
280
283
val frameMetrics = createMockFrameMetrics()
281
284
282
285
var timesCalled = 0
283
- collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
286
+ collector.startCollection { frameStartNanos, frameEndNanos,
287
+ durationNanos, delayNanos,
288
+ isSlow, isFrozen,
289
+ refreshRate ->
284
290
// The frame end is 100 (Choreographer.mLastFrameTimeNanos) plus frame duration
285
291
assertEquals(100 + durationNanos, frameEndNanos)
286
292
timesCalled++
@@ -297,12 +303,14 @@ class SentryFrameMetricsCollectorTest {
297
303
}
298
304
val collector = fixture.getSut(context, buildInfo)
299
305
val listener = collector.getProperty<Window .OnFrameMetricsAvailableListener >(" frameMetricsAvailableListener" )
300
- // FrameMetrics with cpu time of 21 nanoseconds and INTENDED_VSYNC_TIMESTAMP of 50 nanoseconds
301
306
val frameMetrics = createMockFrameMetrics()
302
307
// We don't inject the choreographer field
303
308
304
309
var timesCalled = 0
305
- collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
310
+ collector.startCollection { frameStartNanos, frameEndNanos,
311
+ durationNanos, delayNanos,
312
+ isSlow, isFrozen,
313
+ refreshRate ->
306
314
assertEquals(50 + durationNanos, frameEndNanos)
307
315
timesCalled++
308
316
}
@@ -322,7 +330,10 @@ class SentryFrameMetricsCollectorTest {
322
330
val frameMetrics = createMockFrameMetrics()
323
331
324
332
var timesCalled = 0
325
- collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
333
+ collector.startCollection { frameStartNanos, frameEndNanos,
334
+ durationNanos, delayNanos,
335
+ isSlow, isFrozen,
336
+ refreshRate ->
326
337
assertEquals(21 , durationNanos)
327
338
timesCalled++
328
339
}
@@ -342,7 +353,10 @@ class SentryFrameMetricsCollectorTest {
342
353
whenever(frameMetrics.getMetric(FrameMetrics .INTENDED_VSYNC_TIMESTAMP )).thenReturn(50 )
343
354
var previousEnd = 0L
344
355
var timesCalled = 0
345
- collector.startCollection { frameEndNanos, durationNanos, refreshRate ->
356
+ collector.startCollection { frameStartNanos, frameEndNanos,
357
+ durationNanos, delayNanos,
358
+ isSlow, isFrozen,
359
+ refreshRate ->
346
360
// The second time the listener is called, the frame start is shifted to be equal to the previous frame end
347
361
if (timesCalled > 0 ) {
348
362
assertEquals(previousEnd + durationNanos, frameEndNanos)
@@ -356,25 +370,136 @@ class SentryFrameMetricsCollectorTest {
356
370
assertEquals(2 , timesCalled)
357
371
}
358
372
359
- private fun createMockWindow (): Window {
373
+ @Test
374
+ fun `collector properly reports slow and frozen flags` () {
375
+ val buildInfo = mock<BuildInfoProvider > {
376
+ whenever(it.sdkInfoVersion).thenReturn(Build .VERSION_CODES .O )
377
+ }
378
+ val collector = fixture.getSut(context, buildInfo)
379
+ val listener = collector.getProperty<Window .OnFrameMetricsAvailableListener >(" frameMetricsAvailableListener" )
380
+
381
+ var timesCalled = 0
382
+ var lastIsSlow = false
383
+ var lastIsFrozen = false
384
+
385
+ // when a frame takes less than 16ms, it's not considered slow or frozen
386
+ collector.startCollection { _, _,
387
+ _, _,
388
+ isSlow, isFrozen,
389
+ _ ->
390
+
391
+ lastIsSlow = isSlow
392
+ lastIsFrozen = isFrozen
393
+ timesCalled++
394
+ }
395
+ listener.onFrameMetricsAvailable(createMockWindow(), createMockFrameMetrics(), 0 )
396
+ assertFalse(lastIsSlow)
397
+ assertFalse(lastIsFrozen)
398
+
399
+ // when a frame takes more than 16ms, it's considered slow but not frozen
400
+ listener.onFrameMetricsAvailable(
401
+ createMockWindow(),
402
+ createMockFrameMetrics(
403
+ unknownDelayDuration = 1 + TimeUnit .MILLISECONDS .toNanos(100 )
404
+ ),
405
+ 0
406
+ )
407
+ assertTrue(lastIsSlow)
408
+ assertFalse(lastIsFrozen)
409
+
410
+ // when a frame takes more than 700ms, it's considered slow and frozen
411
+ listener.onFrameMetricsAvailable(
412
+ createMockWindow(),
413
+ createMockFrameMetrics(
414
+ unknownDelayDuration = 1 + TimeUnit .MILLISECONDS .toNanos(1000 )
415
+ ),
416
+ 0
417
+ )
418
+ assertTrue(lastIsSlow)
419
+ assertTrue(lastIsFrozen)
420
+
421
+ // Assert the callbacks were called
422
+ assertEquals(3 , timesCalled)
423
+ }
424
+
425
+ @Test
426
+ fun `collector properly reports frame delay` () {
427
+ val buildInfo = mock<BuildInfoProvider > {
428
+ whenever(it.sdkInfoVersion).thenReturn(Build .VERSION_CODES .O )
429
+ }
430
+ val collector = fixture.getSut(context, buildInfo)
431
+ val listener = collector.getProperty<Window .OnFrameMetricsAvailableListener >(" frameMetricsAvailableListener" )
432
+
433
+ var lastDelay = 0L
434
+
435
+ // when a frame takes less than 16ms, it's not considered slow or frozen
436
+ collector.startCollection { _, _,
437
+ _, delayNanos,
438
+ isSlow, isFrozen,
439
+ _ ->
440
+ lastDelay = delayNanos
441
+ }
442
+ // at 60hz, when the total duration is 10ms, the delay is 0
443
+ listener.onFrameMetricsAvailable(
444
+ createMockWindow(),
445
+ createMockFrameMetrics(
446
+ totalDuration = TimeUnit .MILLISECONDS .toNanos(16 )
447
+ ),
448
+ 0
449
+ )
450
+ assertEquals(0 , lastDelay)
451
+
452
+ // at 60hz, when the total duration is 20ms, the delay is considered ~4ms
453
+ listener.onFrameMetricsAvailable(
454
+ createMockWindow(),
455
+ createMockFrameMetrics(
456
+ totalDuration = TimeUnit .MILLISECONDS .toNanos(20 )
457
+ ),
458
+ 0
459
+ )
460
+ assertEquals(
461
+ // 20ms - 1/60 (~16.6ms) = 4ms
462
+ TimeUnit .MILLISECONDS .toNanos(20 ) - (TimeUnit .SECONDS .toNanos(1 ) / 60.0f ).toLong(),
463
+ lastDelay
464
+ )
465
+
466
+ // at 120hz, when the total duration is 20ms, the delay is considered ~8ms
467
+ listener.onFrameMetricsAvailable(
468
+ createMockWindow(120.0f ),
469
+ createMockFrameMetrics(
470
+ totalDuration = TimeUnit .MILLISECONDS .toNanos(20 )
471
+ ),
472
+ 0
473
+ )
474
+ assertEquals(
475
+ // 20ms - 1/120 (~8.33ms) = 8ms
476
+ TimeUnit .MILLISECONDS .toNanos(20 ) - (TimeUnit .SECONDS .toNanos(1 ) / 120.0f ).toLong(),
477
+ lastDelay
478
+ )
479
+ }
480
+
481
+ private fun createMockWindow (refreshRate : Float = 60F): Window {
360
482
val mockWindow = mock<Window >()
361
483
val mockDisplay = mock<Display >()
362
484
val mockWindowManager = mock<WindowManager >()
363
485
whenever(mockWindow.windowManager).thenReturn(mockWindowManager)
364
486
whenever(mockWindowManager.defaultDisplay).thenReturn(mockDisplay)
365
- whenever(mockDisplay.refreshRate).thenReturn(60F )
487
+ whenever(mockDisplay.refreshRate).thenReturn(refreshRate )
366
488
return mockWindow
367
489
}
368
490
369
- private fun createMockFrameMetrics (): FrameMetrics {
491
+ /* *
492
+ * FrameMetrics with default cpu time of 21 nanoseconds and INTENDED_VSYNC_TIMESTAMP of 50 nanoseconds
493
+ */
494
+ private fun createMockFrameMetrics (unknownDelayDuration : Long = 1, totalDuration : Long = 60): FrameMetrics {
370
495
val frameMetrics = mock<FrameMetrics >()
371
- whenever(frameMetrics.getMetric(FrameMetrics .UNKNOWN_DELAY_DURATION )).thenReturn(1 )
496
+ whenever(frameMetrics.getMetric(FrameMetrics .UNKNOWN_DELAY_DURATION )).thenReturn(unknownDelayDuration )
372
497
whenever(frameMetrics.getMetric(FrameMetrics .INPUT_HANDLING_DURATION )).thenReturn(2 )
373
498
whenever(frameMetrics.getMetric(FrameMetrics .ANIMATION_DURATION )).thenReturn(3 )
374
499
whenever(frameMetrics.getMetric(FrameMetrics .LAYOUT_MEASURE_DURATION )).thenReturn(4 )
375
500
whenever(frameMetrics.getMetric(FrameMetrics .DRAW_DURATION )).thenReturn(5 )
376
501
whenever(frameMetrics.getMetric(FrameMetrics .SYNC_DURATION )).thenReturn(6 )
377
- whenever(frameMetrics.getMetric(FrameMetrics .TOTAL_DURATION )).thenReturn(60 )
502
+ whenever(frameMetrics.getMetric(FrameMetrics .TOTAL_DURATION )).thenReturn(totalDuration )
378
503
whenever(frameMetrics.getMetric(FrameMetrics .INTENDED_VSYNC_TIMESTAMP )).thenReturn(50 )
379
504
return frameMetrics
380
505
}
0 commit comments