1 /* 2 * Copyright (C) 2020 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.server.powerstats; 18 19 import android.content.Context; 20 import android.util.IndentingPrintWriter; 21 import android.util.Slog; 22 23 import com.android.internal.util.FileRotator; 24 25 import java.io.ByteArrayOutputStream; 26 import java.io.File; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.nio.ByteBuffer; 31 import java.util.Date; 32 import java.util.concurrent.locks.ReentrantLock; 33 34 /** 35 * PowerStatsDataStorage implements the on-device storage cache for energy 36 * data. This data must be persisted across boot cycles so we store it 37 * on-device. Versioning of this data is handled by deleting any data that 38 * does not match the current version. The cache is implemented as a circular 39 * buffer using the FileRotator class in android.util. We maintain 48 hours 40 * worth of logs in 12 files (4 hours each). 41 */ 42 public class PowerStatsDataStorage { 43 private static final String TAG = PowerStatsDataStorage.class.getSimpleName(); 44 45 private static final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60; 46 // Rotate files every 4 hours. 47 private static final long ROTATE_AGE_MILLIS = 4 * MILLISECONDS_PER_HOUR; 48 // Store 48 hours worth of data. 49 private static final long DELETE_AGE_MILLIS = 48 * MILLISECONDS_PER_HOUR; 50 51 private final ReentrantLock mLock = new ReentrantLock(); 52 private final File mDataStorageDir; 53 private final String mDataStorageFilename; 54 private final FileRotator mFileRotator; 55 56 private static class DataElement { 57 private static final int LENGTH_FIELD_WIDTH = 4; 58 private static final int MAX_DATA_ELEMENT_SIZE = 32768; 59 60 private byte[] mData; 61 toByteArray()62 private byte[] toByteArray() throws IOException { 63 ByteArrayOutputStream data = new ByteArrayOutputStream(); 64 data.write(ByteBuffer.allocate(LENGTH_FIELD_WIDTH).putInt(mData.length).array()); 65 data.write(mData); 66 return data.toByteArray(); 67 } 68 getData()69 protected byte[] getData() { 70 return mData; 71 } 72 DataElement(byte[] data)73 private DataElement(byte[] data) { 74 mData = data; 75 } 76 DataElement(InputStream in)77 private DataElement(InputStream in) throws IOException { 78 byte[] lengthBytes = new byte[LENGTH_FIELD_WIDTH]; 79 int bytesRead = in.read(lengthBytes); 80 mData = new byte[0]; 81 82 if (bytesRead == LENGTH_FIELD_WIDTH) { 83 int length = ByteBuffer.wrap(lengthBytes).getInt(); 84 85 if (0 < length && length < MAX_DATA_ELEMENT_SIZE) { 86 mData = new byte[length]; 87 bytesRead = in.read(mData); 88 89 if (bytesRead != length) { 90 throw new IOException("Invalid bytes read, expected: " + length 91 + ", actual: " + bytesRead); 92 } 93 } else { 94 throw new IOException("DataElement size is invalid: " + length); 95 } 96 } else { 97 throw new IOException("Did not read " + LENGTH_FIELD_WIDTH + " bytes (" + bytesRead 98 + ")"); 99 } 100 } 101 } 102 103 /** 104 * Used by external classes to read DataElements from on-device storage. 105 * This callback is passed in to the read() function and is called for 106 * each DataElement read from on-device storage. 107 */ 108 public interface DataElementReadCallback { 109 /** 110 * When performing a read of the on-device storage this callback 111 * must be passed in to the read function. The function will be 112 * called for each DataElement read from on-device storage. 113 * 114 * @param data Byte array containing a DataElement payload. 115 */ onReadDataElement(byte[] data)116 void onReadDataElement(byte[] data); 117 } 118 119 private static class DataReader implements FileRotator.Reader { 120 private DataElementReadCallback mCallback; 121 DataReader(DataElementReadCallback callback)122 DataReader(DataElementReadCallback callback) { 123 mCallback = callback; 124 } 125 126 @Override read(InputStream in)127 public void read(InputStream in) throws IOException { 128 while (in.available() > 0) { 129 DataElement dataElement = new DataElement(in); 130 mCallback.onReadDataElement(dataElement.getData()); 131 } 132 } 133 } 134 135 private static class DataRewriter implements FileRotator.Rewriter { 136 byte[] mActiveFileData; 137 byte[] mNewData; 138 DataRewriter(byte[] data)139 DataRewriter(byte[] data) { 140 mActiveFileData = new byte[0]; 141 mNewData = data; 142 } 143 144 @Override reset()145 public void reset() { 146 // ignored 147 } 148 149 @Override read(InputStream in)150 public void read(InputStream in) throws IOException { 151 mActiveFileData = new byte[in.available()]; 152 in.read(mActiveFileData); 153 } 154 155 @Override shouldWrite()156 public boolean shouldWrite() { 157 return true; 158 } 159 160 @Override write(OutputStream out)161 public void write(OutputStream out) throws IOException { 162 out.write(mActiveFileData); 163 out.write(mNewData); 164 } 165 } 166 PowerStatsDataStorage(Context context, File dataStoragePath, String dataStorageFilename)167 public PowerStatsDataStorage(Context context, File dataStoragePath, 168 String dataStorageFilename) { 169 mDataStorageDir = dataStoragePath; 170 mDataStorageFilename = dataStorageFilename; 171 172 if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) { 173 Slog.wtf(TAG, "mDataStorageDir does not exist: " + mDataStorageDir.getPath()); 174 mFileRotator = null; 175 } else { 176 // Delete files written with an old version number. The version is included in the 177 // filename, so any files that don't match the current version number can be deleted. 178 File[] files = mDataStorageDir.listFiles(); 179 for (int i = 0; i < files.length; i++) { 180 // Meter, model, and residency files are stored in the same directory. 181 // 182 // The format of filenames on disk is: 183 // log.powerstats.meter.version.timestamp 184 // log.powerstats.model.version.timestamp 185 // log.powerstats.residency.version.timestamp 186 // 187 // The format of dataStorageFilenames is: 188 // log.powerstats.meter.version 189 // log.powerstats.model.version 190 // log.powerstats.residency.version 191 // 192 // A PowerStatsDataStorage object is created for meter, model, and residency data. 193 // Strip off the version and check that the current file we're checking starts with 194 // the stem (log.powerstats.meter, log.powerstats.model, log.powerstats.residency). 195 // If the stem matches and the version number is different, delete the old file. 196 int versionDot = mDataStorageFilename.lastIndexOf('.'); 197 String beforeVersionDot = mDataStorageFilename.substring(0, versionDot); 198 // Check that the stems match. 199 if (files[i].getName().startsWith(beforeVersionDot)) { 200 // Check that the version number matches. If not, delete the old file. 201 if (!files[i].getName().startsWith(mDataStorageFilename)) { 202 files[i].delete(); 203 } 204 } 205 } 206 207 mFileRotator = new FileRotator(mDataStorageDir, 208 mDataStorageFilename, 209 ROTATE_AGE_MILLIS, 210 DELETE_AGE_MILLIS); 211 } 212 } 213 214 /** 215 * Writes data stored in PowerStatsDataStorage to a file descriptor. 216 * 217 * @param data Byte array to write to on-device storage. Byte array is 218 * converted to a DataElement which prefixes the payload with 219 * the data length. The DataElement is then converted to a byte 220 * array and written to on-device storage. 221 */ write(byte[] data)222 public void write(byte[] data) { 223 if (data != null && data.length > 0) { 224 mLock.lock(); 225 try { 226 long currentTimeMillis = System.currentTimeMillis(); 227 DataElement dataElement = new DataElement(data); 228 mFileRotator.rewriteActive(new DataRewriter(dataElement.toByteArray()), 229 currentTimeMillis); 230 mFileRotator.maybeRotate(currentTimeMillis); 231 } catch (IOException e) { 232 Slog.e(TAG, "Failed to write to on-device storage: " + e); 233 } finally { 234 mLock.unlock(); 235 } 236 } 237 } 238 239 /** 240 * Reads all DataElements stored in on-device storage. For each 241 * DataElement retrieved from on-device storage, callback is called. 242 */ read(DataElementReadCallback callback)243 public void read(DataElementReadCallback callback) throws IOException { 244 mLock.lock(); 245 try { 246 mFileRotator.readMatching(new DataReader(callback), Long.MIN_VALUE, Long.MAX_VALUE); 247 } finally { 248 mLock.unlock(); 249 } 250 } 251 252 /** 253 * Deletes all stored log data. 254 */ deleteLogs()255 public void deleteLogs() { 256 mLock.lock(); 257 try { 258 File[] files = mDataStorageDir.listFiles(); 259 for (int i = 0; i < files.length; i++) { 260 int versionDot = mDataStorageFilename.lastIndexOf('.'); 261 String beforeVersionDot = mDataStorageFilename.substring(0, versionDot); 262 // Check that the stems before the version match. 263 if (files[i].getName().startsWith(beforeVersionDot)) { 264 files[i].delete(); 265 } 266 } 267 } finally { 268 mLock.unlock(); 269 } 270 } 271 272 /** 273 * Dump stats about stored data. 274 */ dump(IndentingPrintWriter ipw)275 public void dump(IndentingPrintWriter ipw) { 276 mLock.lock(); 277 try { 278 final int versionDot = mDataStorageFilename.lastIndexOf('.'); 279 final String beforeVersionDot = mDataStorageFilename.substring(0, versionDot); 280 final File[] files = mDataStorageDir.listFiles(); 281 282 int number = 0; 283 int dataSize = 0; 284 long earliestLogEpochTime = Long.MAX_VALUE; 285 for (int i = 0; i < files.length; i++) { 286 // Check that the stems before the version match. 287 final File file = files[i]; 288 final String fileName = file.getName(); 289 if (files[i].getName().startsWith(beforeVersionDot)) { 290 number++; 291 dataSize += file.length(); 292 final int firstTimeChar = fileName.lastIndexOf('.') + 1; 293 final int endChar = fileName.lastIndexOf('-'); 294 try { 295 final Long startTime = 296 Long.parseLong(fileName.substring(firstTimeChar, endChar)); 297 if (startTime != null && startTime < earliestLogEpochTime) { 298 earliestLogEpochTime = startTime; 299 } 300 } catch (NumberFormatException nfe) { 301 Slog.e(TAG, 302 "Failed to extract start time from file : " + fileName, nfe); 303 } 304 } 305 } 306 307 if (earliestLogEpochTime != Long.MAX_VALUE) { 308 ipw.println("Earliest data time : " + new Date(earliestLogEpochTime)); 309 } else { 310 ipw.println("Failed to parse earliest data time!!!"); 311 } 312 ipw.println("# files : " + number); 313 ipw.println("Total data size (B) : " + dataSize); 314 } finally { 315 mLock.unlock(); 316 } 317 } 318 } 319