#!/bin/bash # # Copyright (C) 2017 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Note: Requires $ANDROID_BUILD_TOP/build/envsetup.sh to have been run. # # This script takes in a logcat containing Sanitizer traces and outputs several # files, prints information regarding the traces, and plots information as well. ALL_PIDS=false USE_TEMP=true DO_REDO=false PACKAGE_NAME="" BAKSMALI_NUM=0 # EXACT_ARG and MIN_ARG are passed to prune_sanitizer_output.py EXACT_ARG="" MIN_ARG=() OFFSET_ARGS=() TIME_ARGS=() usage() { echo "Usage: $0 [options] [LOGCAT_FILE] [CATEGORIES...]" echo " -a" echo " Forces all pids associated with registered dex" echo " files in the logcat to be processed." echo " default: only the last pid is processed" echo echo " -b [DEX_FILE_NUMBER]" echo " Outputs data for the specified baksmali" echo " dump if -p is provided." echo " default: first baksmali dump in order of dex" echo " file registration" echo echo " -d OUT_DIRECTORY" echo " Puts all output in specified directory." echo " If not given, output will be put in a local" echo " temp folder which will be deleted after" echo " execution." echo echo " -e" echo " All traces will have exactly the same number" echo " of categories which is specified by either" echo " the -m argument or by prune_sanitizer_output.py" echo echo " -f" echo " Forces redo of all commands even if output" echo " files exist. Steps are skipped if their output" echo " exist already and this is not enabled." echo echo " -m [MINIMUM_CALLS_PER_TRACE]" echo " Filters out all traces that do not have" echo " at least MINIMUM_CALLS_PER_TRACE lines." echo " default: specified by prune_sanitizer_output.py" echo echo " -o [OFFSET],[OFFSET]" echo " Filters out all Dex File offsets outside the" echo " range between provided offsets. 'inf' can be" echo " provided for infinity." echo " default: 0,inf" echo echo " -p [PACKAGE_NAME]" echo " Using the package name, uses baksmali to get" echo " a dump of the Dex File format for the package." echo echo " -t [TIME_OFFSET],[TIME_OFFSET]" echo " Filters out all time offsets outside the" echo " range between provided offsets. 'inf' can be" echo " provided for infinity." echo " default: 0,inf" echo echo " CATEGORIES are words that are expected to show in" echo " a large subset of symbolized traces. Splits" echo " output based on each word." echo echo " LOGCAT_FILE is the piped output from adb logcat." echo } while getopts ":ab:d:efm:o:p:t:" opt ; do case ${opt} in a) ALL_PIDS=true ;; b) if ! [[ "$OPTARG" -eq "$OPTARG" ]]; then usage exit fi BAKSMALI_NUM=$OPTARG ;; d) USE_TEMP=false OUT_DIR=$OPTARG ;; e) EXACT_ARG='-e' ;; f) DO_REDO=true ;; m) if ! [[ "$OPTARG" -eq "$OPTARG" ]]; then usage exit fi MIN_ARG=( "-m" "$OPTARG" ) ;; o) set -f old_ifs=$IFS IFS="," OFFSET_ARGS=( $OPTARG ) if [[ "${#OFFSET_ARGS[@]}" -ne 2 ]]; then usage exit fi OFFSET_ARGS=( "--offsets" "${OFFSET_ARGS[@]}" ) IFS=$old_ifs set +f ;; t) set -f old_ifs=$IFS IFS="," TIME_ARGS=( $OPTARG ) if [[ "${#TIME_ARGS[@]}" -ne 2 ]]; then usage exit fi TIME_ARGS=( "--times" "${TIME_ARGS[@]}" ) IFS=$old_ifs set +f ;; p) PACKAGE_NAME=$OPTARG ;; \?) usage exit esac done shift $((OPTIND -1)) if [[ $# -lt 1 ]]; then usage exit fi LOGCAT_FILE=$1 NUM_CAT=$(($# - 1)) # Use a temp directory that will be deleted if [[ $USE_TEMP = true ]]; then OUT_DIR=$(mktemp -d --tmpdir="$PWD") DO_REDO=true fi if [[ ! -d "$OUT_DIR" ]]; then mkdir "$OUT_DIR" DO_REDO=true fi # Note: Steps are skipped if their output exists until -f flag is enabled echo "Output folder: $OUT_DIR" # Finds the lines matching pattern criteria and prints out unique instances of # the 3rd word (PID) unique_pids=( $(awk '/RegisterDexFile:/ && !/zygote/ {if(!a[$3]++) print $3}' \ "$LOGCAT_FILE") ) echo "List of pids: ${unique_pids[@]}" if [[ $ALL_PIDS = false ]]; then unique_pids=( ${unique_pids[-1]} ) fi for pid in "${unique_pids[@]}" do echo echo "Current pid: $pid" echo pid_dir=$OUT_DIR/$pid if [[ ! -d "$pid_dir" ]]; then mkdir "$pid_dir" DO_REDO[$pid]=true fi intermediates_dir=$pid_dir/intermediates results_dir=$pid_dir/results logcat_pid_file=$pid_dir/logcat if [[ ! -f "$logcat_pid_file" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true awk "{if(\$3 == $pid) print \$0}" "$LOGCAT_FILE" > "$logcat_pid_file" fi if [[ ! -d "$intermediates_dir" ]]; then mkdir "$intermediates_dir" DO_REDO[$pid]=true fi # Step 1 - Only output lines related to Sanitizer # Folder that holds all file output asan_out=$intermediates_dir/asan_output if [[ ! -f "$asan_out" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true echo "Extracting ASAN output" grep "app_process64" "$logcat_pid_file" > "$asan_out" else echo "Skipped: Extracting ASAN output" fi # Step 2 - Only output lines containing Dex File Start Addresses dex_start=$intermediates_dir/dex_start if [[ ! -f "$dex_start" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true echo "Extracting Start of Dex File(s)" if [[ ! -z "$PACKAGE_NAME" ]]; then awk '/RegisterDexFile:/ && /'"$PACKAGE_NAME"'/ && /\/data\/app/' \ "$logcat_pid_file" > "$dex_start" else grep "RegisterDexFile:" "$logcat_pid_file" > "$dex_start" fi else echo "Skipped: Extracting Start of Dex File(s)" fi # Step 3 - Clean Sanitizer output from Step 2 since logcat cannot # handle large amounts of output. asan_out_filtered=$intermediates_dir/asan_output_filtered if [[ ! -f "$asan_out_filtered" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true echo "Filtering/Cleaning ASAN output" python "$ANDROID_BUILD_TOP"/art/tools/runtime_memusage/prune_sanitizer_output.py \ "$EXACT_ARG" "${MIN_ARG[@]}" -d "$intermediates_dir" "$asan_out" else echo "Skipped: Filtering/Cleaning ASAN output" fi # Step 4 - Retrieve symbolized stack traces from Step 3 output sym_filtered=$intermediates_dir/sym_filtered if [[ ! -f "$sym_filtered" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true echo "Retrieving symbolized traces" "$ANDROID_BUILD_TOP"/development/scripts/stack "$asan_out_filtered" \ > "$sym_filtered" else echo "Skipped: Retrieving symbolized traces" fi # Step 4.5 - Obtain Dex File Format of dex file related to package filtered_dex_start=$intermediates_dir/filtered_dex_start baksmali_dmp_ctr=0 baksmali_dmp_prefix=$intermediates_dir"/baksmali_dex_file_" baksmali_dmp_files=( $baksmali_dmp_prefix* ) baksmali_dmp_arg="--dex-file "${baksmali_dmp_files[$BAKSMALI_NUM]} apk_dex_files=( ) if [[ ! -f "$baksmali_dmp_prefix""$BAKSMALI_NUM" ]] || \ [[ ! -f "$filtered_dex_start" ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then if [[ ! -z "$PACKAGE_NAME" ]]; then DO_REDO[$pid]=true # Extracting Dex File path on device from Dex File related to package apk_directory=$(dirname "$(tail -n1 "$dex_start" | awk "{print \$8}")") for dex_file in $(awk "{print \$8}" "$dex_start"); do apk_dex_files+=( $(basename "$dex_file") ) done apk_oat_files=$(adb shell find "$apk_directory" -name "*.?dex" -type f \ 2> /dev/null) # Pulls the .odex and .vdex files associated with the package for apk_file in $apk_oat_files; do base_name=$(basename "$apk_file") adb pull "$apk_file" "$intermediates_dir/base.${base_name#*.}" done oatdump --oat-file="$intermediates_dir"/base.odex \ --export-dex-to="$intermediates_dir" --output=/dev/null for dex_file in "${apk_dex_files[@]}"; do exported_dex_file=$intermediates_dir/$dex_file"_export.dex" baksmali_dmp_out="$baksmali_dmp_prefix""$((baksmali_dmp_ctr++))" baksmali -JXmx1024M dump "$exported_dex_file" \ > "$baksmali_dmp_out" 2> "$intermediates_dir"/error if ! [[ -s "$baksmali_dmp_out" ]]; then rm "$baksmali_dmp_prefix"* baksmali_dmp_arg="" echo "Failed to retrieve Dex File format" break fi done baksmali_dmp_files=( "$baksmali_dmp_prefix"* ) baksmali_dmp_arg="--dex-file "${baksmali_dmp_files[$BAKSMALI_NUM]} # Gets the baksmali dump associated with BAKSMALI_NUM awk "NR == $((BAKSMALI_NUM + 1))" "$dex_start" > "$filtered_dex_start" results_dir=$results_dir"_"$BAKSMALI_NUM echo "Skipped: Retrieving Dex File format from baksmali; no package given" else cp "$dex_start" "$filtered_dex_start" baksmali_dmp_arg="" fi else awk "NR == $((BAKSMALI_NUM + 1))" "$dex_start" > "$filtered_dex_start" results_dir=$results_dir"_"$BAKSMALI_NUM echo "Skipped: Retrieving Dex File format from baksmali" fi if [[ ! -d "$results_dir" ]]; then mkdir "$results_dir" DO_REDO[$pid]=true fi # Step 5 - Using Steps 2, 3, 4 outputs in order to output graph data # and trace data # Only the category names are needed for the commands giving final output shift time_output=($results_dir/time_output_*.dat) if [[ ! -e ${time_output[0]} ]] || \ [[ "${DO_REDO[$pid]}" = true ]] || \ [[ $DO_REDO = true ]]; then DO_REDO[$pid]=true echo "Creating Categorized Time Table" baksmali_dmp_args=( $baksmali_dmp_arg ) python "$ANDROID_BUILD_TOP"/art/tools/runtime_memusage/symbol_trace_info.py \ -d "$results_dir" "${OFFSET_ARGS[@]}" "${baksmali_dmp_args[@]}" \ "${TIME_ARGS[@]}" "$asan_out_filtered" "$sym_filtered" \ "$filtered_dex_start" "$@" else echo "Skipped: Creating Categorized Time Table" fi # Step 6 - Use graph data from Step 5 to plot graph # Contains the category names used for legend of gnuplot plot_cats="\"Uncategorized $*\"" package_string="" dex_name="" if [[ ! -z "$PACKAGE_NAME" ]]; then package_string="Package name: $PACKAGE_NAME " fi if [[ ! -z "$baksmali_dmp_arg" ]]; then dex_file_path="$(awk "{print \$8}" "$filtered_dex_start" | tail -n1)" dex_name="Dex File name: $(basename "$dex_file_path") " fi echo "Plotting Categorized Time Table" # Plots the information from logcat gnuplot --persist -e \ 'filename(n) = sprintf("'"$results_dir"'/time_output_%d.dat", n); catnames = '"$plot_cats"'; set title "'"$package_string""$dex_name"'PID: '"$pid"'"; set xlabel "Time (milliseconds)"; set ylabel "Dex File Offset (bytes)"; plot for [i=0:'"$NUM_CAT"'] filename(i) using 1:2 title word(catnames, i + 1);' if [[ $USE_TEMP = true ]]; then echo "Removing temp directory and files" rm -rf "$OUT_DIR" fi done