1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Utilities for iotop/top style profiling for android."""
6
7import collections
8import json
9import os
10import subprocess
11import sys
12import urllib
13
14import constants
15import io_stats_parser
16
17
18class DeviceStatsMonitor(object):
19  """Class for collecting device stats such as IO/CPU usage.
20
21  Args:
22      adb: Instance of AndroidComannds.
23      hz: Frequency at which to sample device stats.
24  """
25
26  DEVICE_PATH = constants.TEST_EXECUTABLE_DIR + '/device_stats_monitor'
27  PROFILE_PATH = (constants.DEVICE_PERF_OUTPUT_DIR +
28      '/device_stats_monitor.profile')
29  RESULT_VIEWER_PATH = os.path.abspath(os.path.join(
30      os.path.dirname(os.path.realpath(__file__)), 'device_stats_monitor.html'))
31
32  def __init__(self, adb, hz, build_type):
33    self._adb = adb
34    host_path = os.path.abspath(os.path.join(
35        constants.CHROME_DIR, 'out', build_type, 'device_stats_monitor'))
36    self._adb.PushIfNeeded(host_path, DeviceStatsMonitor.DEVICE_PATH)
37    self._hz = hz
38
39  def Start(self):
40    """Starts device stats monitor on the device."""
41    self._adb.SetFileContents(DeviceStatsMonitor.PROFILE_PATH, '')
42    self._process = subprocess.Popen(
43        ['adb', 'shell', '%s --hz=%d %s' % (
44            DeviceStatsMonitor.DEVICE_PATH, self._hz,
45            DeviceStatsMonitor.PROFILE_PATH)])
46
47  def StopAndCollect(self, output_path):
48    """Stops monitoring and saves results.
49
50    Args:
51      output_path: Path to save results.
52
53    Returns:
54      String of URL to load results in browser.
55    """
56    assert self._process
57    self._adb.KillAll(DeviceStatsMonitor.DEVICE_PATH)
58    self._process.wait()
59    profile = self._adb.GetFileContents(DeviceStatsMonitor.PROFILE_PATH)
60
61    results = collections.defaultdict(list)
62    last_io_stats = None
63    last_cpu_stats = None
64    for line in profile:
65      if ' mmcblk0 ' in line:
66        stats = io_stats_parser.ParseIoStatsLine(line)
67        if last_io_stats:
68          results['sectors_read'].append(stats.num_sectors_read -
69                                         last_io_stats.num_sectors_read)
70          results['sectors_written'].append(stats.num_sectors_written -
71                                            last_io_stats.num_sectors_written)
72        last_io_stats = stats
73      elif line.startswith('cpu '):
74        stats = self._ParseCpuStatsLine(line)
75        if last_cpu_stats:
76          results['user'].append(stats.user - last_cpu_stats.user)
77          results['nice'].append(stats.nice - last_cpu_stats.nice)
78          results['system'].append(stats.system - last_cpu_stats.system)
79          results['idle'].append(stats.idle - last_cpu_stats.idle)
80          results['iowait'].append(stats.iowait - last_cpu_stats.iowait)
81          results['irq'].append(stats.irq - last_cpu_stats.irq)
82          results['softirq'].append(stats.softirq- last_cpu_stats.softirq)
83        last_cpu_stats = stats
84    units = {
85      'sectors_read': 'sectors',
86      'sectors_written': 'sectors',
87      'user': 'jiffies',
88      'nice': 'jiffies',
89      'system': 'jiffies',
90      'idle': 'jiffies',
91      'iowait': 'jiffies',
92      'irq': 'jiffies',
93      'softirq': 'jiffies',
94    }
95    with open(output_path, 'w') as f:
96      f.write('display(%d, %s, %s);' % (self._hz, json.dumps(results), units))
97    return 'file://%s?results=file://%s' % (
98        DeviceStatsMonitor.RESULT_VIEWER_PATH, urllib.quote(output_path))
99
100
101  @staticmethod
102  def _ParseCpuStatsLine(line):
103    """Parses a line of cpu stats into a CpuStats named tuple."""
104    # Field definitions: http://www.linuxhowtos.org/System/procstat.htm
105    cpu_stats = collections.namedtuple('CpuStats',
106                                       ['device',
107                                        'user',
108                                        'nice',
109                                        'system',
110                                        'idle',
111                                        'iowait',
112                                        'irq',
113                                        'softirq',
114                                       ])
115    fields = line.split()
116    return cpu_stats._make([fields[0]] + [int(f) for f in fields[1:8]])
117