chromium/base/android/junit/src/org/chromium/base/jank_tracker/FrameMetricsStoreTest.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.jank_tracker;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import org.chromium.base.FakeTimeTestRule;
import org.chromium.base.TimeUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;

/** Tests for FrameMetricsStore. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class FrameMetricsStoreTest {
    @Rule public FakeTimeTestRule mFakeTimeTestRule = new FakeTimeTestRule();

    private FrameMetricsStore mStore;

    @Before
    public void setUp() {
        mStore = new FrameMetricsStore();
        mStore.initialize();
    }

    @Test
    public void addFrameMeasurementTest() {
        mStore.startTrackingScenario(JankScenario.NEW_TAB_PAGE);

        long frame_start_vsync_ts = 0;
        mStore.addFrameMeasurement(10_000_000L, 0, frame_start_vsync_ts);
        mStore.addFrameMeasurement(12_000_000L, 0, frame_start_vsync_ts);
        mStore.addFrameMeasurement(20_000_000L, 1, frame_start_vsync_ts);
        mStore.addFrameMeasurement(8_000_000L, 2, frame_start_vsync_ts);

        JankMetrics metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);

        assertArrayEquals(
                new long[] {10_000_000L, 12_000_000L, 20_000_000L, 8_000_000L},
                metrics.durationsNs);
        assertArrayEquals(new int[] {0, 0, 1, 2}, metrics.missedVsyncs);

        metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);
        assertEquals(0, metrics.durationsNs.length);
    }

    @Test
    public void takeMetrics_getMetricsWithoutAnyFrames() {
        mStore.startTrackingScenario(JankScenario.NEW_TAB_PAGE);
        JankMetrics metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);

        assertEquals(0, metrics.durationsNs.length);
    }

    @Test
    public void concurrentScenarios() {
        // We want to test 2 things.
        // 1) that concurrent scenarios get the correct frames
        // 2) that the deletion logic runs correctly. Note however that deletion logic is not
        // actually public behaviour but we just want this test to explicitly exercise it to
        // uncover potential bugs.
        mStore.startTrackingScenario(JankScenario.NEW_TAB_PAGE);

        long frame_start_vsync_ts = 1_000_000L;
        mStore.addFrameMeasurement(10_000_000L, 0, frame_start_vsync_ts);
        mStore.addFrameMeasurement(12_000_000L, 0, frame_start_vsync_ts + 1);
        mStore.startTrackingScenario(JankScenario.FEED_SCROLLING);
        mStore.addFrameMeasurement(20_000_000L, 2, frame_start_vsync_ts + 2);
        mStore.addFrameMeasurement(8_000_000L, 1, frame_start_vsync_ts + 3);

        // Stop NEW_TAB_PAGE and now the first two frames will be deleted from the
        // FrameMetricsStore().
        JankMetrics metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);

        assertArrayEquals(
                new long[] {10_000_000L, 12_000_000L, 20_000_000L, 8_000_000L},
                metrics.durationsNs);
        assertArrayEquals(new int[] {0, 0, 2, 1}, metrics.missedVsyncs);

        metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);
        assertEquals(0, metrics.durationsNs.length);

        // Only after that will we stop FEED_SCROLLING and we should only see the last two frames.
        metrics = mStore.stopTrackingScenario(JankScenario.FEED_SCROLLING);
        assertArrayEquals(new long[] {20_000_000L, 8_000_000L}, metrics.durationsNs);
        assertArrayEquals(new int[] {2, 1}, metrics.missedVsyncs);
    }

    @Test
    public void restartScenario() {
        // This test is setup to mainly test removeUnusedFrames() method codepaths.
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        long frame_start_vsync_ts = 1_000_000L;
        mStore.addFrameMeasurement(10_000_000L, 0, frame_start_vsync_ts);
        mStore.addFrameMeasurement(12_000_000L, 1, frame_start_vsync_ts + 1);

        JankMetrics metrics = mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        assertArrayEquals(new long[] {10_000_000L, 12_000_000L}, metrics.durationsNs);
        assertArrayEquals(new int[] {0, 1}, metrics.missedVsyncs);

        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        mStore.addFrameMeasurement(10_000_100L, 2, frame_start_vsync_ts + 2);
        mStore.addFrameMeasurement(11_000_100L, 0, frame_start_vsync_ts + 3);

        mStore.startTrackingScenario(JankScenario.NEW_TAB_PAGE);

        mStore.addFrameMeasurement(12_000_100L, 0, frame_start_vsync_ts + 4);

        metrics = mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING);
        assertArrayEquals(new long[] {10_000_100L, 11_000_100L, 12_000_100L}, metrics.durationsNs);
        assertArrayEquals(new int[] {2, 0, 0}, metrics.missedVsyncs);

        metrics = mStore.stopTrackingScenario(JankScenario.NEW_TAB_PAGE);
        assertArrayEquals(new long[] {12_000_100L}, metrics.durationsNs);
        assertArrayEquals(new int[] {0}, metrics.missedVsyncs);
    }

    @Test
    public void startPendingScenarioBeforeScenarioEnd() {
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        long now = TimeUtils.uptimeMillis();

        long[] frame_timestamps_ns =
                new long[] {
                    now * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame1 start time
                    (now + 1) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame2 start time
                    (now + 2) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // scenario end time
                    (now + 3) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // new scenario start time
                    (now + 4) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame3 start time
                    (now + 5) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame4 start time
                };

        mStore.addFrameMeasurement(10_000_000L, 0, frame_timestamps_ns[0]);
        mStore.addFrameMeasurement(12_000_000L, 3, frame_timestamps_ns[1]);

        // This start scenario shouldn't be blanket ignored instead this should
        // trigger start of the scenario after stop scenario call.
        mFakeTimeTestRule.advanceMillis(
                (frame_timestamps_ns[3] / TimeUtils.NANOSECONDS_PER_MILLISECOND) - now);
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        // The end time of scenario is before the start time of last start calls,
        // this can occur in the field as well since we delay stopTrackingScenario
        // to receive metrics for all the frames in given time range.
        JankMetrics metrics =
                mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING, frame_timestamps_ns[2]);

        assertArrayEquals(new long[] {10_000_000L, 12_000_000L}, metrics.durationsNs);
        assertArrayEquals(new int[] {0, 3}, metrics.missedVsyncs);

        mStore.addFrameMeasurement(10_000_100L, 2, frame_timestamps_ns[4]);
        mStore.addFrameMeasurement(11_000_100L, 0, frame_timestamps_ns[5]);

        metrics = mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        assertArrayEquals(new long[] {10_000_100L, 11_000_100L}, metrics.durationsNs);
        assertArrayEquals(new int[] {2, 0}, metrics.missedVsyncs);
    }

    @Test
    public void multipleStartScenarioBeforeScenarioEnd() {
        /*
         * Testing a scenario where might have a complete scroll within the delay period of
         * processing a finishTrackingScenario request. We typically wait for ~100ms to receive
         * frame metrics corresponding to last few frames in scenario. This test simulates
         * scenario depicted below. '|______|' is the actual scroll start and end, while
         * '-----------|' is the delay in processing finishTrackingScenario call i.e. when we
         * call stopTrackingScenario.
         *
         * Scroll1: |________________________|-----------|
         * Scroll2:                           |____|-----------|
         * Scroll3:                                    |_____________|-----------|
         *
         * As per current architecture expectation is we will loose metrics for scroll2 and scroll3,
         * but this should be fine since the scenario is pretty unrealistic so shouldn't occur often
         * in field data.
         */
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING);

        long now = TimeUtils.uptimeMillis();

        long[] frame_timestamps_ns =
                new long[] {
                    now * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame1 start time
                    (now + 1) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame2 start time
                    (now + 2) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Scroll1 end time
                    (now + 3) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Scroll2 start time
                    (now + 4) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame3 start time
                    (now + 5) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame4 start time
                    (now + 6) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Scroll2 end time
                    (now + 7) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Scroll3 start time
                    (now + 8) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame5 start time
                    (now + 9) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Frame6 start time
                    (now + 10) * TimeUtils.NANOSECONDS_PER_MILLISECOND, // Scroll3 end time
                };

        mStore.addFrameMeasurement(10_000_000L, 0, frame_timestamps_ns[0]); // Frame1
        mStore.addFrameMeasurement(12_000_000L, 1, frame_timestamps_ns[1]); // Frame2

        mFakeTimeTestRule.advanceMillis(
                (frame_timestamps_ns[3] / TimeUtils.NANOSECONDS_PER_MILLISECOND) - now);
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING); // Scroll2 start
        mStore.addFrameMeasurement(10_000_100L, 2, frame_timestamps_ns[4]); // Frame3
        mStore.addFrameMeasurement(11_000_100L, 0, frame_timestamps_ns[5]); // Frame4

        mFakeTimeTestRule.advanceMillis(
                (frame_timestamps_ns[7] / TimeUtils.NANOSECONDS_PER_MILLISECOND)
                        - (frame_timestamps_ns[3] / TimeUtils.NANOSECONDS_PER_MILLISECOND));
        mStore.startTrackingScenario(JankScenario.WEBVIEW_SCROLLING); // Scroll3 start
        mStore.addFrameMeasurement(10_000_100L, 1, frame_timestamps_ns[8]); // Frame5
        mStore.addFrameMeasurement(11_000_100L, 0, frame_timestamps_ns[9]); // Frame6

        // Scroll1 end.
        JankMetrics metrics =
                mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING, frame_timestamps_ns[2]);

        assertArrayEquals(new long[] {10_000_000L, 12_000_000L}, metrics.durationsNs);
        assertArrayEquals(new int[] {0, 1}, metrics.missedVsyncs);

        // Scroll2 end.
        metrics =
                mStore.stopTrackingScenario(JankScenario.WEBVIEW_SCROLLING, frame_timestamps_ns[6]);

        assertArrayEquals(new long[] {}, metrics.durationsNs);
        assertArrayEquals(new int[] {}, metrics.missedVsyncs);

        // Scroll3 end.
        metrics =
                mStore.stopTrackingScenario(
                        JankScenario.WEBVIEW_SCROLLING, frame_timestamps_ns[10]);

        assertArrayEquals(new long[] {}, metrics.durationsNs);
        assertArrayEquals(new int[] {}, metrics.missedVsyncs);
    }

    @Test
    public void multipleActiveScrollScenarios() {
        JankScenario wv_scroll1 = new JankScenario(JankScenario.Type.WEBVIEW_SCROLLING, 1);
        JankScenario wv_scroll2 = new JankScenario(JankScenario.Type.WEBVIEW_SCROLLING, 2);

        mStore.startTrackingScenario(wv_scroll1);
        long frame_start_vsync_ts = 0;
        // Scroll1 frames.
        mStore.addFrameMeasurement(10_000_000L, 0, frame_start_vsync_ts + 1);
        mStore.addFrameMeasurement(11_000_000L, 1, frame_start_vsync_ts + 2);

        mStore.startTrackingScenario(wv_scroll2);

        // Common - scroll1 and scroll 2 frames.
        mStore.addFrameMeasurement(12_000_000L, 2, frame_start_vsync_ts + 3);
        mStore.addFrameMeasurement(13_000_000L, 0, frame_start_vsync_ts + 4);

        JankMetrics scroll1_metrics = mStore.stopTrackingScenario(wv_scroll1);

        // Scrol2 frames.
        mStore.addFrameMeasurement(14_000_000L, 0, frame_start_vsync_ts + 5);
        mStore.addFrameMeasurement(15_000_000L, 1, frame_start_vsync_ts + 6);
        JankMetrics scroll2_metrics = mStore.stopTrackingScenario(wv_scroll2);

        assertArrayEquals(
                new long[] {10_000_000L, 11_000_000L, 12_000_000L, 13_000_000L},
                scroll1_metrics.durationsNs);
        assertArrayEquals(new int[] {0, 1, 2, 0}, scroll1_metrics.missedVsyncs);

        assertArrayEquals(
                new long[] {12_000_000L, 13_000_000L, 14_000_000L, 15_000_000L},
                scroll2_metrics.durationsNs);
        assertArrayEquals(new int[] {2, 0, 0, 1}, scroll2_metrics.missedVsyncs);
    }
}