blob: 70c9d6423e3641220a0dd6f819ad760e7d342bea [file] [log] [blame]
/*
* Copyright (C) 2012 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.tradefed.device;
import com.android.ddmlib.MultiLineReceiver;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.SimpleStats;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Helper class which runs {@code cpustats} continuously on an {@link ITestDevice} and parses the
* output.
* <p>
* Provides a method to record the output of {@code cpustats} and get all the cpu usage measurements
* as well methods for performing calculations on that data to find the mean of the cpu workload,
* the approximate cpu frequency, and the percentage of used cpu frequency.
* </p><p>
* This is meant to be a replacement for {@link TopHelper}, which does not provide stats about cpu
* frequency and has a higher overhead due to measuring processes/threads.
* </p><p>
* The {@code cpustats} command was added in the Jellybean release, so this collector should only be
* used for new tests.
* </p>
* @see TopHelper
*/
public class CpuStatsCollector extends Thread {
private static final String CPU_STATS_CMD = "cpustats -m -d %s";
private final ITestDevice mTestDevice;
private long mDelay;
/**
* Used to distinguish between the different CPU time categories.
*/
enum TimeCategory {
USER,
NICE,
SYS,
IDLE,
IOW,
IRQ,
SIRQ
}
/**
* Class for holding parsed output data for a single {@code cpustats} output.
* <p>
* This class holds parsed output, and also performs simple calculations on that data. The
* methods which perform these calucations should only be called after the object has been
* populated.
* </p>
*/
public static class CpuStats {
public Map<TimeCategory, Integer> mTimeStats = new HashMap<TimeCategory, Integer>();
public Map<Integer, Integer> mFreqStats = new HashMap<Integer, Integer>();
private Map<TimeCategory, Double> mPercentageStats = new HashMap<TimeCategory, Double>();
private Integer mTotalTime = null;
private Double mAverageMhz = null;
/**
* Get the percentage of cycles used on a given category.
*/
public Double getPercentage(TimeCategory category) {
if (!mPercentageStats.containsKey(category)) {
mPercentageStats.put(category, 100.0 * mTimeStats.get(category) / getTotalTime());
}
return mPercentageStats.get(category);
}
/**
* Estimate the MHz used by the cpu during the duration.
* <p>
* This is calculated by:
* </p><code>
* ((sum(c_time) - idle) / sum(c_time)) * (sum(freq * f_time) / sum(f_time))
* </code><p>
* where {@code c_time} is the time for a given category, {@code idle} is the time in the
* idle state, {@code freq} is a frequency and {@code f_time} is the time spent in that
* frequency.
* </p>
*/
public Double getEstimatedMhz() {
if (mFreqStats.isEmpty()) {
return null;
}
return getTotalUsage() * getAverageMhz();
}
/**
* Get the amount of MHz as a percentage of available MHz used by the cpu during the
* duration.
* <p>
* This is calculated by:
* </p><code>
* 100 * sum(freq * f_time) / (max_freq * sum(f_time))
* </code><p>
* where {@code freq} is a frequency, {@code f_time} is the time spent in that frequency,
* and {@code max_freq} is the maximum frequency the cpu is capable of.
* </p>
*/
public Double getUsedMhzPercentage() {
if (mFreqStats.isEmpty()) {
return null;
}
return 100.0 * getAverageMhz() / getMaxMhz();
}
/**
* Get the total usage, or the sum of all the times except idle over the sum of all the
* times.
*/
private Double getTotalUsage() {
return (double) (getTotalTime() - mTimeStats.get(TimeCategory.IDLE)) / getTotalTime();
}
/**
* Get the average MHz.
* <p>
* This is calculated by:
* </p><code>
* sum(freq * f_time) / sum(f_time))
* </code><p>
* where {@code freq} is a frequency and {@code f_time} is the time spent in that frequency.
* </p>
*/
private Double getAverageMhz() {
if (mFreqStats.isEmpty()) {
return null;
}
if (mAverageMhz == null) {
double sumFreqTime = 0.0;
long sumTime = 0;
for (Map.Entry<Integer, Integer> e : mFreqStats.entrySet()) {
sumFreqTime += e.getKey() * e.getValue() / 1000.0;
sumTime += e.getValue();
}
mAverageMhz = sumFreqTime / sumTime;
}
return mAverageMhz;
}
/**
* Get the maximum possible MHz.
*/
private Double getMaxMhz() {
if (mFreqStats.isEmpty()) {
return null;
}
int max = 0;
for (int freq : mFreqStats.keySet()) {
max = Math.max(max, freq);
}
return max / 1000.0;
}
/**
* Get the total amount of time cycles.
*/
private Integer getTotalTime() {
if (mTotalTime == null) {
int sum = 0;
for (int time : mTimeStats.values()) {
sum += time;
}
mTotalTime = sum;
}
return mTotalTime;
}
}
/**
* Receiver which parses the output from {@code cpustats} and optionally logs to a file.
*/
public static class CpuStatsReceiver extends MultiLineReceiver {
private Map<String, List<CpuStats>> mCpuStats = new HashMap<String, List<CpuStats>>(4);
private boolean mIsCancelled = false;
private File mLogFile = null;
private BufferedWriter mLogWriter = null;
public CpuStatsReceiver() {
setTrimLine(false);
}
/**
* Specify a file to log the output to.
* <p>
* This can be called at any time in the receivers life cycle, but only new output will be
* logged to the file.
* </p>
*/
public synchronized void logToFile(File logFile) {
try {
mLogFile = logFile;
mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
} catch (IOException e) {
CLog.e("Error creating file: %s", e.getMessage());
mLogWriter = null;
}
}
/**
* {@inheritDoc}
*/
@Override
public void processNewLines(String[] lines) {
if (mIsCancelled) {
return;
}
synchronized (this) {
if (mLogWriter != null) {
try {
for (String line : lines) {
mLogWriter.write(line + "\n");
}
} catch (IOException e) {
CLog.e("Error writing to file: %s", e.getMessage());
}
}
}
for (String line : lines) {
String[] args = line.trim().split(",");
if (args.length >= 8) {
try {
CpuStats s = new CpuStats();
s.mTimeStats.put(TimeCategory.USER, Integer.parseInt(args[1]));
s.mTimeStats.put(TimeCategory.NICE, Integer.parseInt(args[2]));
s.mTimeStats.put(TimeCategory.SYS, Integer.parseInt(args[3]));
s.mTimeStats.put(TimeCategory.IDLE, Integer.parseInt(args[4]));
s.mTimeStats.put(TimeCategory.IOW, Integer.parseInt(args[5]));
s.mTimeStats.put(TimeCategory.IRQ, Integer.parseInt(args[6]));
s.mTimeStats.put(TimeCategory.SIRQ, Integer.parseInt(args[7]));
for (int i = 0; i + 8 < args.length; i += 2) {
s.mFreqStats.put(Integer.parseInt(args[8 + i]),
Integer.parseInt(args[9 + i]));
}
synchronized(this) {
if (!mCpuStats.containsKey(args[0])) {
mCpuStats.put(args[0], new LinkedList<CpuStats>());
}
mCpuStats.get(args[0]).add(s);
}
} catch (NumberFormatException e) {
CLog.w("Unexpected input: %s", line.trim());
} catch (IndexOutOfBoundsException e) {
CLog.w("Unexpected input: %s", line.trim());
}
} else if (args.length > 1 || !"".equals(args[0])) {
CLog.w("Unexpected input: %s", line.trim());
}
}
}
/**
* Cancels the {@code cpustats} command.
*/
public synchronized void cancel() {
if (mIsCancelled) {
return;
}
mIsCancelled = true;
if (mLogWriter != null) {
try {
mLogWriter.flush();
mLogWriter.close();
} catch (IOException e) {
CLog.e("Error closing writer %s", e.getMessage());
} finally {
mLogWriter = null;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized boolean isCancelled() {
return mIsCancelled;
}
/**
* Get all the parsed data as a map from label to lists of {@link CpuStats} objects.
*/
public synchronized Map<String, List<CpuStats>> getCpuStats() {
Map<String, List<CpuStats>> copy = new HashMap<String, List<CpuStats>>(
mCpuStats.size());
for (String k : mCpuStats.keySet()) {
copy.put(k, new ArrayList<CpuStats>(mCpuStats.get(k)));
}
return copy;
}
}
private CpuStatsReceiver mReceiver = new CpuStatsReceiver();
/**
* Create a {@link CpuStatsCollector}.
*
* @param testDevice The test device
*/
public CpuStatsCollector(ITestDevice testDevice) {
this(testDevice, 1);
}
/**
* Create a {@link CpuStatsCollector} with a delay specified.
*
* @param testDevice The test device
* @param delay The delay time in seconds
*/
public CpuStatsCollector(ITestDevice testDevice, int delay) {
mTestDevice = testDevice;
mDelay = delay;
}
/**
* Specify a file to log output to.
*
* @param logFile the file to log output to.
*/
public void logToFile(File logFile) {
mReceiver.logToFile(logFile);
}
/**
* Cancels the {@code cpustats} command.
*/
public synchronized void cancel() {
mReceiver.cancel();
}
/**
* Gets whether the {@code cpustats} command is canceled.
*
* @return if the {@code cpustats} command is canceled.
*/
public synchronized boolean isCancelled() {
return mReceiver.isCancelled();
}
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
mTestDevice.executeShellCommand(String.format(CPU_STATS_CMD, mDelay), mReceiver);
} catch (DeviceNotAvailableException e) {
CLog.e("Device %s not available: %s", mTestDevice.getSerialNumber(),
e.getMessage());
}
}
/**
* Get the mapping of labels to lists of {@link CpuStats} instances.
*
* @return a mapping of labels to lists of {@link CpuStats} instances. The labels will include
* "Total" and "cpu0"..."cpuN" for each CPU on the device.
*/
public Map<String, List<CpuStats>> getCpuStats() {
return mReceiver.getCpuStats();
}
/**
* Get the mean of the total CPU usage for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average usage as a percentage (0 to 100).
*/
public static Double getTotalPercentageMean(List<CpuStats> cpuStats) {
SimpleStats stats = new SimpleStats();
for (CpuStats s : cpuStats) {
if (s.getTotalUsage() != null) {
stats.add(s.getTotalUsage());
}
}
return 100 * stats.mean();
}
/**
* Get the mean of the user and nice CPU usage for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average usage as a percentage (0 to 100).
*/
public static Double getUserPercentageMean(List<CpuStats> cpuStats) {
return (getPercentageMean(cpuStats, TimeCategory.USER) +
getPercentageMean(cpuStats, TimeCategory.NICE));
}
/**
* Get the mean of the system CPU usage for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average usage as a percentage (0 to 100).
*/
public static Double getSystemPercentageMean(List<CpuStats> cpuStats) {
return getPercentageMean(cpuStats, TimeCategory.SYS);
}
/**
* Get the mean of the iow CPU usage for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average usage as a percentage (0 to 100).
*/
public static Double getIowPercentageMean(List<CpuStats> cpuStats) {
return getPercentageMean(cpuStats, TimeCategory.IOW);
}
/**
* Get the mean of the IRQ and SIRQ CPU usage for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average usage as a percentage (0 to 100).
*/
public static Double getIrqPercentageMean(List<CpuStats> cpuStats) {
return (getPercentageMean(cpuStats, TimeCategory.IRQ) +
getPercentageMean(cpuStats, TimeCategory.SIRQ));
}
/**
* Get the mean of the estimated MHz for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average estimated MHz in MHz.
* @see CpuStats#getEstimatedMhz()
*/
public static Double getEstimatedMhzMean(List<CpuStats> cpuStats) {
SimpleStats stats = new SimpleStats();
for (CpuStats s : cpuStats) {
if (!s.mFreqStats.isEmpty()) {
stats.add(s.getEstimatedMhz());
}
}
return stats.mean();
}
/**
* Get the mean of the used MHz for a list of {@link CpuStats}.
*
* @param cpuStats the list of {@link CpuStats}
* @return The average used MHz as a percentage (0 to 100).
* @see CpuStats#getUsedMhzPercentage()
*/
public static Double getUsedMhzPercentageMean(List<CpuStats> cpuStats) {
SimpleStats stats = new SimpleStats();
for (CpuStats s : cpuStats) {
if (!s.mFreqStats.isEmpty()) {
stats.add(s.getUsedMhzPercentage());
}
}
return stats.mean();
}
/**
* Helper method for calculating the percentage mean for a {@link TimeCategory}.
*/
private static Double getPercentageMean(List<CpuStats> cpuStats, TimeCategory category) {
SimpleStats stats = new SimpleStats();
for (CpuStats s : cpuStats) {
stats.add(s.getPercentage(category));
}
return stats.mean();
}
/**
* Method to access the receiver used to parse the cpu stats. Used for unit testing.
*/
CpuStatsReceiver getReceiver() {
return mReceiver;
}
}