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.app; 18 19 import android.app.Activity; 20 import android.car.Car; 21 import android.car.watchdog.CarWatchdogManager; 22 import android.car.watchdog.IoOveruseStats; 23 import android.car.watchdog.ResourceOveruseStats; 24 import android.content.Intent; 25 import android.os.Bundle; 26 import android.os.SystemClock; 27 import android.util.Log; 28 29 import androidx.annotation.GuardedBy; 30 31 import java.io.File; 32 import java.io.FileDescriptor; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InterruptedIOException; 36 import java.io.PrintWriter; 37 import java.nio.file.Files; 38 import java.util.concurrent.ExecutorService; 39 import java.util.concurrent.Executors; 40 41 public final class CarWatchdogTestActivity extends Activity { 42 private static final String TAG = CarWatchdogTestActivity.class.getSimpleName(); 43 private static final String BYTES_TO_KILL = "bytes_to_kill"; 44 private static final long TEN_MEGABYTES = 1024 * 1024 * 10; 45 private static final long TWO_HUNDRED_MEGABYTES = 1024 * 1024 * 200; 46 private static final int DISK_DELAY_MS = 4000; 47 private static final double WARN_THRESHOLD_PERCENT = 0.8; 48 private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9; 49 50 private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); 51 private final Object mLock = new Object(); 52 53 @GuardedBy("mLock") 54 private CarWatchdogManager mCarWatchdogManager; 55 56 private String mDumpMessage = ""; 57 private Car mCar; 58 private File mTestDir; 59 60 @Override onCreate(Bundle savedInstanceState)61 protected void onCreate(Bundle savedInstanceState) { 62 super.onCreate(savedInstanceState); 63 64 initCarApi(); 65 try { 66 mTestDir = 67 Files.createTempDirectory(getFilesDir().toPath(), "testDir").toFile(); 68 } catch (IOException e) { 69 setDumpMessage("ERROR: " + e.getMessage()); 70 finish(); 71 return; 72 } 73 mExecutor.execute( 74 () -> { 75 synchronized (mLock) { 76 if (mCarWatchdogManager == null) { 77 Log.e(TAG, "CarWatchdogManager is null."); 78 finish(); 79 return; 80 } 81 } 82 IoOveruseListener listener = addResourceOveruseListener(); 83 try { 84 if (!writeToDisk(TEN_MEGABYTES)) { 85 finish(); 86 return; 87 } 88 89 long remainingBytes = fetchRemainingBytes(TEN_MEGABYTES); 90 if (remainingBytes == 0) { 91 Log.d(TAG, "Remaining bytes is 0 after writing " + TEN_MEGABYTES 92 + " bytes to disk."); 93 finish(); 94 return; 95 } 96 97 /* 98 * Warning notification is received as soon as exceeding 99 * |WARN_THRESHOLD_PERCENT|. So, set expected minimum written bytes to 100 * |WARN_THRESHOLD_PERCENT| of the overuse threshold. 101 */ 102 long bytesToWarnThreshold = 103 (long) (TWO_HUNDRED_MEGABYTES * WARN_THRESHOLD_PERCENT); 104 105 listener.setExpectedMinWrittenBytes(bytesToWarnThreshold); 106 107 long bytesToExceedWarnThreshold = 108 (long) Math.ceil(remainingBytes 109 * EXCEED_WARN_THRESHOLD_PERCENT); 110 111 if (!writeToDisk(bytesToExceedWarnThreshold)) { 112 finish(); 113 return; 114 } 115 116 listener.checkIsNotified(); 117 } finally { 118 synchronized (mLock) { 119 mCarWatchdogManager.removeResourceOveruseListener(listener); 120 } 121 /* Foreground mode bytes dumped after removing listener to ensure hostside 122 * receives dump message after test is finished. 123 */ 124 listener.dumpForegroundModeBytes(); 125 } 126 }); 127 } 128 129 @Override onNewIntent(Intent intent)130 protected void onNewIntent(Intent intent) { 131 super.onNewIntent(intent); 132 133 setDumpMessage(""); 134 Bundle extras = intent.getExtras(); 135 if (extras == null) { 136 Log.w(TAG, "onNewIntent: empty extras"); 137 return; 138 } 139 long remainingBytes = extras.getLong(BYTES_TO_KILL); 140 Log.d(TAG, "Bytes to kill: " + remainingBytes); 141 if (remainingBytes == 0) { 142 Log.w(TAG, "onNewIntent: remaining bytes is 0"); 143 return; 144 } 145 mExecutor.execute(() -> { 146 synchronized (mLock) { 147 if (mCarWatchdogManager == null) { 148 Log.e(TAG, "onNewIntent: CarWatchdogManager is null."); 149 finish(); 150 return; 151 } 152 } 153 IoOveruseListener listener = addResourceOveruseListener(); 154 try { 155 listener.setExpectedMinWrittenBytes(TWO_HUNDRED_MEGABYTES); 156 157 writeToDisk(remainingBytes); 158 159 listener.checkIsNotified(); 160 } finally { 161 synchronized (mLock) { 162 mCarWatchdogManager.removeResourceOveruseListener(listener); 163 } 164 /* Foreground mode bytes dumped after removing listener to ensure hostside 165 * receives dump message after test is finished. 166 */ 167 listener.dumpForegroundModeBytes(); 168 } 169 }); 170 } 171 172 @Override dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)173 public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 174 if (mDumpMessage.isEmpty()) { 175 return; 176 } 177 writer.printf("%s: %s\n", TAG, mDumpMessage); 178 Log.i(TAG, "Dumping message: '" + mDumpMessage + "'"); 179 } 180 181 @Override onDestroy()182 protected void onDestroy() { 183 if (mCar != null) { 184 mCar.disconnect(); 185 } 186 if (mTestDir.delete()) { 187 Log.i(TAG, "Deleted directory '" + mTestDir.getAbsolutePath() + "' successfully"); 188 } else { 189 Log.e(TAG, "Failed to delete directory '" + mTestDir.getAbsolutePath() + "'"); 190 } 191 super.onDestroy(); 192 } 193 initCarApi()194 private void initCarApi() { 195 if (mCar != null && mCar.isConnected()) { 196 mCar.disconnect(); 197 mCar = null; 198 } 199 mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, 200 this::initManagers); 201 } 202 initManagers(Car car, boolean ready)203 private void initManagers(Car car, boolean ready) { 204 synchronized (mLock) { 205 if (ready) { 206 mCarWatchdogManager = (CarWatchdogManager) car.getCarManager( 207 Car.CAR_WATCHDOG_SERVICE); 208 Log.d(TAG, "initManagers() completed"); 209 } else { 210 mCarWatchdogManager = null; 211 Log.wtf(TAG, "mCarWatchdogManager set to be null"); 212 } 213 } 214 } 215 addResourceOveruseListener()216 private IoOveruseListener addResourceOveruseListener() { 217 IoOveruseListener listener = new IoOveruseListener(); 218 synchronized (mLock) { 219 mCarWatchdogManager.addResourceOveruseListener(getMainExecutor(), 220 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, listener); 221 } 222 return listener; 223 } 224 writeToDisk(long bytes)225 private boolean writeToDisk(long bytes) { 226 File uniqueFile = new File(mTestDir, Long.toString(System.nanoTime())); 227 boolean result = writeToFile(uniqueFile, bytes); 228 if (uniqueFile.delete()) { 229 Log.i(TAG, "Deleted file: " + uniqueFile.getAbsolutePath()); 230 } else { 231 Log.e(TAG, "Failed to delete file: " + uniqueFile.getAbsolutePath()); 232 } 233 return result; 234 } 235 writeToFile(File uniqueFile, long bytes)236 private boolean writeToFile(File uniqueFile, long bytes) { 237 long writtenBytes = 0; 238 try (FileOutputStream fos = new FileOutputStream(uniqueFile)) { 239 Log.d(TAG, "Attempting to write " + bytes + " bytes"); 240 writtenBytes = writeToFos(fos, bytes); 241 if (writtenBytes < bytes) { 242 setDumpMessage("ERROR: Failed to write '" + bytes 243 + "' bytes to disk. '" + writtenBytes 244 + "' bytes were successfully written, while '" + (bytes - writtenBytes) 245 + "' bytes were pending at the moment the exception occurred."); 246 return false; 247 } 248 fos.getFD().sync(); 249 // Wait for the IO event to propagate to the system 250 Thread.sleep(DISK_DELAY_MS); 251 return true; 252 } catch (IOException | InterruptedException e) { 253 String message; 254 if (e instanceof IOException) { 255 message = "I/O exception"; 256 } else { 257 message = "Thread interrupted"; 258 Thread.currentThread().interrupt(); 259 } 260 if (writtenBytes > 0) { 261 message += " after successfully writing to disk."; 262 } 263 Log.e(TAG, message, e); 264 setDumpMessage("ERROR: " + message); 265 return false; 266 } 267 } 268 writeToFos(FileOutputStream fos, long remainingBytes)269 private long writeToFos(FileOutputStream fos, long remainingBytes) { 270 long totalBytesWritten = 0; 271 while (remainingBytes != 0) { 272 int writeBytes = 273 (int) Math.min(Integer.MAX_VALUE, 274 Math.min(Runtime.getRuntime().freeMemory(), remainingBytes)); 275 try { 276 fos.write(new byte[writeBytes]); 277 } catch (InterruptedIOException e) { 278 Thread.currentThread().interrupt(); 279 continue; 280 } catch (IOException e) { 281 Log.e(TAG, "I/O exception while writing " + writeBytes + " to disk", e); 282 return totalBytesWritten; 283 } 284 totalBytesWritten += writeBytes; 285 remainingBytes -= writeBytes; 286 if (writeBytes > 0 && remainingBytes > 0) { 287 Log.i(TAG, "Total bytes written: " + totalBytesWritten + "/" 288 + (totalBytesWritten + remainingBytes)); 289 } 290 } 291 Log.d(TAG, "Write completed."); 292 return totalBytesWritten; 293 } 294 fetchRemainingBytes(long minWrittenBytes)295 private long fetchRemainingBytes(long minWrittenBytes) { 296 ResourceOveruseStats stats; 297 synchronized (mLock) { 298 stats = mCarWatchdogManager.getResourceOveruseStats( 299 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, 300 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY); 301 } 302 IoOveruseStats ioOveruseStats = stats.getIoOveruseStats(); 303 if (ioOveruseStats == null) { 304 setDumpMessage( 305 "ERROR: No I/O overuse stats available for the application after writing " 306 + minWrittenBytes + " bytes."); 307 return 0; 308 } 309 if (ioOveruseStats.getTotalBytesWritten() < minWrittenBytes) { 310 setDumpMessage("ERROR: Actual written bytes to disk '" + minWrittenBytes 311 + "' don't match written bytes '" + ioOveruseStats.getTotalBytesWritten() 312 + "' returned by get request"); 313 return 0; 314 } 315 Log.d(TAG, ioOveruseStats.toString()); 316 /* 317 * Check for foreground mode bytes given CtsCarApp is running in the foreground 318 * during testing. 319 */ 320 return ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes(); 321 } 322 setDumpMessage(String message)323 private void setDumpMessage(String message) { 324 if (mDumpMessage.startsWith("ERROR:")) { 325 mDumpMessage += ", " + message; 326 } else { 327 mDumpMessage = message; 328 } 329 } 330 331 private final class IoOveruseListener 332 implements CarWatchdogManager.ResourceOveruseListener { 333 private static final int NOTIFICATION_DELAY_MS = 5000; 334 335 private final Object mLock = new Object(); 336 @GuardedBy("mLock") 337 private boolean mNotificationReceived; 338 @GuardedBy("mLock") 339 private long mForegroundModeBytes; 340 341 private long mExpectedMinWrittenBytes; 342 343 @Override onOveruse(ResourceOveruseStats resourceOveruseStats)344 public void onOveruse(ResourceOveruseStats resourceOveruseStats) { 345 synchronized (mLock) { 346 mForegroundModeBytes = -1; 347 mNotificationReceived = true; 348 mLock.notifyAll(); 349 } 350 Log.d(TAG, resourceOveruseStats.toString()); 351 if (resourceOveruseStats.getIoOveruseStats() == null) { 352 setDumpMessage( 353 "ERROR: No I/O overuse stats reported for the application in the overuse " 354 + "notification."); 355 return; 356 } 357 long reportedWrittenBytes = 358 resourceOveruseStats.getIoOveruseStats().getTotalBytesWritten(); 359 if (reportedWrittenBytes < mExpectedMinWrittenBytes) { 360 setDumpMessage("ERROR: Actual written bytes to disk '" + mExpectedMinWrittenBytes 361 + "' don't match written bytes '" + reportedWrittenBytes 362 + "' reported in overuse notification"); 363 return; 364 } 365 synchronized (mLock) { 366 mForegroundModeBytes = 367 resourceOveruseStats.getIoOveruseStats().getRemainingWriteBytes() 368 .getForegroundModeBytes(); 369 } 370 } 371 dumpForegroundModeBytes()372 public void dumpForegroundModeBytes() { 373 synchronized (mLock) { 374 setDumpMessage( 375 "INFO: --Notification-- foregroundModeBytes = " + mForegroundModeBytes); 376 } 377 } 378 setExpectedMinWrittenBytes(long expectedMinWrittenBytes)379 public void setExpectedMinWrittenBytes(long expectedMinWrittenBytes) { 380 mExpectedMinWrittenBytes = expectedMinWrittenBytes; 381 } 382 checkIsNotified()383 public void checkIsNotified() { 384 synchronized (mLock) { 385 long now = SystemClock.uptimeMillis(); 386 long deadline = now + NOTIFICATION_DELAY_MS; 387 while (!mNotificationReceived && now < deadline) { 388 try { 389 mLock.wait(deadline - now); 390 } catch (InterruptedException e) { 391 Thread.currentThread().interrupt(); 392 continue; 393 } finally { 394 now = SystemClock.uptimeMillis(); 395 } 396 break; 397 } 398 if (!mNotificationReceived) { 399 setDumpMessage("ERROR: I/O Overuse notification not received."); 400 } 401 } 402 } 403 } 404 } 405