FormAPI Blog

Making RuboCop 20x Faster

I’ve been using Prettier in all of my JavaScript projects. I also started using the “format on save” feature in VS Code, and once I got used to it, I was hooked. It’s really nice when you don’t have to think about alignment, formatting hashes, or breaking things up into multiple lines. Just press ⌘+S and Prettier formats everything for you.

Whenever I went back to working with Ruby on Rails, I really missed “format on save”. I wanted to set it up with RuboCop, but it was just too slow.

In the following video, I press ⌘+V to paste some spaces, and then I immediately press ⌘+S to save the file:

I tried to put up with this for a while, but it was terrible. I would switch to the terminal to run a test, and the file hadn’t even saved before the test was finished.

I starting thinking of ways to speed this up. I thought about daemonizing the process, similar to Spring for Rails. I googled “rubocop daemon”, stumbled onto the fohte/rubocop-daemon gem, and I ended up spending the whole day working on a pull request to fix some issues.

I started using this on all of my Ruby projects, so I just kept finding new things to fix and improve. One time I started working on a different project, and VS Code just deleted all the files whenever they were saved. So I really couldn’t get anything done until everything was working really well. Here’s some of the things I worked on:

  • Rewrite the Ruby client as a bash script with netcat. (much faster)
  • Restore the default colorized output (there’s no TTY, so the Rainbow library turns it off.)
  • Make clients use the correct exit code (important for pre-commit hooks, etc.)
  • Add a file lock so that multiple servers don’t start at the same time.
  • When started in a subdirectory, traverse the parent directories to find a Gemfile or gems.rb, and use that as the project root. (Otherwise VS Code starts a daemon for every subdirectory.)
  • Figure out how to deal with stdin in the bash script.
  • Make everything resilient and fault tolerant, so that it can always recover after an error.

After this, I was able to lint and format my Ruby files in less than 200ms:

I think this is almost on par with Prettier, and Ruby development feels so much nicer. I’ve also lowered the “Lint Debounce Time” setting to 300ms, and it’s really nice to get immediate feedback about linting errors.

Try it out

If you use RuboCop, then please try this out in your own projects, and let me know if you run into any issues. My changes have been merged into rubocop-daemon, and were released in version 0.3.0.

You can add the gem to your Gemfile:

group :development, :test do
  gem 'rubocop-daemon'
end

Then run bundle install.

You’ll also need to download and install the rubocop-daemon-wrapper script (Instructions taken from the README):

curl https://raw.githubusercontent.com/fohte/rubocop-daemon/master/bin/rubocop-daemon-wrapper -o /tmp/rubocop-daemon-wrapper
sudo mv /tmp/rubocop-daemon-wrapper /usr/local/bin/rubocop-daemon-wrapper
sudo chmod +x /usr/local/bin/rubocop-daemon-wrapper

Finally, to get this working in VS Code, you’ll just need to replace your rubocop binary with the wrapper script:

# Find your rubocop path
$ which rubocop
# => /Users/username/.rvm/gems/ruby-2.5.3/bin/rubocop

# Override rubocop with a symlink to rubocop-daemon-wrapper
$ ln -fs /usr/local/bin/rubocop-daemon-wrapper /Users/username/.rvm/gems/ruby-2.5.3/bin/rubocop

Hopefully there’s a better way to customize the rubocop path for VS Code.

I’ve also posted a comment on this RuboCop issue: “rubocop -a is slow”. (Maybe RuboCop could support daemonization as a native feature?)

Was it worth spending so much time on this?

I save Ruby files way more than 50 times per day, and I shaved off about 2 seconds. So according to this XKCD table, it was time well spent:

XKCD - Is it worth the time?