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