Skip to content

Commit ecc5afe

Browse files
committed
Parallelize tests only when overhead is justified
Parallelizing tests has a cost in terms of database setup and fixture loading. This change makes Rails disable parallelization when the number of tests is below a configurable threshold. When running tests in parallel each process gets its own database instance. On each execution, each process will update each database schema (if needed) and load all the fixtures. This can be very expensive for non trivial datasets. As an example, for HEY, when running a single file with 18 tests, running tests in parallel in my box adds an overhead of 13 seconds versus not parallelizing them. Of course parallelizing is totally worthy when there are many tests to run, but not when running just a few tests. The threshold is configurable via config.active_support.test_parallelization_minimum_number_of_tests, which is 30 50 by default. This also adds some tracing to know how tests are being executed: When in parallel: ``` Running 2829 tests in parallel in 8 processes ``` When not in parallel: ``` Running 15 tests in a single process (parallelization threshold is 30) ```
1 parent d364cfb commit ecc5afe

File tree

8 files changed

+140
-15
lines changed

8 files changed

+140
-15
lines changed

activesupport/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* Parallelize tests only when overhead is justified by the number of them
2+
3+
Running tests in parallel adds overhead in terms of database
4+
setup and fixture loading. Now, Rails will only parallelize test executions when
5+
there are enough tests to make it worth it.
6+
7+
This threshold is 50 by default, and is configurable via:
8+
9+
```ruby
10+
config.active_support.test_parallelization_minimum_number_of_tests = 100
11+
```
12+
13+
*Jorge Manrubia*
14+
115
* OpenSSL constants are now used for Digest computations.
216

317
*Dirkjan Bussink*

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def self.eager_load!
8888

8989
cattr_accessor :test_order # :nodoc:
9090
cattr_accessor :test_parallelization_disabled, default: false # :nodoc:
91+
cattr_accessor :test_parallelization_minimum_number_of_tests, default: 50 # :nodoc:
9192

9293
def self.disable_test_parallelization!
9394
self.test_parallelization_disabled = true unless ENV["PARALLEL_WORKERS"]

activesupport/lib/active_support/test_case.rb

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "active_support/testing/time_helpers"
1313
require "active_support/testing/file_fixtures"
1414
require "active_support/testing/parallelization"
15+
require "active_support/testing/parallelize_executor"
1516
require "concurrent/utility/processor_counter"
1617

1718
module ActiveSupport
@@ -77,20 +78,7 @@ def parallelize(workers: :number_of_processors, with: :processes)
7778

7879
return if workers <= 1 || ActiveSupport.test_parallelization_disabled
7980

80-
executor = case with
81-
when :processes
82-
Testing::Parallelization.new(workers)
83-
when :threads
84-
Minitest::Parallel::Executor.new(workers)
85-
else
86-
raise ArgumentError, "#{with} is not a supported parallelization executor."
87-
end
88-
89-
self.lock_threads = false if defined?(self.lock_threads) && with == :threads
90-
91-
Minitest.parallel_executor = executor
92-
93-
parallelize_me!
81+
Minitest.parallel_executor = ActiveSupport::Testing::ParallelizeExecutor.new(size: workers, with: with)
9482
end
9583

9684
# Set up hook for parallel testing. This can be used if you have multiple

activesupport/lib/active_support/testing/parallelization.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def <<(work)
4242
@queue_server << work
4343
end
4444

45+
def size
46+
@worker_count
47+
end
48+
4549
def shutdown
4650
@queue_server.shutdown
4751
@worker_pool.each { |pid| Process.waitpid pid }
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module Testing
5+
class ParallelizeExecutor # :nodoc:
6+
attr_reader :size, :parallelize_with, :parallel_executor
7+
8+
def initialize(size:, with:)
9+
@size = size
10+
@parallelize_with = with
11+
@parallel_executor = build_parallel_executor
12+
end
13+
14+
def start
15+
parallelize if should_parallelize?
16+
show_execution_info
17+
18+
parallel_executor.start if parallelized?
19+
end
20+
21+
def <<(work)
22+
parallel_executor << work if parallelized?
23+
end
24+
25+
def shutdown
26+
parallel_executor.shutdown if parallelized?
27+
end
28+
29+
private
30+
def build_parallel_executor
31+
case parallelize_with
32+
when :processes
33+
Testing::Parallelization.new(size)
34+
when :threads
35+
ActiveSupport::TestCase.lock_threads = false if defined?(ActiveSupport::TestCase.lock_threads)
36+
Minitest::Parallel::Executor.new(size)
37+
else
38+
raise ArgumentError, "#{parallelize_with} is not a supported parallelization executor."
39+
end
40+
end
41+
42+
def parallelize
43+
@parallelized = true
44+
Minitest::Test.parallelize_me!
45+
end
46+
47+
def parallelized?
48+
@parallelized
49+
end
50+
51+
def should_parallelize?
52+
ENV["PARALLEL_WORKERS"] || tests_count > ActiveSupport.test_parallelization_minimum_number_of_tests
53+
end
54+
55+
def tests_count
56+
@tests_count ||= Minitest::Runnable.runnables.sum { |runnable| runnable.runnable_methods.size }
57+
end
58+
59+
def show_execution_info
60+
puts execution_info
61+
end
62+
63+
def execution_info
64+
if should_parallelize?
65+
"Running #{tests_count} tests in parallel using #{parallel_executor.size} #{parallelize_with}"
66+
else
67+
"Running #{tests_count} tests in a single process (parallelization threshold is #{ActiveSupport.test_parallelization_minimum_number_of_tests})"
68+
end
69+
end
70+
end
71+
end
72+
end

guides/source/testing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,16 @@ end
569569
NOTE: With disabled transactional tests, you have to clean up any data tests
570570
create as changes are not automatically rolled back after the test completes.
571571

572+
### Threshold to parallelize tests
573+
574+
Running tests in parallel adds an overhead in terms of database setup and
575+
fixture loading. Because of this, Rails won't parallelize executions that involve
576+
fewer than 50 tests. You can configure this threshold in your `test.rb`:
577+
578+
```ruby
579+
config.active_support.test_parallelization_minimum_number_of_tests = 100
580+
```
581+
572582
The Test Database
573583
-----------------
574584

railties/test/application/configuration_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,20 @@ class D < C
24212421
assert_equal OpenSSL::Digest::SHA256, ActiveSupport::KeyGenerator.hash_digest_class
24222422
end
24232423

2424+
test "ActiveSupport.test_parallelization_minimum_number_of_tests can be configured via config.active_support.test_parallelization_minimum_number_of_tests" do
2425+
remove_from_config '.*config\.load_defaults.*\n'
2426+
2427+
app_file "config/environments/test.rb", <<-RUBY
2428+
Rails.application.configure do
2429+
config.active_support.test_parallelization_minimum_number_of_tests = 1234
2430+
end
2431+
RUBY
2432+
2433+
app "test"
2434+
2435+
assert_equal 1234, ActiveSupport.test_parallelization_minimum_number_of_tests
2436+
end
2437+
24242438
test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
24252439
class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end
24262440

railties/test/application/test_runner_test.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,9 +567,29 @@ def test_run_in_parallel_with_processes
567567
output = run_test_command(file_name)
568568

569569
assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
570+
assert_match %r{Running \d+ tests in parallel using \d+ processes}, output
570571
assert_no_match "create_table(:users)", output
571572
end
572573

574+
def test_avoid_paralleling_when_number_of_tests_if_below_threshold
575+
exercise_parallelization_regardless_of_machine_core_count(with: :processes, threshold: 100)
576+
577+
file_name = create_parallel_processes_test_file
578+
579+
app_file "db/schema.rb", <<-RUBY
580+
ActiveRecord::Schema.define(version: 1) do
581+
create_table :users do |t|
582+
t.string :name
583+
end
584+
end
585+
RUBY
586+
587+
output = run_test_command(file_name)
588+
589+
assert_match %r{Running \d+ tests in a single process}, output
590+
assert_no_match %r{Running \d+ tests in parallel using \d+ processes}, output
591+
end
592+
573593
def test_parallel_is_disabled_when_single_file_is_run
574594
exercise_parallelization_regardless_of_machine_core_count(with: :processes, force: false)
575595

@@ -1140,12 +1160,14 @@ class ParallelTest < ActiveSupport::TestCase
11401160
RUBY
11411161
end
11421162

1143-
def exercise_parallelization_regardless_of_machine_core_count(with:, force: true)
1163+
def exercise_parallelization_regardless_of_machine_core_count(with:, force: true, threshold: 0)
11441164
file_content = ERB.new(<<-ERB, trim_mode: "-").result_with_hash(with: with.to_s, force: force)
11451165
ENV["RAILS_ENV"] ||= "test"
11461166
require_relative "../config/environment"
11471167
require "rails/test_help"
11481168
1169+
ActiveSupport.test_parallelization_minimum_number_of_tests = #{threshold}
1170+
11491171
class ActiveSupport::TestCase
11501172
<%- if force -%>
11511173
# Force parallelization, even with single files

0 commit comments

Comments
 (0)