/* * 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; } }