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