Stratus3D

A blog on software engineering by Trevor Brown

Asdf Performance Improvements

asdf was rewritten from scratch in Go and released on January 29th. No special attention was given to performance during the rewrite. The goal was feature parity in a more maintainable language. In this post I’m going to share some benchmarks I ran across many versions of asdf and show how the Go rewrite improved performance.

This is the second post I’ve made on asdf performance. The benchmarks I showed in that previous post revealed that the asdf current and asdf reshim commands were extremely slow. asdf reshim took over 20 seconds to run, a big problem when users run this command often, and when custom shim templates often run it automatically under the hood. asdf current is one of the main commands users run to see what versions are current, so while it wasn’t quite as slow, it was still a terrible user experience.

I ended up deciding to rewrite asdf in Go from scratch. Why I chose Go over the other languages I considered is a topic for another post. But I selected Go knowing that it wouldn’t be slower than the current implementation, and would allow me to make future performance optimizations as necessary. So how much of an improvement did the rewrite make?

Benchmarks

These benchmarks were performed on the same computer the benchmarks for the previous asdf performance post:

$ lshw -short
H/W path        Device      Class          Description
======================================================
                            system         20DF0040US (LENOVO_MT_20DF_BU_Think_FM_ThinkPad E550)
/0                          bus            20DF0040US
/0/3                        memory         32KiB L1 cache
/0/4                        processor      Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
/0/4/5                      memory         32KiB L1 cache
/0/4/6                      memory         256KiB L2 cache
/0/4/7                      memory         4MiB L3 cache
/0/8                        memory         8GiB System Memory
/0/8/0                      memory         8GiB SODIMM DDR3 Synchronous 1600 MHz (0.6 ns)
/0/8/1                      memory         DIMM [empty]
/0/30                       memory         128KiB BIOS

I also only performed benchmarks for the new Go version (0.16.5 specifically). The Bash versions of asdf released after the last benchmarks I performed were skipped (0.11.x, 0.12.x, 0.13.x, 0.14.x, 0.15.x). There were no significant performance improvements made in those versions, so differences in performance between those versions and 0.10.0 would be minimal.

Like before I used hyperfine to run the benchmarks. I then used Gnuplot to generate the plots you see in this post.

Results

asdf current

The asdf current is faster than it has ever been before. It is almost 5x faster than the Bash implementation, but still takes about a third of a second to run. It’s fast enough that users will tolerate it, but maybe not fast enough to be used in a shell prompt yet.

asdf exec

From a performance perspective asdf exec is the most critical command asdf provides. All asdf shims invoke it under the hood. Every time you run irb, python, node, under the hood the shims for those commands are invoking asdf exec. The latency here is a penalty you pay every time you invoke a command that’s part of a tool managed by asdf. Thankfully it looks like the new code is over 5x faster than the old Bash code. It runs in about 20 milliseconds. This is fast enough that for a single invocation users won’t notice. And even recursive calls to asdf exec I expect performance to be tolerable for most tasks. I think there is some work we could do to improve this though, given that the Go code was written without any thought to performance.

asdf reshim

asdf reshim used to be the slowest subcommand asdf had. But now it’s almost comparable to the others. Re-generation of all shims now takes just under one second, over 20x faster than the older versions! This is a great improvement to the user experience. The large improvement here is likely because this command is mostly just reading and writing files to the file system. Generating shims isn’t an overly complicated process, but it is iterative and work scales up as the number of installed versions increases. I suspect the main thing slowing down the Bash implementation was the fact that the Bash code had to run numerous sub-processes to generate the shims. It seems this command is now fast enough that no further optimizations are needed.

asdf where

asdf where now runs in 5 milliseconds making it 10x faster than the Bash version.

asdf which

asdf which now runs in 1 millisecond making it 8x faster than the Bash version.

Conclusion

The rewrite in Go significantly improved the performance of asdf, with improvements ranging from 5-20x. Performance is no longer an issue for most commands. asdf exec stands out as a command that would benefit from optimization work in the future.