• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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