blob: 68598669b3d3deea34b6ead1ccf380b38ceab871 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.car.cts;
import static com.google.common.truth.Truth.assertWithMessage;
import android.cts.statsdatom.lib.ConfigUtils;
import android.cts.statsdatom.lib.DeviceUtils;
import android.cts.statsdatom.lib.ReportUtils;
import com.android.compatibility.common.util.PollingCheck;
import com.android.internal.os.StatsdConfigProto.StatsdConfig;
import com.android.os.AtomsProto.Atom;
import com.android.os.AtomsProto.CarWatchdogIoOveruseStats;
import com.android.os.AtomsProto.CarWatchdogIoOveruseStatsReported;
import com.android.os.AtomsProto.CarWatchdogKillStatsReported;
import com.android.os.StatsLog.EventMetricData;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RunWith(DeviceJUnit4ClassRunner.class)
public class CarWatchdogHostTest extends CarHostJUnit4TestCase {
public static final String TAG = CarWatchdogHostTest.class.getSimpleName();
/**
* CarWatchdog app package.
*/
protected static final String WATCHDOG_APP_PKG = "android.car.cts.watchdog.sharedapp";
/**
* CarWatchdog app shared user id.
*/
protected static final String WATCHDOG_APP_SHARED_USER_ID =
"shared:android.car.cts.uid.watchdog.sharedapp";
/**
* The class name of the main activity in the APK.
*/
private static final String ACTIVITY_CLASS = APP_PKG + ".CarWatchdogTestActivity";
/**
* The command to start a custom performance collection with CarWatchdog.
*/
private static final String START_CUSTOM_PERF_COLLECTION_CMD =
"dumpsys android.automotive.watchdog.ICarWatchdog/default --start_perf --max_duration"
+ " 600 --interval 1";
/**
* The command to stop a custom performance collection in CarWatchdog.
*/
private static final String STOP_CUSTOM_PERF_COLLECTION_CMD =
"dumpsys android.automotive.watchdog.ICarWatchdog/default --stop_perf > /dev/null";
/**
* The command to reset I/O overuse counters in the adb shell, which clears any previous
* stats saved by watchdog.
*/
private static final String RESET_RESOURCE_OVERUSE_CMD = String.format(
"dumpsys android.automotive.watchdog.ICarWatchdog/default "
+ "--reset_resource_overuse_stats %s,%s", APP_PKG, WATCHDOG_APP_SHARED_USER_ID);
/**
* The command to get I/O overuse foreground bytes threshold in the adb shell.
*/
private static final String GET_IO_OVERUSE_FOREGROUNG_BYTES_CMD =
"cmd car_service watchdog-io-get-3p-foreground-bytes";
/**
* The command to set I/O overuse foreground bytes threshold in the adb shell.
*/
private static final String SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD =
"cmd car_service watchdog-io-set-3p-foreground-bytes";
private static final String DEFINE_ENABLE_DISPLAY_POWER_POLICY_CMD =
"cmd car_service define-power-policy cts_car_watchdog_enable_display "
+ "--enable DISPLAY";
private static final String DEFINE_DISABLE_DISPLAY_POWER_POLICY_CMD =
"cmd car_service define-power-policy cts_car_watchdog_disable_display "
+ "--disable DISPLAY";
private static final String APPLY_ENABLE_DISPLAY_POWER_POLICY_CMD =
"cmd car_service apply-power-policy cts_car_watchdog_enable_display";
private static final String APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD =
"cmd car_service apply-power-policy cts_car_watchdog_disable_display";
private static final long FIFTY_MEGABYTES = 1024 * 1024 * 50;
private static final long TWO_HUNDRED_MEGABYTES = 1024 * 1024 * 200;
private static final int RECURRING_OVERUSE_COUNT = 3;
private static final Pattern DUMP_PATTERN = Pattern.compile(
"CarWatchdogTestActivity:\\s(.+)");
private static final Pattern FOREGROUND_BYTES_PATTERN = Pattern.compile(
"foregroundModeBytes = (\\d+)");
private static final long WATCHDOG_ACTION_TIMEOUT_MS = 15_000;
private boolean mDidModifyDateTime;
private long mOriginalForegroundBytes;
@Before
public void dateSetUp() throws Exception {
checkAndSetDate();
}
@After
public void dateReset() throws Exception {
checkAndResetDate();
}
@Before
public void setUp() throws Exception {
ConfigUtils.removeConfig(getDevice());
ReportUtils.clearReports(getDevice());
executeCommand(DEFINE_ENABLE_DISPLAY_POWER_POLICY_CMD);
executeCommand(DEFINE_DISABLE_DISPLAY_POWER_POLICY_CMD);
mOriginalForegroundBytes = parseForegroundBytesFromMessage(executeCommand(
GET_IO_OVERUSE_FOREGROUNG_BYTES_CMD));
executeCommand("%s %d", SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD, TWO_HUNDRED_MEGABYTES);
executeCommand("logcat -c");
executeCommand(START_CUSTOM_PERF_COLLECTION_CMD);
executeCommand(RESET_RESOURCE_OVERUSE_CMD);
}
@After
public void tearDown() throws Exception {
ConfigUtils.removeConfig(getDevice());
ReportUtils.clearReports(getDevice());
executeCommand(APPLY_ENABLE_DISPLAY_POWER_POLICY_CMD);
// Enable the CTS packages by running the reset resource overuse command.
executeCommand(RESET_RESOURCE_OVERUSE_CMD);
executeCommand(STOP_CUSTOM_PERF_COLLECTION_CMD);
executeCommand("%s %d", SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD, mOriginalForegroundBytes);
}
@Test
public void testIoOveruseKillAfterDisplayTurnOff() throws Exception {
uploadStatsdConfig(APP_PKG);
for (int i = 0; i < RECURRING_OVERUSE_COUNT; ++i) {
overuseDiskIo(APP_PKG);
verifyAtomIoOveruseStatsReported(APP_PKG, /* overuseTimes= */ i + 1);
ReportUtils.clearReports(getDevice());
}
executeCommand(APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD);
verifyTestAppKilled(APP_PKG);
verifyAtomKillStatsReported(APP_PKG);
}
@Test
public void testIoOveruseKillAfterDisplayTurnOffWithSharedUserIdApp() throws Exception {
uploadStatsdConfig(WATCHDOG_APP_PKG);
for (int i = 0; i < RECURRING_OVERUSE_COUNT; ++i) {
overuseDiskIo(WATCHDOG_APP_PKG);
verifyAtomIoOveruseStatsReported(WATCHDOG_APP_PKG, /* overuseTimes= */ i + 1);
ReportUtils.clearReports(getDevice());
}
executeCommand(APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD);
verifyTestAppKilled(WATCHDOG_APP_PKG);
verifyAtomKillStatsReported(WATCHDOG_APP_PKG);
}
private void uploadStatsdConfig(String packageName) throws Exception {
StatsdConfig.Builder config = ConfigUtils.createConfigBuilder("AID_SYSTEM");
ConfigUtils.addEventMetricForUidAtom(config,
Atom.CAR_WATCHDOG_IO_OVERUSE_STATS_REPORTED_FIELD_NUMBER,
/* uidInAttributionChain= */ false, packageName);
ConfigUtils.addEventMetricForUidAtom(config,
Atom.CAR_WATCHDOG_KILL_STATS_REPORTED_FIELD_NUMBER,
/* uidInAttributionChain= */ false, packageName);
ConfigUtils.uploadConfig(getDevice(), config);
}
private void verifyAtomIoOveruseStatsReported(String packageName, int overuseTimes)
throws Exception {
List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
assertWithMessage("Reported I/O overuse event metrics data").that(data).hasSize(1);
CarWatchdogIoOveruseStatsReported atom =
data.get(0).getAtom().getCarWatchdogIoOveruseStatsReported();
int appUid = DeviceUtils.getAppUid(getDevice(), packageName);
assertWithMessage("UID in atom from " + overuseTimes + " overuse").that(atom.getUid())
.isEqualTo(appUid);
assertWithMessage("Atom has I/O overuse stats from " + overuseTimes + " overuse")
.that(atom.hasIoOveruseStats()).isTrue();
verifyAtomIoOveruseStats(atom.getIoOveruseStats(), overuseTimes * TWO_HUNDRED_MEGABYTES,
"I/O overuse stats atom from " + overuseTimes + " overuse");
}
private void verifyAtomKillStatsReported(String packageName)
throws Exception {
List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
assertWithMessage("Reported kill event metrics data").that(data).hasSize(1);
CarWatchdogKillStatsReported atom =
data.get(0).getAtom().getCarWatchdogKillStatsReported();
int appUid = DeviceUtils.getAppUid(getDevice(), packageName);
assertWithMessage("UID in kill stats").that(atom.getUid()).isEqualTo(appUid);
assertWithMessage("Kill reason from kill stats").that(atom.getKillReason())
.isEqualTo(CarWatchdogKillStatsReported.KillReason.KILLED_ON_IO_OVERUSE);
assertWithMessage("System state from kill stats").that(atom.getSystemState())
.isEqualTo(CarWatchdogKillStatsReported.SystemState.USER_NO_INTERACTION_MODE);
assertWithMessage("Atom has I/O overuse stats from overuse kill")
.that(atom.hasIoOveruseStats()).isTrue();
verifyAtomIoOveruseStats(atom.getIoOveruseStats(),
RECURRING_OVERUSE_COUNT * TWO_HUNDRED_MEGABYTES,
"I/O overuse stats atom from overuse kill");
}
private void verifyAtomIoOveruseStats(CarWatchdogIoOveruseStats ioOveruseStats,
long foregroundWrittenBytes, String statsType) {
assertWithMessage(statsType + " has period").that(ioOveruseStats.hasPeriod()).isTrue();
assertWithMessage("Period in " + statsType).that(ioOveruseStats.getPeriod())
.isEqualTo(CarWatchdogIoOveruseStats.Period.DAILY);
assertWithMessage(statsType + " has threshold").that(ioOveruseStats.hasThreshold())
.isTrue();
assertWithMessage("Foreground threshold bytes in " + statsType)
.that(ioOveruseStats.getThreshold().getForegroundBytes())
.isEqualTo(TWO_HUNDRED_MEGABYTES);
assertWithMessage(statsType + " has written bytes").that(ioOveruseStats.hasWrittenBytes())
.isTrue();
// Watchdog daemon's polling/syncing interval and the disk I/O writes performed by the
// device side app are asynchronous. So, the actual number of bytes written by the app might
// be greater than the expected written bytes. Thus verify that the reported written bytes
// are in the range of 50MiB.
assertWithMessage("Foreground written bytes in " + statsType)
.that(ioOveruseStats.getWrittenBytes().getForegroundBytes())
.isAtLeast(foregroundWrittenBytes);
assertWithMessage("Foreground written bytes in " + statsType)
.that(ioOveruseStats.getWrittenBytes().getForegroundBytes())
.isAtMost(foregroundWrittenBytes + FIFTY_MEGABYTES);
}
private void overuseDiskIo(String packageName) throws Exception {
startMainActivity(packageName);
long remainingBytes = readForegroundBytesFromActivityDump(packageName);
sendBytesToKillApp(remainingBytes, packageName);
remainingBytes = readForegroundBytesFromActivityDump(packageName);
assertWithMessage("Application " + packageName + "'s remaining write bytes")
.that(remainingBytes).isEqualTo(0);
}
private long readForegroundBytesFromActivityDump(String packageName) throws Exception {
AtomicReference<String> notification = new AtomicReference<>();
PollingCheck.check("No notification received in the activity dump",
WATCHDOG_ACTION_TIMEOUT_MS,
() -> {
String dump = fetchActivityDumpsys(packageName);
if (dump.startsWith("INFO") && dump.contains("--Notification--")) {
notification.set(dump);
return true;
}
return false;
});
return parseForegroundBytesFromMessage(notification.get());
}
private long parseForegroundBytesFromMessage(String message) throws IllegalArgumentException {
Matcher m = FOREGROUND_BYTES_PATTERN.matcher(message);
if (m.find()) {
return Long.parseLong(m.group(1));
}
throw new IllegalArgumentException("Invalid message format: " + message);
}
private void verifyTestAppKilled(String packageName) throws Exception {
PollingCheck.check("Failed to kill " + packageName + " application",
WATCHDOG_ACTION_TIMEOUT_MS,
() -> {
// Check activity dump for errors. Throws exception on error.
fetchActivityDumpsys(packageName);
return !isPackageRunning(packageName);
});
}
private String fetchActivityDumpsys(String packageName) throws Exception {
String dump = executeCommand("dumpsys activity %s/%s", packageName, ACTIVITY_CLASS);
Matcher m = DUMP_PATTERN.matcher(dump);
if (!m.find()) {
return "";
}
String message = Objects.requireNonNull(m.group(1)).trim();
if (message.startsWith("ERROR")) {
throw new Exception(message);
}
return message;
}
private void startMainActivity(String packageName) throws Exception {
String result = executeCommand("pm clear %s", packageName);
assertWithMessage("pm clear").that(result.trim()).isEqualTo("Success");
executeCommand("am start -W -a android.intent.action.MAIN -n %s/%s", packageName,
ACTIVITY_CLASS);
assertWithMessage("Is %s running?", packageName).that(isPackageRunning(packageName))
.isTrue();
}
private void sendBytesToKillApp(long remainingBytes, String appPkg) throws Exception {
executeCommand(
"am start -W -a android.intent.action.MAIN -n %s/%s --el bytes_to_kill %d",
appPkg, ACTIVITY_CLASS, remainingBytes);
}
private void checkAndSetDate() throws Exception {
// Get date in ISO-8601 format
LocalDateTime now = LocalDateTime.parse(executeCommand("date +%%FT%%T").trim());
if (now.getHour() < 23) {
return;
}
LocalDateTime nowMinusOneHour = now.minusHours(1);
executeCommand("date %s", nowMinusOneHour);
CLog.d(TAG, "checkAndSetDate: DateTime changed from %s to %s", now, nowMinusOneHour);
mDidModifyDateTime = true;
}
private void checkAndResetDate() throws Exception {
if (!mDidModifyDateTime) {
return;
}
LocalDateTime now = LocalDateTime.parse(executeCommand("date +%%FT%%T").trim());
LocalDateTime nowPlusOneHour = now.plusHours(1);
executeCommand("date %s", nowPlusOneHour);
CLog.d(TAG, "checkAndResetDate: DateTime changed from %s to %s", now, nowPlusOneHour);
}
}