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