Home | History | Annotate | Download | only in device
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tradefed.device;
     18 
     19 import com.android.ddmlib.MultiLineReceiver;
     20 import com.android.tradefed.log.LogUtil.CLog;
     21 import com.android.tradefed.util.SimpleStats;
     22 
     23 import java.io.BufferedWriter;
     24 import java.io.File;
     25 import java.io.FileWriter;
     26 import java.io.IOException;
     27 import java.util.ArrayList;
     28 import java.util.HashMap;
     29 import java.util.LinkedList;
     30 import java.util.List;
     31 import java.util.Map;
     32 
     33 /**
     34  * Helper class which runs {@code cpustats} continuously on an {@link ITestDevice} and parses the
     35  * output.
     36  * <p>
     37  * Provides a method to record the output of {@code cpustats} and get all the cpu usage measurements
     38  * as well methods for performing calculations on that data to find the mean of the cpu workload,
     39  * the approximate cpu frequency, and the percentage of used cpu frequency.
     40  * </p><p>
     41  * This is meant to be a replacement for {@link TopHelper}, which does not provide stats about cpu
     42  * frequency and has a higher overhead due to measuring processes/threads.
     43  * </p><p>
     44  * The {@code cpustats} command was added in the Jellybean release, so this collector should only be
     45  * used for new tests.
     46  * </p>
     47  * @see TopHelper
     48  */
     49 public class CpuStatsCollector extends Thread {
     50     private static final String CPU_STATS_CMD = "cpustats -m -d %s";
     51 
     52     private final ITestDevice mTestDevice;
     53     private long mDelay;
     54 
     55     /**
     56      * Used to distinguish between the different CPU time categories.
     57      */
     58     enum TimeCategory {
     59         USER,
     60         NICE,
     61         SYS,
     62         IDLE,
     63         IOW,
     64         IRQ,
     65         SIRQ
     66     }
     67 
     68     /**
     69      * Class for holding parsed output data for a single {@code cpustats} output.
     70      * <p>
     71      * This class holds parsed output, and also performs simple calculations on that data. The
     72      * methods which perform these calucations should only be called after the object has been
     73      * populated.
     74      * </p>
     75      */
     76     public static class CpuStats {
     77         public Map<TimeCategory, Integer> mTimeStats = new HashMap<TimeCategory, Integer>();
     78         public Map<Integer, Integer> mFreqStats = new HashMap<Integer, Integer>();
     79         private Map<TimeCategory, Double> mPercentageStats = new HashMap<TimeCategory, Double>();
     80         private Integer mTotalTime = null;
     81         private Double mAverageMhz = null;
     82 
     83         /**
     84          * Get the percentage of cycles used on a given category.
     85          */
     86         public Double getPercentage(TimeCategory category) {
     87             if (!mPercentageStats.containsKey(category)) {
     88                 mPercentageStats.put(category, 100.0 * mTimeStats.get(category) / getTotalTime());
     89             }
     90             return mPercentageStats.get(category);
     91         }
     92 
     93         /**
     94          * Estimate the MHz used by the cpu during the duration.
     95          * <p>
     96          * This is calculated by:
     97          * </p><code>
     98          * ((sum(c_time) - idle) / sum(c_time)) * (sum(freq * f_time) / sum(f_time))
     99          * </code><p>
    100          * where {@code c_time} is the time for a given category, {@code idle} is the time in the
    101          * idle state, {@code freq} is a frequency and {@code f_time} is the time spent in that
    102          * frequency.
    103          * </p>
    104          */
    105         public Double getEstimatedMhz() {
    106             if (mFreqStats.isEmpty()) {
    107                 return null;
    108             }
    109             return getTotalUsage() * getAverageMhz();
    110         }
    111 
    112         /**
    113          * Get the amount of MHz as a percentage of available MHz used by the cpu during the
    114          * duration.
    115          * <p>
    116          * This is calculated by:
    117          * </p><code>
    118          * 100 * sum(freq * f_time) / (max_freq * sum(f_time))
    119          * </code><p>
    120          * where {@code freq} is a frequency, {@code f_time} is the time spent in that frequency,
    121          * and {@code max_freq} is the maximum frequency the cpu is capable of.
    122          * </p>
    123          */
    124         public Double getUsedMhzPercentage() {
    125             if (mFreqStats.isEmpty()) {
    126                 return null;
    127             }
    128             return 100.0 * getAverageMhz() / getMaxMhz();
    129         }
    130 
    131         /**
    132          * Get the total usage, or the sum of all the times except idle over the sum of all the
    133          * times.
    134          */
    135         private Double getTotalUsage() {
    136             return (double) (getTotalTime() - mTimeStats.get(TimeCategory.IDLE)) / getTotalTime();
    137         }
    138 
    139         /**
    140          * Get the average MHz.
    141          * <p>
    142          * This is calculated by:
    143          * </p><code>
    144          * sum(freq * f_time) / sum(f_time))
    145          * </code><p>
    146          * where {@code freq} is a frequency and {@code f_time} is the time spent in that frequency.
    147          * </p>
    148          */
    149         private Double getAverageMhz() {
    150             if (mFreqStats.isEmpty()) {
    151                 return null;
    152             }
    153             if (mAverageMhz == null) {
    154                 double sumFreqTime = 0.0;
    155                 long sumTime = 0;
    156                 for (Map.Entry<Integer, Integer> e : mFreqStats.entrySet()) {
    157                     sumFreqTime += e.getKey() * e.getValue() / 1000.0;
    158                     sumTime += e.getValue();
    159                 }
    160                 mAverageMhz = sumFreqTime / sumTime;
    161             }
    162             return mAverageMhz;
    163         }
    164 
    165         /**
    166          * Get the maximum possible MHz.
    167          */
    168         private Double getMaxMhz() {
    169             if (mFreqStats.isEmpty()) {
    170                 return null;
    171             }
    172             int max = 0;
    173             for (int freq : mFreqStats.keySet()) {
    174                 max = Math.max(max, freq);
    175             }
    176             return max / 1000.0;
    177         }
    178 
    179         /**
    180          * Get the total amount of time cycles.
    181          */
    182         private Integer getTotalTime() {
    183             if (mTotalTime == null) {
    184                 int sum = 0;
    185                 for (int time : mTimeStats.values()) {
    186                     sum += time;
    187                 }
    188                 mTotalTime = sum;
    189             }
    190             return mTotalTime;
    191         }
    192     }
    193 
    194     /**
    195      * Receiver which parses the output from {@code cpustats} and optionally logs to a file.
    196      */
    197     public static class CpuStatsReceiver extends MultiLineReceiver {
    198         private Map<String, List<CpuStats>> mCpuStats = new HashMap<String, List<CpuStats>>(4);
    199 
    200         private boolean mIsCancelled = false;
    201         private File mLogFile = null;
    202         private BufferedWriter mLogWriter = null;
    203 
    204         public CpuStatsReceiver() {
    205             setTrimLine(false);
    206         }
    207 
    208         /**
    209          * Specify a file to log the output to.
    210          * <p>
    211          * This can be called at any time in the receivers life cycle, but only new output will be
    212          * logged to the file.
    213          * </p>
    214          */
    215         public synchronized void logToFile(File logFile) {
    216             try {
    217                 mLogFile = logFile;
    218                 mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
    219             } catch (IOException e) {
    220                 CLog.e("IOException when creating a fileWriter:");
    221                 CLog.e(e);
    222                 mLogWriter = null;
    223             }
    224         }
    225 
    226         /**
    227          * {@inheritDoc}
    228          */
    229         @Override
    230         public void processNewLines(String[] lines) {
    231             if (mIsCancelled) {
    232                 return;
    233             }
    234             synchronized (this) {
    235                 if (mLogWriter != null) {
    236                     try {
    237                         for (String line : lines) {
    238                             mLogWriter.write(line + "\n");
    239                         }
    240                     } catch (IOException e) {
    241                         CLog.e("Error writing to file");
    242                         CLog.e(e);
    243                     }
    244                 }
    245             }
    246             for (String line : lines) {
    247                 String[] args = line.trim().split(",");
    248                 if (args.length >= 8) {
    249                     try {
    250                         CpuStats s = new CpuStats();
    251                         s.mTimeStats.put(TimeCategory.USER, Integer.parseInt(args[1]));
    252                         s.mTimeStats.put(TimeCategory.NICE, Integer.parseInt(args[2]));
    253                         s.mTimeStats.put(TimeCategory.SYS, Integer.parseInt(args[3]));
    254                         s.mTimeStats.put(TimeCategory.IDLE, Integer.parseInt(args[4]));
    255                         s.mTimeStats.put(TimeCategory.IOW, Integer.parseInt(args[5]));
    256                         s.mTimeStats.put(TimeCategory.IRQ, Integer.parseInt(args[6]));
    257                         s.mTimeStats.put(TimeCategory.SIRQ, Integer.parseInt(args[7]));
    258                         for (int i = 0; i + 8 < args.length; i += 2) {
    259                             s.mFreqStats.put(Integer.parseInt(args[8 + i]),
    260                                     Integer.parseInt(args[9 + i]));
    261                         }
    262                         synchronized(this) {
    263                             if (!mCpuStats.containsKey(args[0])) {
    264                                 mCpuStats.put(args[0], new LinkedList<CpuStats>());
    265                             }
    266                             mCpuStats.get(args[0]).add(s);
    267                         }
    268                     } catch (NumberFormatException e) {
    269                         CLog.w("Unexpected input: %s", line.trim());
    270                     } catch (IndexOutOfBoundsException e) {
    271                         CLog.w("Unexpected input: %s", line.trim());
    272                     }
    273                 } else if (args.length > 1 || !"".equals(args[0])) {
    274                     CLog.w("Unexpected input: %s", line.trim());
    275                 }
    276             }
    277         }
    278 
    279         /**
    280          * Cancels the {@code cpustats} command.
    281          */
    282         public synchronized void cancel() {
    283             if (mIsCancelled) {
    284                 return;
    285             }
    286             mIsCancelled = true;
    287             if (mLogWriter != null) {
    288                 try {
    289                     mLogWriter.flush();
    290                     mLogWriter.close();
    291                 } catch (IOException e) {
    292                     CLog.e("Error closing writer");
    293                     CLog.e(e);
    294                 } finally {
    295                     mLogWriter = null;
    296                 }
    297             }
    298         }
    299 
    300         /**
    301          * {@inheritDoc}
    302          */
    303         @Override
    304         public synchronized boolean isCancelled() {
    305             return mIsCancelled;
    306         }
    307 
    308         /**
    309          * Get all the parsed data as a map from label to lists of {@link CpuStats} objects.
    310          */
    311         public synchronized Map<String, List<CpuStats>> getCpuStats() {
    312             Map<String, List<CpuStats>> copy = new HashMap<String, List<CpuStats>>(
    313                     mCpuStats.size());
    314             for (String k  : mCpuStats.keySet()) {
    315                 copy.put(k, new ArrayList<CpuStats>(mCpuStats.get(k)));
    316             }
    317             return copy;
    318         }
    319     }
    320 
    321     private CpuStatsReceiver mReceiver = new CpuStatsReceiver();
    322 
    323     /**
    324      * Create a {@link CpuStatsCollector}.
    325      *
    326      * @param testDevice The test device
    327      */
    328     public CpuStatsCollector(ITestDevice testDevice) {
    329         this(testDevice, 1);
    330     }
    331 
    332     /**
    333      * Create a {@link CpuStatsCollector} with a delay specified.
    334      *
    335      * @param testDevice The test device
    336      * @param delay The delay time in seconds
    337      */
    338     public CpuStatsCollector(ITestDevice testDevice, int delay) {
    339         super("CpuStatsCollector");
    340         mTestDevice = testDevice;
    341         mDelay = delay;
    342     }
    343 
    344     /**
    345      * Specify a file to log output to.
    346      *
    347      * @param logFile the file to log output to.
    348      */
    349     public void logToFile(File logFile) {
    350         mReceiver.logToFile(logFile);
    351     }
    352 
    353     /**
    354      * Cancels the {@code cpustats} command.
    355      */
    356     public synchronized void cancel() {
    357         mReceiver.cancel();
    358     }
    359 
    360     /**
    361      * Gets whether the {@code cpustats} command is canceled.
    362      *
    363      * @return if the {@code cpustats} command is canceled.
    364      */
    365     public synchronized boolean isCancelled() {
    366         return mReceiver.isCancelled();
    367     }
    368 
    369     /**
    370      * {@inheritDoc}
    371      */
    372     @Override
    373     public void run() {
    374         try {
    375             mTestDevice.executeShellCommand(String.format(CPU_STATS_CMD, mDelay), mReceiver);
    376         } catch (DeviceNotAvailableException e) {
    377             CLog.e("Device %s not available:", mTestDevice.getSerialNumber());
    378             CLog.e(e);
    379         }
    380     }
    381 
    382     /**
    383      * Get the mapping of labels to lists of {@link CpuStats} instances.
    384      *
    385      * @return a mapping of labels to lists of {@link CpuStats} instances. The labels will include
    386      * "Total" and "cpu0"..."cpuN" for each CPU on the device.
    387      */
    388     public Map<String, List<CpuStats>> getCpuStats() {
    389         return mReceiver.getCpuStats();
    390     }
    391 
    392     /**
    393      * Get the mean of the total CPU usage for a list of {@link CpuStats}.
    394      *
    395      * @param cpuStats the list of {@link CpuStats}
    396      * @return The average usage as a percentage (0 to 100).
    397      */
    398     public static Double getTotalPercentageMean(List<CpuStats> cpuStats) {
    399         SimpleStats stats = new SimpleStats();
    400         for (CpuStats s : cpuStats) {
    401             if (s.getTotalUsage() != null) {
    402                 stats.add(s.getTotalUsage());
    403             }
    404         }
    405         return 100 * stats.mean();
    406     }
    407 
    408     /**
    409      * Get the mean of the user and nice CPU usage for a list of {@link CpuStats}.
    410      *
    411      * @param cpuStats the list of {@link CpuStats}
    412      * @return The average usage as a percentage (0 to 100).
    413      */
    414     public static Double getUserPercentageMean(List<CpuStats> cpuStats) {
    415         return (getPercentageMean(cpuStats, TimeCategory.USER) +
    416                 getPercentageMean(cpuStats, TimeCategory.NICE));
    417     }
    418 
    419     /**
    420      * Get the mean of the system CPU usage for a list of {@link CpuStats}.
    421      *
    422      * @param cpuStats the list of {@link CpuStats}
    423      * @return The average usage as a percentage (0 to 100).
    424      */
    425     public static Double getSystemPercentageMean(List<CpuStats> cpuStats) {
    426         return getPercentageMean(cpuStats, TimeCategory.SYS);
    427     }
    428 
    429     /**
    430      * Get the mean of the iow CPU usage for a list of {@link CpuStats}.
    431      *
    432      * @param cpuStats the list of {@link CpuStats}
    433      * @return The average usage as a percentage (0 to 100).
    434      */
    435     public static Double getIowPercentageMean(List<CpuStats> cpuStats) {
    436         return getPercentageMean(cpuStats, TimeCategory.IOW);
    437     }
    438 
    439     /**
    440      * Get the mean of the IRQ and SIRQ CPU usage for a list of {@link CpuStats}.
    441      *
    442      * @param cpuStats the list of {@link CpuStats}
    443      * @return The average usage as a percentage (0 to 100).
    444      */
    445     public static Double getIrqPercentageMean(List<CpuStats> cpuStats) {
    446         return (getPercentageMean(cpuStats, TimeCategory.IRQ) +
    447                 getPercentageMean(cpuStats, TimeCategory.SIRQ));
    448     }
    449 
    450     /**
    451      * Get the mean of the estimated MHz for a list of {@link CpuStats}.
    452      *
    453      * @param cpuStats the list of {@link CpuStats}
    454      * @return The average estimated MHz in MHz.
    455      * @see CpuStats#getEstimatedMhz()
    456      */
    457     public static Double getEstimatedMhzMean(List<CpuStats> cpuStats) {
    458         SimpleStats stats = new SimpleStats();
    459         for (CpuStats s : cpuStats) {
    460             if (!s.mFreqStats.isEmpty()) {
    461                 stats.add(s.getEstimatedMhz());
    462             }
    463         }
    464         return stats.mean();
    465     }
    466 
    467     /**
    468      * Get the mean of the used MHz for a list of {@link CpuStats}.
    469      *
    470      * @param cpuStats the list of {@link CpuStats}
    471      * @return The average used MHz as a percentage (0 to 100).
    472      * @see CpuStats#getUsedMhzPercentage()
    473      */
    474     public static Double getUsedMhzPercentageMean(List<CpuStats> cpuStats) {
    475         SimpleStats stats = new SimpleStats();
    476         for (CpuStats s : cpuStats) {
    477             if (!s.mFreqStats.isEmpty()) {
    478                 stats.add(s.getUsedMhzPercentage());
    479             }
    480         }
    481         return stats.mean();
    482     }
    483 
    484     /**
    485      * Helper method for calculating the percentage mean for a {@link TimeCategory}.
    486      */
    487     private static Double getPercentageMean(List<CpuStats> cpuStats, TimeCategory category) {
    488         SimpleStats stats = new SimpleStats();
    489         for (CpuStats s : cpuStats) {
    490             stats.add(s.getPercentage(category));
    491         }
    492         return stats.mean();
    493     }
    494 
    495     /**
    496      * Method to access the receiver used to parse the cpu stats. Used for unit testing.
    497      */
    498     CpuStatsReceiver getReceiver() {
    499         return mReceiver;
    500     }
    501 }
    502