rails
Speed up your default Rake task with the multitask -m
option
I recently discovered that Rake has the ability to run tasks in parallel. For Rails projects with many test and linting tasks, this can be a huge time-saver.
tl;dr bin/rake -m
turns your default Rake task into a “multitask” that runs its prerequisites in parallel. You may need to restructure the definition of your default Rake task to take full advantage. One of my projects saw a 3x speed boost!
What’s the default Rake task all about?
The default Rake task is a popular convention across most Ruby projects.
$ bundle exec rake
bin/rake
, which is effectively the same thing.
Commonly, developers will customize the default Rake task in Rails projects to run all sorts of checks, like:
- RSpec
- Cucumber
- RuboCop
- Brakeman
- ESLint
- etc.
This is great, because it sets an easy-to-remember standard for checking that a project is “in good shape”: just cd
into a project, run bin/setup
, then bin/rake
, and most projects will give you a green test suite or some other valuable feedback.
The default Rake task also works its way into a typical development workflow: before pushing code or opening a pull request, developers can run the default Rake task as a way to check that tests pass and the code is following the project’s style guide.
This is especially important in the world of open source, because it means you can more quickly contribute to any given Ruby project without being intimately familiar with the code base.
Too much of a good thing?
The problem, as most Rails developers have probably encountered, is that this default Rake task can get really slow as an app grows in size.
Each linting tool that gets added to the list further slows things down, and more files in your project means these tools take longer to run. Not to mention those annoyingly slow (but necessary!) browser-based system tests.
What can you do to speed this up?
Multitask to the rescue!
As it turns out, Rake has the ability to run the default Rake task significantly faster. It’s been there since 2012, and I only just found it: the -m
option.
$ bin/rake --help
...
-m, --multitask Treat all tasks as multitasks.
...
In Rake, a multitask is able to run its prerequisites in parallel. You can take advantage of this if you define the :default
Rake task as a list of prerequisites, like this:
require_relative "config/application"
Rails.application.load_tasks
Rake::Task[:default].prerequisites.clear if Rake::Task.task_defined?(:default)
desc "Run all checks"
task default: %w[test:all rubocop erblint eslint stylelint] do
puts "All checks passed!"
end
This default Rake task runs all tests (including system tests), plus multiple linters. The
test:all
task is built into Rails; the others are defined elsewhere and aren’t shown in this example.
The magic happens when you run the default task with -m
:
$ bin/rake -m
...
All checks passed!
test:all,
rubocop
, erblint
, eslint
, and stylelint
in parallel. If they all succeed, “All checks passed!” is printed. (I’ve omitted the intermediate task output for brevity.)
How fast is it?
Your results may vary, but on one of my medium-sized Rails projects (14,000 LOC), using the -m
option gave me a 3x speed boost.
$ time bin/rake
...
real 1m18.156s
$ time bin/rake -m
...
real 0m25.850s
-m
option saved almost a full minute on an Intel i9 MacBook Pro (8 cores).
Digging deeper
You may have noticed some extra code in the Rakefile I showed above. Let’s break that down.
Clearing the out-of-the-box behavior
Rake::Task[:default].prerequisites.clear if Rake::Task.task_defined?(:default)
Normally, Rails adds its own prerequisite to the default task. Usually this is test
or spec
, depending on what test framework is installed.
Clearing the existing prerequisites allows you to take full control over what the default Rake task does, rather than using what Rails auto-detects out of the box.
Declaring prerequisites
task default: %w[test:all rubocop erblint eslint stylelint]
Rake allows tasks to be declared as task NAME: [PREREQS]
. When you run bin/rake NAME
, Rake will first run all the PREREQS
tasks.
Normally, these PREREQS
run sequentially, in order from left to right. Passing the -m
option tells Rake to execute them in parallel, in no particular order.
Showing a success message
task default: %w[test:all rubocop erblint eslint stylelint] do
puts "All checks passed!"
end
The primary drawback of running tasks in parallel is that their output gets all mixed together. This makes it hard to read, and it is not immediately clear what happened. Were there any errors?
That’s why it is a good idea to print a message when everything is done. In Rake, the body of a task (i.e. in the block) will only execute once all prerequisites have finished successfully. You can use this as a way to print an indication that everything worked.
References
- The official Rake documentation for multitasks is almost non-existent, but the README does link to a 3-minute video on Rake’s multitask feature that Avdi Grimm published in 2013. The
-m
option is mentioned at 2:16. - The source code for multitasks is surprisingly elegant.
A default Rake task similar to the example in this article is included in my rails-template. Check it out if you want to apply this and many other Rails best practices to your next project.