1# Copyright 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 tempfile
7
8from telemetry import decorators
9from telemetry.core import exceptions
10from telemetry.core import platform
11from telemetry.core import util
12from telemetry.core import video
13from telemetry.core.backends import adb_commands
14from telemetry.core.platform import android_device
15from telemetry.core.platform import linux_based_platform_backend
16from telemetry.core.platform.power_monitor import android_ds2784_power_monitor
17from telemetry.core.platform.power_monitor import android_dumpsys_power_monitor
18from telemetry.core.platform.power_monitor import android_temperature_monitor
19from telemetry.core.platform.power_monitor import monsoon_power_monitor
20from telemetry.core.platform.power_monitor import power_monitor_controller
21from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
22
23# Get build/android scripts into our path.
24util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
25from pylib import screenshot  # pylint: disable=F0401
26from pylib.perf import cache_control  # pylint: disable=F0401
27from pylib.perf import perf_control  # pylint: disable=F0401
28from pylib.perf import thermal_throttle  # pylint: disable=F0401
29
30try:
31  from pylib.perf import surface_stats_collector  # pylint: disable=F0401
32except Exception:
33  surface_stats_collector = None
34
35
36class AndroidPlatformBackend(
37    linux_based_platform_backend.LinuxBasedPlatformBackend):
38  def __init__(self, device):
39    assert device, (
40        'AndroidPlatformBackend can only be initialized from remote device')
41    super(AndroidPlatformBackend, self).__init__(device)
42    self._adb = adb_commands.AdbCommands(device=device.device_id)
43    installed_prebuilt_tools = adb_commands.SetupPrebuiltTools(self._adb)
44    if not installed_prebuilt_tools:
45      logging.error(
46          '%s detected, however prebuilt android tools could not '
47          'be used. To run on Android you must build them first:\n'
48          '  $ ninja -C out/Release android_tools' % device.name)
49      raise exceptions.PlatformError()
50    # Trying to root the device, if possible.
51    if not self._adb.IsRootEnabled():
52      # Ignore result.
53      self._adb.EnableAdbRoot()
54    self._device = self._adb.device()
55    self._enable_performance_mode = device.enable_performance_mode
56    self._surface_stats_collector = None
57    self._perf_tests_setup = perf_control.PerfControl(self._device)
58    self._thermal_throttle = thermal_throttle.ThermalThrottle(self._device)
59    self._raw_display_frame_rate_measurements = []
60    self._can_access_protected_file_contents = \
61        self._device.old_interface.CanAccessProtectedFileContents()
62    power_controller = power_monitor_controller.PowerMonitorController([
63        monsoon_power_monitor.MonsoonPowerMonitor(self._device, self),
64        android_ds2784_power_monitor.DS2784PowerMonitor(self._device, self),
65        android_dumpsys_power_monitor.DumpsysPowerMonitor(self._device, self),
66    ])
67    self._power_monitor = android_temperature_monitor.AndroidTemperatureMonitor(
68        power_controller, self._device)
69    self._video_recorder = None
70    self._installed_applications = None
71    if self._enable_performance_mode:
72      logging.warning('CPU governor will not be set!')
73
74  @classmethod
75  def SupportsDevice(cls, device):
76    return isinstance(device, android_device.AndroidDevice)
77
78  @property
79  def adb(self):
80    return self._adb
81
82  def IsRawDisplayFrameRateSupported(self):
83    return True
84
85  def StartRawDisplayFrameRateMeasurement(self):
86    assert not self._surface_stats_collector
87    # Clear any leftover data from previous timed out tests
88    self._raw_display_frame_rate_measurements = []
89    self._surface_stats_collector = \
90        surface_stats_collector.SurfaceStatsCollector(self._device)
91    self._surface_stats_collector.Start()
92
93  def StopRawDisplayFrameRateMeasurement(self):
94    if not self._surface_stats_collector:
95      return
96
97    self._surface_stats_collector.Stop()
98    for r in self._surface_stats_collector.GetResults():
99      self._raw_display_frame_rate_measurements.append(
100          platform.Platform.RawDisplayFrameRateMeasurement(
101              r.name, r.value, r.unit))
102
103    self._surface_stats_collector = None
104
105  def GetRawDisplayFrameRateMeasurements(self):
106    ret = self._raw_display_frame_rate_measurements
107    self._raw_display_frame_rate_measurements = []
108    return ret
109
110  def SetFullPerformanceModeEnabled(self, enabled):
111    if not self._enable_performance_mode:
112      return
113    if enabled:
114      self._perf_tests_setup.SetHighPerfMode()
115    else:
116      self._perf_tests_setup.SetDefaultPerfMode()
117
118  def CanMonitorThermalThrottling(self):
119    return True
120
121  def IsThermallyThrottled(self):
122    return self._thermal_throttle.IsThrottled()
123
124  def HasBeenThermallyThrottled(self):
125    return self._thermal_throttle.HasBeenThrottled()
126
127  def GetCpuStats(self, pid):
128    if not self._can_access_protected_file_contents:
129      logging.warning('CPU stats cannot be retrieved on non-rooted device.')
130      return {}
131    return super(AndroidPlatformBackend, self).GetCpuStats(pid)
132
133  def GetCpuTimestamp(self):
134    if not self._can_access_protected_file_contents:
135      logging.warning('CPU timestamp cannot be retrieved on non-rooted device.')
136      return {}
137    return super(AndroidPlatformBackend, self).GetCpuTimestamp()
138
139  def PurgeUnpinnedMemory(self):
140    """Purges the unpinned ashmem memory for the whole system.
141
142    This can be used to make memory measurements more stable. Requires root.
143    """
144    if not self._can_access_protected_file_contents:
145      logging.warning('Cannot run purge_ashmem. Requires a rooted device.')
146      return
147
148    if not android_prebuilt_profiler_helper.InstallOnDevice(
149        self._device, 'purge_ashmem'):
150      raise Exception('Error installing purge_ashmem.')
151    (status, output) = self._device.old_interface.GetAndroidToolStatusAndOutput(
152        android_prebuilt_profiler_helper.GetDevicePath('purge_ashmem'),
153        log_result=True)
154    if status != 0:
155      raise Exception('Error while purging ashmem: ' + '\n'.join(output))
156
157  def GetMemoryStats(self, pid):
158    memory_usage = self._device.GetMemoryUsageForPid(pid)
159    if not memory_usage:
160      return {}
161    return {'ProportionalSetSize': memory_usage['Pss'] * 1024,
162            'SharedDirty': memory_usage['Shared_Dirty'] * 1024,
163            'PrivateDirty': memory_usage['Private_Dirty'] * 1024,
164            'VMPeak': memory_usage['VmHWM'] * 1024}
165
166  def GetIOStats(self, pid):
167    return {}
168
169  def GetChildPids(self, pid):
170    child_pids = []
171    ps = self.GetPsOutput(['pid', 'name'])
172    for curr_pid, curr_name in ps:
173      if int(curr_pid) == pid:
174        name = curr_name
175        for curr_pid, curr_name in ps:
176          if curr_name.startswith(name) and curr_name != name:
177            child_pids.append(int(curr_pid))
178        break
179    return child_pids
180
181  @decorators.Cache
182  def GetCommandLine(self, pid):
183    ps = self.GetPsOutput(['pid', 'name'], pid)
184    if not ps:
185      raise exceptions.ProcessGoneException()
186    return ps[0][1]
187
188  def GetOSName(self):
189    return 'android'
190
191  @decorators.Cache
192  def GetOSVersionName(self):
193    return self._device.GetProp('ro.build.id')[0]
194
195  def CanFlushIndividualFilesFromSystemCache(self):
196    return False
197
198  def FlushEntireSystemCache(self):
199    cache = cache_control.CacheControl(self._device)
200    cache.DropRamCaches()
201
202  def FlushSystemCacheForDirectory(self, directory, ignoring=None):
203    raise NotImplementedError()
204
205  def FlushDnsCache(self):
206    self._device.RunShellCommand('ndc resolver flushdefaultif', as_root=True)
207
208  def StopApplication(self, application):
209    """Stop the given |application|.
210       Args:
211       application: The full package name string of the application to launch.
212    """
213    self._adb.device().ForceStop(application)
214
215  def LaunchApplication(
216      self, application, parameters=None, elevate_privilege=False):
217    """Launches the given |application| with a list of |parameters| on the OS.
218
219    Args:
220      application: The full package name string of the application to launch.
221      parameters: A list of parameters to be passed to the ActivityManager.
222      elevate_privilege: Currently unimplemented on Android.
223    """
224    if elevate_privilege:
225      raise NotImplementedError("elevate_privilege isn't supported on android.")
226    if not parameters:
227      parameters = ''
228    result_lines = self._device.RunShellCommand('am start %s %s' %
229                                                (parameters, application))
230    for line in result_lines:
231      if line.startswith('Error: '):
232        raise ValueError('Failed to start "%s" with error\n  %s' %
233                         (application, line))
234
235  def IsApplicationRunning(self, application):
236    return len(self._device.GetPids(application)) > 0
237
238  def CanLaunchApplication(self, application):
239    if not self._installed_applications:
240      self._installed_applications = self._device.RunShellCommand(
241          'pm list packages')
242    return 'package:' + application in self._installed_applications
243
244  def InstallApplication(self, application):
245    self._installed_applications = None
246    self._device.Install(application)
247
248  @decorators.Cache
249  def CanCaptureVideo(self):
250    return self.GetOSVersionName() >= 'K'
251
252  def StartVideoCapture(self, min_bitrate_mbps):
253    """Starts the video capture at specified bitrate."""
254    min_bitrate_mbps = max(min_bitrate_mbps, 0.1)
255    if min_bitrate_mbps > 100:
256      raise ValueError('Android video capture cannot capture at %dmbps. '
257                       'Max capture rate is 100mbps.' % min_bitrate_mbps)
258    if self.is_video_capture_running:
259      self._video_recorder.Stop()
260    self._video_recorder = screenshot.VideoRecorder(
261        self._device, megabits_per_second=min_bitrate_mbps)
262    self._video_recorder.Start()
263    util.WaitFor(self._video_recorder.IsStarted, 5)
264
265  @property
266  def is_video_capture_running(self):
267    return self._video_recorder is not None
268
269  def StopVideoCapture(self):
270    assert self.is_video_capture_running, 'Must start video capture first'
271    self._video_recorder.Stop()
272    video_file_obj = tempfile.NamedTemporaryFile()
273    self._video_recorder.Pull(video_file_obj.name)
274    self._video_recorder = None
275
276    return video.Video(video_file_obj)
277
278  def CanMonitorPower(self):
279    return self._power_monitor.CanMonitorPower()
280
281  def StartMonitoringPower(self, browser):
282    self._power_monitor.StartMonitoringPower(browser)
283
284  def StopMonitoringPower(self):
285    return self._power_monitor.StopMonitoringPower()
286
287  def GetFileContents(self, fname):
288    if not self._can_access_protected_file_contents:
289      logging.warning('%s cannot be retrieved on non-rooted device.' % fname)
290      return ''
291    return '\n'.join(self._device.ReadFile(fname, as_root=True))
292
293  def GetPsOutput(self, columns, pid=None):
294    assert columns == ['pid', 'name'] or columns == ['pid'], \
295        'Only know how to return pid and name. Requested: ' + columns
296    command = 'ps'
297    if pid:
298      command += ' -p %d' % pid
299    ps = self._device.RunShellCommand(command)[1:]
300    output = []
301    for line in ps:
302      data = line.split()
303      curr_pid = data[1]
304      curr_name = data[-1]
305      if columns == ['pid', 'name']:
306        output.append([curr_pid, curr_name])
307      else:
308        output.append([curr_pid])
309    return output
310
311  def RunCommand(self, command):
312    return '\n'.join(self._device.RunShellCommand(command))
313
314  @staticmethod
315  def ParseCStateSample(sample):
316    sample_stats = {}
317    for cpu in sample:
318      values = sample[cpu].splitlines()
319      # Each state has three values after excluding the time value.
320      num_states = (len(values) - 1) / 3
321      names = values[:num_states]
322      times = values[num_states:2 * num_states]
323      cstates = {'C0': int(values[-1]) * 10 ** 6}
324      for i, state in enumerate(names):
325        if state == 'C0':
326          # The Exynos cpuidle driver for the Nexus 10 uses the name 'C0' for
327          # its WFI state.
328          # TODO(tmandel): We should verify that no other Android device
329          # actually reports time in C0 causing this to report active time as
330          # idle time.
331          state = 'WFI'
332        cstates[state] = int(times[i])
333        cstates['C0'] -= int(times[i])
334      sample_stats[cpu] = cstates
335    return sample_stats
336