Using Devise? Want a faster test suite?
Here’s the short version: making your password hashes expensive to compute is great for production environments, but not so much for your tests.
And now the longer version.
Inspired by some recent blog posts, I decided to run perftools.rb against my spec suite to diagnose some slowness.
Low and behold, something really strange appeared at the top of the output:
Finished in 109.78 seconds
Total: 12182 samples
3542 29.1% 29.1% 3542 29.1% BCrypt::Engine.__bc_crypt
2262 18.6% 47.6% 2262 18.6% garbage_collector
1590 13.1% 60.7% 2488 20.4% Kernel#require
Hm… 29.1% of CPU time spent inside BCrypt
? Wondering where that might be
coming from, I started digging around and found this:
Devise.setup do |config|
config.stretches = 10
config.encryptor = :bcrypt
end
Ah! According to the documentation for
bcrypt-ruby a cost factor of 10
(devise turns stretches
into cost factor when using bcrypt) is quite slow.
Well, intentionally slow: “If an attacker was using Ruby to check each
password, they could check ~140,000 passwords a second with MD5 but only ~450
passwords a second with bcrypt().”
Unfortunately, our test suite is the attacker now: most factories depend on a user, and each new user we create has to generate one of these expensive hashes.
So — what would happen if we replace the bcrypt encryptor with our own encryptor class:
# spec/support/devise.rb
module Devise
module Encryptors
class Plain < Base
class << self
def digest(password, *args)
password
end
def salt(*args)
""
end
end
end
end
end
Devise.encryptor = :plain
And with that in place, let’s try running out suite again:
Finished in 65.72 seconds
Total: 8428 samples
2202 26.1% 26.1% 2202 26.1% garbage_collector
1484 17.6% 43.7% 2329 27.6% Kernel#require
684 8.1% 51.9% 684 8.1% IO#write
Success! We managed to save 44 seconds by not encrypting user passwords in
the test environment! Next step? Digging into all that time in
garbage_collector
and Kernel#require
…