Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa253435 authored by Mike Ma's avatar Mike Ma
Browse files

An efficient proc file reader

A less resource hogging reader for reading proc files in string
format. It is singleton and thread-safe, so all downstream clients
share the same instance to avoid duplicate allocation. It reads
the entire proc file all at once (to mimimize the impact to the
kernel) into a reusable char[] buffer (to prevent frequent GC). A
read lock is automatically held for the client when a valid file
iterator is returned. Client MUST call close() to unlock it or take
advantage of try-with-resources.

The reader keeps an error counter. It rejects further requests if it
has accumulated 5 errors, to prevent log spam. The reader also has
a 500ms cache, which can be bypassed with a parameter.

Bug: 111216804
Test: atest FrameworksCoreTests:com.android.internal.os.KernelCpuProcStringReaderTest
Change-Id: Ifa5213a5c7baf95d62f74486815030d9aa54ca28
parent 151046ad
Loading
Loading
Loading
Loading
+269 −0
Original line number Diff line number Diff line
/*
 * 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';
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import org.junit.runners.Suite;
        BatteryStatsUidTest.class,
        BatteryStatsUserLifecycleTests.class,
        KernelCpuProcReaderTest.class,
        KernelCpuProcStringReaderTest.class,
        KernelMemoryBandwidthStatsTest.class,
        KernelSingleUidTimeReaderTest.class,
        KernelUidCpuFreqTimeReaderTest.class,
+323 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}