Nevertheless, she persisted

Speeding Up My Shell Startup

This material is not original, and is unscientific. You can find my shell config (among other things) at https://github.com/hybras/dotfiles

I opened my terminal this morning, and it seemed to take about half a second to start. Unacceptable for someone likes to start and stop their shells on demand (instead of leaving one open) and keeps multiple shells open at a time (instead of using a multiplexer like https://github.com/tmux/tmux/wiki[`tmux`] or https://zellij.dev[zellij]). Every few years, my shell startup scripts either become too slow and/or monstrously complex and I must benchmark and prune them. So here’s the 2022 edition of my spring cleaning (in July).

I’ve been using a new featureful benchmarking tool, https://github.com/sharkdp/hyperfine[`hyperfine`]. I used the following command to measure my startup. I added a single warmup run because I was frequently editing my shell config while benchmarking, which mean my shell plugin manager’s cache kept getting rebuilt. I also benchmarked a shell with no config / default system config by adding --no-rcs to zsh in the following command. This gave me a speedy baseline of under 5 ms. With everything enabled it took 200 ms, which is a big discrepancy. My goal is to get to 120 ms. Depending on the source, this is either a human record or the shortest time to record visual stimulus.

[source, shell]

hyperfine –warmup 1 “zsh –login –interactive -c exit”

== Troubleshooting my plugins

By selectively disabling https://github.com/hybras/dotfiles/blob/e6f7a6f5af2b4c3bce9c68a0f2b2b4a513a004d8/dot_config/sheldon/plugins.toml[my plugins] and my https://github.com/hybras/dotfiles/blob/668c22087dbf2ea75a276004dc20e5ad03ab998a/dot_config/zsh/executable_dot_zshrc#L28[plugin manager] I obtained the following timings chart

|===
| Plugin / Command | Time

| https://sheldon.cli.rs[sheldon] | 10 ms
| fpath+="$(brew --prefix)/share/zsh/site-functions" | 10 ms
| https://starship.rs[starship] | 10 ms
| https://github.com/cantino/mcfly[mcfly] | 5 ms
| http://github.com/zsh-users/zsh-completions[zsh-users/zsh-completions] | 5 ms
| All other plugins combined | < 2 ms
|===

I believed my shell plugins were the source of my slow startup, but all together they accounted for ~40 ms. Significant, but not worth optimizing. Where was the remaining 160 ms coming from?

== The remainder

I turned my attention away from my plugins towards the rest of shell startup scripts: profile, env, rc.

Many plugin managers load your completions for you. However it seems sheldon does not. Even so, I’m not sure how to optimize this. Perhaps only reloading functions and completions on change instead of on startup? Or caching somehow? Definitely an area of further research.

This call to homebrew took 130ms. I finally found a cash cow. Inlining it like I did earlier and setting these variables manually is error prone, but made this step instantaneous (< 2 ms).

== Conclusion

|===
| Plugin / Command | Time

| Sheldon and plugins | 40 ms (reduced)
| brew shellenv | 130 ms (removed)
| Functions and completions | 30 ms (untouched)
|===

I love homebrew, but goddamn is it slow. It is so much slower than other package managers, and other programs in general. Benchmarking brew --help and brew list (list installed packages) confirms this. I attribute this to brew being a ruby program. Here, the problem was compounded by homebrew not being part of the os (unlike linux package managers like apt, yum, apk), which means I must add it to my environment variables on shell startup. There’s nothing I can do to speed brew up, so I’ll settle for working around it.

What I am curious about it that I doubt my shell startup was always this slow. This implies that at some point I was not calling brew. I must have forgotten to avoid calling it on startup when I switched computers in Summer 2021.

I will not be switching away from homebrew, it is by far the nicest package manager I’ve ever used (I’m a slut for 🔴C🟠O🟡L🟢O🔵R🟣S🟤). It’s not like I have a choice though (the only other macos-specific option is ports). Perhaps a cross platform package manager like nix or flatpak is in my future.

#Dev