1# Copyright 2018 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 contextlib
6import os
7import shutil
8import subprocess
9import sys
10import tempfile
11
12from devil import devil_env
13from devil.android import device_signal
14from devil.android.sdk import version_codes
15from pylib import constants
16
17
18def _ProcessType(proc):
19  _, _, suffix = proc.name.partition(':')
20  if not suffix:
21    return 'browser'
22  if suffix.startswith('sandboxed_process'):
23    return 'renderer'
24  if suffix.startswith('privileged_process'):
25    return 'gpu'
26  return None
27
28
29def _GetSpecifiedPID(device, package_name, process_specifier):
30  if process_specifier is None:
31    return None
32
33  # Check for numeric PID
34  try:
35    pid = int(process_specifier)
36    return pid
37  except ValueError:
38    pass
39
40  # Check for exact process name; can be any of these formats:
41  #   <package>:<process name>, i.e. 'org.chromium.chrome:sandboxed_process0'
42  #   :<process name>, i.e. ':sandboxed_process0'
43  #   <process name>, i.e. 'sandboxed_process0'
44  full_process_name = process_specifier
45  if process_specifier.startswith(':'):
46    full_process_name = package_name + process_specifier
47  elif ':' not in process_specifier:
48    full_process_name = '%s:%s' % (package_name, process_specifier)
49  matching_processes = device.ListProcesses(full_process_name)
50  if len(matching_processes) == 1:
51    return matching_processes[0].pid
52  if len(matching_processes) > 1:
53    raise RuntimeError('Found %d processes with name "%s".' % (
54        len(matching_processes), process_specifier))
55
56  # Check for process type (i.e. 'renderer')
57  package_processes = device.ListProcesses(package_name)
58  matching_processes = [p for p in package_processes if (
59      _ProcessType(p) == process_specifier)]
60  if process_specifier == 'renderer' and len(matching_processes) > 1:
61    raise RuntimeError('Found %d renderer processes; please re-run with only '
62                       'one open tab.' % len(matching_processes))
63  if len(matching_processes) != 1:
64    raise RuntimeError('Found %d processes of type "%s".' % (
65        len(matching_processes), process_specifier))
66  return matching_processes[0].pid
67
68
69def _ThreadsForProcess(device, pid):
70  # The thread list output format for 'ps' is the same regardless of version.
71  # Here's the column headers, and a sample line for a thread belonging to
72  # pid 12345 (note that the last few columns are not aligned with headers):
73  #
74  # USER        PID   TID  PPID     VSZ    RSS WCHAN            ADDR S CMD
75  # u0_i101   12345 24680   567 1357902  97531 futex_wait_queue_me e85acd9c S \
76  #     CrRendererMain
77  if device.build_version_sdk >= version_codes.OREO:
78    pid_regex = (
79        r'^[[:graph:]]\{1,\}[[:blank:]]\{1,\}%d[[:blank:]]\{1,\}' % pid)
80    ps_cmd = "ps -T -e | grep '%s'" % pid_regex
81    ps_output_lines = device.RunShellCommand(
82        ps_cmd, shell=True, check_return=True)
83  else:
84    ps_cmd = ['ps', '-p', str(pid), '-t']
85    ps_output_lines = device.RunShellCommand(ps_cmd, check_return=True)
86  result = []
87  for l in ps_output_lines:
88    fields = l.split()
89    # fields[2] is tid, fields[-1] is thread name. Output may include an entry
90    # for the process itself with tid=pid; omit that one.
91    if fields[2] == str(pid):
92      continue
93    result.append((int(fields[2]), fields[-1]))
94  return result
95
96
97def _ThreadType(thread_name):
98  if not thread_name:
99    return 'unknown'
100  if (thread_name.startswith('Chrome_ChildIO') or
101      thread_name.startswith('Chrome_IO')):
102    return 'io'
103  if thread_name.startswith('Compositor'):
104    return 'compositor'
105  if (thread_name.startswith('ChildProcessMai') or
106      thread_name.startswith('CrGpuMain') or
107      thread_name.startswith('CrRendererMain')):
108    return 'main'
109  if thread_name.startswith('RenderThread'):
110    return 'render'
111
112
113def _GetSpecifiedTID(device, pid, thread_specifier):
114  if thread_specifier is None:
115    return None
116
117  # Check for numeric TID
118  try:
119    tid = int(thread_specifier)
120    return tid
121  except ValueError:
122    pass
123
124  # Check for thread type
125  if pid is not None:
126    matching_threads = [t for t in _ThreadsForProcess(device, pid) if (
127        _ThreadType(t[1]) == thread_specifier)]
128    if len(matching_threads) != 1:
129      raise RuntimeError('Found %d threads of type "%s".' % (
130          len(matching_threads), thread_specifier))
131    return matching_threads[0][0]
132
133  return None
134
135
136def PrepareDevice(device):
137  if device.build_version_sdk < version_codes.NOUGAT:
138    raise RuntimeError('Simpleperf profiling is only supported on Android N '
139                       'and later.')
140
141  # Necessary for profiling
142  # https://android-review.googlesource.com/c/platform/system/sepolicy/+/234400
143  device.SetProp('security.perf_harden', '0')
144
145
146def InstallSimpleperf(device, package_name):
147  package_arch = device.GetPackageArchitecture(package_name) or 'armeabi-v7a'
148  host_simpleperf_path = devil_env.config.LocalPath('simpleperf', package_arch)
149  if not host_simpleperf_path:
150    raise Exception('Could not get path to simpleperf executable on host.')
151  device_simpleperf_path = '/'.join(
152      ('/data/local/tmp/profilers', package_arch, 'simpleperf'))
153  device.PushChangedFiles([(host_simpleperf_path, device_simpleperf_path)])
154  return device_simpleperf_path
155
156
157@contextlib.contextmanager
158def RunSimpleperf(device, device_simpleperf_path, package_name,
159                  process_specifier, thread_specifier, profiler_args,
160                  host_out_path):
161  pid = _GetSpecifiedPID(device, package_name, process_specifier)
162  tid = _GetSpecifiedTID(device, pid, thread_specifier)
163  if pid is None and tid is None:
164    raise RuntimeError('Could not find specified process/thread running on '
165                       'device. Make sure the apk is already running before '
166                       'attempting to profile.')
167  profiler_args = list(profiler_args)
168  if profiler_args and profiler_args[0] == 'record':
169    profiler_args.pop(0)
170  if '--call-graph' not in profiler_args and '-g' not in profiler_args:
171    profiler_args.append('-g')
172  if '-f' not in profiler_args:
173    profiler_args.extend(('-f', '1000'))
174  device_out_path = '/data/local/tmp/perf.data'
175  if '-o' in profiler_args:
176    device_out_path = profiler_args[profiler_args.index('-o') + 1]
177  else:
178    profiler_args.extend(('-o', device_out_path))
179
180  if tid:
181    profiler_args.extend(('-t', str(tid)))
182  else:
183    profiler_args.extend(('-p', str(pid)))
184
185  adb_shell_simpleperf_process = device.adb.StartShell(
186      [device_simpleperf_path, 'record'] + profiler_args)
187
188  completed = False
189  try:
190    yield
191    completed = True
192
193  finally:
194    device.KillAll('simpleperf', signum=device_signal.SIGINT, blocking=True,
195                   quiet=True)
196    if completed:
197      adb_shell_simpleperf_process.wait()
198      device.PullFile(device_out_path, host_out_path)
199
200
201def ConvertSimpleperfToPprof(simpleperf_out_path, build_directory,
202                             pprof_out_path):
203  # The simpleperf scripts require the unstripped libs to be installed in the
204  # same directory structure as the libs on the device. Much of the logic here
205  # is just figuring out and creating the necessary directory structure, and
206  # symlinking the unstripped shared libs.
207
208  # Get the set of libs that we can symbolize
209  unstripped_lib_dir = os.path.join(build_directory, 'lib.unstripped')
210  unstripped_libs = set(
211      f for f in os.listdir(unstripped_lib_dir) if f.endswith('.so'))
212
213  # report.py will show the directory structure above the shared libs;
214  # that is the directory structure we need to recreate on the host.
215  script_dir = devil_env.config.LocalPath('simpleperf_scripts')
216  report_path = os.path.join(script_dir, 'report.py')
217  report_cmd = [sys.executable, report_path, '-i', simpleperf_out_path]
218  device_lib_path = None
219  for line in subprocess.check_output(
220      report_cmd, stderr=subprocess.STDOUT).splitlines():
221    fields = line.split()
222    if len(fields) < 5:
223      continue
224    shlib_path = fields[4]
225    shlib_dirname, shlib_basename = shlib_path.rpartition('/')[::2]
226    if shlib_basename in unstripped_libs:
227      device_lib_path = shlib_dirname
228      break
229  if not device_lib_path:
230    raise RuntimeError('No chrome-related symbols in profiling data in %s. '
231                       'Either the process was idle for the entire profiling '
232                       'period, or something went very wrong (and you should '
233                       'file a bug at crbug.com/new with component '
234                       'Speed>Tracing, and assign it to szager@chromium.org).'
235                       % simpleperf_out_path)
236
237  # Recreate the directory structure locally, and symlink unstripped libs.
238  processing_dir = tempfile.mkdtemp()
239  try:
240    processing_lib_dir = os.path.join(
241        processing_dir, 'binary_cache', device_lib_path.lstrip('/'))
242    os.makedirs(processing_lib_dir)
243    for lib in unstripped_libs:
244      unstripped_lib_path = os.path.join(unstripped_lib_dir, lib)
245      processing_lib_path = os.path.join(processing_lib_dir, lib)
246      os.symlink(unstripped_lib_path, processing_lib_path)
247
248    # Run the script to annotate symbols and convert from simpleperf format to
249    # pprof format.
250    pprof_converter_script = os.path.join(
251        script_dir, 'pprof_proto_generator.py')
252    pprof_converter_cmd = [
253        sys.executable, pprof_converter_script, '-i', simpleperf_out_path, '-o',
254        os.path.abspath(pprof_out_path), '--ndk_path',
255        constants.ANDROID_NDK_ROOT
256    ]
257    subprocess.check_output(pprof_converter_cmd, stderr=subprocess.STDOUT,
258                            cwd=processing_dir)
259  finally:
260    shutil.rmtree(processing_dir, ignore_errors=True)
261