Debugging slow bash startup files
31 Oct 2017Recently I found that opening a new bash session (e.g. when opening a new
terminal window) was getting a bit slow on my machine. I take reasonable care
to make sure my dotfiles don’t get too crufty, and I keep them all in version
control.
The following is a walk through of how I went about debugging the issue.
So, how does one go about profiling what bash is doing when starting a login
shell/interactive shell?
My initial thought was to use some kind of system call tracing to see what
files were being opened/executed. dtrace exists on OS X, so let’s try that:
sudo dtruss -ef bash
Sadly, the output isn’t overly useful due to System Integrity Protection. I don’t want to boot into recovery mode, so what are our options?
I regularly add set -o xtrace to my bash scripts … would the same thing
work for my .bashrc? I added the line, and executed bash:
+ source /Users/mnewman/.bash_profile
++ export PATH=/Users/mnewman/bin:/Users/mnewman/perl5/bin:/Users/mnewman/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/mnewman/.rvm/bin
++ PATH=/Users/mnewman/bin:/Users/mnewman/perl5/bin:/Users/mnewman/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/mnewman/.rvm/bin
++ for file in ~/.{path,bash_prompt,exports,aliases,functions,extra}
++ '[' -r /Users/mnewman/.path ']'
++ for file in ~/.{path,bash_prompt,exports,aliases,functions,extra}
++ '[' -r /Users/mnewman/.bash_prompt ']'
...
It looks like that works (the above is showing the start of my .bash_profile,
which is sourced from .bashrc). There is a lot of output there though, and we
still don’t have any timing information. A little searching for variants of
bash add timestamp to each line led me to an SO
answer recommending ts. Looking at the manual page for ts:
$ man ts
NAME
ts - timestamp input
SYNOPSIS
ts [-r] [-i | -s] [format]
DESCRIPTION
ts adds a timestamp to the beginning of each line of input.
The optional format parameter controls how the timestamp is formatted, as used by strftime(3). The default format is "%b %d %H:%M:%S". In addition to the regular strftime
conversion specifications, "%.S" and "%.s" are like "%S" and "%s", but provide subsecond resolution (ie, "30.00001" and "1301682593.00001").
If the -r switch is passed, it instead converts existing timestamps in the input to relative times, such as "15m5s ago". Many common timestamp formats are supported. Note that
the Time::Duration and Date::Parse perl modules are required for this mode to work. Currently, converting localized dates is not supported.
If both -r and a format is passed, the existing timestamps are converted to the specified format.
If the -i or -s switch is passed, ts timestamps incrementally instead. In case of -i, every timestamp will be the time elapsed since the last timestamp. In case of -s, the time
elapsed since start of the program is used. The default format changes to "%H:%M:%S", and "%.S" and "%.s" can be used as well.
So far so good, it looks like we could use ts -i and get the duration of
every command! I’d like to try this out, but how can we redirect the xtrace
output to ts?
Some further Googling led me to this SO
answer, which suggests using the
BASH_XTRACEFD variable to tell bash where to write its xtrace output. After
some trial and error, I added a few lines to my .bashrc:
# open file descriptor 5 such that anything written to /dev/fd/5
# is piped through ts and then to /tmp/timestamps
exec 5> >(ts -i "%.s" >> /tmp/timestamps)
# https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html
export BASH_XTRACEFD="5"
# Enable tracing
set -x
# Source my .bash_profile script, as usual
[ -n "$PS1" ] && source ~/.bash_profile;
Upon restarting bash, this produces (a lot of) output in /tmp/timestamps, and
each line contains an incremental timestamp, like so:
0.000046 ++ which brew
0.003437 +++ brew --prefix
0.025518 ++ '[' -f /usr/local/share/bash-completion/bash_completion ']'
0.000741 +++ brew --prefix
These particular lines tell me that a brew --prefix command executed and took
20ms.
With output like the above, I had enough info to track down a couple of slow loading
scripts (like sourcing nvm.sh) and remove them from my .bashrc/.bash_profile.