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.telemetry; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.car.builtin.util.Slogf; 22 import android.car.telemetry.TelemetryProto; 23 import android.content.Context; 24 import android.os.PersistableBundle; 25 import android.provider.Settings; 26 import android.util.ArrayMap; 27 import android.util.AtomicFile; 28 29 import com.android.car.CarLog; 30 import com.android.car.internal.util.IndentingPrintWriter; 31 import com.android.car.telemetry.MetricsReportProto.MetricsReportContainer; 32 import com.android.car.telemetry.MetricsReportProto.MetricsReportList; 33 import com.android.car.telemetry.util.IoUtils; 34 import com.android.car.telemetry.util.MetricsReportProtoUtils; 35 import com.android.internal.annotations.VisibleForTesting; 36 37 import java.io.File; 38 import java.io.IOException; 39 import java.util.Arrays; 40 import java.util.HashSet; 41 import java.util.Set; 42 import java.util.concurrent.TimeUnit; 43 44 /** 45 * Disk storage for interim and final metrics statistics, as well as for internal data. 46 * All methods in this class should be invoked from the telemetry thread. 47 */ 48 public class ResultStore { 49 50 private static final long STALE_THRESHOLD_MILLIS = 51 TimeUnit.MILLISECONDS.convert(30, TimeUnit.DAYS); 52 @VisibleForTesting 53 static final String INTERIM_RESULT_DIR = "interim"; 54 @VisibleForTesting 55 static final String ERROR_RESULT_DIR = "error"; 56 @VisibleForTesting 57 static final String FINAL_RESULT_DIR = "final"; 58 @VisibleForTesting 59 static final String PUBLISHER_STORAGE_DIR = "publisher"; 60 /** 61 * The following are bundle keys for the annotations. 62 * The metrics report is annotated with the boot count, id, and timestamp. 63 * Together, boot count and id will help clients determine if any report had been dropped. 64 */ 65 @VisibleForTesting 66 static final String BUNDLE_KEY_BOOT_COUNT = "metrics.report.boot_count"; 67 @VisibleForTesting 68 static final String BUNDLE_KEY_ID = "metrics.report.id"; 69 @VisibleForTesting 70 static final String BUNDLE_KEY_TIMESTAMP = "metrics.report.timestamp_millis"; 71 72 /** Map keys are MetricsConfig names, which are also the file names in disk. */ 73 private final ArrayMap<String, InterimResult> mInterimResultCache = new ArrayMap<>(); 74 private final ArrayMap<String, MetricsReportList.Builder> mMetricsReportCache = 75 new ArrayMap<>(); 76 private final ArrayMap<String, TelemetryProto.TelemetryError> mErrorCache = new ArrayMap<>(); 77 /** Keyed by publisher's class name. */ 78 private final ArrayMap<String, PersistableBundle> mPublisherCache = new ArrayMap<>(); 79 /** Keyed by metrics config name, value is how many reports it produced since boot. */ 80 private final ArrayMap<String, Integer> mReportCountMap = new ArrayMap<>(); 81 82 private final Context mContext; 83 private final File mInterimResultDirectory; 84 private final File mErrorResultDirectory; 85 private final File mMetricsReportDirectory; 86 private final File mPublisherDataDirectory; 87 ResultStore(@onNull Context context, @NonNull File rootDirectory)88 public ResultStore(@NonNull Context context, @NonNull File rootDirectory) { 89 mContext = context; 90 mInterimResultDirectory = new File(rootDirectory, INTERIM_RESULT_DIR); 91 mErrorResultDirectory = new File(rootDirectory, ERROR_RESULT_DIR); 92 mMetricsReportDirectory = new File(rootDirectory, FINAL_RESULT_DIR); 93 mPublisherDataDirectory = new File(rootDirectory, PUBLISHER_STORAGE_DIR); 94 mInterimResultDirectory.mkdirs(); 95 mErrorResultDirectory.mkdirs(); 96 mMetricsReportDirectory.mkdirs(); 97 mPublisherDataDirectory.mkdir(); 98 // load interim results and internal data into memory to reduce the frequency of disk access 99 loadInterimResultsIntoMemory(); 100 } 101 102 /** Reads interim results into memory for faster access. */ loadInterimResultsIntoMemory()103 private void loadInterimResultsIntoMemory() { 104 File[] files = mInterimResultDirectory.listFiles(); 105 if (files == null) { 106 return; 107 } 108 for (File file : files) { 109 try { 110 PersistableBundle interimResultBundle = IoUtils.readBundle(file); 111 mInterimResultCache.put(file.getName(), new InterimResult(interimResultBundle)); 112 } catch (IOException e) { 113 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read from disk.", e); 114 // TODO(b/197153560): record failure 115 } 116 } 117 } 118 119 /** 120 * Retrieves interim metrics for the given 121 * {@link android.car.telemetry.TelemetryProto.MetricsConfig}. 122 */ 123 @Nullable getInterimResult(@onNull String metricsConfigName)124 public PersistableBundle getInterimResult(@NonNull String metricsConfigName) { 125 if (!mInterimResultCache.containsKey(metricsConfigName)) { 126 return null; 127 } 128 return mInterimResultCache.get(metricsConfigName).getBundle(); 129 } 130 131 /** 132 * Retrieves final metrics for the given 133 * {@link android.car.telemetry.TelemetryProto.MetricsConfig}. 134 * 135 * @param metricsConfigName name of the MetricsConfig. 136 * @param deleteResult if true, the final result will be deleted from disk. 137 * @return {@link MetricsReportList} that contains all report for the given config. 138 */ 139 @Nullable getMetricsReports( @onNull String metricsConfigName, boolean deleteResult)140 public MetricsReportList getMetricsReports( 141 @NonNull String metricsConfigName, boolean deleteResult) { 142 // the reports may have been stored in memory 143 MetricsReportList.Builder reportList = mMetricsReportCache.get(metricsConfigName); 144 // if not, the reports may have been stored in disk 145 if (reportList == null) { 146 reportList = readMetricsReportList(metricsConfigName); 147 } 148 if (deleteResult) { 149 mMetricsReportCache.remove(metricsConfigName); 150 IoUtils.deleteSilently(mMetricsReportDirectory, metricsConfigName); 151 } 152 return reportList == null ? null : reportList.build(); 153 } 154 155 /** 156 * Retrieves all metrics reports for all configs, keyed by each config name. This call is 157 * not destructive, because this method is only used by 158 * {@link CarTelemetryService#dump(IndentingPrintWriter)}. 159 * 160 * @return All available metrics reports keyed by config names. 161 */ 162 @NonNull getAllMetricsReports()163 public ArrayMap<String, MetricsReportList> getAllMetricsReports() { 164 // reports could be stored in two places, in memory and in disk 165 ArrayMap<String, MetricsReportList> results = new ArrayMap<>(); 166 // first check the in-memory cache 167 for (int i = 0; i < mMetricsReportCache.size(); i++) { 168 results.put(mMetricsReportCache.keyAt(i), mMetricsReportCache.valueAt(i).build()); 169 } 170 // also check the disk 171 File[] files = mMetricsReportDirectory.listFiles(); 172 if (files == null) { 173 return results; 174 } 175 for (File file : files) { 176 // if the metrics reports exist in memory, they have already been added to `results` 177 if (results.containsKey(file.getName())) { 178 continue; // skip already-added results 179 } 180 MetricsReportList.Builder reportList = readMetricsReportList(file.getName()); 181 if (reportList != null) { 182 results.put(file.getName(), reportList.build()); 183 } 184 } 185 return results; 186 } 187 188 /** 189 * Returns the error result produced by the metrics config if exists, null otherwise. 190 * 191 * @param metricsConfigName name of the MetricsConfig. 192 * @param deleteResult if true, the error file will be deleted from disk. 193 * @return the error result if exists, null otherwise. 194 */ 195 @Nullable getErrorResult( @onNull String metricsConfigName, boolean deleteResult)196 public TelemetryProto.TelemetryError getErrorResult( 197 @NonNull String metricsConfigName, boolean deleteResult) { 198 // check in memory storage 199 TelemetryProto.TelemetryError result = mErrorCache.get(metricsConfigName); 200 if (result != null) { 201 if (deleteResult) { 202 mErrorCache.remove(metricsConfigName); 203 } 204 return result; 205 } 206 // check persistent storage 207 File file = new File(mErrorResultDirectory, metricsConfigName); 208 // if no error exists for this metrics config, return immediately 209 if (!file.exists()) { 210 return null; 211 } 212 try { 213 result = TelemetryProto.TelemetryError.parseFrom(new AtomicFile(file).readFully()); 214 if (deleteResult) { 215 file.delete(); 216 } 217 return result; 218 } catch (IOException e) { 219 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to get error result from disk.", e); 220 // TODO(b/197153560): record failure 221 } 222 return null; 223 } 224 225 /** 226 * Retrieves all errors, mapped to each config name. This call is not destructive because 227 * this method is only used by {@link CarTelemetryService#dump(IndentingPrintWriter)}. 228 * 229 * @return the map of errors to each config. 230 */ 231 @NonNull getAllErrorResults()232 public ArrayMap<String, TelemetryProto.TelemetryError> getAllErrorResults() { 233 ArrayMap<String, TelemetryProto.TelemetryError> errors = new ArrayMap<>(mErrorCache); 234 File[] files = mErrorResultDirectory.listFiles(); 235 if (files == null) { 236 return errors; 237 } 238 for (File file : files) { 239 try { 240 TelemetryProto.TelemetryError error = 241 TelemetryProto.TelemetryError.parseFrom(new AtomicFile(file).readFully()); 242 errors.put(file.getName(), error); 243 } catch (IOException e) { 244 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read errors from disk.", e); 245 // TODO(b/197153560): record failure 246 } 247 } 248 return errors; 249 } 250 251 /** 252 * Returns all data associated with the given publisher. 253 * 254 * @param publisherName Class name of the given publisher. 255 * @param deleteData If {@code true}, all data for the publisher will be deleted from cache 256 * and disk. 257 */ 258 @Nullable getPublisherData(@onNull String publisherName, boolean deleteData)259 public PersistableBundle getPublisherData(@NonNull String publisherName, boolean deleteData) { 260 PersistableBundle data = mPublisherCache.get(publisherName); 261 if (data != null) { 262 if (deleteData) { 263 mPublisherCache.remove(publisherName); 264 } 265 return data; 266 } 267 // check persistent storage 268 File file = new File(mPublisherDataDirectory, publisherName); 269 // if no publisher data exists, return immediately 270 if (!file.exists()) { 271 return null; 272 } 273 try { 274 data = IoUtils.readBundle(file); 275 if (deleteData) { 276 file.delete(); 277 } 278 return data; 279 } catch (IOException e) { 280 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read from disk.", e); 281 // TODO(b/197153560): record failure 282 } 283 return null; 284 } 285 286 /** 287 * Stores interim metrics results in memory for the given 288 * {@link android.car.telemetry.TelemetryProto.MetricsConfig}. 289 */ putInterimResult( @onNull String metricsConfigName, @NonNull PersistableBundle result)290 public void putInterimResult( 291 @NonNull String metricsConfigName, @NonNull PersistableBundle result) { 292 mInterimResultCache.put(metricsConfigName, new InterimResult(result, /* dirty = */ true)); 293 } 294 295 /** 296 * Stores metrics report in memory for the given 297 * {@link android.car.telemetry.TelemetryProto.MetricsConfig}. 298 * 299 * If the report is produced via {@code on_metrics_report()} Lua callback, the config is not 300 * considered finished. If the report is produced via {@code on_script_finished()} Lua 301 * callback, the config is finished. 302 */ putMetricsReport( @onNull String metricsConfigName, @NonNull PersistableBundle report, boolean finished)303 public void putMetricsReport( 304 @NonNull String metricsConfigName, 305 @NonNull PersistableBundle report, 306 boolean finished) { 307 // annotate the report with boot count, ID and timestamp 308 annotateReport(metricsConfigName, report); 309 // Every new report should be appended at the end of the report list. The previous reports 310 // may exist in the cache or in the disk. We need to check both places. 311 MetricsReportList.Builder reportList = mMetricsReportCache.get(metricsConfigName); 312 // if no previous reports found in memory, check if there is previous report in disk 313 if (reportList == null) { 314 reportList = readMetricsReportList(metricsConfigName); 315 } 316 // if no previous report found in memory and in disk, create a new MetricsReportList 317 if (reportList == null) { 318 reportList = MetricsReportList.newBuilder(); 319 } 320 // add new metrics report 321 reportList = reportList.addReport( 322 MetricsReportContainer.newBuilder() 323 .setReportBytes(MetricsReportProtoUtils.getByteString(report)) 324 .setIsLastReport(finished)); 325 mMetricsReportCache.put(metricsConfigName, reportList); 326 } 327 328 /** Stores the error object produced by the script. */ putErrorResult( @onNull String metricsConfigName, @NonNull TelemetryProto.TelemetryError error)329 public void putErrorResult( 330 @NonNull String metricsConfigName, @NonNull TelemetryProto.TelemetryError error) { 331 removeInterimResult(metricsConfigName); 332 mErrorCache.put(metricsConfigName, error); 333 } 334 335 /** 336 * Stores PersistableBundle associated with the given publisher in disk-backed cache. 337 * 338 * @param publisherName Class name of the publisher. 339 * @param data PersistableBundle object that encapsulated all data to be stored for 340 * this publisher. 341 */ putPublisherData( @onNull String publisherName, @NonNull PersistableBundle data)342 public void putPublisherData( 343 @NonNull String publisherName, @NonNull PersistableBundle data) { 344 mPublisherCache.put(publisherName, data); 345 } 346 347 /** 348 * Deletes interim result associated with the given MetricsConfig name. 349 */ removeInterimResult(@onNull String metricsConfigName)350 public void removeInterimResult(@NonNull String metricsConfigName) { 351 mInterimResultCache.remove(metricsConfigName); 352 IoUtils.deleteSilently(mInterimResultDirectory, metricsConfigName); 353 } 354 355 /** 356 * Deletes metrics reports associated with the given MetricsConfig name. 357 */ removeMetricsReports(@onNull String metricsConfigName)358 public void removeMetricsReports(@NonNull String metricsConfigName) { 359 mMetricsReportCache.remove(metricsConfigName); 360 IoUtils.deleteSilently(mMetricsReportDirectory, metricsConfigName); 361 } 362 363 /** 364 * Deletes error result associated with the given MetricsConfig name. 365 */ removeErrorResult(@onNull String metricsConfigName)366 public void removeErrorResult(@NonNull String metricsConfigName) { 367 mErrorCache.remove(metricsConfigName); 368 IoUtils.deleteSilently(mErrorResultDirectory, metricsConfigName); 369 } 370 371 /** 372 * Deletes associated publisher data. 373 */ removePublisherData(@onNull String publisherName)374 public void removePublisherData(@NonNull String publisherName) { 375 mPublisherCache.remove(publisherName); 376 IoUtils.deleteSilently(mPublisherDataDirectory, publisherName); 377 } 378 379 /** 380 * Deletes all data associated with the given config name. If result does not exist, this 381 * method does not do anything. 382 */ removeResult(@onNull String metricsConfigName)383 public void removeResult(@NonNull String metricsConfigName) { 384 removeInterimResult(metricsConfigName); 385 removeMetricsReports(metricsConfigName); 386 removeErrorResult(metricsConfigName); 387 mReportCountMap.remove(metricsConfigName); 388 } 389 390 /** Deletes all interim and final results. */ removeAllResults()391 public void removeAllResults() { 392 mInterimResultCache.clear(); 393 mMetricsReportCache.clear(); 394 mErrorCache.clear(); 395 mPublisherCache.clear(); 396 IoUtils.deleteAllSilently(mInterimResultDirectory); 397 IoUtils.deleteAllSilently(mMetricsReportDirectory); 398 IoUtils.deleteAllSilently(mErrorResultDirectory); 399 IoUtils.deleteAllSilently(mPublisherDataDirectory); 400 } 401 402 /** 403 * Returns the names of MetricsConfigs whose script reached a terminal state. 404 */ 405 @NonNull getFinishedMetricsConfigNames()406 public Set<String> getFinishedMetricsConfigNames() { 407 HashSet<String> configNames = new HashSet<>(); 408 configNames.addAll(mMetricsReportCache.keySet()); 409 configNames.addAll(mErrorCache.keySet()); 410 // prevent NPE 411 String[] fileNames = mMetricsReportDirectory.list(); 412 if (fileNames != null) { 413 configNames.addAll(Arrays.asList(fileNames)); 414 } 415 fileNames = mErrorResultDirectory.list(); 416 if (fileNames != null) { 417 configNames.addAll(Arrays.asList(fileNames)); 418 } 419 return configNames; 420 } 421 422 /** Persists data to disk and deletes stale data. */ flushToDisk()423 public void flushToDisk() { 424 writeInterimResultsToFile(); 425 writeMetricsReportsToFile(); 426 writeErrorsToFile(); 427 writePublisherCacheToFile(); 428 IoUtils.deleteOldFiles(STALE_THRESHOLD_MILLIS, 429 mInterimResultDirectory, mMetricsReportDirectory, mErrorResultDirectory, 430 mPublisherDataDirectory); 431 } 432 433 /** Writes dirty interim results to disk. */ writeInterimResultsToFile()434 private void writeInterimResultsToFile() { 435 mInterimResultCache.forEach((metricsConfigName, interimResult) -> { 436 // only write dirty data 437 if (!interimResult.isDirty()) { 438 return; 439 } 440 try { 441 IoUtils.writeBundle( 442 mInterimResultDirectory, metricsConfigName, interimResult.getBundle()); 443 } catch (IOException e) { 444 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e); 445 // TODO(b/197153560): record failure 446 } 447 }); 448 } 449 writeMetricsReportsToFile()450 private void writeMetricsReportsToFile() { 451 mMetricsReportCache.forEach((metricsConfigName, reportList) -> { 452 try { 453 IoUtils.writeProto(mMetricsReportDirectory, metricsConfigName, reportList.build()); 454 } catch (IOException e) { 455 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e); 456 // TODO(b/197153560): record failure 457 } 458 }); 459 } 460 writeErrorsToFile()461 private void writeErrorsToFile() { 462 mErrorCache.forEach((metricsConfigName, telemetryError) -> { 463 try { 464 IoUtils.writeProto(mErrorResultDirectory, metricsConfigName, telemetryError); 465 } catch (IOException e) { 466 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e); 467 // TODO(b/197153560): record failure 468 } 469 }); 470 } 471 writePublisherCacheToFile()472 private void writePublisherCacheToFile() { 473 mPublisherCache.forEach((publisherName, bundle) -> { 474 try { 475 IoUtils.writeBundle(mPublisherDataDirectory, publisherName, bundle); 476 } catch (IOException e) { 477 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write publisher storage to file", e); 478 // TODO(b/197153560): record failure 479 } 480 }); 481 } 482 483 /** 484 * Gets the {@link MetricsReportList} for the given metricsConfigName from disk. 485 * If no report exists, return null. 486 */ 487 @Nullable readMetricsReportList(@onNull String metricsConfigName)488 private MetricsReportList.Builder readMetricsReportList(@NonNull String metricsConfigName) { 489 // check persistent storage 490 File file = new File(mMetricsReportDirectory, metricsConfigName); 491 // if no error exists for this metrics config, return immediately 492 if (!file.exists()) { 493 return null; 494 } 495 try { 496 // return the mutable builder because ResultStore will be modifying the list frequently 497 return MetricsReportList.parseFrom(new AtomicFile(file).readFully()).toBuilder(); 498 } catch (IOException e) { 499 Slogf.w(CarLog.TAG_TELEMETRY, "Failed to get report list from disk.", e); 500 // TODO(b/197153560): record failure 501 } 502 return null; 503 } 504 505 /** 506 * Annotates the report with boot count, id, and timestamp. 507 * 508 * ResultStore will keep track of how many reports are produced by each config since boot. 509 */ annotateReport( @onNull String metricsConfigName, @NonNull PersistableBundle report)510 private void annotateReport( 511 @NonNull String metricsConfigName, @NonNull PersistableBundle report) { 512 report.putLong(BUNDLE_KEY_TIMESTAMP, System.currentTimeMillis()); 513 report.putInt( 514 BUNDLE_KEY_BOOT_COUNT, 515 Settings.Global.getInt( 516 mContext.getContentResolver(), Settings.Global.BOOT_COUNT, -1)); 517 int id = mReportCountMap.getOrDefault(metricsConfigName, 0); 518 id++; 519 report.putInt(BUNDLE_KEY_ID, id); 520 mReportCountMap.put(metricsConfigName, id); 521 } 522 523 /** Wrapper around a result and whether the result should be written to disk. */ 524 private static final class InterimResult { 525 private final PersistableBundle mBundle; 526 private final boolean mDirty; 527 InterimResult(@onNull PersistableBundle bundle)528 private InterimResult(@NonNull PersistableBundle bundle) { 529 mBundle = bundle; 530 mDirty = false; 531 } 532 InterimResult(@onNull PersistableBundle bundle, boolean dirty)533 private InterimResult(@NonNull PersistableBundle bundle, boolean dirty) { 534 mBundle = bundle; 535 mDirty = dirty; 536 } 537 538 @NonNull getBundle()539 private PersistableBundle getBundle() { 540 return mBundle; 541 } 542 isDirty()543 private boolean isDirty() { 544 return mDirty; 545 } 546 } 547 } 548