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.car.telemetry.MetricsConfigKey;
20 import android.os.PersistableBundle;
21 import android.util.ArrayMap;
22 import android.util.AtomicFile;
23 
24 import com.android.car.CarLog;
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.server.utils.Slogf;
27 
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.util.Map;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * Disk storage for interim and final metrics statistics.
37  * All methods in this class should be invoked from the telemetry thread.
38  */
39 public class ResultStore {
40 
41     private static final long STALE_THRESHOLD_MILLIS =
42             TimeUnit.MILLISECONDS.convert(30, TimeUnit.DAYS);
43     @VisibleForTesting
44     static final String INTERIM_RESULT_DIR = "interim";
45     @VisibleForTesting
46     static final String ERROR_RESULT_DIR = "error";
47     @VisibleForTesting
48     static final String FINAL_RESULT_DIR = "final";
49 
50     /** Map keys are MetricsConfig names, which are also the file names in disk. */
51     private final Map<String, InterimResult> mInterimResultCache = new ArrayMap<>();
52 
53     private final File mInterimResultDirectory;
54     private final File mErrorResultDirectory;
55     private final File mFinalResultDirectory;
56 
ResultStore(File rootDirectory)57     ResultStore(File rootDirectory) {
58         mInterimResultDirectory = new File(rootDirectory, INTERIM_RESULT_DIR);
59         mErrorResultDirectory = new File(rootDirectory, ERROR_RESULT_DIR);
60         mFinalResultDirectory = new File(rootDirectory, FINAL_RESULT_DIR);
61         mInterimResultDirectory.mkdirs();
62         mErrorResultDirectory.mkdirs();
63         mFinalResultDirectory.mkdirs();
64         // load results into memory to reduce the frequency of disk access
65         loadInterimResultsIntoMemory();
66     }
67 
68     /** Reads interim results into memory for faster access. */
loadInterimResultsIntoMemory()69     private void loadInterimResultsIntoMemory() {
70         for (File file : mInterimResultDirectory.listFiles()) {
71             PersistableBundle interimResultBundle = readPersistableBundle(new AtomicFile(file));
72             if (interimResultBundle != null) {
73                 mInterimResultCache.put(file.getName(), new InterimResult(interimResultBundle));
74             }
75         }
76     }
77 
78     /**
79      * Retrieves interim metrics for the given
80      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
81      */
getInterimResult(String metricsConfigName)82     public PersistableBundle getInterimResult(String metricsConfigName) {
83         if (!mInterimResultCache.containsKey(metricsConfigName)) {
84             return null;
85         }
86         return mInterimResultCache.get(metricsConfigName).getBundle();
87     }
88 
89     /**
90      * Stores interim metrics results in memory for the given
91      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
92      */
putInterimResult(String metricsConfigName, PersistableBundle result)93     public void putInterimResult(String metricsConfigName, PersistableBundle result) {
94         mInterimResultCache.put(metricsConfigName, new InterimResult(result, /* dirty = */ true));
95     }
96 
97     /**
98      * Retrieves final metrics for the given
99      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
100      *
101      * @param metricsConfigName name of the MetricsConfig.
102      * @param deleteResult      if true, the final result will be deleted from disk.
103      * @return the final result as PersistableBundle if exists, null otherwise
104      */
getFinalResult(String metricsConfigName, boolean deleteResult)105     public PersistableBundle getFinalResult(String metricsConfigName, boolean deleteResult) {
106         AtomicFile atomicFile = new AtomicFile(new File(mFinalResultDirectory, metricsConfigName));
107         // if no final result exists for this metrics config, return immediately
108         if (!atomicFile.getBaseFile().exists()) {
109             return null;
110         }
111         PersistableBundle result = readPersistableBundle(atomicFile);
112         if (deleteResult) {
113             atomicFile.delete();
114         }
115         return result;
116     }
117 
118     /**
119      * Stores final metrics in memory for the given
120      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
121      */
putFinalResult(String metricsConfigName, PersistableBundle result)122     public void putFinalResult(String metricsConfigName, PersistableBundle result) {
123         writePersistableBundle(mFinalResultDirectory, metricsConfigName, result);
124         deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
125         mInterimResultCache.remove(metricsConfigName);
126     }
127 
128     /** Returns the error result produced by the metrics config if exists, null otherwise. */
getError( String metricsConfigName, boolean deleteResult)129     public TelemetryProto.TelemetryError getError(
130             String metricsConfigName, boolean deleteResult) {
131         AtomicFile atomicFile = new AtomicFile(new File(mErrorResultDirectory, metricsConfigName));
132         // if no error exists for this metrics config, return immediately
133         if (!atomicFile.getBaseFile().exists()) {
134             return null;
135         }
136         TelemetryProto.TelemetryError result = null;
137         try {
138             result = TelemetryProto.TelemetryError.parseFrom(atomicFile.readFully());
139         } catch (IOException e) {
140             Slogf.w(CarLog.TAG_TELEMETRY, "Failed to get error result from disk.", e);
141         }
142         if (deleteResult) {
143             atomicFile.delete();
144         }
145         return result;
146     }
147 
148     /** Stores the error object produced by the script. */
putError(String metricsConfigName, TelemetryProto.TelemetryError error)149     public void putError(String metricsConfigName, TelemetryProto.TelemetryError error) {
150         AtomicFile errorFile = new AtomicFile(new File(mErrorResultDirectory, metricsConfigName));
151         FileOutputStream fos = null;
152         try {
153             fos = errorFile.startWrite();
154             fos.write(error.toByteArray());
155             errorFile.finishWrite(fos);
156             deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
157             mInterimResultCache.remove(metricsConfigName);
158         } catch (IOException e) {
159             errorFile.failWrite(fos);
160             Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write data to file", e);
161             // TODO(b/197153560): record failure
162         }
163     }
164 
165     /** Persists data to disk. */
flushToDisk()166     public void flushToDisk() {
167         writeInterimResultsToFile();
168         deleteAllStaleData(mInterimResultDirectory, mFinalResultDirectory);
169     }
170 
171     /**
172      * Deletes script result associated with the given config name. If result does not exist, this
173      * method does not do anything.
174      */
removeResult(MetricsConfigKey key)175     public void removeResult(MetricsConfigKey key) {
176         String metricsConfigName = key.getName();
177         mInterimResultCache.remove(metricsConfigName);
178         deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
179         deleteFileInDirectory(mFinalResultDirectory, metricsConfigName);
180     }
181 
182     /** Deletes all interim and final results stored in disk. */
removeAllResults()183     public void removeAllResults() {
184         mInterimResultCache.clear();
185         for (File interimResult : mInterimResultDirectory.listFiles()) {
186             interimResult.delete();
187         }
188         for (File finalResult : mFinalResultDirectory.listFiles()) {
189             finalResult.delete();
190         }
191     }
192 
193     /** Writes dirty interim results to disk. */
writeInterimResultsToFile()194     private void writeInterimResultsToFile() {
195         mInterimResultCache.forEach((metricsConfigName, interimResult) -> {
196             // only write dirty data
197             if (!interimResult.isDirty()) {
198                 return;
199             }
200             writePersistableBundle(
201                     mInterimResultDirectory, metricsConfigName, interimResult.getBundle());
202         });
203     }
204 
205     /** Deletes data that are older than some threshold in the given directories. */
deleteAllStaleData(File... dirs)206     private void deleteAllStaleData(File... dirs) {
207         long currTimeMs = System.currentTimeMillis();
208         for (File dir : dirs) {
209             for (File file : dir.listFiles()) {
210                 // delete stale data
211                 if (file.lastModified() + STALE_THRESHOLD_MILLIS < currTimeMs) {
212                     file.delete();
213                 }
214             }
215         }
216     }
217 
218     /**
219      * Converts a {@link PersistableBundle} into byte array and saves the results to a file.
220      */
writePersistableBundle( File dir, String metricsConfigName, PersistableBundle result)221     private void writePersistableBundle(
222             File dir, String metricsConfigName, PersistableBundle result) {
223         AtomicFile bundleFile = new AtomicFile(new File(dir, metricsConfigName));
224         FileOutputStream fos = null;
225         try {
226             fos = bundleFile.startWrite();
227             result.writeToStream(fos);
228             bundleFile.finishWrite(fos);
229         } catch (IOException e) {
230             bundleFile.failWrite(fos);
231             Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e);
232             // TODO(b/197153560): record failure
233         }
234     }
235 
236     /**
237      * Reads a {@link PersistableBundle} from the file system.
238      */
readPersistableBundle(AtomicFile atomicFile)239     private PersistableBundle readPersistableBundle(AtomicFile atomicFile) {
240         try (FileInputStream fis = atomicFile.openRead()) {
241             return PersistableBundle.readFromStream(fis);
242         } catch (IOException e) {
243             Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read from disk.", e);
244             // TODO(b/197153560): record failure
245         }
246         return null;
247     }
248 
249     /** Deletes a the given file in the given directory if it exists. */
deleteFileInDirectory(File interimResultDirectory, String metricsConfigName)250     private void deleteFileInDirectory(File interimResultDirectory, String metricsConfigName) {
251         File file = new File(interimResultDirectory, metricsConfigName);
252         file.delete();
253     }
254 
255     /** Wrapper around a result and whether the result should be written to disk. */
256     static final class InterimResult {
257         private final PersistableBundle mBundle;
258         private final boolean mDirty;
259 
InterimResult(PersistableBundle bundle)260         InterimResult(PersistableBundle bundle) {
261             mBundle = bundle;
262             mDirty = false;
263         }
264 
InterimResult(PersistableBundle bundle, boolean dirty)265         InterimResult(PersistableBundle bundle, boolean dirty) {
266             mBundle = bundle;
267             mDirty = dirty;
268         }
269 
getBundle()270         PersistableBundle getBundle() {
271             return mBundle;
272         }
273 
isDirty()274         boolean isDirty() {
275             return mDirty;
276         }
277     }
278 }
279