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