blob: 22c340a721cda6ddca5612ae91ba169673b2631f [file] [log] [blame] [edit]
#!/bin/bash
# Copyright 2024 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Collect system profile information from tools like vmstat and ifconfig
# for the duration of a wrapped command, logging outputs to file.
# This also produces a chrome-tracing formatted version of the
# same data in the log file name-suffixed with .json.
readonly script="$0"
# assume script is always with path prefix, e.g. "./$script"
readonly script_dir="${script%/*}"
readonly script_basename="${script##*/}"
readonly vmstat_trace_tool="$script_dir/vmstat_trace.py"
readonly ifconfig_loop="$script_dir/ifconfig_loop.sh"
readonly ifconfig_trace_tool="$script_dir/ifconfig_trace.py"
function usage() {
cat <<EOF
Usage: $script \
--vmstat-log vmstat_logfile \
--ifconfig-log ifconfig_logfile \
[script_args] -- command...
to run a self-test:
$script --self-test
EOF
}
self_test=0
vmstat_logfile=
ifconfig_logfile=
vmstat="$(which vmstat)" || {
echo "Unable to find vmstat tool. Exiting."
exit 1
}
readonly vmstat_base="$(basename "$vmstat")"
ifconfig="$(which ifconfig)" || {
echo "Unable to find ifconfig tool. Exiting."
exit 1
}
readonly ifconfig_base="$(basename "$ifconfig_loop")"
# extract script options first
# vmstat: always include the timestamp (-t), expected by vmstat_trace.py.
vmstat_args=(-t)
ifconfig_args=()
interval=1 # seconds
prev_opt=
for opt
do
# handle --option arg
if [[ -n "$prev_opt" ]]
then
eval "$prev_opt"=\$opt
prev_opt=
shift
continue
fi
# Extract optarg from --opt=optarg
optarg=
case "$opt" in
-*=*) optarg="${opt#*=}" ;; # remove-prefix, shortest-match
esac
case "$opt" in
--ifconfig-log) prev_opt=ifconfig_logfile ;;
--ifconfig-log=*) ifconfig_logfile="$optarg" ;;
--vmstat-log) prev_opt=vmstat_logfile ;;
--vmstat-log=*) vmstat_logfile="$optarg" ;;
-n) prev_opt=interval ;;
-n=*) interval="$optarg" ;;
--vmstat-arg=*) vmstat_args+=( "$optarg" ) ;;
--ifconfig-arg=*) ifconfig_args+=( "$optarg" ) ;;
--self-test) self_test=1 ;;
--) shift ; break ;;
*) echo "Unknown $0 option: $opt" ; usage ; exit 1 ;;
esac
shift
done
[[ "$self_test" == 1 ]] || [[ -n "$vmstat_logfile" ]] || [[ -n "$ifconfig_logfile" ]] || {
echo "At least one of (--vmstat-log, --ifconfig-log) is required."
exit 1
}
# Everything else after '--' is the command to run.
cmd=("$@")
[[ "$self_test" == 1 ]] || [[ "$#" > 0 ]] || { echo "Missing command to run (after --)." ; exit 1; }
function shell_subprocesses() {
ps --ppid "$$"
}
# Find the subprocess of this shell for 'vmstat' and 'ifconfig'.
# Can't use '$!' because that points to the last command in the pipe chain.
function subprocess_pid() {
tool_basename="$1"
# ps displays only the first 15 characters of the executable.
shell_subprocesses | grep -w "${tool_basename:0:15}" | awk '{print $1;}'
}
function run_self_test() {
echo "Running self-tests only."
echo "==== Testing subprocess_pid()..."
echo "ps is $(which ps), version:"
ps --version
echo "'sleep' in the background"
sleep 10s &
echo "subprocesses of parent shell (pid=$$) are:"
shell_subprocesses
local sleep_pid="$(subprocess_pid "sleep")"
if [[ -z "$sleep_pid" ]]
then
echo "Unable to find subprocess pid for 'sleep'."
return 1
else
echo "Got a sleep pid: $sleep_pid"
kill "$sleep_pid"
echo " PASSED"
fi
}
if [[ "$self_test" == 1 ]]
then
test_status=0
run_self_test || {
test_status="$?"
echo "*** Report test failures to go/fuchsia-build-bug."
}
exit "$test_status"
fi
pids_not_found=0
shutdown_pids=()
# Tag json with the unique fx build id, if it is set. Print to stdout.
trace_preamble_args=()
[[ -z "${FX_BUILD_UUID+x}" ]] || trace_preamble_args=( --metadata="FX_BUILD_UUID:$FX_BUILD_UUID" )
if [[ -n "$vmstat_logfile" ]]
then
rm -f "$vmstat_logfile" "$vmstat_logfile.json"
touch "$vmstat_logfile"
# Launch vmstat in the background, along with any output scanners.
"$vmstat" "${vmstat_args[@]}" "$interval" | \
tee "$vmstat_logfile" | \
"$vmstat_trace_tool" "${trace_preamble_args[@]}" - \
> "$vmstat_logfile.json" &
# Terminating the 'vmstat' process should cascade to the other downstream
# processes via SIGPIPE.
readonly vmstat_pid="$(subprocess_pid "$vmstat_base")"
if [[ -n "$vmstat_pid" ]]
then shutdown_pids+=( "$vmstat_pid" )
else pids_not_found=1
fi
fi
if [[ -n "$ifconfig_logfile" ]]
then
rm -f "$ifconfig_logfile" "$ifconfig_logfile.json"
touch "$ifconfig_logfile"
# Launch ifconfig in the background, along with any output scanners.
"$ifconfig_loop" -n "$interval" "${ifconfig_args[@]}" | \
tee "$ifconfig_logfile" | \
"$ifconfig_trace_tool" "${trace_preamble_args[@]}" - \
> "$ifconfig_logfile.json" &
# Terminating the 'ifconfig_loop.sh' process should cascade to its
# subprocess and the rest of the pipe.
readonly ifconfig_pid="$(subprocess_pid "$ifconfig_base")"
if [[ -n "ifconfig_pid" ]]
then shutdown_pids+=( "$ifconfig_pid" )
else pids_not_found=1
fi
fi
# Terminate vmstat and ifconfig when main command is complete (or interrupted).
function shutdown() {
if [[ "$pids_not_found" > 0 ]]
then cat <<EOF
[$script] Warning: Unable to find at least one of the backgrounded subprocesses
that need to be terminated, so some residual subprocesses may still be running.
Look for potentially unwanted subprocesses to kill with 'ps T'.
See also known issue b/375201428.
If this issue persists, you can:
fx build-profile disable
file a go/fuchsia-build-bug
EOF
fi
if [[ "${#shutdown_pids[@]}" > 0 ]]
then kill "${shutdown_pids[@]}"
fi
}
trap shutdown EXIT
# Run the wrapped command.
cmd_status=0
"${cmd[@]}" || cmd_status="$?"
exit "$cmd_status"