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.
13 from telemetry import decorators
14 from telemetry.core import exceptions
15 from telemetry.core import platform
16 from telemetry.core import util
17 from telemetry.core import video
18 from telemetry.core.backends import adb_commands
19 from telemetry.core.platform import android_device
20 from telemetry.core.platform import linux_based_platform_backend
21 from telemetry.core.platform.power_monitor import android_ds2784_power_monitor
22 from telemetry.core.platform.power_monitor import android_dumpsys_power_monitor
23 from telemetry.core.platform.power_monitor import android_temperature_monitor
24 from telemetry.core.platform.power_monitor import monsoon_power_monitor
25 from telemetry.core.platform.power_monitor import power_monitor_controller
26 from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
27 from telemetry.util import exception_formatter
29 util.AddDirToPythonPath(util.GetChromiumSrcDir(),
30 'third_party', 'webpagereplay')
31 import adb_install_cert # pylint: disable=F0401
32 import certutils # pylint: disable=F0401
34 # Get build/android scripts into our path.
35 util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
36 from pylib import screenshot # pylint: disable=F0401
37 from pylib.device import device_errors # pylint: disable=F0401
38 from pylib.perf import cache_control # pylint: disable=F0401
39 from pylib.perf import perf_control # pylint: disable=F0401
40 from pylib.perf import thermal_throttle # pylint: disable=F0401
43 from pylib.perf import surface_stats_collector # pylint: disable=F0401
45 surface_stats_collector = None
48 class AndroidPlatformBackend(
49 linux_based_platform_backend.LinuxBasedPlatformBackend):
50 def __init__(self, device):
52 'AndroidPlatformBackend can only be initialized from remote device')
53 super(AndroidPlatformBackend, self).__init__(device)
54 self._adb = adb_commands.AdbCommands(device=device.device_id)
55 installed_prebuilt_tools = adb_commands.SetupPrebuiltTools(self._adb)
56 if not installed_prebuilt_tools:
58 '%s detected, however prebuilt android tools could not '
59 'be used. To run on Android you must build them first:\n'
60 ' $ ninja -C out/Release android_tools' % device.name)
61 raise exceptions.PlatformError()
62 # Trying to root the device, if possible.
63 if not self._adb.IsRootEnabled():
65 self._adb.EnableAdbRoot()
66 self._device = self._adb.device()
67 self._enable_performance_mode = device.enable_performance_mode
68 self._surface_stats_collector = None
69 self._perf_tests_setup = perf_control.PerfControl(self._device)
70 self._thermal_throttle = thermal_throttle.ThermalThrottle(self._device)
71 self._raw_display_frame_rate_measurements = []
72 self._can_access_protected_file_contents = \
73 self._device.old_interface.CanAccessProtectedFileContents()
74 power_controller = power_monitor_controller.PowerMonitorController([
75 monsoon_power_monitor.MonsoonPowerMonitor(self._device, self),
76 android_ds2784_power_monitor.DS2784PowerMonitor(self._device, self),
77 android_dumpsys_power_monitor.DumpsysPowerMonitor(self._device, self),
79 self._power_monitor = android_temperature_monitor.AndroidTemperatureMonitor(
80 power_controller, self._device)
81 self._video_recorder = None
82 self._installed_applications = None
84 self._wpr_ca_cert_path = None
85 self._device_cert_util = None
88 def SupportsDevice(cls, device):
89 return isinstance(device, android_device.AndroidDevice)
95 def IsRawDisplayFrameRateSupported(self):
98 def StartRawDisplayFrameRateMeasurement(self):
99 assert not self._surface_stats_collector
100 # Clear any leftover data from previous timed out tests
101 self._raw_display_frame_rate_measurements = []
102 self._surface_stats_collector = \
103 surface_stats_collector.SurfaceStatsCollector(self._device)
104 self._surface_stats_collector.Start()
106 def StopRawDisplayFrameRateMeasurement(self):
107 if not self._surface_stats_collector:
110 self._surface_stats_collector.Stop()
111 for r in self._surface_stats_collector.GetResults():
112 self._raw_display_frame_rate_measurements.append(
113 platform.Platform.RawDisplayFrameRateMeasurement(
114 r.name, r.value, r.unit))
116 self._surface_stats_collector = None
118 def GetRawDisplayFrameRateMeasurements(self):
119 ret = self._raw_display_frame_rate_measurements
120 self._raw_display_frame_rate_measurements = []
123 def SetFullPerformanceModeEnabled(self, enabled):
124 if not self._enable_performance_mode:
125 logging.warning('CPU governor will not be set!')
128 self._perf_tests_setup.SetHighPerfMode()
130 self._perf_tests_setup.SetDefaultPerfMode()
132 def CanMonitorThermalThrottling(self):
135 def IsThermallyThrottled(self):
136 return self._thermal_throttle.IsThrottled()
138 def HasBeenThermallyThrottled(self):
139 return self._thermal_throttle.HasBeenThrottled()
141 def GetCpuStats(self, pid):
142 if not self._can_access_protected_file_contents:
143 logging.warning('CPU stats cannot be retrieved on non-rooted device.')
145 return super(AndroidPlatformBackend, self).GetCpuStats(pid)
147 def GetCpuTimestamp(self):
148 if not self._can_access_protected_file_contents:
149 logging.warning('CPU timestamp cannot be retrieved on non-rooted device.')
151 return super(AndroidPlatformBackend, self).GetCpuTimestamp()
153 def PurgeUnpinnedMemory(self):
154 """Purges the unpinned ashmem memory for the whole system.
156 This can be used to make memory measurements more stable. Requires root.
158 if not self._can_access_protected_file_contents:
159 logging.warning('Cannot run purge_ashmem. Requires a rooted device.')
162 if not android_prebuilt_profiler_helper.InstallOnDevice(
163 self._device, 'purge_ashmem'):
164 raise Exception('Error installing purge_ashmem.')
165 (status, output) = self._device.old_interface.GetAndroidToolStatusAndOutput(
166 android_prebuilt_profiler_helper.GetDevicePath('purge_ashmem'),
169 raise Exception('Error while purging ashmem: ' + '\n'.join(output))
171 def GetMemoryStats(self, pid):
172 memory_usage = self._device.GetMemoryUsageForPid(pid)
175 return {'ProportionalSetSize': memory_usage['Pss'] * 1024,
176 'SharedDirty': memory_usage['Shared_Dirty'] * 1024,
177 'PrivateDirty': memory_usage['Private_Dirty'] * 1024,
178 'VMPeak': memory_usage['VmHWM'] * 1024}
180 def GetIOStats(self, pid):
183 def GetChildPids(self, pid):
185 ps = self.GetPsOutput(['pid', 'name'])
186 for curr_pid, curr_name in ps:
187 if int(curr_pid) == pid:
189 for curr_pid, curr_name in ps:
190 if curr_name.startswith(name) and curr_name != name:
191 child_pids.append(int(curr_pid))
196 def GetCommandLine(self, pid):
197 ps = self.GetPsOutput(['pid', 'name'], pid)
199 raise exceptions.ProcessGoneException()
206 def GetOSVersionName(self):
207 return self._device.GetProp('ro.build.id')[0]
209 def CanFlushIndividualFilesFromSystemCache(self):
212 def FlushEntireSystemCache(self):
213 cache = cache_control.CacheControl(self._device)
214 cache.DropRamCaches()
216 def FlushSystemCacheForDirectory(self, directory, ignoring=None):
217 raise NotImplementedError()
219 def FlushDnsCache(self):
220 self._device.RunShellCommand('ndc resolver flushdefaultif', as_root=True)
222 def StopApplication(self, application):
223 """Stop the given |application|.
226 application: The full package name string of the application to stop.
228 self._device.ForceStop(application)
230 def KillApplication(self, application):
231 """Kill the given application.
234 application: The full package name string of the application to kill.
236 # We use KillAll rather than ForceStop for efficiency reasons.
238 self._adb.device().KillAll(application, retries=0)
240 except device_errors.CommandFailedError:
243 def LaunchApplication(
244 self, application, parameters=None, elevate_privilege=False):
245 """Launches the given |application| with a list of |parameters| on the OS.
248 application: The full package name string of the application to launch.
249 parameters: A list of parameters to be passed to the ActivityManager.
250 elevate_privilege: Currently unimplemented on Android.
252 if elevate_privilege:
253 raise NotImplementedError("elevate_privilege isn't supported on android.")
256 result_lines = self._device.RunShellCommand('am start %s %s' %
257 (parameters, application))
258 for line in result_lines:
259 if line.startswith('Error: '):
260 raise ValueError('Failed to start "%s" with error\n %s' %
263 def IsApplicationRunning(self, application):
264 return len(self._device.GetPids(application)) > 0
266 def CanLaunchApplication(self, application):
267 if not self._installed_applications:
268 self._installed_applications = self._device.RunShellCommand(
270 return 'package:' + application in self._installed_applications
272 def InstallApplication(self, application):
273 self._installed_applications = None
274 self._device.Install(application)
277 def CanCaptureVideo(self):
278 return self.GetOSVersionName() >= 'K'
280 def StartVideoCapture(self, min_bitrate_mbps):
281 """Starts the video capture at specified bitrate."""
282 min_bitrate_mbps = max(min_bitrate_mbps, 0.1)
283 if min_bitrate_mbps > 100:
284 raise ValueError('Android video capture cannot capture at %dmbps. '
285 'Max capture rate is 100mbps.' % min_bitrate_mbps)
286 if self.is_video_capture_running:
287 self._video_recorder.Stop()
288 self._video_recorder = screenshot.VideoRecorder(
289 self._device, megabits_per_second=min_bitrate_mbps)
290 self._video_recorder.Start()
291 util.WaitFor(self._video_recorder.IsStarted, 5)
294 def is_video_capture_running(self):
295 return self._video_recorder is not None
297 def StopVideoCapture(self):
298 assert self.is_video_capture_running, 'Must start video capture first'
299 self._video_recorder.Stop()
300 video_file_obj = tempfile.NamedTemporaryFile()
301 self._video_recorder.Pull(video_file_obj.name)
302 self._video_recorder = None
304 return video.Video(video_file_obj)
306 def CanMonitorPower(self):
307 return self._power_monitor.CanMonitorPower()
309 def StartMonitoringPower(self, browser):
310 self._power_monitor.StartMonitoringPower(browser)
312 def StopMonitoringPower(self):
313 return self._power_monitor.StopMonitoringPower()
315 def GetFileContents(self, fname):
316 if not self._can_access_protected_file_contents:
317 logging.warning('%s cannot be retrieved on non-rooted device.' % fname)
319 return '\n'.join(self._device.ReadFile(fname, as_root=True))
321 def GetPsOutput(self, columns, pid=None):
322 assert columns == ['pid', 'name'] or columns == ['pid'], \
323 'Only know how to return pid and name. Requested: ' + columns
326 command += ' -p %d' % pid
327 ps = self._device.RunShellCommand(command)[1:]
333 if columns == ['pid', 'name']:
334 output.append([curr_pid, curr_name])
336 output.append([curr_pid])
339 def RunCommand(self, command):
340 return '\n'.join(self._device.RunShellCommand(command))
343 def ParseCStateSample(sample):
346 values = sample[cpu].splitlines()
347 # Each state has three values after excluding the time value.
348 num_states = (len(values) - 1) / 3
349 names = values[:num_states]
350 times = values[num_states:2 * num_states]
351 cstates = {'C0': int(values[-1]) * 10 ** 6}
352 for i, state in enumerate(names):
354 # The Exynos cpuidle driver for the Nexus 10 uses the name 'C0' for
356 # TODO(tmandel): We should verify that no other Android device
357 # actually reports time in C0 causing this to report active time as
360 cstates[state] = int(times[i])
361 cstates['C0'] -= int(times[i])
362 sample_stats[cpu] = cstates
365 def SetRelaxSslCheck(self, value):
366 old_flag = self._device.GetProp('socket.relaxsslcheck')
367 self._device.SetProp('socket.relaxsslcheck', value)
370 def ForwardHostToDevice(self, host_port, device_port):
371 self._adb.Forward('tcp:%d' % host_port, device_port)
373 def DismissCrashDialogIfNeeded(self):
374 """Dismiss any error dialogs.
376 Limit the number in case we have an error loop or we are failing to dismiss.
379 if not self._device.old_interface.DismissCrashDialogIfNeeded():
382 def IsAppRunning(self, process_name):
383 """Determine if the given process is running.
386 process_name: The full package name string of the process.
388 pids = self._adb.ExtractPid(process_name)
389 return len(pids) != 0
392 def wpr_ca_cert_path(self):
393 """Path to root certificate installed on browser (or None).
395 If this is set, web page replay will use it to sign HTTPS responses.
397 if self._wpr_ca_cert_path:
398 assert os.path.isfile(self._wpr_ca_cert_path)
399 return self._wpr_ca_cert_path
401 def InstallTestCa(self):
402 """Install a randomly generated root CA on the android device.
404 This allows transparent HTTPS testing with WPR server without need
405 to tweak application network stack.
408 True if the certificate installation succeeded.
410 if certutils.openssl_import_error:
412 'The OpenSSL module is unavailable. '
413 'Will fallback to ignoring certificate errors.')
417 self._wpr_ca_cert_path = os.path.join(tempfile.mkdtemp(), 'testca.pem')
418 certutils.write_dummy_ca_cert(*certutils.generate_dummy_ca_cert(),
419 cert_path=self._wpr_ca_cert_path)
420 self._device_cert_util = adb_install_cert.AndroidCertInstaller(
421 self._adb.device_serial(), None, self._wpr_ca_cert_path)
422 logging.info('Installing test certificate authority on device: %s',
423 self._adb.device_serial())
424 self._device_cert_util.install_cert(overwrite_cert=True)
426 # Fallback to ignoring certificate errors.
428 exception_formatter.PrintFormattedException(
429 msg=('Unable to install test certificate authority on device: %s. '
430 'Will fallback to ignoring certificate errors.'
431 % self._adb.device_serial()))
435 def RemoveTestCa(self):
436 """Remove root CA generated by previous call to InstallTestCa().
438 Removes the test root certificate from both the device and host machine.
440 if not self._wpr_ca_cert_path:
443 if self._device_cert_util:
444 self._device_cert_util.remove_cert()
446 shutil.rmtree(os.path.dirname(self._wpr_ca_cert_path), ignore_errors = True)
447 self._wpr_ca_cert_path = None
448 self._device_cert_util = None
450 def PushProfile(self, package, new_profile_dir):
451 """Replace application profile with files found on host machine.
453 Pushing the profile is slow, so we don't want to do it every time.
454 Avoid this by pushing to a safe location using PushChangedFiles, and
455 then copying into the correct location on each test run.
458 package: The full package name string of the application for which the
459 profile is to be updated.
460 new_profile_dir: Location where profile to be pushed is stored on the
463 (profile_parent, profile_base) = os.path.split(new_profile_dir)
464 # If the path ends with a '/' python split will return an empty string for
465 # the base name; so we now need to get the base name from the directory.
467 profile_base = os.path.basename(profile_parent)
469 saved_profile_location = '/sdcard/profile/%s' % profile_base
470 self._device.PushChangedFiles([(new_profile_dir, saved_profile_location)])
472 profile_dir = self._GetProfileDir(package)
473 self._device.old_interface.EfficientDeviceDirectoryCopy(
474 saved_profile_location, profile_dir)
475 dumpsys = self._device.RunShellCommand('dumpsys package %s' % package)
476 id_line = next(line for line in dumpsys if 'userId=' in line)
477 uid = re.search(r'\d+', id_line).group()
478 files = self._device.RunShellCommand(
479 'ls "%s"' % profile_dir, as_root=True)
481 paths = ['%s%s' % (profile_dir, f) for f in files]
483 extended_path = '%s %s/* %s/*/* %s/*/*/*' % (path, path, path, path)
484 self._device.RunShellCommand(
485 'chown %s.%s %s' % (uid, uid, extended_path))
487 def RemoveProfile(self, package, ignore_list):
488 """Delete application profile on device.
491 package: The full package name string of the application for which the
492 profile is to be deleted.
493 ignore_list: List of files to keep.
495 profile_dir = self._GetProfileDir(package)
496 files = self._device.RunShellCommand(
497 'ls "%s"' % profile_dir, as_root=True)
498 paths = ['"%s%s"' % (profile_dir, f) for f in files
499 if f not in ignore_list]
500 self._device.RunShellCommand('rm -r %s' % ' '.join(paths), as_root=True)
502 def PullProfile(self, package, output_profile_path):
503 """Copy application profile from device to host machine.
506 package: The full package name string of the application for which the
507 profile is to be copied.
508 output_profile_dir: Location where profile to be stored on host machine.
510 profile_dir = self._GetProfileDir(package)
511 logging.info("Pulling profile directory from device: '%s'->'%s'.",
512 profile_dir, output_profile_path)
513 # To minimize bandwidth it might be good to look at whether all the data
514 # pulled down is really needed e.g. .pak files.
515 if not os.path.exists(output_profile_path):
516 os.makedirs(output_profile_path)
517 files = self._device.RunShellCommand('ls "%s"' % profile_dir)
519 # Don't pull lib, since it is created by the installer.
521 source = '%s%s' % (profile_dir, f)
522 dest = os.path.join(output_profile_path, f)
523 # self._adb.Pull(source, dest) doesn't work because its timeout
524 # is fixed in android's adb_interface at 60 seconds, which may
525 # be too short to pull the cache.
526 cmd = 'pull %s %s' % (source, dest)
527 self._device.old_interface.Adb().SendCommand(cmd, timeout_time=240)
529 def _GetProfileDir(self, package):
530 """Returns the on-device location where the application profile is stored
531 based on Android convention.
534 package: The full package name string of the application.
536 return '/data/data/%s/' % package
538 def SetDebugApp(self, package):
539 """Set application to debugging.
542 package: The full package name string of the application.
544 if self._adb.IsUserBuild():
545 logging.debug('User build device, setting debug app')
546 self._device.RunShellCommand('am set-debug-app --persistent %s' % package)
548 def GetStandardOutput(self, number_of_lines=500):
549 """Returns most recent lines of logcat dump.
552 number_of_lines: Number of lines of log to return.
554 return '\n'.join(self.adb.device().RunShellCommand(
555 'logcat -d -t %d' % number_of_lines))
557 def GetStackTrace(self, target_arch):
558 """Returns stack trace.
560 The stack trace consists of raw logcat dump, logcat dump with symbols,
561 and stack info from tomstone files.
564 target_arch: String specifying device architecture (eg. arm, arm64, mips,
567 def Decorate(title, content):
568 return "%s\n%s\n%s\n" % (title, content, '*' * 80)
569 # Get the last lines of logcat (large enough to contain stacktrace)
570 logcat = self.GetStandardOutput()
571 ret = Decorate('Logcat', logcat)
572 stack = os.path.join(util.GetChromiumSrcDir(), 'third_party',
573 'android_platform', 'development', 'scripts', 'stack')
574 # Try to symbolize logcat.
575 if os.path.exists(stack):
578 cmd.append('--arch=%s' % target_arch)
579 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
580 ret += Decorate('Stack from Logcat', p.communicate(input=logcat)[0])
582 # Try to get tombstones.
583 tombstones = os.path.join(util.GetChromiumSrcDir(), 'build', 'android',
585 if os.path.exists(tombstones):
586 ret += Decorate('Tombstones',
587 subprocess.Popen([tombstones, '-w', '--device',
588 self._adb.device_serial()],
589 stdout=subprocess.PIPE).communicate()[0])