Files

248 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: simpleperf_hotspots.sh <perf.data> [output-dir] [--serial SERIAL] [--first-party-regex REGEX]
Generates Simpleperf self-time, children/inclusive, and optional CSV reports
for a focused Android profiling capture.
Options:
--serial SERIAL adb serial for device-side fallback.
--first-party-regex REGEX Extra grep pattern for likely app-owned rows.
Environment:
ANDROID_PERF_FIRST_PARTY_REGEX Default first-party grep pattern
ANDROID_PERF_SERIAL adb serial for device-side fallback
ANDROID_PERF_DEVICE_DATA device perf.data path for fallback
USAGE
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
perf_data=""
output_dir=""
first_party_regex="${ANDROID_PERF_FIRST_PARTY_REGEX:-}"
serial="${ANDROID_PERF_SERIAL:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--first-party-regex)
if [[ -z "${2:-}" ]]; then
echo "--first-party-regex requires a value" >&2
usage
exit 2
fi
first_party_regex="$2"
shift 2
;;
--serial)
if [[ -z "${2:-}" ]]; then
echo "--serial requires a value" >&2
usage
exit 2
fi
serial="$2"
shift 2
;;
-*)
echo "Unknown argument: $1" >&2
usage
exit 2
;;
*)
if [[ -z "$perf_data" ]]; then
perf_data="$1"
elif [[ -z "$output_dir" ]]; then
output_dir="$1"
else
echo "Unexpected argument: $1" >&2
usage
exit 2
fi
shift
;;
esac
done
if [[ -z "$perf_data" ]]; then
usage
exit 2
fi
if [[ -n "$first_party_regex" ]]; then
set +e
awk -v re="$first_party_regex" 'BEGIN { _ = ("" ~ re); exit 0 }' >/dev/null 2>&1
regex_status=$?
set -e
if [[ $regex_status -ne 0 ]]; then
echo "Invalid --first-party-regex: $first_party_regex" >&2
exit 2
fi
fi
perf_data_exists=0
if [[ -f "$perf_data" ]]; then
perf_data_exists=1
fi
if [[ "$perf_data_exists" -eq 1 && "$perf_data" != /* ]]; then
perf_data="$(cd "$(dirname "$perf_data")" && pwd)/$(basename "$perf_data")"
fi
output_dir="${output_dir:-$(dirname "$perf_data")}"
mkdir -p "$output_dir"
if [[ "$output_dir" != /* ]]; then
output_dir="$(cd "$output_dir" && pwd)"
fi
search_roots=()
for env_var in ANDROID_NDK_HOME ANDROID_HOME ANDROID_SDK_ROOT; do
value="${!env_var:-}"
if [[ -n "$value" && -d "$value" ]]; then
search_roots+=("$value")
fi
done
default_sdk="$HOME/Library/Android/sdk"
if [[ -d "$default_sdk" ]]; then
search_roots+=("$default_sdk")
fi
android_studio_simpleperf="/Applications/Android Studio.app/Contents/plugins/android/resources/simpleperf"
if [[ -d "$android_studio_simpleperf" ]]; then
search_roots+=("$android_studio_simpleperf")
fi
simpleperf_bin=""
if command -v simpleperf >/dev/null 2>&1; then
candidate_simpleperf="$(command -v simpleperf)"
if [[ -x "$candidate_simpleperf" ]] && "$candidate_simpleperf" --version >/dev/null 2>&1; then
simpleperf_bin="$candidate_simpleperf"
fi
fi
if [[ -z "$simpleperf_bin" ]] && (( ${#search_roots[@]} > 0 )); then
while IFS= read -r simpleperf_path; do
if [[ -x "$simpleperf_path" ]] && "$simpleperf_path" --version >/dev/null 2>&1; then
simpleperf_bin="$simpleperf_path"
break
fi
done < <(
{
find "${search_roots[@]}" -path '*/darwin-*/simpleperf' -type f -print
find "${search_roots[@]}" -path '*/simpleperf' -type f -print
} 2>/dev/null | awk '!seen[$0]++'
)
fi
print_report_summary() {
local children_report="$1"
printf '\nTop inclusive rows:\n'
awk 'seen || /^(Overhead|Children)/ { seen = 1; print }' "$children_report" | sed -n '1,80p'
printf '\nLikely first-party rows:\n'
if [[ -n "$first_party_regex" ]]; then
awk -v re="$first_party_regex" '
function is_sample_row(columns) {
return columns >= 7 && $1 ~ /%$/ && $2 ~ /%$/
}
{
sub(/^[[:space:]]+/, "")
columns = split($0, fields, /[[:space:]]{2,}/)
if (is_sample_row(columns)) {
shared_object = fields[6]
symbol = fields[7]
if ((shared_object " " symbol) ~ re) {
print
found = 1
}
}
}
END {
if (!found) {
print "No rows matched the first-party regex in Shared Object or Symbol columns."
}
}
' "$children_report" | sed -n '1,120p'
else
printf 'No first-party regex configured. Re-run with --first-party-regex scoped to the app package or app-owned modules.\n'
fi
}
if [[ -z "$simpleperf_bin" ]]; then
if ! command -v adb >/dev/null 2>&1; then
echo "Host Simpleperf unavailable and adb is not installed or not on PATH." >&2
echo "Install Android platform-tools, set PATH for adb, or install host Simpleperf." >&2
exit 1
fi
if [[ -z "$serial" ]]; then
connected_devices=()
while IFS= read -r connected_device; do
connected_devices+=("$connected_device")
done < <(
adb devices 2>/dev/null |
awk 'NR > 1 && $2 == "device" { print $1 }'
)
if [[ ${#connected_devices[@]} -eq 1 ]]; then
serial="${connected_devices[0]}"
else
echo "Host Simpleperf unavailable and no adb serial was provided." >&2
echo "Pass --serial or set ANDROID_PERF_SERIAL so device-side fallback reads the intended target." >&2
adb devices >&2 || true
exit 1
fi
fi
device_perf_data="${ANDROID_PERF_DEVICE_DATA:-/data/local/tmp/perf.data}"
if [[ -n "$serial" ]] && adb -s "$serial" shell test -f "$device_perf_data" 2>/dev/null; then
self_report="$output_dir/simpleperf-self.txt"
children_report="$output_dir/simpleperf-children.txt"
adb -s "$serial" shell simpleperf report -i "$device_perf_data" > "$self_report"
adb -s "$serial" shell simpleperf report -i "$device_perf_data" --children > "$children_report"
printf 'Simpleperf binary: device %s\n' "$serial"
printf 'Wrote: %s\n' "$self_report"
printf 'Wrote: %s\n' "$children_report"
printf 'First-party regex: %s\n' "${first_party_regex:-<none>}"
print_report_summary "$children_report"
exit 0
fi
echo "Could not find host Simpleperf on PATH or under: ${search_roots[*]:-(no search roots)}" >&2
echo "Also could not use device-side fallback. Set ANDROID_PERF_SERIAL and keep perf.data on the device, or install host Simpleperf." >&2
exit 1
fi
if [[ "$perf_data_exists" -ne 1 ]]; then
echo "perf.data not found: $perf_data" >&2
exit 1
fi
self_report="$output_dir/simpleperf-self.txt"
children_report="$output_dir/simpleperf-children.txt"
csv_report="$output_dir/simpleperf.csv"
"$simpleperf_bin" report -i "$perf_data" > "$self_report"
"$simpleperf_bin" report -i "$perf_data" --children > "$children_report"
if "$simpleperf_bin" report -i "$perf_data" --children --csv > "$csv_report" 2>/dev/null; then
:
else
rm -f "$csv_report"
fi
printf 'Simpleperf binary: %s\n' "$simpleperf_bin"
printf 'Wrote: %s\n' "$self_report"
printf 'Wrote: %s\n' "$children_report"
[[ -f "$csv_report" ]] && printf 'Wrote: %s\n' "$csv_report"
printf 'First-party regex: %s\n' "${first_party_regex:-<none>}"
print_report_summary "$children_report"