blob: 9b614fce15918aaa2736a6988c216860bb945c95 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.helpers;
import static com.android.helpers.MetricUtility.constructKey;
import static com.android.helpers.JankCollectionHelper.GFXINFO_COMMAND_GET;
import static com.android.helpers.JankCollectionHelper.GFXINFO_COMMAND_RESET;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.TOTAL_FRAMES;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.JANKY_FRAMES_COUNT;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.JANKY_FRAMES_PRCNT;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.FRAME_TIME_50TH;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.FRAME_TIME_90TH;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.FRAME_TIME_95TH;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.FRAME_TIME_99TH;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_MISSED_VSYNC;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_HIGH_INPUT_LATENCY;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_SLOW_UI_THREAD;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_SLOW_BITMAP_UPLOADS;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_SLOW_DRAW;
import static com.android.helpers.JankCollectionHelper.GfxInfoMetric.NUM_FRAME_DEADLINE_MISSED;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;
import android.support.test.uiautomator.UiDevice;
import androidx.test.runner.AndroidJUnit4;
import java.io.IOException;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/** Android Unit tests for {@link JankCollectionHelper}. */
@RunWith(AndroidJUnit4.class)
public class JankCollectionHelperTest {
private static final String GFXINFO_RESET_FORMAT =
"\n\n** Graphics info for pid 9999 [%s] **"
+ "\n"
+ "\nTotal frames rendered: 0"
+ "\nJanky frames: 0 (00.00%%)"
+ "\n50th percentile: 0ms"
+ "\n90th percentile: 0ms"
+ "\n95th percentile: 0ms"
+ "\n99th percentile: 0ms"
+ "\nNumber Missed Vsync: 0"
+ "\nNumber High input latency: 0"
+ "\nNumber Slow UI thread: 0"
+ "\nNumber Slow bitmap uploads: 0"
+ "\nNumber Slow issue draw commands: 0"
+ "\nNumber Frame deadline missed: 0";
private static final String GFXINFO_GET_FORMAT =
"\n\n** Graphics info for pid 9999 [%s] **"
+ "\n"
+ "\nTotal frames rendered: 900"
+ "\nJanky frames: 300 (33.33%%)"
+ "\n50th percentile: 150ms"
+ "\n90th percentile: 190ms"
+ "\n95th percentile: 195ms"
+ "\n99th percentile: 199ms"
+ "\nNumber Missed Vsync: 1"
+ "\nNumber High input latency: 2"
+ "\nNumber Slow UI thread: 3"
+ "\nNumber Slow bitmap uploads: 4"
+ "\nNumber Slow issue draw commands: 5"
+ "\nNumber Frame deadline missed: 6";
private @Mock UiDevice mUiDevice;
private JankCollectionHelper mHelper;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mHelper = Mockito.spy(new JankCollectionHelper());
when(mHelper.getDevice()).thenReturn(mUiDevice);
}
/** Test track a single, valid package. */
@Test
public void testCollect_valuesMatch() throws Exception {
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg1"));
mockGetCommand("pkg1", String.format(GFXINFO_GET_FORMAT, "pkg1"));
mHelper.addTrackedPackages("pkg1");
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
assertThat(metrics.get(buildMetricKey("pkg1", TOTAL_FRAMES.getMetricId())))
.isEqualTo(900.0);
assertThat(metrics.get(buildMetricKey("pkg1", JANKY_FRAMES_COUNT.getMetricId())))
.isEqualTo(300.0);
assertThat(metrics.get(buildMetricKey("pkg1", JANKY_FRAMES_PRCNT.getMetricId())))
.isEqualTo(33.33);
assertThat(metrics.get(buildMetricKey("pkg1", FRAME_TIME_50TH.getMetricId())))
.isEqualTo(150.0);
assertThat(metrics.get(buildMetricKey("pkg1", FRAME_TIME_90TH.getMetricId())))
.isEqualTo(190.0);
assertThat(metrics.get(buildMetricKey("pkg1", FRAME_TIME_95TH.getMetricId())))
.isEqualTo(195.0);
assertThat(metrics.get(buildMetricKey("pkg1", FRAME_TIME_99TH.getMetricId())))
.isEqualTo(199.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_MISSED_VSYNC.getMetricId())))
.isEqualTo(1.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_HIGH_INPUT_LATENCY.getMetricId())))
.isEqualTo(2.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_SLOW_UI_THREAD.getMetricId())))
.isEqualTo(3.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_SLOW_BITMAP_UPLOADS.getMetricId())))
.isEqualTo(4.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_SLOW_DRAW.getMetricId()))).isEqualTo(5.0);
assertThat(metrics.get(buildMetricKey("pkg1", NUM_FRAME_DEADLINE_MISSED.getMetricId())))
.isEqualTo(6.0);
mHelper.stopCollecting();
}
/** Test track a single, valid package. */
@Test
public void testCollect_singlePackage() throws Exception {
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg1"));
mockGetCommand("pkg1", String.format(GFXINFO_GET_FORMAT, "pkg1"));
mHelper.addTrackedPackages("pkg1");
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
for (String key : metrics.keySet()) {
assertWithMessage("All keys must contains the single watched package name.")
.that(key)
.contains("pkg1");
}
mHelper.stopCollecting();
}
/** Test track multiple valid packages. */
@Test
public void testCollect_multiPackage() throws Exception {
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg1"));
mockGetCommand("pkg1", String.format(GFXINFO_GET_FORMAT, "pkg1"));
mockResetCommand("pkg2", String.format(GFXINFO_RESET_FORMAT, "pkg2"));
mockGetCommand("pkg2", String.format(GFXINFO_GET_FORMAT, "pkg2"));
mockResetCommand("pkg3", String.format(GFXINFO_RESET_FORMAT, "pkg3"));
mockGetCommand("pkg3", String.format(GFXINFO_GET_FORMAT, "pkg3"));
mHelper.addTrackedPackages("pkg1", "pkg2");
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
// Assert against all keys that they only match expected packages.
for (String key : metrics.keySet()) {
assertWithMessage("All keys must contains one of the 2 watched package names.")
.that(key)
.containsMatch(".*pkg(1|2).*");
assertWithMessage("The unwatched package should not be included in metrics.")
.that(key)
.doesNotContain("pkg3");
}
// Assert that it contains keys for both packages being watched.
assertThat(metrics).containsKey(buildMetricKey("pkg1", TOTAL_FRAMES.getMetricId()));
assertThat(metrics).containsKey(buildMetricKey("pkg2", TOTAL_FRAMES.getMetricId()));
mHelper.stopCollecting();
}
/** Test track all packages when unspecified. */
@Test
public void testCollect_allPackages() throws Exception {
String resetOutput =
String.join(
"\n",
String.format(GFXINFO_RESET_FORMAT, "pkg1"),
String.format(GFXINFO_RESET_FORMAT, "pkg2"),
String.format(GFXINFO_RESET_FORMAT, "pkg3"));
String getOutput =
String.join(
"\n",
String.format(GFXINFO_GET_FORMAT, "pkg1"),
String.format(GFXINFO_GET_FORMAT, "pkg2"),
String.format(GFXINFO_GET_FORMAT, "pkg3"));
mockResetCommand("", resetOutput);
mockGetCommand("", getOutput);
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
// Assert against all keys that they only match expected packages.
for (String key : metrics.keySet()) {
assertWithMessage("All keys must contains one of the output package names.")
.that(key)
.containsMatch(".*pkg(1|2|3).*");
}
// Assert that it contains keys for all packages being watched.
assertThat(metrics).containsKey(buildMetricKey("pkg1", TOTAL_FRAMES.getMetricId()));
assertThat(metrics).containsKey(buildMetricKey("pkg2", TOTAL_FRAMES.getMetricId()));
assertThat(metrics).containsKey(buildMetricKey("pkg3", TOTAL_FRAMES.getMetricId()));
mHelper.stopCollecting();
}
/** Test that it collects available fields, even if some are missing. */
@Test
public void testCollect_ignoreMissingFields() throws Exception {
String missingResets =
"\n\n** Graphics info for pid 9999 [pkg1] **"
+ "\n"
+ "\nTotal frames rendered: 0"
+ "\nJanky frames: 0 (00.00%%)"
+ "\nNumber Missed Vsync: 0"
+ "\nNumber High input latency: 0"
+ "\nNumber Slow UI thread: 0"
+ "\nNumber Slow bitmap uploads: 0"
+ "\nNumber Slow issue draw commands: 0"
+ "\nNumber Frame deadline missed: 0";
String missingGets =
"\n\n** Graphics info for pid 9999 [pkg1] **"
+ "\n"
+ "\nTotal frames rendered: 900"
+ "\nJanky frames: 300 (33.33%)"
+ "\nNumber Missed Vsync: 1"
+ "\nNumber High input latency: 2"
+ "\nNumber Slow UI thread: 3"
+ "\nNumber Slow bitmap uploads: 4"
+ "\nNumber Slow issue draw commands: 5"
+ "\nNumber Frame deadline missed: 6";
mockResetCommand("pkg1", missingResets);
mockGetCommand("pkg1", missingGets);
mHelper.addTrackedPackages("pkg1");
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
assertThat(metrics.keySet())
.containsExactly(
buildMetricKey("pkg1", TOTAL_FRAMES.getMetricId()),
buildMetricKey("pkg1", JANKY_FRAMES_COUNT.getMetricId()),
buildMetricKey("pkg1", JANKY_FRAMES_PRCNT.getMetricId()),
buildMetricKey("pkg1", NUM_MISSED_VSYNC.getMetricId()),
buildMetricKey("pkg1", NUM_HIGH_INPUT_LATENCY.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_UI_THREAD.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_BITMAP_UPLOADS.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_DRAW.getMetricId()),
buildMetricKey("pkg1", NUM_FRAME_DEADLINE_MISSED.getMetricId()));
mHelper.stopCollecting();
}
/** Test that it collects known fields, even if some are unknown. */
@Test
public void testCollect_ignoreUnknownField() throws Exception {
String extraFields =
"\nWhatever: 1"
+ "\nWhateverClose: 2"
+ "\nWhateverNotSo: 3"
+ "\nWhateverBlahs: 4";
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT + extraFields, "pkg1"));
mockGetCommand("pkg1", String.format(GFXINFO_GET_FORMAT + extraFields, "pkg1"));
mHelper.addTrackedPackages("pkg1");
mHelper.startCollecting();
Map<String, Double> metrics = mHelper.getMetrics();
assertThat(metrics.keySet())
.containsExactly(
buildMetricKey("pkg1", TOTAL_FRAMES.getMetricId()),
buildMetricKey("pkg1", JANKY_FRAMES_COUNT.getMetricId()),
buildMetricKey("pkg1", JANKY_FRAMES_PRCNT.getMetricId()),
buildMetricKey("pkg1", FRAME_TIME_50TH.getMetricId()),
buildMetricKey("pkg1", FRAME_TIME_90TH.getMetricId()),
buildMetricKey("pkg1", FRAME_TIME_95TH.getMetricId()),
buildMetricKey("pkg1", FRAME_TIME_99TH.getMetricId()),
buildMetricKey("pkg1", NUM_MISSED_VSYNC.getMetricId()),
buildMetricKey("pkg1", NUM_HIGH_INPUT_LATENCY.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_UI_THREAD.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_BITMAP_UPLOADS.getMetricId()),
buildMetricKey("pkg1", NUM_SLOW_DRAW.getMetricId()),
buildMetricKey("pkg1", NUM_FRAME_DEADLINE_MISSED.getMetricId()));
mHelper.stopCollecting();
}
/** Test that it continues resetting even if certain packages throw for some reason. */
@Test
public void testCollect_delayExceptions_onReset() throws Exception {
// Package 1 is problematic to reset, but package 2 and 3 are good.
String cmd = String.format(GFXINFO_COMMAND_RESET, "pkg1");
when(mUiDevice.executeShellCommand(cmd)).thenThrow(new RuntimeException());
mockResetCommand("pkg2", String.format(GFXINFO_RESET_FORMAT, "pkg2"));
mockResetCommand("pkg3", String.format(GFXINFO_RESET_FORMAT, "pkg3"));
mHelper.addTrackedPackages("pkg1", "pkg2", "pkg3");
try {
mHelper.startCollecting();
fail("Should have thrown an exception resetting pkg1.");
} catch (Exception e) {
// assert that all of the packages were reset and pass.
InOrder inOrder = inOrder(mUiDevice);
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg1"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg2"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg3"));
}
}
/** Test that it continues collecting even if certain packages throw for some reason. */
@Test
public void testCollect_delayExceptions_onGet() throws Exception {
// Package 1 is problematic to reset, but package 2 and 3 are good.
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg1"));
mockResetCommand("pkg2", String.format(GFXINFO_RESET_FORMAT, "pkg2"));
mockResetCommand("pkg3", String.format(GFXINFO_RESET_FORMAT, "pkg3"));
String cmd = String.format(GFXINFO_COMMAND_GET, "pkg1");
when(mUiDevice.executeShellCommand(cmd)).thenThrow(new RuntimeException());
mockGetCommand("pkg2", String.format(GFXINFO_GET_FORMAT, "pkg2"));
mockGetCommand("pkg3", String.format(GFXINFO_GET_FORMAT, "pkg3"));
mHelper.addTrackedPackages("pkg1", "pkg2", "pkg3");
try {
mHelper.startCollecting();
mHelper.getMetrics();
fail("Should have thrown an exception getting pkg1.");
} catch (Exception e) {
// assert that all of the packages were reset and gotten and pass.
InOrder inOrder = inOrder(mUiDevice);
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg1"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg2"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_RESET, "pkg3"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_GET, "pkg1"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_GET, "pkg2"));
inOrder.verify(mUiDevice)
.executeShellCommand(String.format(GFXINFO_COMMAND_GET, "pkg3"));
}
}
/** Test that it fails if the {@code gfxinfo} metrics cannot be cleared. */
@Test
public void testFailures_cannotClear() throws Exception {
String cmd = String.format(JankCollectionHelper.GFXINFO_COMMAND_RESET, "");
when(mUiDevice.executeShellCommand(cmd)).thenReturn("");
try {
mHelper.startCollecting();
fail("Should have thrown an exception.");
} catch (RuntimeException e) {
// pass
}
}
/** Test that it fails when encountering an {@code IOException} on reset. */
@Test
public void testFailures_ioFailure() throws Exception {
String cmd = String.format(JankCollectionHelper.GFXINFO_COMMAND_RESET, "");
when(mUiDevice.executeShellCommand(cmd)).thenThrow(new IOException());
try {
mHelper.startCollecting();
fail("Should have thrown an exception.");
} catch (RuntimeException e) {
// pass
}
}
/** Test that it fails when the package does not show up on reset. */
@Test
public void testFailures_noPackageOnReset() throws Exception {
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg2"));
mHelper.addTrackedPackages("pkg1");
try {
mHelper.startCollecting();
fail("Should have thrown an exception.");
} catch (RuntimeException e) {
// pass
}
}
/** Test that it fails when the package does not show up on get. */
@Test
public void testFailures_noPackageOnGet() throws Exception {
mockResetCommand("pkg1", String.format(GFXINFO_RESET_FORMAT, "pkg1"));
mockGetCommand("pkg1", String.format(GFXINFO_GET_FORMAT, "pkg2"));
mHelper.addTrackedPackages("pkg1");
try {
mHelper.startCollecting();
mHelper.getMetrics();
fail("Should have thrown an exception.");
} catch (RuntimeException e) {
// pass
}
}
private String buildMetricKey(String pkg, String id) {
return constructKey(JankCollectionHelper.GFXINFO_METRICS_PREFIX, pkg, id);
}
private void mockResetCommand(String pkg, String output) throws IOException {
String cmd = String.format(GFXINFO_COMMAND_RESET, pkg);
when(mUiDevice.executeShellCommand(cmd)).thenReturn(output);
}
private void mockGetCommand(String pkg, String output) throws IOException {
String cmd = String.format(GFXINFO_COMMAND_GET, pkg);
when(mUiDevice.executeShellCommand(cmd)).thenReturn(output);
}
}