summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/tools_webrtc/android/profiling/perf_setup.sh
blob: 9c6b0f98eac847c359aacb5169bc8ea1f7e2f477 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
#!/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 <build_dir>\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 <src>/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 <src>/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