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