Speeding up Test Runs with fork

drbrain | Sun, 09 Apr 2006 03:47:00 GMT

Posted in , , ,

Loading Rails takes a significant portion of your test run time, especially when you want to run only one test file or one test method. On my Powerbook loading Rails takes between four and six seconds. If you're frequently running unit tests this constant overhead can quickly become annoying.

When using autotest I may have to wait as much as ten seconds (five seconds between scans for changes, four seconds to load rails, one second to run the test) before I know if my changes fixed a problem or not. Ten seconds is past the threshold where I can keep paying attention which makes my mind wander. (A wandering mind is no good for productive work.) Also, those extra four seconds of loading Rails per test start to add up. I may load rails hundreds of times in a day just to run a tiny test.

There's one already existing way to reduce or eliminate that constant overhead of loading Rails. In development mode Rails reloads files to keep things running without restarting Rails on every change. I prefer to have an environment that is guaranteed to be clean when the tests start and reloading files removes this option.

Since I want Rails loaded without any application code I chose to create a process that would load rails then open up a server socket and wait for connections. When a connection comes in the process will fork to make a copy of the environment that can then load the application and run the tests.

A regular test run for just one file runs like this:

$ time ruby test/controllers/route_controller_test.rb Loaded suite test/controllers/route_controller_test
Started
......................................................
Finished in 13.192465 seconds.

54 tests, 268 assertions, 0 failures, 0 errors

real    0m17.884s
user    0m8.147s
sys     0m1.424s

The difference between the real time and the Test::Unit run time accounts for Rails and app loading overhead, about five seconds.

I've tentatively named the parent process spawner 'ruby_fork' and the client 'ruby_fork_client', so you start up the parent process:

$ RAILS_ENV='test' ruby_fork -r rubygems -e 'require_gem "rails"'
/Users/drbrain/Links/ZT/bin/ruby_fork Running as PID 3570 on 9084

ruby_fork understands -r, -I and -e just like regular ruby so I can just load Rails and none of the rest of my application.

Then I run ruby_fork_client which takes its arguments and passes them across to the child process and then reads from the socket and prints to STDOUT.

$ time ruby_fork_client -r test/controllers/route_controller_test.rb
Loaded suite /Users/drbrain/Links/ZT/bin/ruby_fork
Started
......................................................
Finished in 12.442556 seconds.

54 tests, 268 assertions, 0 failures, 0 errors

real    0m13.947s
user    0m0.077s
sys     0m0.022s

Now that extra time spent loading Rails is gone and I'm left with application loading and Test::Unit overhead which is miniscule in comparison.

ruby_fork is not Rails specific. The server and client can do anything they like, so this has applications beyond testing Rails (for example, handling incoming mail) or even Rails itself.

I'd like to release ruby_fork and ruby_fork_client as part of ZenTest but I'll be holding it until 3.3.0. Currently ZenTest is almost ready for release and ruby_fork and ruby_fork_client needs to act more like a regular invocation of ruby. comments

Comments RSS FEED

What about using Threads instead to keep it Windows friendly?

Daniel Berger said about 3 hours later

Ruby threads? Won’t work because you’ve stomped all over your clean environment. It could work if you didn’t want to just run tests and needed a persistent environment.

POSIX threads require C and a version of Ruby that behaves poorly on FreeBSD. I think you’d still have the shared environment problem there though.

Eric Hodel said about 5 hours later

Hm…it could probably be done with CreateProcess() then.

Daniel Berger said 1 day later

No, Windows will never be able to pull this one off. The clever thing with fork() is that it’s extremely cheap. It just creates a new record for the child process in the kernel and kicks it off. The child gets a reference to the parents memory and that memory gets copied only on write. Since the libraries and the setup for Rails usually don’t get modified at runtime huge amounts of time and memory are saved.

CreateProcess() in Windows is not that smart.

Btw, Eric, can we possibly get hold of this code somewhere?

Jon Tirsen said 3 days later

ruby_fork will be in the next release of ZenTest. Conference schedules are keeping us busy, so probably not for three weeks.

Eric Hodel said 3 days later

Comments are disabled