summaryrefslogtreecommitdiff
path: root/core/java/com/android/internal/os/KernelCpuProcStringReader.java
blob: b04fd47e8d9fd4edee506ce57afd5028a5e65acf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import android.os.StrictMode;
import android.util.Slog;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Reads human-readable cpu time proc files.
 *
 * It is implemented as singletons for built-in kernel proc files. Get___Instance() method will
 * return corresponding reader instance. In order to prevent frequent GC, it reuses the same char[]
 * to store data read from proc files.
 *
 * A KernelCpuProcStringReader instance keeps an error counter. When the number of read errors
 * within that instance accumulates to 5, this instance will reject all further read requests.
 *
 * Data fetched within last 500ms is considered fresh, since the reading lifecycle can take up to
 * 100ms. KernelCpuProcStringReader always tries to use cache if it is fresh and valid, but it can
 * be disabled through a parameter.
 *
 * A KernelCpuProcReader instance is thread-safe. It acquires a write lock when reading the proc
 * file, releases it right after, then acquires a read lock before returning a ProcFileIterator.
 * Caller is responsible for closing ProcFileIterator (also auto-closable) after reading, otherwise
 * deadlock will occur.
 */
public class KernelCpuProcStringReader {
    private static final String TAG = KernelCpuProcStringReader.class.getSimpleName();
    private static final int ERROR_THRESHOLD = 5;
    // Data read within the last 500ms is considered fresh.
    private static final long FRESHNESS = 500L;
    private static final int MAX_BUFFER_SIZE = 1024 * 1024;

    private static final String PROC_UID_FREQ_TIME = "/proc/uid_time_in_state";
    private static final String PROC_UID_ACTIVE_TIME = "/proc/uid_concurrent_active_time";
    private static final String PROC_UID_CLUSTER_TIME = "/proc/uid_concurrent_policy_time";
    private static final String PROC_UID_USER_SYS_TIME = "/proc/uid_cputime/show_uid_stat";

    private static final KernelCpuProcStringReader FREQ_TIME_READER =
            new KernelCpuProcStringReader(PROC_UID_FREQ_TIME);
    private static final KernelCpuProcStringReader ACTIVE_TIME_READER =
            new KernelCpuProcStringReader(PROC_UID_ACTIVE_TIME);
    private static final KernelCpuProcStringReader CLUSTER_TIME_READER =
            new KernelCpuProcStringReader(PROC_UID_CLUSTER_TIME);
    private static final KernelCpuProcStringReader USER_SYS_TIME_READER =
            new KernelCpuProcStringReader(PROC_UID_USER_SYS_TIME);

    static KernelCpuProcStringReader getFreqTimeReaderInstance() {
        return FREQ_TIME_READER;
    }

    static KernelCpuProcStringReader getActiveTimeReaderInstance() {
        return ACTIVE_TIME_READER;
    }

    static KernelCpuProcStringReader getClusterTimeReaderInstance() {
        return CLUSTER_TIME_READER;
    }

    static KernelCpuProcStringReader getUserSysTimeReaderInstance() {
        return USER_SYS_TIME_READER;
    }

    private int mErrors = 0;
    private final Path mFile;
    private final Clock mClock;
    private char[] mBuf;
    private int mSize;
    private long mLastReadTime = 0;
    private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock mReadLock = mLock.readLock();
    private final ReentrantReadWriteLock.WriteLock mWriteLock = mLock.writeLock();

    public KernelCpuProcStringReader(String file) {
        this(file, Clock.SYSTEM_CLOCK);
    }

    public KernelCpuProcStringReader(String file, Clock clock) {
        mFile = Paths.get(file);
        mClock = clock;
    }

    /**
     * @see #open(boolean) Default behavior is trying to use cache.
     */
    public ProcFileIterator open() {
        return open(false);
    }

    /**
     * Opens the proc file and buffers all its content, which can be traversed through a
     * ProcFileIterator.
     *
     * This method will tolerate at most 5 errors. After that, it will always return null. This is
     * to save resources and to prevent log spam.
     *
     * This method is thread-safe. It first checks if there are other threads holding read/write
     * lock. If there are, it assumes data is fresh and reuses the data.
     *
     * A read lock is automatically acquired when a valid ProcFileIterator is returned. Caller MUST
     * call {@link ProcFileIterator#close()} when it is done to release the lock.
     *
     * @param ignoreCache If true, ignores the cache and refreshes the data anyway.
     * @return A {@link ProcFileIterator} to iterate through the file content, or null if there is
     * error.
     */
    public ProcFileIterator open(boolean ignoreCache) {
        if (mErrors >= ERROR_THRESHOLD) {
            return null;
        }

        if (ignoreCache) {
            mWriteLock.lock();
        } else {
            mReadLock.lock();
            if (dataValid()) {
                return new ProcFileIterator(mSize);
            }
            mReadLock.unlock();
            mWriteLock.lock();
            if (dataValid()) {
                // Recheck because another thread might have written data just before we did.
                mReadLock.lock();
                mWriteLock.unlock();
                return new ProcFileIterator(mSize);
            }
        }

        // At this point, write lock is held and data is invalid.
        int total = 0;
        int curr;
        mSize = 0;
        final int oldMask = StrictMode.allowThreadDiskReadsMask();
        try (BufferedReader r = Files.newBufferedReader(mFile)) {
            if (mBuf == null) {
                mBuf = new char[1024];
            }
            while ((curr = r.read(mBuf, total, mBuf.length - total)) >= 0) {
                total += curr;
                if (total == mBuf.length) {
                    // Hit the limit. Resize buffer.
                    if (mBuf.length == MAX_BUFFER_SIZE) {
                        mErrors++;
                        Slog.e(TAG, "Proc file too large: " + mFile);
                        return null;
                    }
                    mBuf = Arrays.copyOf(mBuf, Math.min(mBuf.length << 1, MAX_BUFFER_SIZE));
                }
            }
            mSize = total;
            mLastReadTime = mClock.elapsedRealtime();
            // ReentrantReadWriteLock allows lock downgrading.
            mReadLock.lock();
            return new ProcFileIterator(total);
        } catch (FileNotFoundException | NoSuchFileException e) {
            mErrors++;
            Slog.w(TAG, "File not found. It's normal if not implemented: " + mFile);
        } catch (IOException e) {
            mErrors++;
            Slog.e(TAG, "Error reading " + mFile, e);
        } finally {
            StrictMode.setThreadPolicyMask(oldMask);
            mWriteLock.unlock();
        }
        return null;
    }

    private boolean dataValid() {
        return mSize > 0 && (mClock.elapsedRealtime() - mLastReadTime < FRESHNESS);
    }

    /**
     * An autoCloseable iterator to iterate through a string proc file line by line. User must call
     * close() when finish using to prevent deadlock.
     */
    public class ProcFileIterator implements AutoCloseable {
        private final int mSize;
        private int mPos;

        public ProcFileIterator(int size) {
            mSize = size;
        }

        /** @return Whether there are more lines in the iterator. */
        public boolean hasNextLine() {
            return mPos < mSize;
        }

        /**
         * Fetches the next line. Note that all subsequent return values share the same char[]
         * under the hood.
         *
         * @return A {@link java.nio.CharBuffer} containing the next line without the new line
         * symbol.
         */
        public CharBuffer nextLine() {
            if (mPos >= mSize) {
                return null;
            }
            int i = mPos;
            // Move i to the next new line symbol, which is always '\n' in Android.
            while (i < mSize && mBuf[i] != '\n') {
                i++;
            }
            int start = mPos;
            mPos = i + 1;
            return CharBuffer.wrap(mBuf, start, i - start);
        }

        /** Total size of the proc file in chars. */
        public int size() {
            return mSize;
        }

        /** Must call close at the end to release the read lock! Or use try-with-resources. */
        public void close() {
            mReadLock.unlock();
        }


    }

    /**
     * Converts all numbers in the CharBuffer into longs, and puts into the given long[].
     *
     * Space and colon are treated as delimiters. All other chars are not allowed. All numbers
     * are non-negative. To avoid GC, caller should try to use the same array for all calls.
     *
     * This method also resets the given buffer to the original position before return so that
     * it can be read again.
     *
     * @param buf   The char buffer to be converted.
     * @param array An array to store the parsed numbers.
     * @return The number of elements written to the given array. -1 if buf is null, -2 if buf
     * contains invalid char, -3 if any number overflows.
     */
    public static int asLongs(CharBuffer buf, long[] array) {
        if (buf == null) {
            return -1;
        }
        final int initialPos = buf.position();
        int count = 0;
        long num = -1;
        char c;

        while (buf.remaining() > 0 && count < array.length) {
            c = buf.get();
            if (!(isNumber(c) || c == ' ' || c == ':')) {
                buf.position(initialPos);
                return -2;
            }
            if (num < 0) {
                if (isNumber(c)) {
                    num = c - '0';
                }
            } else {
                if (isNumber(c)) {
                    num = num * 10 + c - '0';
                    if (num < 0) {
                        buf.position(initialPos);
                        return -3;
                    }
                } else {
                    array[count++] = num;
                    num = -1;
                }
            }
        }
        if (num >= 0) {
            array[count++] = num;
        }
        buf.position(initialPos);
        return count;
    }

    private static boolean isNumber(char c) {
        return c >= '0' && c <= '9';
    }
}