1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.car.cts; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import android.cts.statsdatom.lib.ConfigUtils; 22 import android.cts.statsdatom.lib.DeviceUtils; 23 import android.cts.statsdatom.lib.ReportUtils; 24 25 import com.android.compatibility.common.util.ApiLevelUtil; 26 import com.android.compatibility.common.util.PollingCheck; 27 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 28 import com.android.os.AtomsProto.Atom; 29 import com.android.os.AtomsProto.CarWatchdogIoOveruseStats; 30 import com.android.os.AtomsProto.CarWatchdogIoOveruseStatsReported; 31 import com.android.os.AtomsProto.CarWatchdogKillStatsReported; 32 import com.android.os.StatsLog.EventMetricData; 33 import com.android.tradefed.log.LogUtil.CLog; 34 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 35 36 import org.junit.After; 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 41 import java.time.LocalDateTime; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Objects; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.atomic.AtomicReference; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 @RunWith(DeviceJUnit4ClassRunner.class) 51 public class CarWatchdogHostTest extends CarHostJUnit4TestCase { 52 53 /** 54 * CarWatchdog app package. 55 */ 56 protected static final String WATCHDOG_APP_PKG = "android.car.cts.watchdog.sharedapp"; 57 58 /** 59 * Second CarWatchdog app package. 60 */ 61 protected static final String WATCHDOG_APP_PKG_2 = "android.car.cts.watchdog.second.sharedapp"; 62 63 /** 64 * CarWatchdog app shared user id. 65 */ 66 protected static final String WATCHDOG_APP_SHARED_USER_ID = 67 "shared:android.car.cts.uid.watchdog.sharedapp"; 68 69 /** 70 * The class name of the main activity in the APK. 71 */ 72 private static final String ACTIVITY_CLASS = APP_PKG + ".CarWatchdogTestActivity"; 73 74 /** 75 * The command to start a custom performance collection with CarWatchdog. 76 */ 77 private static final String START_CUSTOM_PERF_COLLECTION_CMD = 78 "dumpsys android.automotive.watchdog.ICarWatchdog/default --start_perf --max_duration" 79 + " 600 --interval 1"; 80 81 /** 82 * The command to stop a custom performance collection in CarWatchdog. 83 */ 84 private static final String STOP_CUSTOM_PERF_COLLECTION_CMD = 85 "dumpsys android.automotive.watchdog.ICarWatchdog/default --stop_perf"; 86 87 /** 88 * The command to reset I/O overuse counters in the adb shell, which clears any previous 89 * stats saved by watchdog. 90 */ 91 private static final String RESET_RESOURCE_OVERUSE_CMD = String.format( 92 "dumpsys android.automotive.watchdog.ICarWatchdog/default " 93 + "--reset_resource_overuse_stats %s,%s", APP_PKG, WATCHDOG_APP_SHARED_USER_ID); 94 95 /** 96 * The command to get I/O overuse foreground bytes threshold in the adb shell. 97 */ 98 private static final String GET_IO_OVERUSE_FOREGROUNG_BYTES_CMD = 99 "cmd car_service watchdog-io-get-3p-foreground-bytes"; 100 101 /** 102 * The command to set I/O overuse foreground bytes threshold in the adb shell. 103 */ 104 private static final String SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD = 105 "cmd car_service watchdog-io-set-3p-foreground-bytes"; 106 107 private static final String DEFINE_ENABLE_DISPLAY_POWER_POLICY_CMD = 108 "cmd car_service define-power-policy cts_car_watchdog_enable_display " 109 + "--enable DISPLAY"; 110 111 private static final String DEFINE_DISABLE_DISPLAY_POWER_POLICY_CMD = 112 "cmd car_service define-power-policy cts_car_watchdog_disable_display " 113 + "--disable DISPLAY"; 114 115 private static final String APPLY_ENABLE_DISPLAY_POWER_POLICY_CMD = 116 "cmd car_service apply-power-policy cts_car_watchdog_enable_display"; 117 118 private static final String APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD = 119 "cmd car_service apply-power-policy cts_car_watchdog_disable_display"; 120 121 private static final String START_CUSTOM_COLLECTION_SUCCESS_MSG = 122 "Successfully started custom perf collection"; 123 124 private static final long FIFTY_MEGABYTES = 1024 * 1024 * 50; 125 private static final long TWO_HUNDRED_MEGABYTES = 1024 * 1024 * 200; 126 127 private static final int RECURRING_OVERUSE_COUNT = 3; 128 129 private static final Pattern DUMP_PATTERN = Pattern.compile( 130 "CarWatchdogTestActivity:\\s(.+)"); 131 132 private static final Pattern FOREGROUND_BYTES_PATTERN = Pattern.compile( 133 "foregroundModeBytes = (\\d+)"); 134 135 private static final int BUILD_VERSION_CODE_TIRAMISU = 33; 136 137 // System event performance data collections are extended for at least 30 seconds after 138 // receiving the corresponding system event completion notification. During these periods 139 // (on <= Android T releases), a custom collection cannot be started. Thus, retry starting 140 // custom collection for at least twice this duration. 141 private static final long START_CUSTOM_COLLECTION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60); 142 private static final long WATCHDOG_ACTION_TIMEOUT_MS = 30_000; 143 144 private boolean mDidModifyDateTime; 145 private long mOriginalForegroundBytes; 146 147 @Before dateSetUp()148 public void dateSetUp() throws Exception { 149 checkAndSetDate(); 150 } 151 152 @After dateReset()153 public void dateReset() throws Exception { 154 checkAndResetDate(); 155 } 156 157 @Before setUp()158 public void setUp() throws Exception { 159 ConfigUtils.removeConfig(getDevice()); 160 ReportUtils.clearReports(getDevice()); 161 executeCommand(DEFINE_ENABLE_DISPLAY_POWER_POLICY_CMD); 162 executeCommand(DEFINE_DISABLE_DISPLAY_POWER_POLICY_CMD); 163 mOriginalForegroundBytes = parseForegroundBytesFromMessage(executeCommand( 164 GET_IO_OVERUSE_FOREGROUNG_BYTES_CMD)); 165 executeCommand("%s %d", SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD, TWO_HUNDRED_MEGABYTES); 166 executeCommand("logcat -c"); 167 startCustomCollection(); 168 executeCommand(RESET_RESOURCE_OVERUSE_CMD); 169 } 170 171 @After tearDown()172 public void tearDown() throws Exception { 173 ConfigUtils.removeConfig(getDevice()); 174 ReportUtils.clearReports(getDevice()); 175 executeCommand(APPLY_ENABLE_DISPLAY_POWER_POLICY_CMD); 176 // Enable the CTS packages by running the reset resource overuse command. 177 executeCommand(RESET_RESOURCE_OVERUSE_CMD); 178 executeCommand(STOP_CUSTOM_PERF_COLLECTION_CMD); 179 executeCommand("%s %d", SET_IO_OVERUSE_FOREGROUNG_BYTES_CMD, mOriginalForegroundBytes); 180 } 181 182 @Test testIoOveruseKillAfterDisplayTurnOff()183 public void testIoOveruseKillAfterDisplayTurnOff() throws Exception { 184 uploadStatsdConfig(APP_PKG); 185 186 for (int i = 0; i < RECURRING_OVERUSE_COUNT; ++i) { 187 overuseDiskIo(APP_PKG, getTestRunningUserId()); 188 verifyAtomIoOveruseStatsReported(APP_PKG, getTestRunningUserId(), 189 /* overuseTimes= */ i + 1); 190 ReportUtils.clearReports(getDevice()); 191 } 192 193 executeCommand(APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD); 194 195 verifyTestAppsKilled(APP_PKG); 196 verifyAtomKillStatsReported(APP_PKG, getTestRunningUserId()); 197 } 198 199 @Test testIoOveruseKillAfterDisplayTurnOffWithSharedUserIdApps()200 public void testIoOveruseKillAfterDisplayTurnOffWithSharedUserIdApps() throws Exception { 201 // Stats collection is based on uid. Packages with shared uids can be used interchangeably. 202 uploadStatsdConfig(WATCHDOG_APP_PKG); 203 204 for (int i = 0; i < RECURRING_OVERUSE_COUNT; i++) { 205 overuseDiskIo(i % 2 == 0 ? WATCHDOG_APP_PKG : WATCHDOG_APP_PKG_2, 206 getTestRunningUserId()); 207 verifyAtomIoOveruseStatsReported(i % 2 == 0 ? WATCHDOG_APP_PKG_2 : WATCHDOG_APP_PKG, 208 getTestRunningUserId(), /* overuseTimes= */ i + 1); 209 ReportUtils.clearReports(getDevice()); 210 } 211 212 executeCommand(APPLY_DISABLE_DISPLAY_POWER_POLICY_CMD); 213 214 verifyTestAppsKilled(WATCHDOG_APP_PKG, WATCHDOG_APP_PKG_2); 215 verifyAtomKillStatsReported(WATCHDOG_APP_PKG, getTestRunningUserId()); 216 } 217 uploadStatsdConfig(String packageName)218 private void uploadStatsdConfig(String packageName) throws Exception { 219 StatsdConfig.Builder config = ConfigUtils.createConfigBuilder("AID_SYSTEM"); 220 ConfigUtils.addEventMetricForUidAtom(config, 221 Atom.CAR_WATCHDOG_IO_OVERUSE_STATS_REPORTED_FIELD_NUMBER, 222 /* uidInAttributionChain= */ false, packageName); 223 ConfigUtils.addEventMetricForUidAtom(config, 224 Atom.CAR_WATCHDOG_KILL_STATS_REPORTED_FIELD_NUMBER, 225 /* uidInAttributionChain= */ false, packageName); 226 ConfigUtils.uploadConfig(getDevice(), config); 227 } 228 verifyAtomIoOveruseStatsReported(String packageName, int userId, int overuseTimes)229 private void verifyAtomIoOveruseStatsReported(String packageName, int userId, int overuseTimes) 230 throws Exception { 231 List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice()); 232 assertWithMessage("Reported I/O overuse event metrics data").that(data).hasSize(1); 233 234 CarWatchdogIoOveruseStatsReported atom = 235 data.get(0).getAtom().getCarWatchdogIoOveruseStatsReported(); 236 237 int appUid = DeviceUtils.getAppUidForUser(getDevice(), packageName, userId); 238 assertWithMessage("UID in atom from " + overuseTimes + " overuse").that(atom.getUid()) 239 .isEqualTo(appUid); 240 assertWithMessage("Atom has I/O overuse stats from " + overuseTimes + " overuse") 241 .that(atom.hasIoOveruseStats()).isTrue(); 242 verifyAtomIoOveruseStats(atom.getIoOveruseStats(), overuseTimes * TWO_HUNDRED_MEGABYTES, 243 "I/O overuse stats atom from " + overuseTimes + " overuse"); 244 } 245 verifyAtomKillStatsReported(String packageName, int userId)246 private void verifyAtomKillStatsReported(String packageName, int userId) 247 throws Exception { 248 List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice()); 249 assertWithMessage("Reported kill event metrics data").that(data).hasSize(1); 250 251 CarWatchdogKillStatsReported atom = 252 data.get(0).getAtom().getCarWatchdogKillStatsReported(); 253 254 int appUid = DeviceUtils.getAppUidForUser(getDevice(), packageName, userId); 255 assertWithMessage("UID in kill stats").that(atom.getUid()).isEqualTo(appUid); 256 assertWithMessage("Kill reason from kill stats").that(atom.getKillReason()) 257 .isEqualTo(CarWatchdogKillStatsReported.KillReason.KILLED_ON_IO_OVERUSE); 258 assertWithMessage("System state from kill stats").that(atom.getSystemState()) 259 .isEqualTo(CarWatchdogKillStatsReported.SystemState.USER_NO_INTERACTION_MODE); 260 assertWithMessage("Atom has I/O overuse stats from overuse kill") 261 .that(atom.hasIoOveruseStats()).isTrue(); 262 verifyAtomIoOveruseStats(atom.getIoOveruseStats(), 263 RECURRING_OVERUSE_COUNT * TWO_HUNDRED_MEGABYTES, 264 "I/O overuse stats atom from overuse kill"); 265 } 266 verifyAtomIoOveruseStats(CarWatchdogIoOveruseStats ioOveruseStats, long foregroundWrittenBytes, String statsType)267 private void verifyAtomIoOveruseStats(CarWatchdogIoOveruseStats ioOveruseStats, 268 long foregroundWrittenBytes, String statsType) { 269 assertWithMessage(statsType + " has period").that(ioOveruseStats.hasPeriod()).isTrue(); 270 assertWithMessage("Period in " + statsType).that(ioOveruseStats.getPeriod()) 271 .isEqualTo(CarWatchdogIoOveruseStats.Period.DAILY); 272 assertWithMessage(statsType + " has threshold").that(ioOveruseStats.hasThreshold()) 273 .isTrue(); 274 assertWithMessage("Foreground threshold bytes in " + statsType) 275 .that(ioOveruseStats.getThreshold().getForegroundBytes()) 276 .isEqualTo(TWO_HUNDRED_MEGABYTES); 277 assertWithMessage(statsType + " has written bytes").that(ioOveruseStats.hasWrittenBytes()) 278 .isTrue(); 279 // Watchdog daemon's polling/syncing interval and the disk I/O writes performed by the 280 // device side app are asynchronous. So, the actual number of bytes written by the app might 281 // be greater than the expected written bytes. Thus verify that the reported written bytes 282 // are in the range of 50MiB. 283 assertWithMessage("Foreground written bytes in " + statsType) 284 .that(ioOveruseStats.getWrittenBytes().getForegroundBytes()) 285 .isAtLeast(foregroundWrittenBytes); 286 assertWithMessage("Foreground written bytes in " + statsType) 287 .that(ioOveruseStats.getWrittenBytes().getForegroundBytes()) 288 .isAtMost(foregroundWrittenBytes + FIFTY_MEGABYTES); 289 } 290 overuseDiskIo(String packageName, int userId)291 private void overuseDiskIo(String packageName, int userId) throws Exception { 292 startMainActivity(packageName, userId); 293 294 long remainingBytes = readForegroundBytesFromActivityDump(packageName); 295 296 sendBytesToKillApp(remainingBytes, packageName, userId); 297 298 remainingBytes = readForegroundBytesFromActivityDump(packageName); 299 300 assertWithMessage("Application " + packageName + "'s remaining write bytes") 301 .that(remainingBytes).isEqualTo(0); 302 } 303 readForegroundBytesFromActivityDump(String packageName)304 private long readForegroundBytesFromActivityDump(String packageName) throws Exception { 305 AtomicReference<String> notification = new AtomicReference<>(); 306 PollingCheck.check("No notification received in the activity dump", 307 WATCHDOG_ACTION_TIMEOUT_MS, 308 () -> { 309 String dump = fetchActivityDumpsys(packageName); 310 if (dump.startsWith("INFO") && dump.contains("--Notification--")) { 311 notification.set(dump); 312 return true; 313 } 314 return false; 315 }); 316 317 return parseForegroundBytesFromMessage(notification.get()); 318 } 319 parseForegroundBytesFromMessage(String message)320 private long parseForegroundBytesFromMessage(String message) throws IllegalArgumentException { 321 Matcher m = FOREGROUND_BYTES_PATTERN.matcher(message); 322 if (m.find()) { 323 return Long.parseLong(m.group(1)); 324 } 325 throw new IllegalArgumentException("Invalid message format: " + message); 326 } 327 verifyTestAppsKilled(String... packageNames)328 private void verifyTestAppsKilled(String... packageNames) throws Exception { 329 ArrayList<String> packages = new ArrayList<>(List.of(packageNames)); 330 try { 331 PollingCheck.check("Failed to kill applications", WATCHDOG_ACTION_TIMEOUT_MS, () -> { 332 for (int i = packages.size() - 1; i >= 0; i--) { 333 // Check activity dump for errors. Throws exception on error. 334 String packageName = packages.get(i); 335 fetchActivityDumpsys(packageName); 336 if (!isPackageRunning(packageName)) { 337 packages.remove(i); 338 } 339 } 340 return packages.isEmpty(); 341 }); 342 } catch (AssertionError e) { 343 assertWithMessage("Failed to kill applications: %s", packages).fail(); 344 } 345 } 346 fetchActivityDumpsys(String packageName)347 private String fetchActivityDumpsys(String packageName) throws Exception { 348 String dump = executeCommand("dumpsys activity %s/%s", packageName, ACTIVITY_CLASS); 349 Matcher m = DUMP_PATTERN.matcher(dump); 350 if (!m.find()) { 351 return ""; 352 } 353 String message = Objects.requireNonNull(m.group(1)).trim(); 354 if (message.startsWith("ERROR")) { 355 throw new Exception(message); 356 } 357 return message; 358 } 359 startMainActivity(String packageName, int userId)360 private void startMainActivity(String packageName, int userId) throws Exception { 361 String result = executeCommand("pm clear --user %d %s", userId, packageName); 362 assertWithMessage("pm clear").that(result.trim()).isEqualTo("Success"); 363 364 executeCommand("am start --user %d -W -a android.intent.action.MAIN -n %s/%s", 365 userId, packageName, ACTIVITY_CLASS); 366 367 assertWithMessage("Is %s running?", packageName).that(isPackageRunning(packageName)) 368 .isTrue(); 369 } 370 sendBytesToKillApp(long remainingBytes, String appPkg, int userId)371 private void sendBytesToKillApp(long remainingBytes, String appPkg, int userId) 372 throws Exception { 373 executeCommand( 374 "am start --user %d -W -a android.intent.action.MAIN -n %s/%s" 375 + " --el bytes_to_kill %d", 376 userId, appPkg, ACTIVITY_CLASS, remainingBytes); 377 } 378 checkAndSetDate()379 private void checkAndSetDate() throws Exception { 380 // Get date in ISO-8601 format 381 LocalDateTime now = LocalDateTime.parse(executeCommand("date +%%FT%%T").trim()); 382 if (now.getHour() < 23) { 383 return; 384 } 385 executeCommand("date %s", now.minusHours(1)); 386 CLog.d("DateTime changed from %s to %s", now, now.minusHours(1)); 387 mDidModifyDateTime = true; 388 } 389 checkAndResetDate()390 private void checkAndResetDate() throws Exception { 391 if (!mDidModifyDateTime) { 392 return; 393 } 394 LocalDateTime now = LocalDateTime.parse(executeCommand("date +%%FT%%T").trim()); 395 executeCommand("date %s", now.plusHours(1)); 396 CLog.d("DateTime changed from %s to %s", now, now.plusHours(1)); 397 } 398 startCustomCollection()399 private void startCustomCollection() throws Exception { 400 if (ApiLevelUtil.isAfter(getDevice(), BUILD_VERSION_CODE_TIRAMISU)) { 401 String result = executeCommand(START_CUSTOM_PERF_COLLECTION_CMD); 402 assertWithMessage("Custom collection start message").that(result) 403 .contains(START_CUSTOM_COLLECTION_SUCCESS_MSG); 404 return; 405 } 406 // TODO(b/261869056): Remove the polling check once it is safe to remove. 407 PollingCheck.check("Failed to start custom collect performance data collection", 408 START_CUSTOM_COLLECTION_TIMEOUT_MS, 409 () -> { 410 String result = executeCommand(START_CUSTOM_PERF_COLLECTION_CMD); 411 return result.contains(START_CUSTOM_COLLECTION_SUCCESS_MSG) || result.isEmpty(); 412 }); 413 } 414 } 415