#!/bin/bash # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. # # Use of this source code is governed by a BSD-style license # that can be found in the LICENSE file in the root of the source # tree. An additional intellectual property rights grant can be found # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. # # Usage: # # It is assumed that a release build of AppRTCMobile exists and has been # installed on an Android device which supports USB debugging. # # Source this script once from the WebRTC src/ directory and resolve any # reported issues. Add relative path to build directory as parameter. # Required tools will be downloaded if they don't already exist. # # Once all tests are passed, a list of available functions will be given. # Use these functions to do the actual profiling and visualization of the # results. # # Note that, using a rooted device is recommended since it allows us to # resolve kernel symbols (kallsyms) as well. # # Example usage: # # > . tools_webrtc/android/profiling/perf_setup.sh out/Release # > perf_record 120 # > flame_graph # > plot_flame_graph # > perf_cleanup if [ -n "$ZSH_VERSION" ]; then # Running inside zsh. SCRIPT_PATH="${(%):-%N}" else # Running inside something else (most likely bash). SCRIPT_PATH="${BASH_SOURCE[0]}" fi SCRIPT_DIR="$(cd $(dirname "$SCRIPT_PATH") && pwd -P)" source "${SCRIPT_DIR}/utilities.sh" # Root directory for local symbol cache. SYMBOL_DIR="${TMPDIR:-/tmp}/android_symbols" # Used as a temporary folder on the Android device for data storage. DEV_TMP_DIR="/data/local/tmp" # Relative path to native shared library containing symbols. NATIVE_LIB_PATH="/lib.unstripped/libjingle_peerconnection_so.so" # Name of application package for the AppRTCMobile demo. APP_NAME="org.appspot.apprtc" # Make sure we're being sourced. if [[ -n "${BASH_VERSION}" && "${BASH_SOURCE:-$0}" == "$0" ]]; then error "perf_setup must be sourced" exit 1 fi function usage() { printf "usage: . perf_setup.sh \n" } # Ensure that user includes name of build directory (e.g. out/Release) as # input parameter. Store path in BUILD_DIR. if [[ "$#" -eq 1 ]]; then if is_not_dir "$1"; then error "$1 is invalid" return 1 fi BUILD_DIR="$1" else error "Missing required parameter". usage return 1 fi # Full (relative) path to the libjingle_peerconnection_so.so file. function native_shared_lib_path() { echo "${BUILD_DIR}${NATIVE_LIB_PATH}" } # Target CPU architecture for the native shared library. # Example: AArch64. function native_shared_lib_arch() { readelf -h $(native_shared_lib_path) | grep Machine | awk '{print $2}' } # Returns true if the device architecture and the build target are the same. function arch_is_ok() { if [[ "$(dev_arch)" == "aarch64" ]] \ && [[ "$(native_shared_lib_arch)" == "AArch64" ]]; then return 0 elif [[ "$(dev_arch)" == "aarch32" ]] \ && [[ "$(native_shared_lib_arch)" == "AArch32" ]]; then return 0 else return 1 fi } # Copies the native shared library from the local host to the symbol cache # which is used by simpleperf as base when searching for symbols. function copy_native_shared_library_to_symbol_cache() { local arm_lib="arm" if [[ "$(native_shared_lib_arch)" == "AArch64" ]]; then arm_lib="arm64" fi for num in 1 2; do local dir="${SYMBOL_DIR}/data/app/${APP_NAME}-${num}/lib/${arm_lib}" mkdir -p "${dir}" cp -u $(native_shared_lib_path) "${dir}" done } # Copy kernel symbols from device to symbol cache in tmp. function copy_kernel_symbols_from_device_to_symbol_cache() { local symbol_cache="${SYMBOL_DIR}/kallsyms" adb pull /proc/kallsyms "${symbol_cache}" } 1> /dev/null # Download the correct version of 'simpleperf' to $DEV_TMP_DIR # on the device and enable profiling. function copy_simpleperf_to_device() { local perf_binary [[ $(dev_arch) == "aarch64" ]] \ && perf_binary="/arm64/simpleperf" \ || perf_binary="/arm/simpleperf" # Copy the simpleperf binary from local host to temp folder on device. adb push "${SCRIPT_DIR}/simpleperf/bin/android${perf_binary}" \ "${DEV_TMP_DIR}" 1> /dev/null # Copy simpleperf from temp folder to the application package. adb shell run-as "${APP_NAME}" cp "${DEV_TMP_DIR}/simpleperf" . adb shell run-as "${APP_NAME}" chmod a+x simpleperf # Enable profiling on the device. enable_profiling # Allows usage of running report commands on the device. if image_is_root; then enable_report_symbols fi } # Copy the recorded 'perf.data' file from the device to the current directory. # TODO(henrika): add support for specifying the destination. function pull_perf_data_from_device() { adb shell run-as "${APP_NAME}" cp perf.data /sdcard/perf.data adb pull sdcard/perf.data . } 1> /dev/null # Wraps calls to simpleperf report. Used by e.g. perf_report_threads. # A valid profile input file must exist in the current folder. # TODO(henrika): possibly add support to add path to alternative input file. function perf_report() { local perf_data="perf.data" is_file "${perf_data}" \ && simpleperf report \ -n \ -i "${perf_data}" \ "$@" \ || error "$(pwd)/${perf_data} is invalid" } # Removes the folder specified as input parameter. Mainly intended for removal # of simpleperf and Flame Graph tools. function remove_tool() { local tool_dir="$1" if is_dir "${tool_dir}"; then echo "Removing ${tool_dir}..." rm -rf "${tool_dir}" path_remove "${tool_dir}" fi } # Utility method which deletes the downloaded simpleperf tool from the repo. # It also removes the simpleperf root folder from PATH. function rm_simpleperf() { remove_tool "${SCRIPT_DIR}/simpleperf" } # Utility method which deletes the downloaded Flame Graph tool from the repo. # It also removes the Flame Graph root folder from PATH. function rm_flame_graph() { remove_tool "${SCRIPT_DIR}/flamegraph" } # Lists the main available functions after sourcing this script. function print_function_help() { printf "\nAvailable functions in this shell:\n" printf " perf_record [duration, default=60sec]\n" printf " perf_report_threads\n" printf " perf_report_bins\n" printf " perf_report_symbols\n" printf " perf_report_graph\n" printf " perf_report_graph_callee\n" printf " perf_update\n" printf " perf_cleanup\n" printf " flame_graph\n" printf " plot_flame_graph\n" } function cleanup() { unset -f main } # ----------------------------------------------------------------------------- # Main methods to be used after sourcing the main script. # ----------------------------------------------------------------------------- # Call this method after the application as been rebuilt and installed on the # device to ensure that symbols are up-to-date. function perf_update() { copy_native_shared_library_to_symbol_cache if image_is_root; then copy_kernel_symbols_from_device_to_symbol_cache fi } # Record stack frame based call graphs while using the application. # We use default events (cpu-cycles), and write records to 'perf.data' in the # tmp folder on the device. Default duration is 60 seconds but it can be changed # by adding one parameter. As soon as the recording is done, 'perf.data' is # copied to the directory from which this method is called and a summary of # the load distribution per thread is printed. function perf_record() { if app_is_running "${APP_NAME}"; then # Ensure that the latest native shared library exists in the local cache. copy_native_shared_library_to_symbol_cache local duration=60 if [ "$#" -eq 1 ]; then duration="$1" fi local pid=$(find_app_pid "${APP_NAME}") echo "Profiling PID $pid for $duration seconds (media must be is active)..." adb shell run-as "${APP_NAME}" ./simpleperf record \ --call-graph fp \ -p "${pid}" \ -f 1000 \ --duration "${duration}" \ --log error # Copy profile results from device to current directory. pull_perf_data_from_device # Print out a summary report (load per thread). perf_report_threads | tail -n +6 else # AppRTCMobile was not enabled. Start it up automatically and ask the user # to start media and then call this method again. warning "AppRTCMobile must be active" app_start "${APP_NAME}" echo "Start media and then call perf_record again..." fi } # Analyze the profile report and show samples per threads. function perf_report_threads() { perf_report --sort comm } 2> /dev/null # Analyze the profile report and show samples per binary. function perf_report_bins() { perf_report --sort dso } 2> /dev/null # Analyze the profile report and show samples per symbol. function perf_report_symbols() { perf_report --sort symbol --symfs "${SYMBOL_DIR}" } # Print call graph showing how functions call others. function perf_report_graph() { perf_report -g caller --symfs "${SYMBOL_DIR}" } # Print call graph showing how functions are called from others. function perf_report_graph_callee() { perf_report -g callee --symfs "${SYMBOL_DIR}" } # Plots the default Flame Graph file if no parameter is provided. # If a parameter is given, it will be used as file name instead of the default. function plot_flame_graph() { local file_name="flame_graph.svg" if [[ "$#" -eq 1 ]]; then file_name="$1" fi # Open up the SVG file in Chrome. Try unstable first and revert to stable # if unstable fails. google-chrome-unstable "${file_name}" \ || google-chrome-stable "${file_name}" \ || error "failed to find any Chrome instance" } 2> /dev/null # Generate Flame Graph in interactive SVG format. # First input parameter corresponds to output file name and second input # parameter is the heading of the plot. # Defaults will be utilized if parameters are not provided. # See https://github.com/brendangregg/FlameGraph for details on Flame Graph. function flame_graph() { local perf_data="perf.data" if is_not_file $perf_data; then error "$(pwd)/${perf_data} is invalid" return 1 fi local file_name="flame_graph.svg" local title="WebRTC Flame Graph" if [[ "$#" -eq 1 ]]; then file_name="$1" fi if [[ "$#" -eq 2 ]]; then file_name="$1" title="$2" fi if image_is_not_root; then report_sample.py \ --symfs "${SYMBOL_DIR}" \ perf.data >out.perf else report_sample.py \ --symfs "${SYMBOL_DIR}" \ --kallsyms "${SYMBOL_DIR}/kallsyms" \ perf.data >out.perf fi stackcollapse-perf.pl out.perf >out.folded flamegraph.pl --title="${title}" out.folded >"${file_name}" rm out.perf rm out.folded } # Remove all downloaded third-party tools. function perf_cleanup () { rm_simpleperf rm_flame_graph } main() { printf "%s\n" "Preparing profiling of AppRTCMobile on Android:" # Verify that this script is called from the root folder of WebRTC, # i.e., the src folder one step below where the .gclient file exists. local -r project_root_dir=$(pwd) local dir=${project_root_dir##*/} if [[ "${dir}" != "src" ]]; then error "script must be called from the WebRTC project root (src) folder" return 1 fi ok "project root: ${project_root_dir}" # Verify that user has sourced envsetup.sh. # TODO(henrika): might be possible to remove this check. if [[ -z "$ENVSETUP_GYP_CHROME_SRC" ]]; then error "must source envsetup script first" return 1 fi ok "envsetup script has been sourced" # Given that envsetup is sourced, the adb tool should be accessible but # do one extra check just in case. local adb_full_path=$(which adb); if [[ ! -x "${adb_full_path}" ]]; then error "unable to find the Android Debug Bridge (adb) tool" return 1 fi ok "adb tool is working" # Exactly one Android device must be connected. if ! one_device_connected; then error "one device must be connected" return 1 fi ok "one device is connected via USB" # Restart adb with root permissions if needed. if image_is_root && adb_has_no_root_permissions; then adb root ok "adb is running as root" fi # Create an empty symbol cache in the tmp folder. # TODO(henrika): it might not be required to start from a clean cache. is_dir "${SYMBOL_DIR}" && rm -rf "${SYMBOL_DIR}" mkdir "${SYMBOL_DIR}" \ && ok "empty symbol cache created at ${SYMBOL_DIR}" \ || error "failed to create symbol cache" # Ensure that path to the native library with symbols is valid. local native_lib=$(native_shared_lib_path) if is_not_file ${native_lib}; then error "${native_lib} is not a valid file" return 1 fi ok "native library: "${native_lib}"" # Verify that the architechture of the device matches the architecture # of the native library. if ! arch_is_ok; then error "device is $(dev_arch) and lib is $(native_shared_lib_arch)" return 1 fi ok "device is $(dev_arch) and lib is $(native_shared_lib_arch)" # Copy native shared library to symbol cache after creating an # application specific tree structure under ${SYMBOL_DIR}/data. copy_native_shared_library_to_symbol_cache ok "native library copied to ${SYMBOL_DIR}/data/app/${APP_NAME}" # Verify that the application is installed on the device. if ! app_is_installed "${APP_NAME}"; then error "${APP_NAME} is not installed on the device" return 1 fi ok "${APP_NAME} is installed on the device" # Download simpleperf to /tools_webrtc/android/profiling/simpleperf/. # Cloning will only take place if the target does not already exist. # The PATH variable will also be updated. # TODO(henrika): would it be better to use a target outside the WebRTC repo? local simpleperf_dir="${SCRIPT_DIR}/simpleperf" if is_not_dir "${simpleperf_dir}"; then echo "Dowloading simpleperf..." git clone https://android.googlesource.com/platform/prebuilts/simpleperf \ "${simpleperf_dir}" chmod u+x "${simpleperf_dir}/report_sample.py" fi path_add "${simpleperf_dir}" ok "${simpleperf_dir}" is added to PATH # Update the PATH variable with the path to the Linux version of simpleperf. local simpleperf_linux_dir="${SCRIPT_DIR}/simpleperf/bin/linux/x86_64/" if is_not_dir "${simpleperf_linux_dir}"; then error "${simpleperf_linux_dir} is invalid" return 1 fi path_add "${simpleperf_linux_dir}" ok "${simpleperf_linux_dir}" is added to PATH # Copy correct version (arm or arm64) of simpleperf to the device # and enable profiling at the same time. if ! copy_simpleperf_to_device; then error "failed to install simpleperf on the device" return 1 fi ok "simpleperf is installed on the device" # Refresh the symbol cache and read kernal symbols from device if not # already done. perf_update ok "symbol cache is updated" # Download Flame Graph to /tools_webrtc/android/profiling/flamegraph/. # Cloning will only take place if the target does not already exist. # The PATH variable will also be updated. # TODO(henrika): would it be better to use a target outside the WebRTC repo? local flamegraph_dir="${SCRIPT_DIR}/flamegraph" if is_not_dir "${flamegraph_dir}"; then echo "Dowloading Flame Graph visualization tool..." git clone https://github.com/brendangregg/FlameGraph.git "${flamegraph_dir}" fi path_add "${flamegraph_dir}" ok "${flamegraph_dir}" is added to PATH print_function_help cleanup return 0 } # Only call main() if proper input parameter has been provided. if is_set $BUILD_DIR; then main "$@" fi