diff options
3 files changed, 593 insertions, 0 deletions
diff --git a/core/java/com/android/internal/os/KernelCpuProcStringReader.java b/core/java/com/android/internal/os/KernelCpuProcStringReader.java new file mode 100644 index 000000000000..22435ae7647a --- /dev/null +++ b/core/java/com/android/internal/os/KernelCpuProcStringReader.java @@ -0,0 +1,269 @@ +/* + * 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.os.SystemClock; +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.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 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); + + public static KernelCpuProcStringReader getFreqTimeReaderInstance() { + return FREQ_TIME_READER; + } + + public static KernelCpuProcStringReader getActiveTimeReaderInstance() { + return ACTIVE_TIME_READER; + } + + public static KernelCpuProcStringReader getClusterTimeReaderInstance() { + return CLUSTER_TIME_READER; + } + + private int mErrors = 0; + private final Path mFile; + 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) { + mFile = Paths.get(file); + } + + /** + * @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 = SystemClock.elapsedRealtime(); + // ReentrantReadWriteLock allows lock downgrading. + mReadLock.lock(); + return new ProcFileIterator(total); + } catch (FileNotFoundException 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 && (SystemClock.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; + } + + /** + * 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); + } + + /** + * Fetches the next line, converts all numbers into long, and puts into the given long[]. + * To avoid GC, caller should try to use the same array for all calls. All non-numeric + * chars are treated as delimiters. All numbers are non-negative. + * + * @param array An array to store the parsed numbers. + * @return The number of elements written to the given array. -1 if there is no more line. + */ + public int nextLineAsArray(long[] array) { + CharBuffer buf = nextLine(); + if (buf == null) { + return -1; + } + int count = 0; + long num = -1; + char c; + + while (buf.remaining() > 0 && count < array.length) { + c = buf.get(); + if (num < 0) { + if (isNumber(c)) { + num = c - '0'; + } + } else { + if (isNumber(c)) { + num = num * 10 + c - '0'; + } else { + array[count++] = num; + num = -1; + } + } + } + if (num >= 0) { + array[count++] = num; + } + return count; + } + + /** 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(); + } + + private boolean isNumber(char c) { + return c >= '0' && c <= '9'; + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java index b798042f4761..3cfc6443b15e 100644 --- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java +++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java @@ -38,6 +38,7 @@ import org.junit.runners.Suite; BatteryStatsUidTest.class, BatteryStatsUserLifecycleTests.class, KernelCpuProcReaderTest.class, + KernelCpuProcStringReaderTest.class, KernelMemoryBandwidthStatsTest.class, KernelSingleUidTimeReaderTest.class, KernelUidCpuFreqTimeReaderTest.class, diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuProcStringReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuProcStringReaderTest.java new file mode 100644 index 000000000000..dae9eb57e237 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuProcStringReaderTest.java @@ -0,0 +1,323 @@ +/* + * 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.FileUtils; +import android.os.SystemClock; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +/** + * Test class for {@link KernelCpuProcStringReader}. + * + * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuProcStringReaderTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuProcStringReaderTest { + private File mRoot; + private File mTestDir; + private File mTestFile; + private Random mRand = new Random(12345); + private KernelCpuProcStringReader mReader; + + private Context getContext() { + return InstrumentationRegistry.getContext(); + } + + @Before + public void setUp() { + mTestDir = getContext().getDir("test", Context.MODE_PRIVATE); + mRoot = getContext().getFilesDir(); + mTestFile = new File(mTestDir, "test.file"); + mReader = new KernelCpuProcStringReader(mTestFile.getAbsolutePath()); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteContents(mTestDir); + FileUtils.deleteContents(mRoot); + } + + + /** + * Tests that reading will return null if the file does not exist. + */ + @Test + public void testReadInvalidFile() throws Exception { + assertEquals(null, mReader.open()); + } + + /** + * Tests that reading will always return null after 5 failures. + */ + @Test + public void testReadErrorsLimit() throws Exception { + for (int i = 0; i < 3; i++) { + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + assertNull(iter); + } + SystemClock.sleep(50); + } + final String data = "018n9x134yrm9sry01298yMF1X980Ym908u98weruwe983^(*)0N)&tu09281my\n"; + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + assertEquals(data.length(), iter.size()); + assertEquals(data, iter.nextLine().toString() + '\n'); + } + assertTrue(mTestFile.delete()); + for (int i = 0; i < 3; i++) { + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open(true)) { + assertNull(iter); + } + SystemClock.sleep(50); + } + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open(true)) { + assertNull(iter); + } + } + + /** Tests nextLine functionality. */ + @Test + public void testReadLine() throws Exception { + final String data = "10103: 0 0 0 1 5 3 1 2 0 0 3 0 0 0 0 2 2 330 0 0 0 0 1 0 0 0 0 0 0 0" + + " 0 0 0 0 0 0 0 0 0 0 0 13\n" + + "50083: 0 0 0 29 0 13 0 4 5 0 0 0 0 0 1 0 0 15 0 0 0 0 0 0 1 0 0 0 0 1 0 1 7 0 " + + "0 1 1 1 0 2 0 221\n" + + "50227: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 196 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" + + " 0 2 0 0 0 2 721\n" + + "10158: 0 0 0 0 19 3 9 1 0 7 4 3 3 3 1 3 10 893 2 0 3 0 0 0 0 0 0 0 0 1 0 2 0 0" + + " 1 2 10 0 0 0 1 58\n" + + "50138: 0 0 0 8 7 0 0 0 0 0 0 0 0 0 0 0 0 322 0 0 0 3 0 5 0 0 3 0 0 0 0 1 0 0 0" + + " 0 0 2 0 0 7 707\n"; + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + assertEquals( + "10103: 0 0 0 1 5 3 1 2 0 0 3 0 0 0 0 2 2 330 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0" + + " 0 0 0 0 0 0 0 13", + iter.nextLine().toString()); + assertEquals( + "50083: 0 0 0 29 0 13 0 4 5 0 0 0 0 0 1 0 0 15 0 0 0 0 0 0 1 0 0 0 0 1 0 1 7 " + + "0 0 1 1 1 0 2 0 221", + iter.nextLine().toString()); + long[] actual = new long[43]; + iter.nextLineAsArray(actual); + assertArrayEquals( + new long[]{50227, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 196, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 721}, + actual); + assertEquals( + "10158: 0 0 0 0 19 3 9 1 0 7 4 3 3 3 1 3 10 893 2 0 3 0 0 0 0 0 0 0 0 1 0 2 0" + + " 0 1 2 10 0 0 0 1 58", + iter.nextLine().toString()); + assertEquals( + "50138: 0 0 0 8 7 0 0 0 0 0 0 0 0 0 0 0 0 322 0 0 0 3 0 5 0 0 3 0 0 0 0 1 0 0" + + " 0 0 0 2 0 0 7 707", + iter.nextLine().toString()); + } + } + + /** Stress tests read functionality. */ + @Test + public void testMultipleRead() throws Exception { + for (int i = 0; i < 100; i++) { + final String data = getTestString(600, 150); + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + String[] lines = data.split("\n"); + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open(true)) { + for (String line : lines) { + assertEquals(line, iter.nextLine().toString()); + } + } + assertTrue(mTestFile.delete()); + } + } + + /** Tests nextLineToArray functionality. */ + @Test + public void testReadLineToArray() throws Exception { + final long[][] data = getTestArray(800, 50); + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(arrayToString(data)); + } + long[] actual = new long[50]; + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + for (long[] expected : data) { + assertEquals(50, iter.nextLineAsArray(actual)); + assertArrayEquals(expected, actual); + } + } + } + + /** + * Tests that reading a file over the limit (1MB) will return null. + */ + @Test + public void testReadOverLimit() throws Exception { + final String data = getTestString(1, 1024 * 1024 + 1); + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + assertNull(iter); + } + } + + /** + * Tests concurrent reading with 5 threads. + */ + @Test + public void testConcurrent() throws Exception { + final String data = getTestString(200, 150); + final String data1 = getTestString(180, 120); + final String[] lines = data.split("\n"); + final String[] lines1 = data1.split("\n"); + final List<Throwable> errs = Collections.synchronizedList(new ArrayList<>()); + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data); + } + // An additional thread for modifying the file content. + ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(11); + final CountDownLatch ready = new CountDownLatch(10); + final CountDownLatch start = new CountDownLatch(1); + final CountDownLatch modify = new CountDownLatch(1); + final CountDownLatch done = new CountDownLatch(10); + + // Schedules 5 threads to be executed together now, and 5 to be executed after file is + // modified. + for (int i = 0; i < 5; i++) { + threadPool.submit(() -> { + ready.countDown(); + try { + start.await(); + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + for (String line : lines) { + assertEquals(line, iter.nextLine().toString()); + } + } + } catch (Throwable e) { + errs.add(e); + } finally { + done.countDown(); + } + }); + threadPool.submit(() -> { + ready.countDown(); + try { + start.await(); + // Wait for file modification. + modify.await(); + try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { + for (String line : lines1) { + assertEquals(line, iter.nextLine().toString()); + } + } + } catch (Throwable e) { + errs.add(e); + } finally { + done.countDown(); + } + }); + } + + assertTrue("Prep timed out", ready.await(100, TimeUnit.MILLISECONDS)); + start.countDown(); + + threadPool.schedule(() -> { + assertTrue(mTestFile.delete()); + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(data1); + modify.countDown(); + } catch (Throwable e) { + errs.add(e); + } + }, 600, TimeUnit.MILLISECONDS); + + assertTrue("Execution timed out", done.await(3, TimeUnit.SECONDS)); + threadPool.shutdownNow(); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + errs.forEach(e -> e.printStackTrace(pw)); + + assertTrue("All Exceptions:\n" + sw.toString(), errs.isEmpty()); + } + + private String getTestString(int lines, int charsPerLine) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < lines; i++) { + for (int j = 0; j < charsPerLine; j++) { + sb.append((char) (mRand.nextInt(93) + 32)); + } + sb.append('\n'); + } + return sb.toString(); + } + + private long[][] getTestArray(int lines, int numPerLine) { + return IntStream.range(0, lines).mapToObj( + (i) -> mRand.longs(numPerLine, 0, Long.MAX_VALUE).toArray()).toArray(long[][]::new); + } + + private String arrayToString(long[][] array) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < array.length; i++) { + sb.append(array[i][0]).append(':'); + for (int j = 1; j < array[0].length; j++) { + sb.append(' ').append(array[i][j]); + } + sb.append('\n'); + } + return sb.toString(); + } +} |