1# Copyright (c) 2013 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
5import logging
6import subprocess
7import tempfile
8
9from telemetry.core import bitmap
10from telemetry.core import exceptions
11from telemetry.core import platform
12from telemetry.core import util
13from telemetry.core.platform import proc_supporting_platform_backend
14
15# Get build/android scripts into our path.
16util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
17from pylib import screenshot  # pylint: disable=F0401
18from pylib.perf import cache_control  # pylint: disable=F0401
19from pylib.perf import perf_control  # pylint: disable=F0401
20from pylib.perf import thermal_throttle  # pylint: disable=F0401
21
22try:
23  from pylib.perf import surface_stats_collector  # pylint: disable=F0401
24except Exception:
25  surface_stats_collector = None
26
27
28_HOST_APPLICATIONS = [
29    'avconv',
30    'ipfw',
31    ]
32
33
34class AndroidPlatformBackend(
35    proc_supporting_platform_backend.ProcSupportingPlatformBackend):
36  def __init__(self, adb, no_performance_mode):
37    super(AndroidPlatformBackend, self).__init__()
38    self._adb = adb
39    self._surface_stats_collector = None
40    self._perf_tests_setup = perf_control.PerfControl(self._adb)
41    self._thermal_throttle = thermal_throttle.ThermalThrottle(self._adb)
42    self._no_performance_mode = no_performance_mode
43    self._raw_display_frame_rate_measurements = []
44    self._host_platform_backend = platform.CreatePlatformBackendForCurrentOS()
45    self._can_access_protected_file_contents = \
46        self._adb.CanAccessProtectedFileContents()
47    self._video_recorder = None
48    self._video_output = None
49    if self._no_performance_mode:
50      logging.warning('CPU governor will not be set!')
51
52  def IsRawDisplayFrameRateSupported(self):
53    return True
54
55  def StartRawDisplayFrameRateMeasurement(self):
56    assert not self._surface_stats_collector
57    # Clear any leftover data from previous timed out tests
58    self._raw_display_frame_rate_measurements = []
59    self._surface_stats_collector = \
60        surface_stats_collector.SurfaceStatsCollector(self._adb)
61    self._surface_stats_collector.Start()
62
63  def StopRawDisplayFrameRateMeasurement(self):
64    self._surface_stats_collector.Stop()
65    for r in self._surface_stats_collector.GetResults():
66      self._raw_display_frame_rate_measurements.append(
67          platform.Platform.RawDisplayFrameRateMeasurement(
68              r.name, r.value, r.unit))
69
70    self._surface_stats_collector = None
71
72  def GetRawDisplayFrameRateMeasurements(self):
73    ret = self._raw_display_frame_rate_measurements
74    self._raw_display_frame_rate_measurements = []
75    return ret
76
77  def SetFullPerformanceModeEnabled(self, enabled):
78    if self._no_performance_mode:
79      return
80    if enabled:
81      self._perf_tests_setup.SetHighPerfMode()
82    else:
83      self._perf_tests_setup.SetDefaultPerfMode()
84
85  def CanMonitorThermalThrottling(self):
86    return True
87
88  def IsThermallyThrottled(self):
89    return self._thermal_throttle.IsThrottled()
90
91  def HasBeenThermallyThrottled(self):
92    return self._thermal_throttle.HasBeenThrottled()
93
94  def GetSystemCommitCharge(self):
95    for line in self._adb.RunShellCommand('dumpsys meminfo', log_result=False):
96      if line.startswith('Total PSS: '):
97        return int(line.split()[2]) * 1024
98    return 0
99
100  def GetCpuStats(self, pid):
101    if not self._can_access_protected_file_contents:
102      logging.warning('CPU stats cannot be retrieved on non-rooted device.')
103      return {}
104    return super(AndroidPlatformBackend, self).GetCpuStats(pid)
105
106  def GetCpuTimestamp(self):
107    if not self._can_access_protected_file_contents:
108      logging.warning('CPU timestamp cannot be retrieved on non-rooted device.')
109      return {}
110    return super(AndroidPlatformBackend, self).GetCpuTimestamp()
111
112  def GetMemoryStats(self, pid):
113    self._adb.PurgeUnpinnedAshmem()
114    memory_usage = self._adb.GetMemoryUsageForPid(pid)[0]
115    return {'ProportionalSetSize': memory_usage['Pss'] * 1024,
116            'SharedDirty': memory_usage['Shared_Dirty'] * 1024,
117            'PrivateDirty': memory_usage['Private_Dirty'] * 1024,
118            'VMPeak': memory_usage['VmHWM'] * 1024}
119
120  def GetIOStats(self, pid):
121    return {}
122
123  def GetChildPids(self, pid):
124    child_pids = []
125    ps = self._GetPsOutput(['pid', 'name'])
126    for curr_pid, curr_name in ps:
127      if int(curr_pid) == pid:
128        name = curr_name
129        for curr_pid, curr_name in ps:
130          if curr_name.startswith(name) and curr_name != name:
131            child_pids.append(int(curr_pid))
132        break
133    return child_pids
134
135  def GetCommandLine(self, pid):
136    ps = self._GetPsOutput(['pid', 'name'])
137    for curr_pid, curr_name in ps:
138      if int(curr_pid) == pid:
139        return curr_name
140    raise exceptions.ProcessGoneException()
141
142  def GetOSName(self):
143    return 'android'
144
145  def GetOSVersionName(self):
146    return self._adb.GetBuildId()[0]
147
148  def CanFlushIndividualFilesFromSystemCache(self):
149    return False
150
151  def FlushEntireSystemCache(self):
152    cache = cache_control.CacheControl(self._adb)
153    cache.DropRamCaches()
154
155  def FlushSystemCacheForDirectory(self, directory, ignoring=None):
156    raise NotImplementedError()
157
158  def LaunchApplication(self, application, parameters=None):
159    if application in _HOST_APPLICATIONS:
160      self._host_platform_backend.LaunchApplication(application, parameters)
161      return
162    if not parameters:
163      parameters = ''
164    self._adb.RunShellCommand('am start ' + parameters + ' ' + application)
165
166  def IsApplicationRunning(self, application):
167    if application in _HOST_APPLICATIONS:
168      return self._host_platform_backend.IsApplicationRunning(application)
169    return len(self._adb.ExtractPid(application)) > 0
170
171  def CanLaunchApplication(self, application):
172    if application in _HOST_APPLICATIONS:
173      return self._host_platform_backend.CanLaunchApplication(application)
174    return True
175
176  def InstallApplication(self, application):
177    if application in _HOST_APPLICATIONS:
178      self._host_platform_backend.InstallApplication(application)
179      return
180    raise NotImplementedError(
181        'Please teach Telemetry how to install ' + application)
182
183  def CanCaptureVideo(self):
184    return self.GetOSVersionName() >= 'K'
185
186  def StartVideoCapture(self, min_bitrate_mbps):
187    min_bitrate_mbps = max(min_bitrate_mbps, 0.1)
188    if min_bitrate_mbps > 100:
189      raise ValueError('Android video capture cannot capture at %dmbps. '
190                       'Max capture rate is 100mbps.' % min_bitrate_mbps)
191    self._video_output = tempfile.mkstemp()[1]
192    if self._video_recorder:
193      self._video_recorder.Stop()
194    self._video_recorder = screenshot.VideoRecorder(
195        self._adb, self._video_output, megabits_per_second=min_bitrate_mbps)
196    self._video_recorder.Start()
197    util.WaitFor(self._video_recorder.IsStarted, 5)
198
199  def StopVideoCapture(self):
200    assert self._video_recorder, 'Must start video capture first'
201    self._video_recorder.Stop()
202    self._video_output = self._video_recorder.Pull()
203    self._video_recorder = None
204    for frame in self._FramesFromMp4(self._video_output):
205      yield frame
206
207  def _FramesFromMp4(self, mp4_file):
208    if not self.CanLaunchApplication('avconv'):
209      self.InstallApplication('avconv')
210
211    def GetDimensions(video):
212      proc = subprocess.Popen(['avconv', '-i', video], stderr=subprocess.PIPE)
213      for line in proc.stderr.readlines():
214        if 'Video:' in line:
215          dimensions = line.split(',')[2]
216          dimensions = map(int, dimensions.split()[0].split('x'))
217          break
218      proc.wait()
219      assert dimensions, 'Failed to determine video dimensions'
220      return dimensions
221
222    def GetFrameTimestampMs(stderr):
223      """Returns the frame timestamp in integer milliseconds from the dump log.
224
225      The expected line format is:
226      '  dts=1.715  pts=1.715\n'
227
228      We have to be careful to only read a single timestamp per call to avoid
229      deadlock because avconv interleaves its writes to stdout and stderr.
230      """
231      while True:
232        line = ''
233        next_char = ''
234        while next_char != '\n':
235          next_char = stderr.read(1)
236          line += next_char
237        if 'pts=' in line:
238          return int(1000 * float(line.split('=')[-1]))
239
240    dimensions = GetDimensions(mp4_file)
241    frame_length = dimensions[0] * dimensions[1] * 3
242    frame_data = bytearray(frame_length)
243
244    # Use rawvideo so that we don't need any external library to parse frames.
245    proc = subprocess.Popen(['avconv', '-i', mp4_file, '-vcodec',
246                             'rawvideo', '-pix_fmt', 'rgb24', '-dump',
247                             '-loglevel', 'debug', '-f', 'rawvideo', '-'],
248                            stderr=subprocess.PIPE, stdout=subprocess.PIPE)
249    while True:
250      num_read = proc.stdout.readinto(frame_data)
251      if not num_read:
252        raise StopIteration
253      assert num_read == len(frame_data), 'Unexpected frame size: %d' % num_read
254      yield (GetFrameTimestampMs(proc.stderr),
255             bitmap.Bitmap(3, dimensions[0], dimensions[1], frame_data))
256
257  def _GetFileContents(self, fname):
258    if not self._can_access_protected_file_contents:
259      logging.warning('%s cannot be retrieved on non-rooted device.' % fname)
260      return ''
261    return '\n'.join(
262        self._adb.GetProtectedFileContents(fname, log_result=False))
263
264  def _GetPsOutput(self, columns, pid=None):
265    assert columns == ['pid', 'name'] or columns == ['pid'], \
266        'Only know how to return pid and name. Requested: ' + columns
267    command = 'ps'
268    if pid:
269      command += ' -p %d' % pid
270    ps = self._adb.RunShellCommand(command, log_result=False)[1:]
271    output = []
272    for line in ps:
273      data = line.split()
274      curr_pid = data[1]
275      curr_name = data[-1]
276      if columns == ['pid', 'name']:
277        output.append([curr_pid, curr_name])
278      else:
279        output.append([curr_pid])
280    return output
281