Stopwatch.sh: A Simple Tool to Measure Elapsed Time

When you want to measure how long it takes for a computer to do something, there are a lot of tools at your disposal. Stopwatch.sh is one I keep up my sleeve for when none of those other tools fit.

Why

There are a bunch of reasons!

Geez, continuous integration (CI) is taking a while.

Will this deploy ever finish?

If this background job runs longer than X, it will get killed!

How

There are many ways to find out how long a command takes:

  • Some long-running tools measure themselves (like test frameworks and package managers).
  • Maybe your terminal tells you how long each command takes. (Shoutout to warp.dev.)
  • If you’re viewing output in a log that contains timestamps, you can always do the math yourself.
  • Your CI host builds durations into the UI.
  • Lastly, you can always throw time in front of a single command.

Doing Better?

Once upon a time, I was trying to understand where time was going in a CI job with hopes of improving it. The CI pipeline is comprised of multiple phases, and the web user interface (UI) helpfully tells us how long each phase takes. But, the phase contained multiple commands, and there wasn’t a great way to see how long each command was taking.

For example, here’s how GitHub Actions currently presents the output of a multi-line step:

How long do each of these commands take?

For the record, some of the other techniques above apply here. You can view a raw log with timestamps, and you could prefix each command with “time.” But I wanted to do better.

One More Way

I threw together a simple shell script and called it “stopwatch.” The idea is that you can sprinkle it among a series of commands to see how long each interval takes:


> ./stopwatch.sh 
Started stopwatch..
> echo "loading cache"
loading cache
> echo "signing in to CLI tool"
signing in to CLI tool
> ./stopwatch.sh initialization
Stopwatch: Completed interval "initialization" in 0m28s. (running total 0m28s)
> echo "apt install foo"
apt install foo
> echo "yarn install"
yarn install
> ./stopwatch.sh install dependencies
Stopwatch: Completed interval "install" in 0m21s. (running total 0m49s)
> echo "compile code"
compile code
> ./stopwatch.sh build
Stopwatch: Completed interval "build" in 0m10s. (running total 0m59s)
> echo "run tests"
run tests
> ./stopwatch.sh test
Stopwatch: Completed interval "test" in 0m16s. (running total 1m15s)
> 

Applying this to the GitHub example above, we get nice, legible durations:

That’s better.

The Script

Here’s the entire script:


#!/bin/bash

file_start=.stopwatch-start
file_prev=.stopwatch-prev

current_time=$(date +%s)

format() {
  ((m = ${1} / 60))
  ((s = ${1} % 60))
  printf "%0dm%02ds\n" "$m" "$s"
}

interval_name=$1

if test -f "$file_prev" && test -f "$file_start"; then
  read -r time_prev <$file_prev
  read -r time_start <$file_start
  if [[ "$time_start" == "" ]] || [[ "$time_prev" == "" ]]; then
    echo "unable to read start/prev times ('$time_start'/'$time_prev')"
    exit 1
  fi
  duration_total=$((current_time - time_start))
  duration_lap=$((current_time - time_prev))
  echo "Stopwatch: Completed interval \"$interval_name\" in $(format $duration_lap). (running total $(format $duration_total))"
else
  echo "Started stopwatch.."
  echo "$current_time" >$file_start
fi

echo "$current_time" >$file_prev

Another copy exists on GitHub.

Shell Scripts

For all its warts as a language, shell scripting has gained immortality by being so ubiquitous. Your project may have a better language available for small utilities like this, but if you choose sh*, then you know you’ll be able to reuse it down the road, regardless of future projects’ languages.

Still, I write them infrequently enough that it’s nice to keep a few examples around and pull from those. Hopefully, you find this one useful!

For more shell scripts, see these posts:


*Technically this post’s script is a bash script, not a POSIX shell script. To be maximally portable, you can use a `#!/bin/sh` shebang, and Shellcheck to help avoid bash-isms.