38

I'm attempting to speed up a collection of scripts that invoke subshells and do all sorts of things. I was wonder if there are any tools available to time the execution of a shell script and its nested shells and report on which parts of the script are the most expensive.

For example, if I had a script like the following.

#!/bin/bash

echo "hello"
echo $(date)
echo "goodbye"

I would like to know how long each of the three lines took. time will only only give me total time for the script. bash -x is interesting but does not include timestamps or other timing information.

2
  • 2
    you can put time for each of the command, like time echo "hello"
    – ajreal
    Commented Dec 2, 2010 at 14:45
  • 1
    @ajreal, that's a valid answer, why don't you post it?
    – Fred Foo
    Commented Dec 2, 2010 at 14:49

7 Answers 7

60

You can set PS4 to show the time and line number. Doing this doesn't require installing any utilities and works without redirecting stderr to stdout.

For this script:

#!/bin/bash -x
# Note the -x flag above, it is required for this to work
PS4='+ $(date "+%s.%N ($LINENO) ")'
for i in {0..2}
do
    echo $i
done
sleep 1
echo done

The output looks like:

+ PS4='+ $(date "+%s.%N ($LINENO) ")'
+ 1291311776.108610290 (3) for i in '{0..2}'
+ 1291311776.120680354 (5) echo 0
0
+ 1291311776.133917546 (3) for i in '{0..2}'
+ 1291311776.146386339 (5) echo 1
1
+ 1291311776.158646585 (3) for i in '{0..2}'
+ 1291311776.171003138 (5) echo 2
2
+ 1291311776.183450114 (7) sleep 1
+ 1291311777.203053652 (8) echo done
done

This assumes GNU date, but you can change the output specification to anything you like or whatever matches the version of date that you use.

Note: If you have an existing script that you want to do this with without modifying it, you can do this:

PS4='+ $(date "+%s.%N ($LINENO) ")' bash -x scriptname

In the upcoming Bash 5, you will be able to save forking date (but you get microseconds instead of nanoseconds):

PS4='+ $EPOCHREALTIME ($LINENO) '
2
  • 8
    Note the -x at the top in #!/bin/bash -x is important Commented May 11, 2014 at 18:36
  • 1
    I love you, will you marry me? Stackoverflow once again, for the win! Commented Jun 26, 2019 at 13:06
11

You could pipe the output of running under -x through to something that timestamps each line when it is received. For example, tai64n from djb's daemontools. At a basic example,

sh -x slow.sh 2>&1 | tai64n | tai64nlocal

This conflates stdout and stderr but it does give everything a timestamp. You'd have to then analyze the output to find expensive lines and correlate that back to your source.

You might also conceivably find using strace helpful. For example,

strace -f -ttt -T -o /tmp/analysis.txt slow.sh

This will produce a very detailed report, with lots of timing information in /tmp/analysis.txt, but at a per-system call level, which might be too detailed.

2
  • Also a good answer! I prefer the first because I don't have to find any additional packages, but this is nice that you don't have to modify the script you're profiling. Thanks! Commented Dec 3, 2010 at 14:31
  • 2
    I used this to find the slowest line in my script: github.com/brycepg/notebooks/blob/master/… Commented Nov 29, 2016 at 13:05
1

Sounds like you want to time each echo. If echo is all that you're doing this is easy

alias echo='time echo'

If you're running other command this obviously won't be sufficient.

1

Updated

See enable_profiler/disable_profiler in https://github.com/vlovich/bashrc-wrangler/blob/master/bash.d/000-setup

which is what I use now. I haven't tested on all version of BASH & specifically but if you have the ts utility installed it works very well with low overhead.

Old

My preferred approach is below. Reason is that it supports OSX as well (which doesn't have high precision date) & runs even if you don't have bc installed.

#!/bin/bash

_profiler_check_precision() {
    if [ -z "$PROFILE_HIGH_PRECISION" ]; then
        #debug "Precision of timer is unknown"
        if which bc > /dev/null 2>&1 && date '+%s.%N' | grep -vq '\.N$'; then
            PROFILE_HIGH_PRECISION=y
        else
            PROFILE_HIGH_PRECISION=n
        fi
    fi
}


_profiler_ts() {
    _profiler_check_precision

    if [ "y" = "$PROFILE_HIGH_PRECISION" ]; then
        date '+%s.%N'
    else
        date '+%s'
    fi
}

profile_mark() {
    _PROF_START="$(_profiler_ts)"
}

profile_elapsed() {
    _profiler_check_precision

    local NOW="$(_profiler_ts)"
    local ELAPSED=

    if [ "y" = "$PROFILE_HIGH_PRECISION" ]; then
        ELAPSED="$(echo "scale=10; $NOW - $_PROF_START" | bc | sed 's/\(\.[0-9]\{0,3\}\)[0-9]*$/\1/')"
    else
        ELAPSED=$((NOW - _PROF_START))
    fi

    echo "$ELAPSED"
}

do_something() {
    local _PROF_START
    profile_mark
    sleep 10
    echo "Took $(profile_elapsed) seconds"
}
7
  • The Bash builtin time will do this for you. Commented Oct 16, 2013 at 14:32
  • 1
    Note that under msys with bc installed, this script added about 300ms of overhead (roughly 150ms to call which && date | grep, and 150ms to call echo | bc | sed). Under WSL, the overhead dropped to roughly 5ms. This difference was measured against calling time on the same command.
    – Johann
    Commented Mar 31, 2020 at 14:46
  • @Pauseduntilfurthernotice. do you have any suggestion on how to actually apply this? The point of this is to enable it so that you can see how long each command took to execute to find slow downs. AFAIK time would require you to manually annotate each step.
    – Vitali
    Commented Mar 31, 2020 at 16:16
  • @Johann that's correct. msys/cygwin have terrible bash performance because the overhead of spawning an executable using win32 APIs is drastically higher on Windows than on Linux. Your perf drops back to reasonable levels on WSL because WSL uses the NT kernel directly rather than sitting on Win32 so it bypasses that overhead.
    – Vitali
    Commented Mar 31, 2020 at 16:18
  • @Vitali: See my answer which uses PS4. You are correct in that time would be cumbersome for each step. But you could use it for process of elimination by timing suspect blocks using time { a bunch of lines } and shrinking the block(s) on subsequent trials. Commented Jun 28, 2022 at 21:22
1

Here's a simple method that works on almost every Unix and needs no special software:

  • enable shell tracing, e.g. with set -x
  • pipe the output of the script through logger:
sh -x ./slow_script 2>&1 | logger

This will writes the output to syslog, which automatically adds a time stamp to every message. If you use Linux with journald, you can get high-precision time stamps using

journalctl -o short-monotonic _COMM=logger

Many traditional syslog daemons also offer high precision time stamps (milliseconds should be sufficient for shell scripts).

Here's an example from a script that I was just profiling in this manner:

[1940949.100362] bremer root[16404]: + zcat /boot/symvers-5.3.18-57-default.gz
[1940949.111138] bremer root[16404]: + '[' -e /var/tmp/weak-modules2.OmYvUn/symvers-5.3.18-57-default ']'
[1940949.111315] bremer root[16404]: + args=(-E $tmpdir/symvers-$krel)
[1940949.111484] bremer root[16404]: ++ /usr/sbin/depmod -b / -ae -E /var/tmp/weak-modules2.OmYvUn/symvers-5.3.18-57-default 5.3.18-57>
[1940952.455272] bremer root[16404]: + output=
[1940952.455738] bremer root[16404]: + status=0

where you can see that the "depmod" command is taking a lot of time.

0

Copied from here:

Since I've ended up here at least twice now, I implemented a solution:

https://github.com/walles/shellprof

It runs your script, transparently clocks all lines printed, and at the end prints a top 10 list of the lines that were on screen the longest:

~/s/shellprof (master|✔) $ ./shellprof ./testcase.sh
quick
slow
quick

Timings for printed lines:
1.01s: slow
0.00s: <<<PROGRAM START>>>
0.00s: quick
0.00s: quick
~/s/shellprof (master|✔) $
-4

I'm not aware of any shell profiling tools.

Historically one just rewrites too-slow shell scripts in Perl, Python, Ruby, or even C.

A less drastic idea would be to use a faster shell than bash. Dash and ash are available for all Unix-style systems and are typically quite a bit smaller and faster.

1
  • 10
    But how do you know that it's slow or that a faster shell would improve performance if you can't profile and so don't know where the bottleneck is?
    – sorpigal
    Commented Dec 2, 2010 at 15:55

Not the answer you're looking for? Browse other questions tagged or ask your own question.