• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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