#!/usr/bin/env bash # This program is part of Aspersa (http://code.google.com/p/aspersa/) # ######################################################################## # This script aggregates GDB stack traces for a selected program. By default it # does mysqld. # # Author: Baron Schwartz, based on a script by Domas Mituzas at # poormansprofiler.org # ######################################################################## # Print a usage message and exit. usage() { if [ "${OPT_ERR}" ]; then echo "${OPT_ERR}" fi cat <<-USAGE Usage: $0 [OPTIONS] [FILE] $0 does two things: 1) get a GDB backtrace 2) aggregate it. If you specify a FILE, then step 1) is not performed. Options: -b BINARY Which binary to trace (default mysqld) -i ITERATIONS How many traces to gather and aggregate (default 1) -k KEEPFILE Keep the raw traces in this file after aggregation -l NUMBER Aggregate only first NUMBER functions; 0=infinity (default 0) -p PID Process ID of the process to trace; overrides -b -s SLEEPTIME Number of seconds to sleep between iterations (default 0) USAGE exit 1 } # Actually does the aggregation. The arguments are the max number of functions # to aggregate, and the files to read. If maxlen=0, it means infinity. We have # to pass the maxlen argument into this function to make maxlen testable. aggregate_stacktrace() { maxlen="$1"; cat > /tmp/aspersa.awk <::/ ) { if ( 0 == gsub(/<[^<>]*>/, "", targ) ) { break; } } # Further shorten argument lists. while ( targ ~ /\\(/ ) { if ( 0 == gsub(/\\([^()]*\\)/, "", targ) ) { break; } } # Remove void and const decorators. gsub(/ ?(void|const) ?/, "", targ); gsub(/ /, "", targ); } else if ( targ ~ /\\?\\?/ && \$2 ~ /[1-9]/ ) { # Substitute ?? by the name of the library. targ = \$NF; while ( targ ~ /\\// ) { targ = substr(targ, index(targ, "/") + 1); } targ = substr(targ, 1, index(targ, ".") - 1); targ = targ "::??"; } } else { targ = \$2; } # get rid of long symbol names such as 'pthread_cond_wait@@GLIBC_2.3.2' if ( targ ~ /@@/ ) { fname = substr(targ, 1, index(targ, "@@") - 1); } else { fname = targ; } if ( ${maxlen:-0} == 0 || c < ${maxlen:-0} ) { if (s != "" ) { s = s "," fname; } else { s = fname; } } c++; } END { print s } EOF awk -f /tmp/aspersa.awk "$2" | sort | uniq -c | sort -r -n -k 1,1 rm -f /tmp/aspersa } # The main program to run. main() { rm -f /tmp/aspersa # Get command-line options for o; do case "${o}" in --) shift; break; ;; --help) usage; ;; -b) shift; OPT_b="${1}"; shift; ;; -i) shift; OPT_i="${1}"; shift; ;; -k) shift; OPT_k="${1}"; shift; ;; -l) shift; OPT_l="${1}"; shift; ;; -p) shift; OPT_p="${1}"; shift; ;; -s) shift; OPT_s="${1}"; shift; ;; -*) OPT_ERR="Unknown option ${o}." usage ;; esac done export OPT_i="${OPT_i:-1}"; export OPT_k="${OPT_k:-}"; export OPT_l="${OPT_l:-0}"; export OPT_b="${OPT_b:-mysqld}"; export OPT_p="${OPT_p:-}"; export OPT_s="${OPT_s:-0}"; if [ -z "${1}" ]; then # There's no file to analyze, so we'll make one. if [ -z "${OPT_p}" ]; then OPT_p=$(pidof -s "${OPT_b}" 2>/dev/null); if [ -z "${OPT_p}" ]; then OPT_p=$(pgrep -o -x "${OPT_b}" 2>/dev/null) fi if [ -z "${OPT_p}" ]; then OPT_p=$(ps -eaf | grep "${OPT_b}" | grep -v grep | awk '{print $2}' | head -n1); fi fi date; for x in $(seq 1 $OPT_i); do gdb -ex "set pagination 0" -ex "thread apply all bt" -batch -p $OPT_p >> "${OPT_k:-/tmp/aspersa}" date +'TS %N.%s %F %T' >> "${OPT_k:-/tmp/aspersa}" sleep $OPT_s done fi if [ $# -eq 0 ]; then aggregate_stacktrace "${OPT_l}" "${OPT_k:-/tmp/aspersa}" rm -f /tmp/aspersa else aggregate_stacktrace "${OPT_l}" "$@" fi } # Execute the program if it was not included from another file. This makes it # possible to include without executing, and thus test. if [ "$(basename "$0")" = "pmp" ] || [ "$(basename "$0")" = "bash" -a "$_" = "$0" ]; then main "$@" fi