1#!/bin/bash
2
3# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
4#
5# Use of this source code is governed by a BSD-style license
6# that can be found in the LICENSE file in the root of the source
7# tree. An additional intellectual property rights grant can be found
8# in the file PATENTS.  All contributing project authors may
9# be found in the AUTHORS file in the root of the source tree.
10#
11# Usage:
12#
13#   It is assumed that a release build of AppRTCMobile exists and has been
14#   installed on an Android device which supports USB debugging.
15#
16#   Source this script once from the WebRTC src/ directory and resolve any
17#   reported issues. Add relative path to build directory as parameter.
18#   Required tools will be downloaded if they don't already exist.
19#
20#   Once all tests are passed, a list of available functions will be given.
21#   Use these functions to do the actual profiling and visualization of the
22#   results.
23#
24#   Note that, using a rooted device is recommended since it allows us to
25#   resolve kernel symbols (kallsyms) as well.
26#
27# Example usage:
28#
29#   > . tools_webrtc/android/profiling/perf_setup.sh out/Release
30#   > perf_record 120
31#   > flame_graph
32#   > plot_flame_graph
33#   > perf_cleanup
34
35if [ -n "$ZSH_VERSION" ]; then
36  # Running inside zsh.
37  SCRIPT_PATH="${(%):-%N}"
38else
39  # Running inside something else (most likely bash).
40  SCRIPT_PATH="${BASH_SOURCE[0]}"
41fi
42SCRIPT_DIR="$(cd $(dirname "$SCRIPT_PATH") && pwd -P)"
43source "${SCRIPT_DIR}/utilities.sh"
44
45# Root directory for local symbol cache.
46SYMBOL_DIR="${TMPDIR:-/tmp}/android_symbols"
47# Used as a temporary folder on the Android device for data storage.
48DEV_TMP_DIR="/data/local/tmp"
49# Relative path to native shared library containing symbols.
50NATIVE_LIB_PATH="/lib.unstripped/libjingle_peerconnection_so.so"
51# Name of application package for the AppRTCMobile demo.
52APP_NAME="org.appspot.apprtc"
53
54# Make sure we're being sourced.
55if [[ -n "${BASH_VERSION}" && "${BASH_SOURCE:-$0}" == "$0" ]]; then
56  error "perf_setup must be sourced"
57  exit 1
58fi
59
60function usage() {
61  printf "usage: . perf_setup.sh <build_dir>\n"
62}
63
64# Ensure that user includes name of build directory (e.g. out/Release) as
65# input parameter. Store path in BUILD_DIR.
66if [[ "$#" -eq 1 ]]; then
67  if is_not_dir "$1"; then
68    error "$1 is invalid"
69    return 1
70  fi
71  BUILD_DIR="$1"
72else
73  error "Missing required parameter".
74  usage
75  return 1
76fi
77
78# Full (relative) path to the libjingle_peerconnection_so.so file.
79function native_shared_lib_path() {
80  echo "${BUILD_DIR}${NATIVE_LIB_PATH}"
81}
82
83# Target CPU architecture for the native shared library.
84# Example: AArch64.
85function native_shared_lib_arch() {
86  readelf -h $(native_shared_lib_path) | grep Machine | awk '{print $2}'
87}
88
89# Returns true if the device architecture and the build target are the same.
90function arch_is_ok() {
91  if [[ "$(dev_arch)" == "aarch64" ]] \
92    && [[ "$(native_shared_lib_arch)" == "AArch64" ]]; then
93    return 0
94  elif [[ "$(dev_arch)" == "aarch32" ]] \
95    && [[ "$(native_shared_lib_arch)" == "AArch32" ]]; then
96    return 0
97  else
98    return 1
99  fi
100}
101
102# Copies the native shared library from the local host to the symbol cache
103# which is used by simpleperf as base when searching for symbols.
104function copy_native_shared_library_to_symbol_cache() {
105  local arm_lib="arm"
106  if [[ "$(native_shared_lib_arch)" == "AArch64" ]]; then
107    arm_lib="arm64"
108  fi
109  for num in 1 2; do
110    local dir="${SYMBOL_DIR}/data/app/${APP_NAME}-${num}/lib/${arm_lib}"
111    mkdir -p "${dir}"
112    cp -u $(native_shared_lib_path) "${dir}"
113  done
114}
115
116# Copy kernel symbols from device to symbol cache in tmp.
117function copy_kernel_symbols_from_device_to_symbol_cache() {
118  local symbol_cache="${SYMBOL_DIR}/kallsyms"
119  adb pull /proc/kallsyms "${symbol_cache}"
120} 1> /dev/null
121
122# Download the correct version of 'simpleperf' to $DEV_TMP_DIR
123# on the device and enable profiling.
124function copy_simpleperf_to_device() {
125  local perf_binary
126  [[ $(dev_arch) == "aarch64" ]] \
127    && perf_binary="/arm64/simpleperf" \
128    || perf_binary="/arm/simpleperf"
129  # Copy the simpleperf binary from local host to temp folder on device.
130  adb push "${SCRIPT_DIR}/simpleperf/bin/android${perf_binary}" \
131    "${DEV_TMP_DIR}" 1> /dev/null
132  # Copy simpleperf from temp folder to the application package.
133  adb shell run-as "${APP_NAME}" cp "${DEV_TMP_DIR}/simpleperf" .
134  adb shell run-as "${APP_NAME}" chmod a+x simpleperf
135  # Enable profiling on the device.
136  enable_profiling
137  # Allows usage of running report commands on the device.
138  if image_is_root; then
139    enable_report_symbols
140  fi
141}
142
143# Copy the recorded 'perf.data' file from the device to the current directory.
144# TODO(henrika): add support for specifying the destination.
145function pull_perf_data_from_device() {
146  adb shell run-as "${APP_NAME}" cp perf.data /sdcard/perf.data
147  adb pull sdcard/perf.data .
148} 1> /dev/null
149
150
151# Wraps calls to simpleperf report. Used by e.g. perf_report_threads.
152# A valid profile input file must exist in the current folder.
153# TODO(henrika): possibly add support to add path to alternative input file.
154function perf_report() {
155  local perf_data="perf.data"
156  is_file "${perf_data}" \
157    && simpleperf report \
158      -n \
159      -i "${perf_data}" \
160      "$@" \
161    || error "$(pwd)/${perf_data} is invalid"
162}
163
164# Removes the folder specified as input parameter. Mainly intended for removal
165# of simpleperf and Flame Graph tools.
166function remove_tool() {
167  local tool_dir="$1"
168  if is_dir "${tool_dir}"; then
169    echo "Removing ${tool_dir}..."
170    rm -rf "${tool_dir}"
171    path_remove "${tool_dir}"
172  fi
173}
174
175# Utility method which deletes the downloaded simpleperf tool from the repo.
176# It also removes the simpleperf root folder from PATH.
177function rm_simpleperf() {
178  remove_tool "${SCRIPT_DIR}/simpleperf"
179}
180
181# Utility method which deletes the downloaded Flame Graph tool from the repo.
182# It also removes the Flame Graph root folder from PATH.
183function rm_flame_graph() {
184  remove_tool "${SCRIPT_DIR}/flamegraph"
185}
186
187# Lists the main available functions after sourcing this script.
188function print_function_help() {
189  printf "\nAvailable functions in this shell:\n"
190  printf " perf_record [duration, default=60sec]\n"
191  printf " perf_report_threads\n"
192  printf " perf_report_bins\n"
193  printf " perf_report_symbols\n"
194  printf " perf_report_graph\n"
195  printf " perf_report_graph_callee\n"
196  printf " perf_update\n"
197  printf " perf_cleanup\n"
198  printf " flame_graph\n"
199  printf " plot_flame_graph\n"
200}
201
202function cleanup() {
203  unset -f main
204}
205
206# -----------------------------------------------------------------------------
207# Main methods to be used after sourcing the main script.
208# -----------------------------------------------------------------------------
209
210# Call this method after the application as been rebuilt and installed on the
211# device to ensure that symbols are up-to-date.
212function perf_update() {
213  copy_native_shared_library_to_symbol_cache
214  if image_is_root; then
215    copy_kernel_symbols_from_device_to_symbol_cache
216  fi
217}
218
219# Record stack frame based call graphs while using the application.
220# We use default events (cpu-cycles), and write records to 'perf.data' in the
221# tmp folder on the device. Default duration is 60 seconds but it can be changed
222# by adding one parameter. As soon as the recording is done, 'perf.data' is
223# copied to the directory from which this method is called and a summary of
224# the load distribution per thread is printed.
225function perf_record() {
226  if app_is_running "${APP_NAME}"; then
227    # Ensure that the latest native shared library exists in the local cache.
228    copy_native_shared_library_to_symbol_cache
229    local duration=60
230    if [ "$#" -eq 1 ]; then
231      duration="$1"
232    fi
233    local pid=$(find_app_pid "${APP_NAME}")
234    echo "Profiling PID $pid for $duration seconds (media must be is active)..."
235    adb shell run-as "${APP_NAME}" ./simpleperf record \
236      --call-graph fp \
237      -p "${pid}" \
238      -f 1000 \
239      --duration "${duration}" \
240      --log error
241    # Copy profile results from device to current directory.
242    pull_perf_data_from_device
243    # Print out a summary report (load per thread).
244    perf_report_threads | tail -n +6
245  else
246    # AppRTCMobile was not enabled. Start it up automatically and ask the user
247    # to start media and then call this method again.
248    warning "AppRTCMobile must be active"
249    app_start "${APP_NAME}"
250    echo "Start media and then call perf_record again..."
251  fi
252}
253
254# Analyze the profile report and show samples per threads.
255function perf_report_threads() {
256  perf_report --sort comm
257} 2> /dev/null
258
259# Analyze the profile report and show samples per binary.
260function perf_report_bins() {
261  perf_report --sort dso
262} 2> /dev/null
263
264# Analyze the profile report and show samples per symbol.
265function perf_report_symbols() {
266  perf_report --sort symbol --symfs "${SYMBOL_DIR}"
267}
268
269# Print call graph showing how functions call others.
270function perf_report_graph() {
271  perf_report -g caller --symfs "${SYMBOL_DIR}"
272}
273
274# Print call graph showing how functions are called from others.
275function perf_report_graph_callee() {
276  perf_report -g callee --symfs "${SYMBOL_DIR}"
277}
278
279# Plots the default Flame Graph file if no parameter is provided.
280# If a parameter is given, it will be used as file name instead of the default.
281function plot_flame_graph() {
282  local file_name="flame_graph.svg"
283  if [[ "$#" -eq 1 ]]; then
284    file_name="$1"
285  fi
286  # Open up the SVG file in Chrome. Try unstable first and revert to stable
287  # if unstable fails.
288  google-chrome-unstable "${file_name}" \
289    || google-chrome-stable "${file_name}" \
290    || error "failed to find any Chrome instance"
291} 2> /dev/null
292
293# Generate Flame Graph in interactive SVG format.
294# First input parameter corresponds to output file name and second input
295# parameter is the heading of the plot.
296# Defaults will be utilized if parameters are not provided.
297# See https://github.com/brendangregg/FlameGraph for details on Flame Graph.
298function flame_graph() {
299  local perf_data="perf.data"
300  if is_not_file $perf_data; then
301    error "$(pwd)/${perf_data} is invalid"
302    return 1
303  fi
304  local file_name="flame_graph.svg"
305  local title="WebRTC Flame Graph"
306  if [[ "$#" -eq 1 ]]; then
307    file_name="$1"
308  fi
309  if [[ "$#" -eq 2 ]]; then
310    file_name="$1"
311    title="$2"
312  fi
313  if image_is_not_root; then
314    report_sample.py \
315      --symfs "${SYMBOL_DIR}" \
316      perf.data >out.perf
317  else
318    report_sample.py \
319      --symfs "${SYMBOL_DIR}" \
320      --kallsyms "${SYMBOL_DIR}/kallsyms" \
321      perf.data >out.perf
322  fi
323  stackcollapse-perf.pl out.perf >out.folded
324  flamegraph.pl --title="${title}" out.folded >"${file_name}"
325  rm out.perf
326  rm out.folded
327}
328
329# Remove all downloaded third-party tools.
330function perf_cleanup () {
331  rm_simpleperf
332  rm_flame_graph
333}
334
335main() {
336  printf "%s\n" "Preparing profiling of AppRTCMobile on Android:"
337  # Verify that this script is called from the root folder of WebRTC,
338  # i.e., the src folder one step below where the .gclient file exists.
339  local -r project_root_dir=$(pwd)
340  local dir=${project_root_dir##*/}
341  if [[ "${dir}" != "src" ]]; then
342    error "script must be called from the WebRTC project root (src) folder"
343    return 1
344  fi
345  ok "project root: ${project_root_dir}"
346
347  # Verify that user has sourced envsetup.sh.
348  # TODO(henrika): might be possible to remove this check.
349  if [[ -z "$ENVSETUP_GYP_CHROME_SRC" ]]; then
350    error "must source envsetup script first"
351    return 1
352  fi
353  ok "envsetup script has been sourced"
354
355  # Given that envsetup is sourced, the adb tool should be accessible but
356  # do one extra check just in case.
357  local adb_full_path=$(which adb);
358  if [[ ! -x "${adb_full_path}" ]]; then
359    error "unable to find the Android Debug Bridge (adb) tool"
360    return 1
361  fi
362  ok "adb tool is working"
363
364  # Exactly one Android device must be connected.
365  if ! one_device_connected; then
366    error "one device must be connected"
367    return 1
368  fi
369  ok "one device is connected via USB"
370
371  # Restart adb with root permissions if needed.
372  if image_is_root && adb_has_no_root_permissions; then
373    adb root
374    ok "adb is running as root"
375  fi
376
377  # Create an empty symbol cache in the tmp folder.
378  # TODO(henrika): it might not be required to start from a clean cache.
379  is_dir "${SYMBOL_DIR}" && rm -rf "${SYMBOL_DIR}"
380  mkdir "${SYMBOL_DIR}" \
381    && ok "empty symbol cache created at ${SYMBOL_DIR}" \
382    || error "failed to create symbol cache"
383
384  # Ensure that path to the native library with symbols is valid.
385  local native_lib=$(native_shared_lib_path)
386  if is_not_file ${native_lib}; then
387    error "${native_lib} is not a valid file"
388    return 1
389  fi
390  ok "native library: "${native_lib}""
391
392  # Verify that the architechture of the device matches the architecture
393  # of the native library.
394  if ! arch_is_ok; then
395    error "device is $(dev_arch) and lib is $(native_shared_lib_arch)"
396    return 1
397  fi
398  ok "device is $(dev_arch) and lib is $(native_shared_lib_arch)"
399
400  # Copy native shared library to symbol cache after creating an
401  # application specific tree structure under ${SYMBOL_DIR}/data.
402  copy_native_shared_library_to_symbol_cache
403  ok "native library copied to ${SYMBOL_DIR}/data/app/${APP_NAME}"
404
405  # Verify that the application is installed on the device.
406  if ! app_is_installed "${APP_NAME}"; then
407    error "${APP_NAME} is not installed on the device"
408    return 1
409  fi
410  ok "${APP_NAME} is installed on the device"
411
412  # Download simpleperf to <src>/tools_webrtc/android/profiling/simpleperf/.
413  # Cloning will only take place if the target does not already exist.
414  # The PATH variable will also be updated.
415  # TODO(henrika): would it be better to use a target outside the WebRTC repo?
416  local simpleperf_dir="${SCRIPT_DIR}/simpleperf"
417  if is_not_dir "${simpleperf_dir}"; then
418    echo "Dowloading simpleperf..."
419    git clone https://android.googlesource.com/platform/prebuilts/simpleperf \
420      "${simpleperf_dir}"
421    chmod u+x "${simpleperf_dir}/report_sample.py"
422  fi
423  path_add "${simpleperf_dir}"
424  ok "${simpleperf_dir}" is added to PATH
425
426  # Update the PATH variable with the path to the Linux version of simpleperf.
427  local simpleperf_linux_dir="${SCRIPT_DIR}/simpleperf/bin/linux/x86_64/"
428  if is_not_dir "${simpleperf_linux_dir}"; then
429    error "${simpleperf_linux_dir} is invalid"
430    return 1
431  fi
432  path_add "${simpleperf_linux_dir}"
433  ok "${simpleperf_linux_dir}" is added to PATH
434
435  # Copy correct version (arm or arm64) of simpleperf to the device
436  # and enable profiling at the same time.
437  if ! copy_simpleperf_to_device; then
438    error "failed to install simpleperf on the device"
439    return 1
440  fi
441  ok "simpleperf is installed on the device"
442
443  # Refresh the symbol cache and read kernal symbols from device if not
444  # already done.
445  perf_update
446  ok "symbol cache is updated"
447
448  # Download Flame Graph to <src>/tools_webrtc/android/profiling/flamegraph/.
449  # Cloning will only take place if the target does not already exist.
450  # The PATH variable will also be updated.
451  # TODO(henrika): would it be better to use a target outside the WebRTC repo?
452  local flamegraph_dir="${SCRIPT_DIR}/flamegraph"
453  if is_not_dir "${flamegraph_dir}"; then
454    echo "Dowloading Flame Graph visualization tool..."
455    git clone https://github.com/brendangregg/FlameGraph.git "${flamegraph_dir}"
456  fi
457  path_add "${flamegraph_dir}"
458  ok "${flamegraph_dir}" is added to PATH
459
460  print_function_help
461
462  cleanup
463
464  return 0
465}
466
467# Only call main() if proper input parameter has been provided.
468if is_set $BUILD_DIR; then
469  main "$@"
470fi
471