diff options
| author | 2018-11-27 19:28:24 +0000 | |
|---|---|---|
| committer | 2018-11-27 19:28:24 +0000 | |
| commit | fa2b85579e4b75c31e411207b6dca026421d1489 (patch) | |
| tree | e54be7c8f0abde5fea9b7b15236bc9cbd21cdce5 | |
| parent | 7f2ad6633244b497214b7a5c941dfc1227b3cb54 (diff) | |
| parent | e45b9b16998aac201fa6462f10a348b0d56ab102 (diff) | |
Merge "New Kernel Per-UID CPU Time Readers"
8 files changed, 2049 insertions, 48 deletions
diff --git a/core/java/com/android/internal/os/KernelCpuProcStringReader.java b/core/java/com/android/internal/os/KernelCpuProcStringReader.java index 22435ae7647a..b3aec0c2a561 100644 --- a/core/java/com/android/internal/os/KernelCpuProcStringReader.java +++ b/core/java/com/android/internal/os/KernelCpuProcStringReader.java @@ -25,6 +25,7 @@ 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; @@ -59,6 +60,7 @@ public class KernelCpuProcStringReader { 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); @@ -66,19 +68,25 @@ public class KernelCpuProcStringReader { 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); - public static KernelCpuProcStringReader getFreqTimeReaderInstance() { + static KernelCpuProcStringReader getFreqTimeReaderInstance() { return FREQ_TIME_READER; } - public static KernelCpuProcStringReader getActiveTimeReaderInstance() { + static KernelCpuProcStringReader getActiveTimeReaderInstance() { return ACTIVE_TIME_READER; } - public static KernelCpuProcStringReader getClusterTimeReaderInstance() { + static KernelCpuProcStringReader getClusterTimeReaderInstance() { return CLUSTER_TIME_READER; } + static KernelCpuProcStringReader getUserSysTimeReaderInstance() { + return USER_SYS_TIME_READER; + } + private int mErrors = 0; private final Path mFile; private char[] mBuf; @@ -164,12 +172,12 @@ public class KernelCpuProcStringReader { // ReentrantReadWriteLock allows lock downgrading. mReadLock.lock(); return new ProcFileIterator(total); - } catch (FileNotFoundException e) { + } 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); + Slog.e(TAG, "Error reading " + mFile, e); } finally { StrictMode.setThreadPolicyMask(oldMask); mWriteLock.unlock(); @@ -193,6 +201,11 @@ public class KernelCpuProcStringReader { 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. @@ -214,44 +227,6 @@ public class KernelCpuProcStringReader { 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; @@ -262,8 +237,63 @@ public class KernelCpuProcStringReader { mReadLock.unlock(); } - private boolean isNumber(char c) { - return c >= '0' && c <= '9'; + + } + + /** + * 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'; } } diff --git a/core/java/com/android/internal/os/KernelCpuUidTimeReader.java b/core/java/com/android/internal/os/KernelCpuUidTimeReader.java new file mode 100644 index 000000000000..7021b5781347 --- /dev/null +++ b/core/java/com/android/internal/os/KernelCpuUidTimeReader.java @@ -0,0 +1,776 @@ +/* + * 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 com.android.internal.os.KernelCpuProcStringReader.asLongs; +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.StrictMode; +import android.os.SystemClock; +import android.util.IntArray; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.KernelCpuProcStringReader.ProcFileIterator; + +import java.io.BufferedReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.CharBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Reads per-UID CPU time proc files. Concrete implementations are all nested inside. + * + * This class uses a throttler to reject any {@link #readDelta} or {@link #readAbsolute} call + * within {@link #mMinTimeBetweenRead}. The throttler can be enable / disabled via a param in + * the constructor. + * + * This class and its subclasses are NOT thread-safe and NOT designed to be accessed by more than + * one caller since each caller has its own view of delta. + * + * @param <T> The type of CPU time for the callback. + */ +public abstract class KernelCpuUidTimeReader<T> { + protected static final boolean DEBUG = false; + private static final long DEFAULT_MIN_TIME_BETWEEN_READ = 1000L; // In milliseconds + + final String mTag = this.getClass().getSimpleName(); + final SparseArray<T> mLastTimes = new SparseArray<>(); + final KernelCpuProcStringReader mReader; + final boolean mThrottle; + private long mMinTimeBetweenRead = DEFAULT_MIN_TIME_BETWEEN_READ; + private long mLastReadTimeMs = 0; + + /** + * Callback interface for processing each line of the proc file. + * + * @param <T> The type of CPU time for the callback function. + */ + public interface Callback<T> { + /** + * @param uid UID of the app + * @param time Time spent. The exact data structure depends on subclass implementation. + */ + void onUidCpuTime(int uid, T time); + } + + KernelCpuUidTimeReader(KernelCpuProcStringReader reader, boolean throttle) { + mReader = reader; + mThrottle = throttle; + } + + /** + * Reads the proc file, calling into the callback with a delta of time for each UID. + * + * @param cb The callback to invoke for each line of the proc file. If null,the data is + * consumed and subsequent calls to readDelta will provide a fresh delta. + */ + public void readDelta(@Nullable Callback<T> cb) { + if (!mThrottle) { + readDeltaImpl(cb); + return; + } + final long currTimeMs = SystemClock.elapsedRealtime(); + if (currTimeMs < mLastReadTimeMs + mMinTimeBetweenRead) { + if (DEBUG) { + Slog.d(mTag, "Throttle readDelta"); + } + return; + } + readDeltaImpl(cb); + mLastReadTimeMs = currTimeMs; + } + + /** + * Reads the proc file, calling into the callback with cumulative time for each UID. + * + * @param cb The callback to invoke for each line of the proc file. It cannot be null. + */ + public void readAbsolute(Callback<T> cb) { + if (!mThrottle) { + readAbsoluteImpl(cb); + return; + } + final long currTimeMs = SystemClock.elapsedRealtime(); + if (currTimeMs < mLastReadTimeMs + mMinTimeBetweenRead) { + if (DEBUG) { + Slog.d(mTag, "Throttle readAbsolute"); + } + return; + } + readAbsoluteImpl(cb); + mLastReadTimeMs = currTimeMs; + } + + abstract void readDeltaImpl(@Nullable Callback<T> cb); + + abstract void readAbsoluteImpl(Callback<T> callback); + + /** + * Removes the UID from internal accounting data. This method, overridden in + * {@link KernelCpuUidUserSysTimeReader}, also removes the UID from the kernel module. + * + * @param uid The UID to remove. + * @see KernelCpuUidUserSysTimeReader#removeUid(int) + */ + public void removeUid(int uid) { + mLastTimes.delete(uid); + } + + /** + * Removes UIDs in a given range from internal accounting data. This method, overridden in + * {@link KernelCpuUidUserSysTimeReader}, also removes the UIDs from the kernel module. + * + * @param startUid the first uid to remove. + * @param endUid the last uid to remove. + * @see KernelCpuUidUserSysTimeReader#removeUidsInRange(int, int) + */ + public void removeUidsInRange(int startUid, int endUid) { + if (endUid < startUid) { + Slog.e(mTag, "start UID " + startUid + " > end UID " + endUid); + return; + } + mLastTimes.put(startUid, null); + mLastTimes.put(endUid, null); + final int firstIndex = mLastTimes.indexOfKey(startUid); + final int lastIndex = mLastTimes.indexOfKey(endUid); + mLastTimes.removeAtRange(firstIndex, lastIndex - firstIndex + 1); + } + + /** + * Set the minimum time in milliseconds between reads. If throttle is not enabled, this method + * has no effect. + * + * @param minTimeBetweenRead The minimum time in milliseconds. + */ + public void setThrottle(long minTimeBetweenRead) { + if (mThrottle && minTimeBetweenRead >= 0) { + mMinTimeBetweenRead = minTimeBetweenRead; + } + } + + /** + * Reads /proc/uid_cputime/show_uid_stat which has the line format: + * + * uid: user_time_micro_seconds system_time_micro_seconds power_in_milli-amp-micro_seconds + * + * This provides the time a UID's processes spent executing in user-space and kernel-space. + * The file contains a monotonically increasing count of time for a single boot. This class + * maintains the previous results of a call to {@link #readDelta} in order to provide a proper + * delta. + */ + public static class KernelCpuUidUserSysTimeReader extends KernelCpuUidTimeReader<long[]> { + private static final String REMOVE_UID_PROC_FILE = "/proc/uid_cputime/remove_uid_range"; + + // [uid, user_time, system_time, (maybe) power_in_milli-amp-micro_seconds] + private final long[] mBuffer = new long[4]; + // A reusable array to hold [user_time, system_time] for the callback. + private final long[] mUsrSysTime = new long[2]; + + public KernelCpuUidUserSysTimeReader(boolean throttle) { + super(KernelCpuProcStringReader.getUserSysTimeReaderInstance(), throttle); + } + + @VisibleForTesting + public KernelCpuUidUserSysTimeReader(KernelCpuProcStringReader reader, boolean throttle) { + super(reader, throttle); + } + + @Override + void readDeltaImpl(@Nullable Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (iter == null) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) < 3) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + final int uid = (int) mBuffer[0]; + long[] lastTimes = mLastTimes.get(uid); + if (lastTimes == null) { + lastTimes = new long[2]; + mLastTimes.put(uid, lastTimes); + } + final long currUsrTimeUs = mBuffer[1]; + final long currSysTimeUs = mBuffer[2]; + mUsrSysTime[0] = currUsrTimeUs - lastTimes[0]; + mUsrSysTime[1] = currSysTimeUs - lastTimes[1]; + + if (mUsrSysTime[0] < 0 || mUsrSysTime[1] < 0) { + Slog.e(mTag, "Negative user/sys time delta for UID=" + uid + + "\nPrev times: u=" + lastTimes[0] + " s=" + lastTimes[1] + + " Curr times: u=" + currUsrTimeUs + " s=" + currSysTimeUs); + } else if (mUsrSysTime[0] > 0 || mUsrSysTime[1] > 0) { + if (cb != null) { + cb.onUidCpuTime(uid, mUsrSysTime); + } + } + lastTimes[0] = currUsrTimeUs; + lastTimes[1] = currSysTimeUs; + } + } + } + + @Override + void readAbsoluteImpl(Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (iter == null) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) < 3) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + mUsrSysTime[0] = mBuffer[1]; // User time in microseconds + mUsrSysTime[1] = mBuffer[2]; // System time in microseconds + cb.onUidCpuTime((int) mBuffer[0], mUsrSysTime); + } + } + } + + @Override + public void removeUid(int uid) { + super.removeUid(uid); + removeUidsFromKernelModule(uid, uid); + } + + @Override + public void removeUidsInRange(int startUid, int endUid) { + super.removeUidsInRange(startUid, endUid); + removeUidsFromKernelModule(startUid, endUid); + } + + /** + * Removes UIDs in a given range from the kernel module and internal accounting data. Only + * {@link BatteryStatsImpl} and its child processes should call this, as the change on + * Kernel is + * visible system wide. + * + * @param startUid the first uid to remove + * @param endUid the last uid to remove + */ + private void removeUidsFromKernelModule(int startUid, int endUid) { + Slog.d(mTag, "Removing uids " + startUid + "-" + endUid); + final int oldMask = StrictMode.allowThreadDiskWritesMask(); + try (FileWriter writer = new FileWriter(REMOVE_UID_PROC_FILE)) { + writer.write(startUid + "-" + endUid); + writer.flush(); + } catch (IOException e) { + Slog.e(mTag, "failed to remove uids " + startUid + " - " + endUid + + " from uid_cputime module", e); + } finally { + StrictMode.setThreadPolicyMask(oldMask); + } + } + } + + /** + * Reads /proc/uid_time_in_state which has the format: + * + * uid: [freq1] [freq2] [freq3] ... + * [uid1]: [time in freq1] [time in freq2] [time in freq3] ... + * [uid2]: [time in freq1] [time in freq2] [time in freq3] ... + * ... + * + * This provides the times a UID's processes spent executing at each different cpu frequency. + * The file contains a monotonically increasing count of time for a single boot. This class + * maintains the previous results of a call to {@link #readDelta} in order to provide a proper + * delta. + */ + public static class KernelCpuUidFreqTimeReader extends KernelCpuUidTimeReader<long[]> { + private static final String UID_TIMES_PROC_FILE = "/proc/uid_time_in_state"; + // We check the existence of proc file a few times (just in case it is not ready yet when we + // start reading) and if it is not available, we simply ignore further read requests. + private static final int MAX_ERROR_COUNT = 5; + + private final Path mProcFilePath; + private long[] mBuffer; + private long[] mCurTimes; + private long[] mDeltaTimes; + private long[] mCpuFreqs; + + private int mFreqCount = 0; + private int mErrors = 0; + private boolean mPerClusterTimesAvailable; + private boolean mAllUidTimesAvailable = true; + + public KernelCpuUidFreqTimeReader(boolean throttle) { + this(UID_TIMES_PROC_FILE, KernelCpuProcStringReader.getFreqTimeReaderInstance(), + throttle); + } + + @VisibleForTesting + public KernelCpuUidFreqTimeReader(String procFile, KernelCpuProcStringReader reader, + boolean throttle) { + super(reader, throttle); + mProcFilePath = Paths.get(procFile); + } + + /** + * @return Whether per-cluster times are available. + */ + public boolean perClusterTimesAvailable() { + return mPerClusterTimesAvailable; + } + + /** + * @return Whether all-UID times are available. + */ + public boolean allUidTimesAvailable() { + return mAllUidTimesAvailable; + } + + /** + * @return A map of all UIDs to their CPU time-in-state array in milliseconds. + */ + public SparseArray<long[]> getAllUidCpuFreqTimeMs() { + return mLastTimes; + } + + /** + * Reads a list of CPU frequencies from /proc/uid_time_in_state. Uses a given PowerProfile + * to determine if per-cluster times are available. + * + * @param powerProfile The PowerProfile to compare against. + * @return A long[] of CPU frequencies in Hz. + */ + public long[] readFreqs(@NonNull PowerProfile powerProfile) { + checkNotNull(powerProfile); + if (mCpuFreqs != null) { + // No need to read cpu freqs more than once. + return mCpuFreqs; + } + if (!mAllUidTimesAvailable) { + return null; + } + final int oldMask = StrictMode.allowThreadDiskReadsMask(); + try (BufferedReader reader = Files.newBufferedReader(mProcFilePath)) { + if (readFreqs(reader.readLine()) == null) { + return null; + } + } catch (IOException e) { + if (++mErrors >= MAX_ERROR_COUNT) { + mAllUidTimesAvailable = false; + } + Slog.e(mTag, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e); + return null; + } finally { + StrictMode.setThreadPolicyMask(oldMask); + } + // Check if the freqs in the proc file correspond to per-cluster freqs. + final IntArray numClusterFreqs = extractClusterInfoFromProcFileFreqs(); + final int numClusters = powerProfile.getNumCpuClusters(); + if (numClusterFreqs.size() == numClusters) { + mPerClusterTimesAvailable = true; + for (int i = 0; i < numClusters; ++i) { + if (numClusterFreqs.get(i) != powerProfile.getNumSpeedStepsInCpuCluster(i)) { + mPerClusterTimesAvailable = false; + break; + } + } + } else { + mPerClusterTimesAvailable = false; + } + Slog.i(mTag, "mPerClusterTimesAvailable=" + mPerClusterTimesAvailable); + return mCpuFreqs; + } + + private long[] readFreqs(String line) { + if (line == null) { + return null; + } + final String[] lineArray = line.split(" "); + if (lineArray.length <= 1) { + Slog.wtf(mTag, "Malformed freq line: " + line); + return null; + } + mFreqCount = lineArray.length - 1; + mCpuFreqs = new long[mFreqCount]; + mCurTimes = new long[mFreqCount]; + mDeltaTimes = new long[mFreqCount]; + mBuffer = new long[mFreqCount + 1]; + for (int i = 0; i < mFreqCount; ++i) { + mCpuFreqs[i] = Long.parseLong(lineArray[i + 1], 10); + } + return mCpuFreqs; + } + + @Override + void readDeltaImpl(@Nullable Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + final int uid = (int) mBuffer[0]; + long[] lastTimes = mLastTimes.get(uid); + if (lastTimes == null) { + lastTimes = new long[mFreqCount]; + mLastTimes.put(uid, lastTimes); + } + copyToCurTimes(); + boolean notify = false; + boolean valid = true; + for (int i = 0; i < mFreqCount; i++) { + // Unit is 10ms. + mDeltaTimes[i] = mCurTimes[i] - lastTimes[i]; + if (mDeltaTimes[i] < 0) { + Slog.e(mTag, "Negative delta from freq time proc: " + mDeltaTimes[i]); + valid = false; + } + notify |= mDeltaTimes[i] > 0; + } + if (notify && valid) { + System.arraycopy(mCurTimes, 0, lastTimes, 0, mFreqCount); + if (cb != null) { + cb.onUidCpuTime(uid, mDeltaTimes); + } + } + } + } + } + + @Override + void readAbsoluteImpl(Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + copyToCurTimes(); + cb.onUidCpuTime((int) mBuffer[0], mCurTimes); + } + } + } + + private void copyToCurTimes() { + for (int i = 0; i < mFreqCount; i++) { + mCurTimes[i] = mBuffer[i + 1] * 10; + } + } + + private boolean checkPrecondition(ProcFileIterator iter) { + if (iter == null || !iter.hasNextLine()) { + // Error logged in KernelCpuProcStringReader. + return false; + } + CharBuffer line = iter.nextLine(); + if (mCpuFreqs != null) { + return true; + } + return readFreqs(line.toString()) != null; + } + + /** + * Extracts no. of cpu clusters and no. of freqs in each of these clusters from the freqs + * read from the proc file. + * + * We need to assume that freqs in each cluster are strictly increasing. + * For e.g. if the freqs read from proc file are: 12, 34, 15, 45, 12, 15, 52. Then it means + * there are 3 clusters: (12, 34), (15, 45), (12, 15, 52) + * + * @return an IntArray filled with no. of freqs in each cluster. + */ + private IntArray extractClusterInfoFromProcFileFreqs() { + final IntArray numClusterFreqs = new IntArray(); + int freqsFound = 0; + for (int i = 0; i < mFreqCount; ++i) { + freqsFound++; + if (i + 1 == mFreqCount || mCpuFreqs[i + 1] <= mCpuFreqs[i]) { + numClusterFreqs.add(freqsFound); + freqsFound = 0; + } + } + return numClusterFreqs; + } + } + + /** + * Reads /proc/uid_concurrent_active_time and reports CPU active time to BatteryStats to + * compute {@link PowerProfile#POWER_CPU_ACTIVE}. + * + * /proc/uid_concurrent_active_time has the following format: + * cpus: n + * uid0: time0a, time0b, ..., time0n, + * uid1: time1a, time1b, ..., time1n, + * uid2: time2a, time2b, ..., time2n, + * ... + * where n is the total number of cpus (num_possible_cpus) + * timeXn means the CPU time that a UID X spent running concurrently with n other processes. + * + * The file contains a monotonically increasing count of time for a single boot. This class + * maintains the previous results of a call to {@link #readDelta} in order to provide a + * proper delta. + */ + public static class KernelCpuUidActiveTimeReader extends KernelCpuUidTimeReader<Long> { + private int mCores = 0; + private long[] mBuffer; + + public KernelCpuUidActiveTimeReader(boolean throttle) { + super(KernelCpuProcStringReader.getActiveTimeReaderInstance(), throttle); + } + + @VisibleForTesting + public KernelCpuUidActiveTimeReader(KernelCpuProcStringReader reader, boolean throttle) { + super(reader, throttle); + } + + @Override + void readDeltaImpl(@Nullable Callback<Long> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + int uid = (int) mBuffer[0]; + long cpuActiveTime = sumActiveTime(mBuffer); + if (cpuActiveTime > 0) { + long delta = cpuActiveTime - mLastTimes.get(uid, 0L); + if (delta > 0) { + mLastTimes.put(uid, cpuActiveTime); + if (cb != null) { + cb.onUidCpuTime(uid, delta); + } + } else if (delta < 0) { + Slog.e(mTag, "Negative delta from active time proc: " + delta); + } + } + } + } + } + + @Override + void readAbsoluteImpl(Callback<Long> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + long cpuActiveTime = sumActiveTime(mBuffer); + if (cpuActiveTime > 0) { + cb.onUidCpuTime((int) mBuffer[0], cpuActiveTime); + } + } + } + } + + private static long sumActiveTime(long[] times) { + // UID is stored at times[0]. + double sum = 0; + for (int i = 1; i < times.length; i++) { + sum += (double) times[i] * 10 / i; // Unit is 10ms. + } + return (long) sum; + } + + private boolean checkPrecondition(ProcFileIterator iter) { + if (iter == null || !iter.hasNextLine()) { + // Error logged in KernelCpuProcStringReader. + return false; + } + CharBuffer line = iter.nextLine(); + if (mCores > 0) { + return true; + } + + String str = line.toString(); + if (!str.startsWith("cpus:")) { + Slog.wtf(mTag, "Malformed uid_concurrent_active_time line: " + line); + return false; + } + int cores = Integer.parseInt(str.substring(5).trim(), 10); + if (cores <= 0) { + Slog.wtf(mTag, "Malformed uid_concurrent_active_time line: " + line); + return false; + } + mCores = cores; + mBuffer = new long[mCores + 1]; // UID is stored at mBuffer[0]. + return true; + } + } + + + /** + * Reads /proc/uid_concurrent_policy_time and reports CPU cluster times to BatteryStats to + * compute cluster power. See {@link PowerProfile#getAveragePowerForCpuCluster(int)}. + * + * /proc/uid_concurrent_policy_time has the following format: + * policyX: x policyY: y policyZ: z... + * uid1, time1a, time1b, ..., time1n, + * uid2, time2a, time2b, ..., time2n, + * ... + * The first line lists all policies (i.e. clusters) followed by # cores in each policy. + * Each uid is followed by x time entries corresponding to the time it spent on clusterX + * running concurrently with 0, 1, 2, ..., x - 1 other processes, then followed by y, z, ... + * time entries. + * + * The file contains a monotonically increasing count of time for a single boot. This class + * maintains the previous results of a call to {@link #readDelta} in order to provide a + * proper delta. + */ + public static class KernelCpuUidClusterTimeReader extends KernelCpuUidTimeReader<long[]> { + private int mNumClusters; + private int mNumCores; + private int[] mCoresOnClusters; // # cores on each cluster. + private long[] mBuffer; // To store data returned from ProcFileIterator. + private long[] mCurTime; + private long[] mDeltaTime; + + public KernelCpuUidClusterTimeReader(boolean throttle) { + super(KernelCpuProcStringReader.getClusterTimeReaderInstance(), throttle); + } + + @VisibleForTesting + public KernelCpuUidClusterTimeReader(KernelCpuProcStringReader reader, boolean throttle) { + super(reader, throttle); + } + + @Override + void readDeltaImpl(@Nullable Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + int uid = (int) mBuffer[0]; + long[] lastTimes = mLastTimes.get(uid); + if (lastTimes == null) { + lastTimes = new long[mNumClusters]; + mLastTimes.put(uid, lastTimes); + } + sumClusterTime(); + boolean valid = true; + boolean notify = false; + for (int i = 0; i < mNumClusters; i++) { + mDeltaTime[i] = mCurTime[i] - lastTimes[i]; + if (mDeltaTime[i] < 0) { + Slog.e(mTag, "Negative delta from cluster time proc: " + mDeltaTime[i]); + valid = false; + } + notify |= mDeltaTime[i] > 0; + } + if (notify && valid) { + System.arraycopy(mCurTime, 0, lastTimes, 0, mNumClusters); + if (cb != null) { + cb.onUidCpuTime(uid, mDeltaTime); + } + } + } + } + } + + @Override + void readAbsoluteImpl(Callback<long[]> cb) { + try (ProcFileIterator iter = mReader.open(!mThrottle)) { + if (!checkPrecondition(iter)) { + return; + } + CharBuffer buf; + while ((buf = iter.nextLine()) != null) { + if (asLongs(buf, mBuffer) != mBuffer.length) { + Slog.wtf(mTag, "Invalid line: " + buf.toString()); + continue; + } + sumClusterTime(); + cb.onUidCpuTime((int) mBuffer[0], mCurTime); + } + } + } + + private void sumClusterTime() { + // UID is stored at mBuffer[0]. + int core = 1; + for (int i = 0; i < mNumClusters; i++) { + double sum = 0; + for (int j = 1; j <= mCoresOnClusters[i]; j++) { + sum += (double) mBuffer[core++] * 10 / j; // Unit is 10ms. + } + mCurTime[i] = (long) sum; + } + } + + private boolean checkPrecondition(ProcFileIterator iter) { + if (iter == null || !iter.hasNextLine()) { + // Error logged in KernelCpuProcStringReader. + return false; + } + CharBuffer line = iter.nextLine(); + if (mNumClusters > 0) { + return true; + } + // Parse # cores in clusters. + String[] lineArray = line.toString().split(" "); + if (lineArray.length % 2 != 0) { + Slog.wtf(mTag, "Malformed uid_concurrent_policy_time line: " + line); + return false; + } + int[] clusters = new int[lineArray.length / 2]; + int cores = 0; + for (int i = 0; i < clusters.length; i++) { + if (!lineArray[i * 2].startsWith("policy")) { + Slog.wtf(mTag, "Malformed uid_concurrent_policy_time line: " + line); + return false; + } + clusters[i] = Integer.parseInt(lineArray[i * 2 + 1], 10); + cores += clusters[i]; + } + mNumClusters = clusters.length; + mNumCores = cores; + mCoresOnClusters = clusters; + mBuffer = new long[cores + 1]; + mCurTime = new long[mNumClusters]; + mDeltaTime = new long[mNumClusters]; + return true; + } + } + +} 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 3cfc6443b15e..225515e9e3f3 100644 --- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java +++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java @@ -39,6 +39,10 @@ import org.junit.runners.Suite; BatteryStatsUserLifecycleTests.class, KernelCpuProcReaderTest.class, KernelCpuProcStringReaderTest.class, + KernelCpuUidActiveTimeReaderTest.class, + KernelCpuUidClusterTimeReaderTest.class, + KernelCpuUidFreqTimeReaderTest.class, + KernelCpuUidUserSysTimeReaderTest.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 index dae9eb57e237..2663f2bc8ae1 100644 --- a/core/tests/coretests/src/com/android/internal/os/KernelCpuProcStringReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuProcStringReaderTest.java @@ -37,6 +37,7 @@ import java.io.BufferedWriter; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.CharBuffer; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; @@ -149,7 +150,7 @@ public class KernelCpuProcStringReaderTest { + "0 0 1 1 1 0 2 0 221", iter.nextLine().toString()); long[] actual = new long[43]; - iter.nextLineAsArray(actual); + KernelCpuProcStringReader.asLongs(iter.nextLine(), 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}, @@ -183,7 +184,7 @@ public class KernelCpuProcStringReaderTest { } } - /** Tests nextLineToArray functionality. */ + /** Tests reading lines, then converting to long[]. */ @Test public void testReadLineToArray() throws Exception { final long[][] data = getTestArray(800, 50); @@ -193,12 +194,32 @@ public class KernelCpuProcStringReaderTest { long[] actual = new long[50]; try (KernelCpuProcStringReader.ProcFileIterator iter = mReader.open()) { for (long[] expected : data) { - assertEquals(50, iter.nextLineAsArray(actual)); + CharBuffer cb = iter.nextLine(); + String before = cb.toString(); + assertEquals(50, KernelCpuProcStringReader.asLongs(cb, actual)); assertArrayEquals(expected, actual); + assertEquals("Buffer not reset to the pos before reading", before, cb.toString()); } } } + /** Tests error handling of converting to long[]. */ + @Test + public void testLineToArrayErrorHandling() { + long[] actual = new long[100]; + String invalidChar = "123: -1234 456"; + String overflow = "123: 999999999999999999999999999999999999999999999999999999999 123"; + CharBuffer cb = CharBuffer.wrap("----" + invalidChar + "+++", 4, 4 + invalidChar.length()); + assertEquals("Failed to report err for: " + invalidChar, -2, + KernelCpuProcStringReader.asLongs(cb, actual)); + assertEquals("Buffer not reset to the same pos before reading", invalidChar, cb.toString()); + + cb = CharBuffer.wrap("----" + overflow + "+++", 4, 4 + overflow.length()); + assertEquals("Failed to report err for: " + overflow, -3, + KernelCpuProcStringReader.asLongs(cb, actual)); + assertEquals("Buffer not reset to the pos before reading", overflow, cb.toString()); + } + /** * Tests that reading a file over the limit (1MB) will return null. */ diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuUidActiveTimeReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidActiveTimeReaderTest.java new file mode 100644 index 000000000000..adafda04d516 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidActiveTimeReaderTest.java @@ -0,0 +1,262 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.FileUtils; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SparseLongArray; + +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidActiveTimeReader; + +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.IOException; +import java.nio.file.Files; +import java.util.Random; + +/** + * Test class for {@link KernelCpuUidActiveTimeReader}. + * + * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuUidActiveTimeReaderTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuUidActiveTimeReaderTest { + private File mTestDir; + private File mTestFile; + private KernelCpuUidActiveTimeReader mReader; + private VerifiableCallback mCallback; + + private Random mRand = new Random(12345); + private final int mCpus = 4; + private final String mHeadline = "cpus: 4\n"; + private final int[] mUids = {0, 1, 22, 333, 4444, 55555}; + + private Context getContext() { + return InstrumentationRegistry.getContext(); + } + + @Before + public void setUp() { + mTestDir = getContext().getDir("test", Context.MODE_PRIVATE); + mTestFile = new File(mTestDir, "test.file"); + mReader = new KernelCpuUidActiveTimeReader( + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + mCallback = new VerifiableCallback(); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteContents(mTestDir); + FileUtils.deleteContents(getContext().getFilesDir()); + } + + @Test + public void testReadDelta() throws Exception { + final long[][] times = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], getActiveTime(times[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that a second call will only return deltas. + mCallback.clear(); + final long[][] newTimes1 = increaseTime(times); + writeToFile(mHeadline + uidLines(mUids, newTimes1)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], getActiveTime(newTimes1[i]) - getActiveTime(times[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that there won't be a callback if the proc file values didn't change. + mCallback.clear(); + mReader.readDelta(mCallback); + mCallback.verifyNoMoreInteractions(); + + // Verify that calling with a null callback doesn't result in any crashes + mCallback.clear(); + final long[][] newTimes2 = increaseTime(newTimes1); + writeToFile(mHeadline + uidLines(mUids, newTimes2)); + mReader.readDelta(null); + mCallback.verifyNoMoreInteractions(); + + // Verify that the readDelta call will only return deltas when + // the previous call had null callback. + mCallback.clear(); + final long[][] newTimes3 = increaseTime(newTimes2); + writeToFile(mHeadline + uidLines(mUids, newTimes3)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], getActiveTime(newTimes3[i]) - getActiveTime(newTimes2[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadAbsolute() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times1[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that a second call should still return absolute values + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times2[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadDeltaDecreasedTime() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + + // Verify that there should not be a callback for a particular UID if its time decreases. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + System.arraycopy(times1[0], 0, times2[0], 0, mCpus); + times2[0][0] = 100; + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times2[i]) - getActiveTime(times1[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + + // Verify that the internal state was not modified. + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + times3[0] = increaseTime(times1)[0]; + writeToFile(mHeadline + uidLines(mUids, times3)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], getActiveTime(times3[0]) - getActiveTime(times1[0])); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times3[i]) - getActiveTime(times2[i])); + } + mCallback.verifyNoMoreInteractions(); + } + + @Test + public void testReadDeltaNegativeTime() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + + // Verify that there should not be a callback for a particular UID if its time is -ve. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + times2[0][0] *= -1; + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times2[i]) - getActiveTime(times1[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + + // Verify that the internal state was not modified. + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + times3[0] = increaseTime(times1)[0]; + writeToFile(mHeadline + uidLines(mUids, times3)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], getActiveTime(times3[0]) - getActiveTime(times1[0])); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], getActiveTime(times3[i]) - getActiveTime(times2[i])); + } + mCallback.verifyNoMoreInteractions(); + } + + private String uidLines(int[] uids, long[][] times) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < uids.length; i++) { + sb.append(uids[i]).append(':'); + for (int j = 0; j < times[i].length; j++) { + sb.append(' ').append(times[i][j] / 10); + } + sb.append('\n'); + } + return sb.toString(); + } + + private void writeToFile(String s) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(s); + w.flush(); + } + } + + private long[][] increaseTime(long[][] original) { + long[][] newTime = new long[original.length][original[0].length]; + for (int i = 0; i < original.length; i++) { + for (int j = 0; j < original[0].length; j++) { + newTime[i][j] = original[i][j] + mRand.nextInt(10000) * 1000 + 1000; + } + } + return newTime; + } + + private long getActiveTime(long[] times) { + return times[0] + times[1] / 2 + times[2] / 3 + times[3] / 4; + } + + private class VerifiableCallback implements KernelCpuUidTimeReader.Callback<Long> { + SparseLongArray mData = new SparseLongArray(); + + public void verify(int uid, long time) { + assertEquals(time, mData.get(uid)); + mData.delete(uid); + } + + public void clear() { + mData.clear(); + } + + @Override + public void onUidCpuTime(int uid, Long time) { + mData.put(uid, time); + } + + public void verifyNoMoreInteractions() { + assertEquals(0, mData.size()); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuUidClusterTimeReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidClusterTimeReaderTest.java new file mode 100644 index 000000000000..ad20d84bfc60 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidClusterTimeReaderTest.java @@ -0,0 +1,278 @@ +/* + * 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.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.FileUtils; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SparseArray; + +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidClusterTimeReader; + +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.IOException; +import java.nio.file.Files; +import java.util.Random; + +/** + * Test class for {@link KernelCpuUidClusterTimeReader}. + * + * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuUidClusterTimeReaderTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuUidClusterTimeReaderTest { + private File mTestDir; + private File mTestFile; + private KernelCpuUidClusterTimeReader mReader; + private VerifiableCallback mCallback; + + private Random mRand = new Random(12345); + private final int mCpus = 6; + private final String mHeadline = "policy0: 4 policy4: 2\n"; + private final int[] mUids = {0, 1, 22, 333, 4444, 55555}; + + private Context getContext() { + return InstrumentationRegistry.getContext(); + } + + @Before + public void setUp() { + mTestDir = getContext().getDir("test", Context.MODE_PRIVATE); + mTestFile = new File(mTestDir, "test.file"); + mReader = new KernelCpuUidClusterTimeReader( + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + mCallback = new VerifiableCallback(); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteContents(mTestDir); + FileUtils.deleteContents(getContext().getFilesDir()); + } + + @Test + public void testReadDelta() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], clusterTime(times1[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that a second call will only return deltas. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], subtract(clusterTime(times2[i]), clusterTime(times1[i]))); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that there won't be a callback if the proc file values didn't change. + mCallback.clear(); + mReader.readDelta(mCallback); + mCallback.verifyNoMoreInteractions(); + + // Verify that calling with a null callback doesn't result in any crashes + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + writeToFile(mHeadline + uidLines(mUids, times3)); + mReader.readDelta(null); + mCallback.verifyNoMoreInteractions(); + + // Verify that the readDelta call will only return deltas when + // the previous call had null callback. + mCallback.clear(); + final long[][] times4 = increaseTime(times3); + writeToFile(mHeadline + uidLines(mUids, times4)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], subtract(clusterTime(times4[i]), clusterTime(times3[i]))); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadAbsolute() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], clusterTime(times1[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that a second call should still return absolute values + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], clusterTime(times2[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadDeltaDecreasedTime() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + + // Verify that there should not be a callback for a particular UID if its time decreases. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + System.arraycopy(times1[0], 0, times2[0], 0, mCpus); + times2[0][0] = 100; + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(clusterTime(times2[i]), clusterTime(times1[i]))); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + + // Verify that the internal state was not modified. + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + times3[0] = increaseTime(times1)[0]; + writeToFile(mHeadline + uidLines(mUids, times3)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], subtract(clusterTime(times3[0]), clusterTime(times1[0]))); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(clusterTime(times3[i]), clusterTime(times2[i]))); + } + mCallback.verifyNoMoreInteractions(); + } + + @Test + public void testReadDeltaNegativeTime() throws Exception { + final long[][] times1 = increaseTime(new long[mUids.length][mCpus]); + writeToFile(mHeadline + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + + // Verify that there should not be a callback for a particular UID if its time decreases. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + times2[0][0] *= -1; + writeToFile(mHeadline + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(clusterTime(times2[i]), clusterTime(times1[i]))); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + + // Verify that the internal state was not modified. + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + times3[0] = increaseTime(times1)[0]; + writeToFile(mHeadline + uidLines(mUids, times3)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], subtract(clusterTime(times3[0]), clusterTime(times1[0]))); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(clusterTime(times3[i]), clusterTime(times2[i]))); + } + mCallback.verifyNoMoreInteractions(); + } + + private long[] clusterTime(long[] times) { + // Assumes 4 + 2 cores + return new long[]{times[0] + times[1] / 2 + times[2] / 3 + times[3] / 4, + times[4] + times[5] / 2}; + } + + private String uidLines(int[] uids, long[][] times) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < uids.length; i++) { + sb.append(uids[i]).append(':'); + for (int j = 0; j < times[i].length; j++) { + sb.append(' ').append(times[i][j] / 10); + } + sb.append('\n'); + } + return sb.toString(); + } + + private void writeToFile(String s) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(s); + w.flush(); + } + } + + private long[][] increaseTime(long[][] original) { + long[][] newTime = new long[original.length][original[0].length]; + for (int i = 0; i < original.length; i++) { + for (int j = 0; j < original[0].length; j++) { + newTime[i][j] = original[i][j] + mRand.nextInt(10000) * 1000 + 1000; + } + } + return newTime; + } + + private long[] subtract(long[] a1, long[] a2) { + long[] val = new long[a1.length]; + for (int i = 0; i < val.length; ++i) { + val[i] = a1[i] - a2[i]; + } + return val; + } + + private class VerifiableCallback implements KernelCpuUidTimeReader.Callback<long[]> { + SparseArray<long[]> mData = new SparseArray<>(); + + public void verify(int uid, long[] cpuTimes) { + long[] array = mData.get(uid); + assertNotNull(array); + assertArrayEquals(cpuTimes, array); + mData.remove(uid); + } + + public void clear() { + mData.clear(); + } + + @Override + public void onUidCpuTime(int uid, long[] times) { + long[] array = new long[times.length]; + System.arraycopy(times, 0, array, 0, array.length); + mData.put(uid, array); + } + + public void verifyNoMoreInteractions() { + assertEquals(0, mData.size()); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuUidFreqTimeReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidFreqTimeReaderTest.java new file mode 100644 index 000000000000..1d3a98a89d95 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidFreqTimeReaderTest.java @@ -0,0 +1,359 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.FileUtils; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SparseArray; + +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidFreqTimeReader; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Random; + +/** + * Test class for {@link KernelCpuUidFreqTimeReader}. + * + * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuUidFreqTimeReaderTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuUidFreqTimeReaderTest { + private File mTestDir; + private File mTestFile; + private KernelCpuUidFreqTimeReader mReader; + private VerifiableCallback mCallback; + @Mock + private PowerProfile mPowerProfile; + + private Random mRand = new Random(12345); + private final int[] mUids = {0, 1, 22, 333, 4444, 55555}; + + private Context getContext() { + return InstrumentationRegistry.getContext(); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTestDir = getContext().getDir("test", Context.MODE_PRIVATE); + mTestFile = new File(mTestDir, "test.file"); + mReader = new KernelCpuUidFreqTimeReader(mTestFile.getAbsolutePath(), + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + mCallback = new VerifiableCallback(); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteContents(mTestDir); + FileUtils.deleteContents(getContext().getFilesDir()); + } + + @Test + public void testReadFreqs_perClusterTimesNotAvailable() throws Exception { + final long[][] freqs = { + {1, 12, 123, 1234}, + {1, 12, 123, 23, 123, 1234, 12345, 123456}, + {1, 12, 123, 23, 123, 1234, 12345, 123456, 12, 123, 12345}, + {1, 12, 123, 23, 2345, 234567} + }; + final int[] numClusters = {2, 2, 3, 1}; + final int[][] numFreqs = {{3, 6}, {4, 5}, {3, 5, 4}, {3}}; + for (int i = 0; i < freqs.length; ++i) { + mReader = new KernelCpuUidFreqTimeReader(mTestFile.getAbsolutePath(), + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + setCpuClusterFreqs(numClusters[i], numFreqs[i]); + writeToFile(freqsLine(freqs[i])); + long[] actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs[i], actualFreqs); + final String errMsg = String.format("Freqs=%s, nClusters=%d, nFreqs=%s", + Arrays.toString(freqs[i]), numClusters[i], Arrays.toString(numFreqs[i])); + assertFalse(errMsg, mReader.perClusterTimesAvailable()); + + // Verify that a second call won't read the proc file again + assertTrue(mTestFile.delete()); + actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs[i], actualFreqs); + assertFalse(errMsg, mReader.perClusterTimesAvailable()); + } + } + + @Test + public void testReadFreqs_perClusterTimesAvailable() throws Exception { + final long[][] freqs = { + {1, 12, 123, 1234}, + {1, 12, 123, 23, 123, 1234, 12345, 123456}, + {1, 12, 123, 23, 123, 1234, 12345, 123456, 12, 123, 12345, 1234567} + }; + final int[] numClusters = {1, 2, 3}; + final int[][] numFreqs = {{4}, {3, 5}, {3, 5, 4}}; + for (int i = 0; i < freqs.length; ++i) { + mReader = new KernelCpuUidFreqTimeReader(mTestFile.getAbsolutePath(), + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + setCpuClusterFreqs(numClusters[i], numFreqs[i]); + writeToFile(freqsLine(freqs[i])); + long[] actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs[i], actualFreqs); + final String errMsg = String.format("Freqs=%s, nClusters=%d, nFreqs=%s", + Arrays.toString(freqs[i]), numClusters[i], Arrays.toString(numFreqs[i])); + assertTrue(errMsg, mReader.perClusterTimesAvailable()); + + // Verify that a second call won't read the proc file again + assertTrue(mTestFile.delete()); + actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs[i], actualFreqs); + assertTrue(errMsg, mReader.perClusterTimesAvailable()); + } + } + + @Test + public void testReadDelta() throws Exception { + final long[] freqs = {110, 123, 145, 167, 289, 997}; + final long[][] times = increaseTime(new long[mUids.length][freqs.length]); + + writeToFile(freqsLine(freqs) + uidLines(mUids, times)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], times[i]); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that readDelta also reads the frequencies if not already available. + assertTrue(mTestFile.delete()); + long[] actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs, actualFreqs); + + // Verify that a second call will only return deltas. + mCallback.clear(); + final long[][] newTimes1 = increaseTime(times); + writeToFile(freqsLine(freqs) + uidLines(mUids, newTimes1)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], subtract(newTimes1[i], times[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that there won't be a callback if the proc file values didn't change. + mCallback.clear(); + mReader.readDelta(mCallback); + mCallback.verifyNoMoreInteractions(); + + // Verify that calling with a null callback doesn't result in any crashes + mCallback.clear(); + final long[][] newTimes2 = increaseTime(newTimes1); + writeToFile(freqsLine(freqs) + uidLines(mUids, newTimes2)); + mReader.readDelta(null); + mCallback.verifyNoMoreInteractions(); + + // Verify that the readDelta call will only return deltas when + // the previous call had null callback. + mCallback.clear(); + final long[][] newTimes3 = increaseTime(newTimes2); + writeToFile(freqsLine(freqs) + uidLines(mUids, newTimes3)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; ++i) { + mCallback.verify(mUids[i], subtract(newTimes3[i], newTimes2[i])); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadAbsolute() throws Exception { + final long[] freqs = {110, 123, 145, 167, 289, 997}; + final long[][] times1 = increaseTime(new long[mUids.length][freqs.length]); + + writeToFile(freqsLine(freqs) + uidLines(mUids, times1)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times1[i]); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that readDelta also reads the frequencies if not already available. + assertTrue(mTestFile.delete()); + long[] actualFreqs = mReader.readFreqs(mPowerProfile); + assertArrayEquals(freqs, actualFreqs); + + // Verify that a second call should still return absolute values + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + writeToFile(freqsLine(freqs) + uidLines(mUids, times2)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times2[i]); + } + mCallback.verifyNoMoreInteractions(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadDeltaWrongData() throws Exception { + final long[] freqs = {110, 123, 145, 167, 289, 997}; + final long[][] times1 = increaseTime(new long[mUids.length][freqs.length]); + + writeToFile(freqsLine(freqs) + uidLines(mUids, times1)); + mReader.readDelta(mCallback); + + // Verify that there should not be a callback for a particular UID if its time decreases. + mCallback.clear(); + final long[][] times2 = increaseTime(times1); + times2[0][0] = 1000; + writeToFile(freqsLine(freqs) + uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times2[i], times1[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that the internal state was not modified. + mCallback.clear(); + final long[][] times3 = increaseTime(times2); + times3[0] = increaseTime(times1)[0]; + writeToFile(freqsLine(freqs) + uidLines(mUids, times3)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], subtract(times3[0], times1[0])); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times3[i], times2[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that there is no callback if any value in the proc file is -ve. + mCallback.clear(); + final long[][] times4 = increaseTime(times3); + times4[0][0] *= -1; + writeToFile(freqsLine(freqs) + uidLines(mUids, times4)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; ++i) { + mCallback.verify(mUids[i], subtract(times4[i], times3[i])); + } + mCallback.verifyNoMoreInteractions(); + + // Verify that the internal state was not modified when the proc file had -ve value. + mCallback.clear(); + final long[][] times5 = increaseTime(times4); + times5[0] = increaseTime(times3)[0]; + writeToFile(freqsLine(freqs) + uidLines(mUids, times5)); + mReader.readDelta(mCallback); + mCallback.verify(mUids[0], subtract(times5[0], times3[0])); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times5[i], times4[i])); + } + + assertTrue(mTestFile.delete()); + } + + private String freqsLine(long[] freqs) { + final StringBuilder sb = new StringBuilder(); + sb.append("uid:"); + for (int i = 0; i < freqs.length; ++i) { + sb.append(" " + freqs[i]); + } + return sb.append('\n').toString(); + } + + private void setCpuClusterFreqs(int numClusters, int... clusterFreqs) { + assertEquals(numClusters, clusterFreqs.length); + when(mPowerProfile.getNumCpuClusters()).thenReturn(numClusters); + for (int i = 0; i < numClusters; ++i) { + when(mPowerProfile.getNumSpeedStepsInCpuCluster(i)).thenReturn(clusterFreqs[i]); + } + } + + private String uidLines(int[] uids, long[][] times) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < uids.length; i++) { + sb.append(uids[i]).append(':'); + for (int j = 0; j < times[i].length; j++) { + sb.append(' ').append(times[i][j] / 10); + } + sb.append('\n'); + } + return sb.toString(); + } + + private void writeToFile(String s) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(s); + w.flush(); + } + } + + private long[][] increaseTime(long[][] original) { + long[][] newTime = new long[original.length][original[0].length]; + for (int i = 0; i < original.length; i++) { + for (int j = 0; j < original[0].length; j++) { + newTime[i][j] = original[i][j] + mRand.nextInt(10000) * 10 + 10; + } + } + return newTime; + } + + private long[] subtract(long[] a1, long[] a2) { + long[] val = new long[a1.length]; + for (int i = 0; i < val.length; ++i) { + val[i] = a1[i] - a2[i]; + } + return val; + } + + private class VerifiableCallback implements KernelCpuUidTimeReader.Callback<long[]> { + SparseArray<long[]> mData = new SparseArray<>(); + + public void verify(int uid, long[] cpuTimes) { + long[] array = mData.get(uid); + assertNotNull(array); + assertArrayEquals(cpuTimes, array); + mData.remove(uid); + } + + public void clear() { + mData.clear(); + } + + @Override + public void onUidCpuTime(int uid, long[] times) { + long[] array = new long[times.length]; + System.arraycopy(times, 0, array, 0, array.length); + mData.put(uid, array); + } + + public void verifyNoMoreInteractions() { + assertEquals(0, mData.size()); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuUidUserSysTimeReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidUserSysTimeReaderTest.java new file mode 100644 index 000000000000..9b4512b8b9bd --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuUidUserSysTimeReaderTest.java @@ -0,0 +1,271 @@ +/* + * 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.assertNotNull; +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 android.util.SparseArray; + +import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidUserSysTimeReader; + +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.IOException; +import java.nio.file.Files; +import java.util.Random; + +/** + * Test class for {@link KernelCpuUidUserSysTimeReader}. + * + * $ atest FrameworksCoreTests:com.android.internal.os.KernelCpuUidUserSysTimeReaderTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KernelCpuUidUserSysTimeReaderTest { + private File mTestDir; + private File mTestFile; + private KernelCpuUidTimeReader.KernelCpuUidUserSysTimeReader mReader; + private VerifiableCallback mCallback; + + private Random mRand = new Random(12345); + private final int[] mUids = {0, 1, 22, 333, 4444, 55555}; + private final long[][] mInitialTimes = new long[][]{ + {15334000, 310964000}, + {537000, 114000}, + {40000, 10000}, + {170000, 57000}, + {5377000, 867000}, + {47000, 17000} + }; + + private Context getContext() { + return InstrumentationRegistry.getContext(); + } + + @Before + public void setUp() { + mTestDir = getContext().getDir("test", Context.MODE_PRIVATE); + mTestFile = new File(mTestDir, "test.file"); + mReader = new KernelCpuUidUserSysTimeReader( + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), false); + mCallback = new VerifiableCallback(); + } + + @After + public void tearDown() throws Exception { + FileUtils.deleteContents(mTestDir); + FileUtils.deleteContents(getContext().getFilesDir()); + } + + @Test + public void testThrottler() throws Exception { + mReader = new KernelCpuUidUserSysTimeReader( + new KernelCpuProcStringReader(mTestFile.getAbsolutePath()), true); + mReader.setThrottle(500); + + writeToFile(uidLines(mUids, mInitialTimes)); + mReader.readDelta(mCallback); + assertEquals(6, mCallback.mData.size()); + + long[][] times1 = increaseTime(mInitialTimes); + writeToFile(uidLines(mUids, times1)); + mCallback.clear(); + mReader.readDelta(mCallback); + assertEquals(0, mCallback.mData.size()); + + SystemClock.sleep(600); + + long[][] times2 = increaseTime(times1); + writeToFile(uidLines(mUids, times2)); + mCallback.clear(); + mReader.readDelta(mCallback); + assertEquals(6, mCallback.mData.size()); + + long[][] times3 = increaseTime(times2); + writeToFile(uidLines(mUids, times3)); + mCallback.clear(); + mReader.readDelta(mCallback); + assertEquals(0, mCallback.mData.size()); + } + + @Test + public void testReadDelta() throws Exception { + final long[][] times1 = mInitialTimes; + writeToFile(uidLines(mUids, times1)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times1[i]); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + + // Verify that a second call will only return deltas. + final long[][] times2 = increaseTime(times1); + writeToFile(uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times2[i], times1[i])); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + + // Verify that there won't be a callback if the proc file values didn't change. + mReader.readDelta(mCallback); + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + + // Verify that calling with a null callback doesn't result in any crashes + final long[][] times3 = increaseTime(times2); + writeToFile(uidLines(mUids, times3)); + mReader.readDelta(null); + + // Verify that the readDelta call will only return deltas when + // the previous call had null callback. + final long[][] times4 = increaseTime(times3); + writeToFile(uidLines(mUids, times4)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times4[i], times3[i])); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadDeltaWrongData() throws Exception { + final long[][] times1 = mInitialTimes; + writeToFile(uidLines(mUids, times1)); + mReader.readDelta(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times1[i]); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + + // Verify that there should not be a callback for a particular UID if its time decreases. + final long[][] times2 = increaseTime(times1); + times2[0][0] = 1000; + writeToFile(uidLines(mUids, times2)); + mReader.readDelta(mCallback); + for (int i = 1; i < mUids.length; i++) { + mCallback.verify(mUids[i], subtract(times2[i], times1[i])); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + assertTrue(mTestFile.delete()); + } + + @Test + public void testReadAbsolute() throws Exception { + final long[][] times1 = mInitialTimes; + writeToFile(uidLines(mUids, times1)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times1[i]); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + + // Verify that a second call should still return absolute values + final long[][] times2 = increaseTime(times1); + writeToFile(uidLines(mUids, times2)); + mReader.readAbsolute(mCallback); + for (int i = 0; i < mUids.length; i++) { + mCallback.verify(mUids[i], times2[i]); + } + mCallback.verifyNoMoreInteractions(); + mCallback.clear(); + assertTrue(mTestFile.delete()); + } + + private String uidLines(int[] uids, long[][] times) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < uids.length; i++) { + sb.append(uids[i]).append(':'); + for (int j = 0; j < times[i].length; j++) { + sb.append(' ').append(times[i][j]); + } + sb.append('\n'); + } + return sb.toString(); + } + + private void writeToFile(String s) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(mTestFile.toPath())) { + w.write(s); + w.flush(); + } + } + + private long[][] increaseTime(long[][] original) { + long[][] newTime = new long[original.length][original[0].length]; + for (int i = 0; i < original.length; i++) { + for (int j = 0; j < original[0].length; j++) { + newTime[i][j] = original[i][j] + mRand.nextInt(1000) * 1000 + 1000; + } + } + return newTime; + } + + private long[] subtract(long[] a1, long[] a2) { + long[] val = new long[a1.length]; + for (int i = 0; i < val.length; ++i) { + val[i] = a1[i] - a2[i]; + } + return val; + } + + private class VerifiableCallback implements KernelCpuUidTimeReader.Callback<long[]> { + SparseArray<long[]> mData = new SparseArray<>(); + + public void verify(int uid, long[] cpuTimes) { + long[] array = mData.get(uid); + assertNotNull(array); + assertArrayEquals(cpuTimes, array); + mData.remove(uid); + } + + public void clear() { + mData.clear(); + } + + @Override + public void onUidCpuTime(int uid, long[] times) { + long[] array = new long[times.length]; + System.arraycopy(times, 0, array, 0, array.length); + mData.put(uid, array); + } + + public void verifyNoMoreInteractions() { + assertEquals(0, mData.size()); + } + } +} |