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 com.android.car.watchdog; 18 19 import static com.android.car.watchdog.TimeSource.ZONE_OFFSET; 20 21 import android.annotation.Nullable; 22 import android.annotation.UserIdInt; 23 import android.automotive.watchdog.PerStateBytes; 24 import android.car.watchdog.IoOveruseStats; 25 import android.car.watchdog.PackageKillableState.KillableState; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.database.Cursor; 29 import android.database.SQLException; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteOpenHelper; 32 import android.os.Process; 33 import android.util.ArrayMap; 34 import android.util.ArraySet; 35 import android.util.IntArray; 36 import android.util.Slog; 37 import android.util.SparseArray; 38 39 import com.android.car.CarLog; 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.server.utils.Slogf; 43 44 import java.io.File; 45 import java.time.Instant; 46 import java.time.Period; 47 import java.time.ZonedDateTime; 48 import java.time.temporal.ChronoUnit; 49 import java.time.temporal.TemporalUnit; 50 import java.util.ArrayList; 51 import java.util.Comparator; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.Objects; 55 56 /** 57 * Defines the database to store/retrieve system resource stats history from local storage. 58 */ 59 public final class WatchdogStorage { 60 private static final String TAG = CarLog.tagFor(WatchdogStorage.class); 61 private static final int RETENTION_PERIOD_IN_DAYS = 30; 62 /* Stats are stored on a daily basis. */ 63 public static final TemporalUnit STATS_TEMPORAL_UNIT = ChronoUnit.DAYS; 64 /* Number of days to retain the stats in local storage. */ 65 public static final Period RETENTION_PERIOD = 66 Period.ofDays(RETENTION_PERIOD_IN_DAYS).normalized(); 67 public static final String ZONE_MODIFIER = "utc"; 68 public static final String DATE_MODIFIER = "unixepoch"; 69 70 private final WatchdogDbHelper mDbHelper; 71 private final ArrayMap<String, UserPackage> mUserPackagesByKey = new ArrayMap<>(); 72 private final ArrayMap<String, UserPackage> mUserPackagesById = new ArrayMap<>(); 73 private TimeSource mTimeSource; 74 private final Object mLock = new Object(); 75 // Cache of today's I/O overuse stats collected during the previous boot. The data contained in 76 // the cache won't change until the next boot, so it is safe to cache the data in memory. 77 @GuardedBy("mLock") 78 private final List<IoUsageStatsEntry> mTodayIoUsageStatsEntries = new ArrayList<>(); 79 WatchdogStorage(Context context, TimeSource timeSource)80 public WatchdogStorage(Context context, TimeSource timeSource) { 81 this(context, /* useDataSystemCarDir= */ true, timeSource); 82 } 83 84 @VisibleForTesting WatchdogStorage(Context context, boolean useDataSystemCarDir, TimeSource timeSource)85 WatchdogStorage(Context context, boolean useDataSystemCarDir, TimeSource timeSource) { 86 mTimeSource = timeSource; 87 mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir, mTimeSource); 88 } 89 90 /** Releases resources. */ release()91 public void release() { 92 mDbHelper.close(); 93 } 94 95 /** Handles database shrink. */ shrinkDatabase()96 public void shrinkDatabase() { 97 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 98 mDbHelper.onShrink(db); 99 } 100 } 101 102 /** Saves the given user package settings entries and returns whether the change succeeded. */ saveUserPackageSettings(List<UserPackageSettingsEntry> entries)103 public boolean saveUserPackageSettings(List<UserPackageSettingsEntry> entries) { 104 ArraySet<Integer> usersWithMissingIds = new ArraySet<>(); 105 boolean isWriteSuccessful = false; 106 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 107 try { 108 db.beginTransaction(); 109 for (int i = 0; i < entries.size(); ++i) { 110 UserPackageSettingsEntry entry = entries.get(i); 111 // Note: DO NOT replace existing entries in the UserPackageSettingsTable because 112 // the replace operation deletes the old entry and inserts a new entry in the 113 // table. This deletes the entries (in other tables) that are associated with 114 // the old userPackageId. And also the userPackageId is auto-incremented. 115 if (mUserPackagesByKey.get(UserPackage.getKey(entry.userId, entry.packageName)) 116 != null && UserPackageSettingsTable.updateEntry(db, entry)) { 117 continue; 118 } 119 usersWithMissingIds.add(entry.userId); 120 if (!UserPackageSettingsTable.replaceEntry(db, entry)) { 121 return false; 122 } 123 } 124 db.setTransactionSuccessful(); 125 isWriteSuccessful = true; 126 } finally { 127 db.endTransaction(); 128 } 129 populateUserPackages(db, usersWithMissingIds); 130 } 131 return isWriteSuccessful; 132 } 133 134 /** Returns the user package setting entries. */ getUserPackageSettings()135 public List<UserPackageSettingsEntry> getUserPackageSettings() { 136 ArrayMap<String, UserPackageSettingsEntry> entriesById; 137 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 138 entriesById = UserPackageSettingsTable.querySettings(db); 139 } 140 List<UserPackageSettingsEntry> entries = new ArrayList<>(entriesById.size()); 141 for (int i = 0; i < entriesById.size(); ++i) { 142 String userPackageId = entriesById.keyAt(i); 143 UserPackageSettingsEntry entry = entriesById.valueAt(i); 144 UserPackage userPackage = new UserPackage(userPackageId, entry.userId, 145 entry.packageName); 146 mUserPackagesByKey.put(userPackage.getKey(), userPackage); 147 mUserPackagesById.put(userPackage.userPackageId, userPackage); 148 entries.add(entry); 149 } 150 return entries; 151 } 152 153 /** Saves the given I/O usage stats. Returns true only on success. */ saveIoUsageStats(List<IoUsageStatsEntry> entries)154 public boolean saveIoUsageStats(List<IoUsageStatsEntry> entries) { 155 return saveIoUsageStats(entries, /* shouldCheckRetention= */ true); 156 } 157 158 /** Returns the saved I/O usage stats for the current day. */ getTodayIoUsageStats()159 public List<IoUsageStatsEntry> getTodayIoUsageStats() { 160 synchronized (mLock) { 161 if (!mTodayIoUsageStatsEntries.isEmpty()) { 162 return new ArrayList<>(mTodayIoUsageStatsEntries); 163 } 164 long includingStartEpochSeconds = mTimeSource.getCurrentDate().toEpochSecond(); 165 long excludingEndEpochSeconds = mTimeSource.getCurrentDateTime().toEpochSecond(); 166 ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsagesById; 167 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 168 ioUsagesById = IoUsageStatsTable.queryStats(db, includingStartEpochSeconds, 169 excludingEndEpochSeconds); 170 } 171 for (int i = 0; i < ioUsagesById.size(); ++i) { 172 String userPackageId = ioUsagesById.keyAt(i); 173 UserPackage userPackage = mUserPackagesById.get(userPackageId); 174 if (userPackage == null) { 175 Slogf.i(TAG, 176 "Failed to find user id and package name for user package id: '%s'", 177 userPackageId); 178 continue; 179 } 180 mTodayIoUsageStatsEntries.add(new IoUsageStatsEntry( 181 userPackage.userId, userPackage.packageName, 182 ioUsagesById.valueAt(i))); 183 } 184 return new ArrayList<>(mTodayIoUsageStatsEntries); 185 } 186 } 187 188 /** Deletes user package settings and resource overuse stats. */ deleteUserPackage(@serIdInt int userId, String packageName)189 public void deleteUserPackage(@UserIdInt int userId, String packageName) { 190 UserPackage userPackage = mUserPackagesByKey.get(UserPackage.getKey(userId, packageName)); 191 if (userPackage == null) { 192 Slogf.w(TAG, "Failed to find user package id for user id '%d' and package '%s", 193 userId, packageName); 194 return; 195 } 196 mUserPackagesByKey.remove(userPackage.getKey()); 197 mUserPackagesById.remove(userPackage.userPackageId); 198 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 199 UserPackageSettingsTable.deleteUserPackage(db, userId, packageName); 200 } 201 } 202 203 /** 204 * Returns the aggregated historical I/O overuse stats for the given user package or 205 * {@code null} when stats are not available. 206 */ 207 @Nullable getHistoricalIoOveruseStats(@serIdInt int userId, String packageName, int numDaysAgo)208 public IoOveruseStats getHistoricalIoOveruseStats(@UserIdInt int userId, String packageName, 209 int numDaysAgo) { 210 ZonedDateTime currentDate = mTimeSource.getCurrentDate(); 211 long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond(); 212 long excludingEndEpochSeconds = currentDate.toEpochSecond(); 213 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 214 UserPackage userPackage = mUserPackagesByKey.get( 215 UserPackage.getKey(userId, packageName)); 216 if (userPackage == null) { 217 /* Packages without historical stats don't have userPackage entry. */ 218 return null; 219 } 220 return IoUsageStatsTable.queryIoOveruseStatsForUserPackageId(db, 221 userPackage.userPackageId, includingStartEpochSeconds, 222 excludingEndEpochSeconds); 223 } 224 } 225 226 /** 227 * Returns daily system-level I/O usage summaries for the given period or {@code null} when 228 * summaries are not available. 229 */ getDailySystemIoUsageSummaries( long includingStartEpochSeconds, long excludingEndEpochSeconds)230 public @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary> getDailySystemIoUsageSummaries( 231 long includingStartEpochSeconds, long excludingEndEpochSeconds) { 232 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 233 return IoUsageStatsTable.queryDailySystemIoUsageSummaries(db, 234 includingStartEpochSeconds, excludingEndEpochSeconds); 235 } 236 } 237 238 /** 239 * Returns top N disk I/O users' daily I/O usage summaries for the given period or {@code null} 240 * when summaries are not available. 241 */ getTopUsersDailyIoUsageSummaries( int numTopUsers, long minSystemTotalWrittenBytes, long includingStartEpochSeconds, long excludingEndEpochSeconds)242 public @Nullable List<UserPackageDailySummaries> getTopUsersDailyIoUsageSummaries( 243 int numTopUsers, long minSystemTotalWrittenBytes, long includingStartEpochSeconds, 244 long excludingEndEpochSeconds) { 245 ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById; 246 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 247 long systemTotalWrittenBytes = IoUsageStatsTable.querySystemTotalWrittenBytes(db, 248 includingStartEpochSeconds, excludingEndEpochSeconds); 249 if (systemTotalWrittenBytes < minSystemTotalWrittenBytes) { 250 return null; 251 } 252 summariesById = IoUsageStatsTable.queryTopUsersDailyIoUsageSummaries(db, 253 numTopUsers, includingStartEpochSeconds, excludingEndEpochSeconds); 254 } 255 if (summariesById == null) { 256 return null; 257 } 258 ArrayList<UserPackageDailySummaries> userPackageDailySummaries = new ArrayList<>(); 259 for (int i = 0; i < summariesById.size(); ++i) { 260 String id = summariesById.keyAt(i); 261 UserPackage userPackage = mUserPackagesById.get(id); 262 if (userPackage == null) { 263 Slogf.i(TAG, 264 "Failed to find user id and package name for user package id: '%s'", 265 id); 266 continue; 267 } 268 userPackageDailySummaries.add(new UserPackageDailySummaries(userPackage.userId, 269 userPackage.packageName, summariesById.valueAt(i))); 270 } 271 userPackageDailySummaries 272 .sort(Comparator.comparingLong(UserPackageDailySummaries::getTotalWrittenBytes) 273 .reversed()); 274 return userPackageDailySummaries; 275 } 276 277 /** 278 * Returns the aggregated historical overuses minus the forgiven overuses for all saved 279 * packages. Forgiven overuses are overuses that have been attributed previously to a package's 280 * recurring overuse. 281 */ getNotForgivenHistoricalIoOveruses(int numDaysAgo)282 public List<NotForgivenOverusesEntry> getNotForgivenHistoricalIoOveruses(int numDaysAgo) { 283 ZonedDateTime currentDate = 284 mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT); 285 long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond(); 286 long excludingEndEpochSeconds = currentDate.toEpochSecond(); 287 ArrayMap<String, Integer> notForgivenOverusesById; 288 try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) { 289 notForgivenOverusesById = IoUsageStatsTable.queryNotForgivenHistoricalOveruses(db, 290 includingStartEpochSeconds, excludingEndEpochSeconds); 291 } 292 List<NotForgivenOverusesEntry> notForgivenOverusesEntries = new ArrayList<>(); 293 for (int i = 0; i < notForgivenOverusesById.size(); i++) { 294 String id = notForgivenOverusesById.keyAt(i); 295 UserPackage userPackage = mUserPackagesById.get(id); 296 if (userPackage == null) { 297 Slogf.w(TAG, 298 "Failed to find user id and package name for user package id: '%s'", 299 id); 300 continue; 301 } 302 notForgivenOverusesEntries.add(new NotForgivenOverusesEntry(userPackage.userId, 303 userPackage.packageName, notForgivenOverusesById.valueAt(i))); 304 } 305 return notForgivenOverusesEntries; 306 } 307 308 /** 309 * Forgives all historical overuses between yesterday and {@code numDaysAgo} 310 * for a list of specific {@code userIds} and {@code packageNames}. 311 */ forgiveHistoricalOveruses(SparseArray<List<String>> packagesByUserId, int numDaysAgo)312 public void forgiveHistoricalOveruses(SparseArray<List<String>> packagesByUserId, 313 int numDaysAgo) { 314 if (packagesByUserId.size() == 0) { 315 Slogf.w(TAG, "No I/O usage stats provided to forgive historical overuses."); 316 return; 317 } 318 ZonedDateTime currentDate = 319 mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT); 320 long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond(); 321 long excludingEndEpochSeconds = currentDate.toEpochSecond(); 322 List<String> userPackageIds = new ArrayList<>(); 323 for (int i = 0; i < packagesByUserId.size(); i++) { 324 int userId = packagesByUserId.keyAt(i); 325 List<String> packages = packagesByUserId.valueAt(i); 326 for (int pkgIdx = 0; pkgIdx < packages.size(); pkgIdx++) { 327 UserPackage userPackage = 328 mUserPackagesByKey.get(UserPackage.getKey(userId, packages.get(pkgIdx))); 329 if (userPackage == null) { 330 // Packages without historical stats don't have userPackage entry. 331 continue; 332 } 333 userPackageIds.add(userPackage.userPackageId); 334 } 335 } 336 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 337 IoUsageStatsTable.forgiveHistoricalOverusesForPackage(db, userPackageIds, 338 includingStartEpochSeconds, excludingEndEpochSeconds); 339 } 340 } 341 342 /** 343 * Deletes all user package settings and resource stats for all non-alive users. 344 * 345 * @param aliveUserIds Array of alive user ids. 346 */ syncUsers(int[] aliveUserIds)347 public void syncUsers(int[] aliveUserIds) { 348 IntArray aliveUsers = IntArray.wrap(aliveUserIds); 349 for (int i = mUserPackagesByKey.size() - 1; i >= 0; --i) { 350 UserPackage userPackage = mUserPackagesByKey.valueAt(i); 351 if (aliveUsers.indexOf(userPackage.userId) == -1) { 352 mUserPackagesByKey.removeAt(i); 353 mUserPackagesById.remove(userPackage.userPackageId); 354 } 355 } 356 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 357 UserPackageSettingsTable.syncUserPackagesWithAliveUsers(db, aliveUsers); 358 } 359 } 360 361 @VisibleForTesting saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention)362 boolean saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention) { 363 ZonedDateTime currentDate = mTimeSource.getCurrentDate(); 364 List<ContentValues> rows = new ArrayList<>(entries.size()); 365 for (int i = 0; i < entries.size(); ++i) { 366 IoUsageStatsEntry entry = entries.get(i); 367 UserPackage userPackage = mUserPackagesByKey.get( 368 UserPackage.getKey(entry.userId, entry.packageName)); 369 if (userPackage == null) { 370 Slogf.i(TAG, "Failed to find user package id for user id '%d' and package '%s", 371 entry.userId, entry.packageName); 372 continue; 373 } 374 android.automotive.watchdog.IoOveruseStats ioOveruseStats = 375 entry.ioUsage.getInternalIoOveruseStats(); 376 ZonedDateTime statsDate = Instant.ofEpochSecond(ioOveruseStats.startTime) 377 .atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT); 378 if (shouldCheckRetention && STATS_TEMPORAL_UNIT.between(statsDate, currentDate) 379 >= RETENTION_PERIOD.get(STATS_TEMPORAL_UNIT)) { 380 continue; 381 } 382 long statsDateEpochSeconds = statsDate.toEpochSecond(); 383 rows.add(IoUsageStatsTable.getContentValues( 384 userPackage.userPackageId, entry, statsDateEpochSeconds)); 385 } 386 try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) { 387 return atomicReplaceEntries(db, IoUsageStatsTable.TABLE_NAME, rows); 388 } 389 } 390 populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users)391 private void populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users) { 392 List<UserPackage> userPackages = UserPackageSettingsTable.queryUserPackages(db, users); 393 if (userPackages == null) { 394 return; 395 } 396 for (int i = 0; i < userPackages.size(); ++i) { 397 UserPackage userPackage = userPackages.get(i); 398 mUserPackagesByKey.put(userPackage.getKey(), userPackage); 399 mUserPackagesById.put(userPackage.userPackageId, userPackage); 400 } 401 } 402 atomicReplaceEntries(SQLiteDatabase db, String tableName, List<ContentValues> rows)403 private static boolean atomicReplaceEntries(SQLiteDatabase db, String tableName, 404 List<ContentValues> rows) { 405 if (rows.isEmpty()) { 406 return true; 407 } 408 try { 409 db.beginTransaction(); 410 for (int i = 0; i < rows.size(); ++i) { 411 try { 412 if (db.replaceOrThrow(tableName, null, rows.get(i)) == -1) { 413 Slogf.e(TAG, "Failed to insert %s entry [%s]", tableName, rows.get(i)); 414 return false; 415 } 416 } catch (SQLException e) { 417 Slog.e(TAG, "Failed to insert " + tableName + " entry [" + rows.get(i) + "]", 418 e); 419 return false; 420 } 421 } 422 db.setTransactionSuccessful(); 423 } finally { 424 db.endTransaction(); 425 } 426 return true; 427 } 428 429 /** Defines the user package settings entry stored in the UserPackageSettingsTable. */ 430 static final class UserPackageSettingsEntry { 431 public final @UserIdInt int userId; 432 public final String packageName; 433 public final @KillableState int killableState; 434 UserPackageSettingsEntry(@serIdInt int userId, String packageName, @KillableState int killableState)435 UserPackageSettingsEntry(@UserIdInt int userId, String packageName, 436 @KillableState int killableState) { 437 this.userId = userId; 438 this.packageName = packageName; 439 this.killableState = killableState; 440 } 441 442 @Override equals(Object obj)443 public boolean equals(Object obj) { 444 if (obj == this) { 445 return true; 446 } 447 if (!(obj instanceof UserPackageSettingsEntry)) { 448 return false; 449 } 450 UserPackageSettingsEntry other = (UserPackageSettingsEntry) obj; 451 return userId == other.userId && packageName.equals(other.packageName) 452 && killableState == other.killableState; 453 } 454 455 @Override hashCode()456 public int hashCode() { 457 return Objects.hash(userId, packageName, killableState); 458 } 459 460 @Override toString()461 public String toString() { 462 return new StringBuilder().append("UserPackageSettingsEntry{userId: ").append(userId) 463 .append(", packageName: ").append(packageName) 464 .append(", killableState: ").append(killableState).append('}') 465 .toString(); 466 } 467 } 468 469 /** Defines the daily summaries for user packages. */ 470 static final class UserPackageDailySummaries { 471 public final @UserIdInt int userId; 472 public final String packageName; 473 public final List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries; 474 private final long mTotalWrittenBytes; 475 UserPackageDailySummaries(@serIdInt int userId, String packageName, List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries)476 UserPackageDailySummaries(@UserIdInt int userId, String packageName, 477 List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries) { 478 this.userId = userId; 479 this.packageName = packageName; 480 this.dailyIoUsageSummaries = dailyIoUsageSummaries; 481 this.mTotalWrittenBytes = computeTotalWrittenBytes(); 482 } 483 484 @Override equals(Object obj)485 public boolean equals(Object obj) { 486 if (obj == this) { 487 return true; 488 } 489 if (!(obj instanceof UserPackageDailySummaries)) { 490 return false; 491 } 492 UserPackageDailySummaries other = (UserPackageDailySummaries) obj; 493 return userId == other.userId && packageName.equals(other.packageName) 494 && dailyIoUsageSummaries.equals(other.dailyIoUsageSummaries); 495 } 496 497 @Override hashCode()498 public int hashCode() { 499 return Objects.hash(userId, packageName, dailyIoUsageSummaries, mTotalWrittenBytes); 500 } 501 502 @Override toString()503 public String toString() { 504 return new StringBuilder().append("UserPackageDailySummaries{userId: ").append(userId) 505 .append(", packageName: ").append(packageName) 506 .append(", dailyIoUsageSummaries: ").append(dailyIoUsageSummaries).append('}') 507 .toString(); 508 } 509 getTotalWrittenBytes()510 long getTotalWrittenBytes() { 511 return mTotalWrittenBytes; 512 } 513 computeTotalWrittenBytes()514 long computeTotalWrittenBytes() { 515 long totalBytes = 0; 516 for (int i = 0; i < dailyIoUsageSummaries.size(); ++i) { 517 AtomsProto.CarWatchdogPerStateBytes writtenBytes = 518 dailyIoUsageSummaries.get(i).getWrittenBytes(); 519 if (writtenBytes.hasForegroundBytes()) { 520 totalBytes += writtenBytes.getForegroundBytes(); 521 } 522 if (writtenBytes.hasBackgroundBytes()) { 523 totalBytes += writtenBytes.getBackgroundBytes(); 524 } 525 if (writtenBytes.hasGarageModeBytes()) { 526 totalBytes += writtenBytes.getGarageModeBytes(); 527 } 528 } 529 return totalBytes; 530 } 531 } 532 533 /** 534 * Defines the contents and queries for the user package settings table. 535 */ 536 static final class UserPackageSettingsTable { 537 public static final String TABLE_NAME = "user_package_settings"; 538 public static final String COLUMN_USER_PACKAGE_ID = "user_package_id"; 539 public static final String COLUMN_PACKAGE_NAME = "package_name"; 540 public static final String COLUMN_USER_ID = "user_id"; 541 public static final String COLUMN_KILLABLE_STATE = "killable_state"; 542 createTable(SQLiteDatabase db)543 public static void createTable(SQLiteDatabase db) { 544 StringBuilder createCommand = new StringBuilder(); 545 createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (") 546 // Maximum value for COLUMN_USER_PACKAGE_ID is the max integer size supported by 547 // the database. Thus, the number of entries that can be inserted into this 548 // table is bound by this upper limit for the lifetime of the device 549 // (i.e., Even when a userId is reused, the previous user_package_ids for 550 // the corresponding userId won't be reused). When the IDs are exhausted, 551 // any new insert operation will result in the error "database or disk is full". 552 .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER PRIMARY KEY AUTOINCREMENT, ") 553 .append(COLUMN_PACKAGE_NAME).append(" TEXT NOT NULL, ") 554 .append(COLUMN_USER_ID).append(" INTEGER NOT NULL, ") 555 .append(COLUMN_KILLABLE_STATE).append(" INTEGER NOT NULL, ") 556 .append("UNIQUE(").append(COLUMN_PACKAGE_NAME) 557 .append(", ").append(COLUMN_USER_ID).append("))"); 558 db.execSQL(createCommand.toString()); 559 Slogf.i(TAG, "Successfully created the %s table in the %s database version %d", 560 TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION); 561 } 562 updateEntry(SQLiteDatabase db, UserPackageSettingsEntry entry)563 public static boolean updateEntry(SQLiteDatabase db, UserPackageSettingsEntry entry) { 564 ContentValues values = new ContentValues(); 565 values.put(COLUMN_KILLABLE_STATE, entry.killableState); 566 567 StringBuilder whereClause = new StringBuilder(COLUMN_PACKAGE_NAME).append(" = ? AND ") 568 .append(COLUMN_USER_ID).append(" = ?"); 569 String[] whereArgs = new String[]{entry.packageName, String.valueOf(entry.userId)}; 570 571 if (db.update(TABLE_NAME, values, whereClause.toString(), whereArgs) < 1) { 572 Slogf.e(TAG, "Failed to update %d entry with package name: %s and user id: %d", 573 TABLE_NAME, entry.packageName, entry.userId); 574 return false; 575 } 576 return true; 577 } 578 replaceEntry(SQLiteDatabase db, UserPackageSettingsEntry entry)579 public static boolean replaceEntry(SQLiteDatabase db, UserPackageSettingsEntry entry) { 580 ContentValues values = new ContentValues(); 581 values.put(COLUMN_USER_ID, entry.userId); 582 values.put(COLUMN_PACKAGE_NAME, entry.packageName); 583 values.put(COLUMN_KILLABLE_STATE, entry.killableState); 584 585 if (db.replaceOrThrow(UserPackageSettingsTable.TABLE_NAME, null, values) == -1) { 586 Slogf.e(TAG, "Failed to replaced %s entry [%s]", TABLE_NAME, values); 587 return false; 588 } 589 return true; 590 } 591 querySettings(SQLiteDatabase db)592 public static ArrayMap<String, UserPackageSettingsEntry> querySettings(SQLiteDatabase db) { 593 StringBuilder queryBuilder = new StringBuilder(); 594 queryBuilder.append("SELECT ") 595 .append(COLUMN_USER_PACKAGE_ID).append(", ") 596 .append(COLUMN_USER_ID).append(", ") 597 .append(COLUMN_PACKAGE_NAME).append(", ") 598 .append(COLUMN_KILLABLE_STATE) 599 .append(" FROM ").append(TABLE_NAME); 600 601 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) { 602 ArrayMap<String, UserPackageSettingsEntry> entriesById = new ArrayMap<>( 603 cursor.getCount()); 604 while (cursor.moveToNext()) { 605 entriesById.put(cursor.getString(0), new UserPackageSettingsEntry( 606 cursor.getInt(1), cursor.getString(2), cursor.getInt(3))); 607 } 608 return entriesById; 609 } 610 } 611 612 /** 613 * Returns the UserPackage entries for the given users. When no users are provided or no 614 * data returned by the DB query, returns null. 615 */ 616 @Nullable queryUserPackages(SQLiteDatabase db, ArraySet<Integer> users)617 public static List<UserPackage> queryUserPackages(SQLiteDatabase db, 618 ArraySet<Integer> users) { 619 int numUsers = users.size(); 620 if (numUsers == 0) { 621 return null; 622 } 623 StringBuilder queryBuilder = new StringBuilder(); 624 queryBuilder.append("SELECT ") 625 .append(COLUMN_USER_PACKAGE_ID).append(", ") 626 .append(COLUMN_USER_ID).append(", ") 627 .append(COLUMN_PACKAGE_NAME) 628 .append(" FROM ").append(TABLE_NAME) 629 .append(" WHERE ").append(COLUMN_USER_ID).append(" IN ("); 630 for (int i = 0; i < numUsers; ++i) { 631 queryBuilder.append(users.valueAt(i)); 632 if (i < numUsers - 1) { 633 queryBuilder.append(", "); 634 } 635 } 636 queryBuilder.append(")"); 637 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) { 638 if (cursor.getCount() == 0) { 639 return null; 640 } 641 List<UserPackage> userPackages = new ArrayList<>(cursor.getCount()); 642 while (cursor.moveToNext()) { 643 userPackages.add(new UserPackage( 644 cursor.getString(0), cursor.getInt(1), cursor.getString(2))); 645 } 646 return userPackages; 647 } 648 } 649 deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId, String packageName)650 public static void deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId, 651 String packageName) { 652 String whereClause = COLUMN_USER_ID + "= ? and " + COLUMN_PACKAGE_NAME + "= ?"; 653 String[] whereArgs = new String[]{String.valueOf(userId), packageName}; 654 int deletedRows = db.delete(TABLE_NAME, whereClause, whereArgs); 655 Slogf.i(TAG, "Deleted %d user package settings db rows for user %d and package %s", 656 deletedRows, userId, packageName); 657 } 658 syncUserPackagesWithAliveUsers(SQLiteDatabase db, IntArray aliveUsers)659 public static void syncUserPackagesWithAliveUsers(SQLiteDatabase db, IntArray aliveUsers) { 660 StringBuilder queryBuilder = new StringBuilder(); 661 for (int i = 0; i < aliveUsers.size(); ++i) { 662 if (i == 0) { 663 queryBuilder.append(COLUMN_USER_ID).append(" NOT IN ("); 664 } else { 665 queryBuilder.append(", "); 666 } 667 queryBuilder.append(aliveUsers.get(i)); 668 if (i == aliveUsers.size() - 1) { 669 queryBuilder.append(")"); 670 } 671 } 672 int deletedRows = db.delete(TABLE_NAME, queryBuilder.toString(), new String[]{}); 673 Slogf.i(TAG, "Deleted %d user package settings db rows while syncing with alive users", 674 deletedRows); 675 } 676 } 677 678 /** Defines the I/O usage entry stored in the IoUsageStatsTable. */ 679 static final class IoUsageStatsEntry { 680 public final @UserIdInt int userId; 681 public final String packageName; 682 public final WatchdogPerfHandler.PackageIoUsage ioUsage; 683 IoUsageStatsEntry(@serIdInt int userId, String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage)684 IoUsageStatsEntry(@UserIdInt int userId, 685 String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage) { 686 this.userId = userId; 687 this.packageName = packageName; 688 this.ioUsage = ioUsage; 689 } 690 } 691 692 /** Defines the not forgiven overuses stored in the IoUsageStatsTable. */ 693 static final class NotForgivenOverusesEntry { 694 public final @UserIdInt int userId; 695 public final String packageName; 696 public final int notForgivenOveruses; 697 NotForgivenOverusesEntry(@serIdInt int userId, String packageName, int notForgivenOveruses)698 NotForgivenOverusesEntry(@UserIdInt int userId, 699 String packageName, int notForgivenOveruses) { 700 this.userId = userId; 701 this.packageName = packageName; 702 this.notForgivenOveruses = notForgivenOveruses; 703 } 704 705 @Override equals(Object obj)706 public boolean equals(Object obj) { 707 if (this == obj) { 708 return true; 709 } 710 if (!(obj instanceof NotForgivenOverusesEntry)) { 711 return false; 712 } 713 NotForgivenOverusesEntry other = (NotForgivenOverusesEntry) obj; 714 return userId == other.userId 715 && packageName.equals(other.packageName) 716 && notForgivenOveruses == other.notForgivenOveruses; 717 } 718 719 @Override hashCode()720 public int hashCode() { 721 return Objects.hash(userId, packageName, notForgivenOveruses); 722 } 723 724 @Override toString()725 public String toString() { 726 return "NotForgivenOverusesEntry {UserId: " + userId 727 + ", Package name: " + packageName 728 + ", Not forgiven overuses: " + notForgivenOveruses + "}"; 729 } 730 } 731 732 /** 733 * Defines the contents and queries for the I/O usage stats table. 734 */ 735 static final class IoUsageStatsTable { 736 public static final String TABLE_NAME = "io_usage_stats"; 737 public static final String COLUMN_USER_PACKAGE_ID = "user_package_id"; 738 public static final String COLUMN_DATE_EPOCH = "date_epoch"; 739 public static final String COLUMN_NUM_OVERUSES = "num_overuses"; 740 public static final String COLUMN_NUM_FORGIVEN_OVERUSES = "num_forgiven_overuses"; 741 public static final String COLUMN_NUM_TIMES_KILLED = "num_times_killed"; 742 public static final String COLUMN_WRITTEN_FOREGROUND_BYTES = "written_foreground_bytes"; 743 public static final String COLUMN_WRITTEN_BACKGROUND_BYTES = "written_background_bytes"; 744 public static final String COLUMN_WRITTEN_GARAGE_MODE_BYTES = "written_garage_mode_bytes"; 745 /* Below columns will be null for historical stats i.e., when the date != current date. */ 746 public static final String COLUMN_REMAINING_FOREGROUND_WRITE_BYTES = 747 "remaining_foreground_write_bytes"; 748 public static final String COLUMN_REMAINING_BACKGROUND_WRITE_BYTES = 749 "remaining_background_write_bytes"; 750 public static final String COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES = 751 "remaining_garage_mode_write_bytes"; 752 public static final String COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES = 753 "forgiven_foreground_write_bytes"; 754 public static final String COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES = 755 "forgiven_background_write_bytes"; 756 public static final String COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES = 757 "forgiven_garage_mode_write_bytes"; 758 createTable(SQLiteDatabase db)759 public static void createTable(SQLiteDatabase db) { 760 StringBuilder createCommand = new StringBuilder(); 761 createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (") 762 .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER NOT NULL, ") 763 .append(COLUMN_DATE_EPOCH).append(" INTEGER NOT NULL, ") 764 .append(COLUMN_NUM_OVERUSES).append(" INTEGER NOT NULL, ") 765 .append(COLUMN_NUM_FORGIVEN_OVERUSES).append(" INTEGER NOT NULL, ") 766 .append(COLUMN_NUM_TIMES_KILLED).append(" INTEGER NOT NULL, ") 767 .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" INTEGER, ") 768 .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" INTEGER, ") 769 .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" INTEGER, ") 770 .append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append(" INTEGER, ") 771 .append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append(" INTEGER, ") 772 .append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ") 773 .append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append(" INTEGER, ") 774 .append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append(" INTEGER, ") 775 .append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ") 776 .append("PRIMARY KEY (").append(COLUMN_USER_PACKAGE_ID).append(", ") 777 .append(COLUMN_DATE_EPOCH).append("), FOREIGN KEY (") 778 .append(COLUMN_USER_PACKAGE_ID).append(") REFERENCES ") 779 .append(UserPackageSettingsTable.TABLE_NAME).append(" (") 780 .append(UserPackageSettingsTable.COLUMN_USER_PACKAGE_ID) 781 .append(") ON DELETE CASCADE)"); 782 db.execSQL(createCommand.toString()); 783 Slogf.i(TAG, "Successfully created the %s table in the %s database version %d", 784 TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION); 785 } 786 getContentValues( String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds)787 public static ContentValues getContentValues( 788 String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds) { 789 android.automotive.watchdog.IoOveruseStats ioOveruseStats = 790 entry.ioUsage.getInternalIoOveruseStats(); 791 ContentValues values = new ContentValues(); 792 values.put(COLUMN_USER_PACKAGE_ID, userPackageId); 793 values.put(COLUMN_DATE_EPOCH, statsDateEpochSeconds); 794 values.put(COLUMN_NUM_OVERUSES, ioOveruseStats.totalOveruses); 795 values.put(COLUMN_NUM_FORGIVEN_OVERUSES, entry.ioUsage.getForgivenOveruses()); 796 values.put(COLUMN_NUM_TIMES_KILLED, entry.ioUsage.getTotalTimesKilled()); 797 values.put( 798 COLUMN_WRITTEN_FOREGROUND_BYTES, ioOveruseStats.writtenBytes.foregroundBytes); 799 values.put( 800 COLUMN_WRITTEN_BACKGROUND_BYTES, ioOveruseStats.writtenBytes.backgroundBytes); 801 values.put( 802 COLUMN_WRITTEN_GARAGE_MODE_BYTES, ioOveruseStats.writtenBytes.garageModeBytes); 803 values.put(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES, 804 ioOveruseStats.remainingWriteBytes.foregroundBytes); 805 values.put(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES, 806 ioOveruseStats.remainingWriteBytes.backgroundBytes); 807 values.put(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES, 808 ioOveruseStats.remainingWriteBytes.garageModeBytes); 809 android.automotive.watchdog.PerStateBytes forgivenWriteBytes = 810 entry.ioUsage.getForgivenWriteBytes(); 811 values.put(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES, forgivenWriteBytes.foregroundBytes); 812 values.put(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES, forgivenWriteBytes.backgroundBytes); 813 values.put(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES, forgivenWriteBytes.garageModeBytes); 814 return values; 815 } 816 queryStats( SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)817 public static ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> queryStats( 818 SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) { 819 StringBuilder queryBuilder = new StringBuilder(); 820 queryBuilder.append("SELECT ") 821 .append(COLUMN_USER_PACKAGE_ID).append(", ") 822 .append("MIN(").append(COLUMN_DATE_EPOCH).append("), ") 823 .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ") 824 .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append("), ") 825 .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ") 826 .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ") 827 .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ") 828 .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ") 829 .append("SUM(").append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append("), ") 830 .append("SUM(").append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append("), ") 831 .append("SUM(").append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append("), ") 832 .append("SUM(").append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append("), ") 833 .append("SUM(").append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append("), ") 834 .append("SUM(").append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(") ") 835 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 836 .append(COLUMN_DATE_EPOCH).append(">= ? and ") 837 .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ") 838 .append(COLUMN_USER_PACKAGE_ID); 839 String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds), 840 String.valueOf(excludingEndEpochSeconds)}; 841 842 ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsageById = new ArrayMap<>(); 843 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 844 while (cursor.moveToNext()) { 845 android.automotive.watchdog.IoOveruseStats ioOveruseStats = 846 new android.automotive.watchdog.IoOveruseStats(); 847 ioOveruseStats.startTime = cursor.getLong(1); 848 ioOveruseStats.durationInSeconds = 849 excludingEndEpochSeconds - includingStartEpochSeconds; 850 ioOveruseStats.totalOveruses = cursor.getInt(2); 851 ioOveruseStats.writtenBytes = new PerStateBytes(); 852 ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(5); 853 ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(6); 854 ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(7); 855 ioOveruseStats.remainingWriteBytes = new PerStateBytes(); 856 ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(8); 857 ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(9); 858 ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(10); 859 PerStateBytes forgivenWriteBytes = new PerStateBytes(); 860 forgivenWriteBytes.foregroundBytes = cursor.getLong(11); 861 forgivenWriteBytes.backgroundBytes = cursor.getLong(12); 862 forgivenWriteBytes.garageModeBytes = cursor.getLong(13); 863 864 ioUsageById.put(cursor.getString(0), new WatchdogPerfHandler.PackageIoUsage( 865 ioOveruseStats, forgivenWriteBytes, 866 /* forgivenOveruses= */ cursor.getInt(3), 867 /* totalTimesKilled= */ cursor.getInt(4))); 868 } 869 } 870 return ioUsageById; 871 } 872 queryIoOveruseStatsForUserPackageId( SQLiteDatabase db, String userPackageId, long includingStartEpochSeconds, long excludingEndEpochSeconds)873 public static @Nullable IoOveruseStats queryIoOveruseStatsForUserPackageId( 874 SQLiteDatabase db, String userPackageId, long includingStartEpochSeconds, 875 long excludingEndEpochSeconds) { 876 StringBuilder queryBuilder = new StringBuilder(); 877 queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ") 878 .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ") 879 .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ") 880 .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ") 881 .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ") 882 .append("MIN(").append(COLUMN_DATE_EPOCH).append(") ") 883 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 884 .append(COLUMN_USER_PACKAGE_ID).append("=? and ") 885 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 886 .append(COLUMN_DATE_EPOCH).append("< ?"); 887 String[] selectionArgs = new String[]{userPackageId, 888 String.valueOf(includingStartEpochSeconds), 889 String.valueOf(excludingEndEpochSeconds)}; 890 long totalOveruses = 0; 891 long totalTimesKilled = 0; 892 long totalBytesWritten = 0; 893 long earliestEpochSecond = excludingEndEpochSeconds; 894 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 895 if (cursor.getCount() == 0) { 896 return null; 897 } 898 while (cursor.moveToNext()) { 899 totalOveruses += cursor.getLong(0); 900 totalTimesKilled += cursor.getLong(1); 901 totalBytesWritten += cursor.getLong(2) + cursor.getLong(3) + cursor.getLong(4); 902 earliestEpochSecond = Math.min(cursor.getLong(5), earliestEpochSecond); 903 } 904 } 905 if (totalBytesWritten == 0) { 906 return null; 907 } 908 long durationInSeconds = excludingEndEpochSeconds - earliestEpochSecond; 909 IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder( 910 earliestEpochSecond, durationInSeconds); 911 statsBuilder.setTotalOveruses(totalOveruses); 912 statsBuilder.setTotalTimesKilled(totalTimesKilled); 913 statsBuilder.setTotalBytesWritten(totalBytesWritten); 914 return statsBuilder.build(); 915 } 916 queryNotForgivenHistoricalOveruses( SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)917 public static ArrayMap<String, Integer> queryNotForgivenHistoricalOveruses( 918 SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) { 919 StringBuilder queryBuilder = new StringBuilder("SELECT ") 920 .append(COLUMN_USER_PACKAGE_ID).append(", ") 921 .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ") 922 .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append(") ") 923 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 924 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 925 .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ") 926 .append(COLUMN_USER_PACKAGE_ID); 927 String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds), 928 String.valueOf(excludingEndEpochSeconds)}; 929 ArrayMap<String, Integer> notForgivenOverusesById = new ArrayMap<>(); 930 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 931 while (cursor.moveToNext()) { 932 if (cursor.getInt(1) <= cursor.getInt(2)) { 933 continue; 934 } 935 notForgivenOverusesById.put(cursor.getString(0), 936 cursor.getInt(1) - cursor.getInt(2)); 937 } 938 } 939 return notForgivenOverusesById; 940 } 941 forgiveHistoricalOverusesForPackage(SQLiteDatabase db, List<String> userPackageIds, long includingStartEpochSeconds, long excludingEndEpochSeconds)942 public static void forgiveHistoricalOverusesForPackage(SQLiteDatabase db, 943 List<String> userPackageIds, long includingStartEpochSeconds, 944 long excludingEndEpochSeconds) { 945 if (userPackageIds.isEmpty()) { 946 Slogf.e(TAG, "No user package ids provided to forgive historical overuses."); 947 return; 948 } 949 StringBuilder updateQueryBuilder = new StringBuilder("UPDATE ").append(TABLE_NAME) 950 .append(" SET ") 951 .append(COLUMN_NUM_FORGIVEN_OVERUSES).append("=").append(COLUMN_NUM_OVERUSES) 952 .append(" WHERE ") 953 .append(COLUMN_DATE_EPOCH).append(">= ").append(includingStartEpochSeconds) 954 .append(" and ") 955 .append(COLUMN_DATE_EPOCH).append("< ").append(excludingEndEpochSeconds); 956 for (int i = 0; i < userPackageIds.size(); i++) { 957 if (i == 0) { 958 updateQueryBuilder.append(" and ").append(COLUMN_USER_PACKAGE_ID) 959 .append(" IN ("); 960 } else { 961 updateQueryBuilder.append(", "); 962 } 963 updateQueryBuilder.append(userPackageIds.get(i)); 964 if (i == userPackageIds.size() - 1) { 965 updateQueryBuilder.append(")"); 966 } 967 } 968 969 db.execSQL(updateQueryBuilder.toString()); 970 Slogf.i(TAG, "Attempted to forgive overuses for I/O usage stats entries on pid %d", 971 Process.myPid()); 972 } 973 974 public static @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary> queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)975 queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds, 976 long excludingEndEpochSeconds) { 977 StringBuilder queryBuilder = new StringBuilder(); 978 queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ") 979 .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ") 980 .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ") 981 .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ") 982 .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER) 983 .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ") 984 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 985 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 986 .append(COLUMN_DATE_EPOCH).append(" < ? ") 987 .append("GROUP BY stats_date_epoch ") 988 .append("HAVING SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ") 989 .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ") 990 .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") > 0 ") 991 .append("ORDER BY stats_date_epoch ASC"); 992 993 String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds), 994 String.valueOf(excludingEndEpochSeconds)}; 995 List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries = new ArrayList<>(); 996 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 997 if (cursor.getCount() == 0) { 998 return null; 999 } 1000 while (cursor.moveToNext()) { 1001 summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder() 1002 .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes( 1003 /* foregroundBytes= */ cursor.getLong(1), 1004 /* backgroundBytes= */ cursor.getLong(2), 1005 /* garageModeBytes= */ cursor.getLong(3))) 1006 .setOveruseCount(cursor.getInt(0)) 1007 .build()); 1008 } 1009 } 1010 return summaries; 1011 } 1012 querySystemTotalWrittenBytes(SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)1013 public static long querySystemTotalWrittenBytes(SQLiteDatabase db, 1014 long includingStartEpochSeconds, long excludingEndEpochSeconds) { 1015 StringBuilder queryBuilder = new StringBuilder(); 1016 queryBuilder.append("SELECT SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ") 1017 .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ") 1018 .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") ") 1019 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 1020 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 1021 .append(COLUMN_DATE_EPOCH).append(" < ? "); 1022 1023 String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds), 1024 String.valueOf(excludingEndEpochSeconds)}; 1025 long totalWrittenBytes = 0; 1026 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 1027 while (cursor.moveToNext()) { 1028 totalWrittenBytes += cursor.getLong(0); 1029 } 1030 } 1031 return totalWrittenBytes; 1032 } 1033 1034 public static @Nullable ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> queryTopUsersDailyIoUsageSummaries(SQLiteDatabase db, int numTopUsers, long includingStartEpochSeconds, long excludingEndEpochSeconds)1035 queryTopUsersDailyIoUsageSummaries(SQLiteDatabase db, int numTopUsers, 1036 long includingStartEpochSeconds, long excludingEndEpochSeconds) { 1037 StringBuilder innerQueryBuilder = new StringBuilder(); 1038 innerQueryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID) 1039 .append(" FROM (SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ") 1040 .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ") 1041 .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ") 1042 .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") AS total_written_bytes ") 1043 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 1044 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 1045 .append(COLUMN_DATE_EPOCH).append(" < ?") 1046 .append(" GROUP BY ").append(COLUMN_USER_PACKAGE_ID) 1047 .append(" ORDER BY total_written_bytes DESC LIMIT ").append(numTopUsers) 1048 .append(')'); 1049 1050 StringBuilder queryBuilder = new StringBuilder(); 1051 queryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ") 1052 .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ") 1053 .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ") 1054 .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ") 1055 .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ") 1056 .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER) 1057 .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ") 1058 .append("FROM ").append(TABLE_NAME).append(" WHERE ") 1059 .append(COLUMN_DATE_EPOCH).append(" >= ? and ") 1060 .append(COLUMN_DATE_EPOCH).append(" < ? and (") 1061 .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" > 0 or ") 1062 .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" > 0 or ") 1063 .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" > 0) and ") 1064 .append(COLUMN_USER_PACKAGE_ID) 1065 .append(" in (").append(innerQueryBuilder) 1066 .append(") GROUP BY stats_date_epoch, ").append(COLUMN_USER_PACKAGE_ID) 1067 .append(" ORDER BY ").append(COLUMN_USER_PACKAGE_ID) 1068 .append(", stats_date_epoch ASC"); 1069 1070 String[] selectionArgs = new String[]{ 1071 // Outer query selection arguments. 1072 String.valueOf(includingStartEpochSeconds), 1073 String.valueOf(excludingEndEpochSeconds), 1074 // Inner query selection arguments. 1075 String.valueOf(includingStartEpochSeconds), 1076 String.valueOf(excludingEndEpochSeconds)}; 1077 1078 ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById = 1079 new ArrayMap<>(); 1080 try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) { 1081 if (cursor.getCount() == 0) { 1082 return null; 1083 } 1084 while (cursor.moveToNext()) { 1085 String id = cursor.getString(0); 1086 List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries = 1087 summariesById.get(id); 1088 if (summaries == null) { 1089 summaries = new ArrayList<>(); 1090 } 1091 summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder() 1092 .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes( 1093 /* foregroundBytes= */ cursor.getLong(2), 1094 /* backgroundBytes= */ cursor.getLong(3), 1095 /* garageModeBytes= */ cursor.getLong(4))) 1096 .setOveruseCount(cursor.getInt(1)) 1097 .build()); 1098 summariesById.put(id, summaries); 1099 } 1100 } 1101 return summariesById; 1102 } 1103 truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate)1104 public static void truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate) { 1105 String selection = COLUMN_DATE_EPOCH + " <= ?"; 1106 String[] selectionArgs = { String.valueOf(latestTruncateDate.toEpochSecond()) }; 1107 1108 int rows = db.delete(TABLE_NAME, selection, selectionArgs); 1109 Slogf.i(TAG, "Truncated %d I/O usage stats entries on pid %d", rows, Process.myPid()); 1110 } 1111 trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate)1112 public static void trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate) { 1113 ContentValues values = new ContentValues(); 1114 values.putNull(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES); 1115 values.putNull(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES); 1116 values.putNull(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES); 1117 values.putNull(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES); 1118 values.putNull(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES); 1119 values.putNull(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES); 1120 1121 String selection = COLUMN_DATE_EPOCH + " < ?"; 1122 String[] selectionArgs = { String.valueOf(currentDate.toEpochSecond()) }; 1123 1124 int rows = db.update(TABLE_NAME, values, selection, selectionArgs); 1125 Slogf.i(TAG, "Trimmed %d I/O usage stats entries on pid %d", rows, Process.myPid()); 1126 } 1127 } 1128 1129 /** 1130 * Defines the Watchdog database and database level operations. 1131 */ 1132 static final class WatchdogDbHelper extends SQLiteOpenHelper { 1133 public static final String DATABASE_NAME = "car_watchdog.db"; 1134 1135 private static final int DATABASE_VERSION = 2; 1136 1137 private ZonedDateTime mLatestShrinkDate; 1138 private TimeSource mTimeSource; 1139 WatchdogDbHelper(Context context, boolean useDataSystemCarDir, TimeSource timeSource)1140 WatchdogDbHelper(Context context, boolean useDataSystemCarDir, TimeSource timeSource) { 1141 /* Use device protected storage because CarService may need to access the database 1142 * before the user has authenticated. 1143 */ 1144 super(context.createDeviceProtectedStorageContext(), useDataSystemCarDir 1145 ? new File(CarWatchdogService.getWatchdogDirFile(), DATABASE_NAME) 1146 .getAbsolutePath() 1147 : DATABASE_NAME, 1148 /* name= */ null, DATABASE_VERSION); 1149 mTimeSource = timeSource; 1150 } 1151 1152 @Override onCreate(SQLiteDatabase db)1153 public void onCreate(SQLiteDatabase db) { 1154 UserPackageSettingsTable.createTable(db); 1155 IoUsageStatsTable.createTable(db); 1156 } 1157 1158 @Override onConfigure(SQLiteDatabase db)1159 public void onConfigure(SQLiteDatabase db) { 1160 db.setForeignKeyConstraintsEnabled(true); 1161 } 1162 close()1163 public synchronized void close() { 1164 super.close(); 1165 mLatestShrinkDate = null; 1166 } 1167 onShrink(SQLiteDatabase db)1168 public void onShrink(SQLiteDatabase db) { 1169 ZonedDateTime currentDate = mTimeSource.getCurrentDate(); 1170 if (currentDate.equals(mLatestShrinkDate)) { 1171 return; 1172 } 1173 IoUsageStatsTable.truncateToDate(db, currentDate.minus(RETENTION_PERIOD)); 1174 IoUsageStatsTable.trimHistoricalStats(db, currentDate); 1175 mLatestShrinkDate = currentDate; 1176 Slogf.i(TAG, "Shrunk watchdog database for the date '%s'", mLatestShrinkDate); 1177 } 1178 1179 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)1180 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { 1181 if (oldVersion != 1) { 1182 return; 1183 } 1184 // Upgrade logic from version 1 to 2. 1185 int upgradeVersion = oldVersion; 1186 db.beginTransaction(); 1187 try { 1188 upgradeToVersion2(db); 1189 db.setTransactionSuccessful(); 1190 upgradeVersion = currentVersion; 1191 Slogf.i(TAG, "Successfully upgraded database from version %d to %d", oldVersion, 1192 upgradeVersion); 1193 } finally { 1194 db.endTransaction(); 1195 } 1196 if (upgradeVersion != currentVersion) { 1197 Slogf.i(TAG, "Failed to upgrade database from version %d to %d. " 1198 + "Attempting to recreate database.", oldVersion, currentVersion); 1199 recreateDatabase(db); 1200 } 1201 } 1202 1203 /** 1204 * Upgrades the given {@code db} to version 2. 1205 * 1206 * <p>Database version 2 replaces the primary key in {@link UserPackageSettingsTable} with 1207 * an auto-incrementing integer ID and uses the ID (instead of its rowid) as one of 1208 * the primary keys in {@link IoUsageStatsTable} along with a foreign key dependency. 1209 * 1210 * <p>Only the entries from {@link UserPackageSettingsTable} are migrated to the version 2 1211 * database because in version 1 only the current day's entries in {@link IoUsageStatsTable} 1212 * are mappable to the former table and dropping these entries is tolerable. 1213 */ upgradeToVersion2(SQLiteDatabase db)1214 private void upgradeToVersion2(SQLiteDatabase db) { 1215 String oldUserPackageSettingsTable = UserPackageSettingsTable.TABLE_NAME + "_old_v1"; 1216 StringBuilder execSql = new StringBuilder("ALTER TABLE ") 1217 .append(UserPackageSettingsTable.TABLE_NAME) 1218 .append(" RENAME TO ").append(oldUserPackageSettingsTable); 1219 db.execSQL(execSql.toString()); 1220 1221 execSql = new StringBuilder("DROP TABLE IF EXISTS ") 1222 .append(IoUsageStatsTable.TABLE_NAME); 1223 db.execSQL(execSql.toString()); 1224 1225 UserPackageSettingsTable.createTable(db); 1226 IoUsageStatsTable.createTable(db); 1227 1228 execSql = new StringBuilder("INSERT INTO ").append(UserPackageSettingsTable.TABLE_NAME) 1229 .append(" (").append(UserPackageSettingsTable.COLUMN_PACKAGE_NAME).append(", ") 1230 .append(UserPackageSettingsTable.COLUMN_USER_ID).append(", ") 1231 .append(UserPackageSettingsTable.COLUMN_KILLABLE_STATE).append(") ") 1232 .append("SELECT ").append(UserPackageSettingsTable.COLUMN_PACKAGE_NAME) 1233 .append(", ").append(UserPackageSettingsTable.COLUMN_USER_ID).append(", ") 1234 .append(UserPackageSettingsTable.COLUMN_KILLABLE_STATE).append(" FROM ") 1235 .append(oldUserPackageSettingsTable); 1236 db.execSQL(execSql.toString()); 1237 1238 execSql = new StringBuilder("DROP TABLE IF EXISTS ") 1239 .append(oldUserPackageSettingsTable); 1240 db.execSQL(execSql.toString()); 1241 } 1242 recreateDatabase(SQLiteDatabase db)1243 private void recreateDatabase(SQLiteDatabase db) { 1244 db.execSQL(new StringBuilder("DROP TABLE IF EXISTS ") 1245 .append(UserPackageSettingsTable.TABLE_NAME).toString()); 1246 db.execSQL(new StringBuilder("DROP TABLE IF EXISTS ") 1247 .append(IoUsageStatsTable.TABLE_NAME).toString()); 1248 1249 onCreate(db); 1250 Slogf.e(TAG, "Successfully recreated database version %d", DATABASE_VERSION); 1251 } 1252 } 1253 1254 1255 private static final class UserPackage { 1256 public final String userPackageId; 1257 public final @UserIdInt int userId; 1258 public final String packageName; 1259 UserPackage(String userPackageId, @UserIdInt int userId, String packageName)1260 UserPackage(String userPackageId, @UserIdInt int userId, String packageName) { 1261 this.userPackageId = userPackageId; 1262 this.userId = userId; 1263 this.packageName = packageName; 1264 } 1265 getKey()1266 public String getKey() { 1267 return getKey(userId, packageName); 1268 } 1269 getKey(int userId, String packageName)1270 public static String getKey(int userId, String packageName) { 1271 return String.format(Locale.ENGLISH, "%d:%s", userId, packageName); 1272 } 1273 1274 @Override toString()1275 public String toString() { 1276 return new StringBuilder("UserPackage{userPackageId: ").append(userPackageId) 1277 .append(", userId: ").append(userId) 1278 .append(", packageName: ").append(packageName).append("}").toString(); 1279 } 1280 } 1281 } 1282