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