| /* |
| * 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); |
| } |
| } |