drake 0.8.4.1.1.0 → 0.8.4.1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rake/parallel.rb CHANGED
@@ -1,43 +1,148 @@
1
+ #
2
+ # Parallel task execution for Rake.
3
+ #
4
+ # The comp_tree package is used to create a computation tree which
5
+ # executes Rake tasks.
6
+ #
7
+ # Tasks are first collected with a single-threaded dry run. This
8
+ # expands file rules, resolves prequisite names and finds task
9
+ # arguments. A computation tree is then built with the gathered
10
+ # tasks.
11
+ #
12
+ # Note that prerequisites are context-dependent; it is therefore not
13
+ # possible to create a 1-to-1 mapping between Rake::Tasks and
14
+ # CompTree::Nodes.
15
+ #
16
+ # Author: James M. Lawrence <quixoticsycophant@gmail.com>
17
+ #
1
18
 
2
- require 'rake/comp_tree/comp_tree'
19
+ require 'comp_tree'
3
20
 
4
21
  module Rake
5
- module TaskManager
6
- def invoke_parallel(root_task_name) # :nodoc:
7
- CompTree.build do |driver|
8
- #
9
- # Build the computation tree from task prereqs.
10
- #
11
- self.parallel.tasks.each_pair { |task_name, cache|
12
- task = self[task_name]
13
- task_args, prereqs = cache
14
- children_names = prereqs.map { |child| child.name }
15
- driver.define(task_name, *children_names) {
16
- task.execute(task_args)
17
- }
18
- }
22
+ module Parallel #:nodoc:
23
+ class Driver
24
+ # Tasks collected during the dry-run phase.
25
+ attr_reader :tasks
26
+
27
+ # Prevent invoke inside invoke.
28
+ attr_reader :mutex
29
+
30
+ def initialize
31
+ @tasks = Hash.new
32
+ @mutex = Mutex.new
33
+ end
19
34
 
20
- root_node = driver.nodes[root_task_name]
21
-
22
- #
23
- # If there were nothing to do, there would be no root node.
24
- #
25
- if root_node
26
- #
27
- # Mark computation nodes without a function as computed.
28
- #
29
- root_node.each_downward { |node|
30
- unless node.function
31
- node.computed = true
35
+ #
36
+ # Top-level parallel invocation.
37
+ #
38
+ # Called from Task#invoke (routed through Task#invoke_parallel).
39
+ #
40
+ def invoke(threads, task, *task_args)
41
+ if @mutex.try_lock
42
+ begin
43
+ @tasks.clear
44
+
45
+ # dry run task collector
46
+ task.invoke_serial(*task_args)
47
+
48
+ if @tasks.has_key? task
49
+ # hand it off to comp_tree
50
+ compute(task, threads)
32
51
  end
33
- }
52
+ ensure
53
+ @mutex.unlock
54
+ end
55
+ else
56
+ raise InvokeInsideInvoke
57
+ end
58
+ end
59
+
60
+ #
61
+ # Build and run the computation tree.
62
+ #
63
+ # Called from Parallel::Driver#invoke.
64
+ #
65
+ def compute(root_task, threads)
66
+ CompTree.build do |driver|
67
+ # keep this around for optimization
68
+ needed_prereq_names = Array.new
69
+
70
+ @tasks.each_pair do |task, (task_args, prereqs)|
71
+ # if a prereq is not needed then it didn't get into @tasks
72
+ needed_prereq_names.clear
73
+ prereqs.each do |prereq|
74
+ needed_prereq_names << prereq.name if @tasks.has_key? prereq
75
+ end
76
+
77
+ # define a computation node which executes the task
78
+ driver.define(task.name, *needed_prereq_names) {
79
+ task.execute(task_args)
80
+ }
81
+ end
34
82
 
35
- #
36
- # Launch the computation.
37
- #
38
- driver.compute(root_node.name, self.num_threads)
83
+ # punch it
84
+ driver.compute(root_task.name, threads)
39
85
  end
40
86
  end
41
87
  end
88
+
89
+ module ApplicationMixin
90
+ def parallel
91
+ @parallel ||= Driver.new
92
+ end
93
+ end
94
+
95
+ module TaskMixin
96
+ #
97
+ # Top-level parallel invocation.
98
+ #
99
+ # Called from Task#invoke.
100
+ #
101
+ def invoke_parallel(*task_args)
102
+ application.parallel.invoke(application.options.threads, self, *task_args)
103
+ end
104
+
105
+ #
106
+ # Collect tasks for parallel execution.
107
+ #
108
+ # Called from Task#invoke_with_call_chain.
109
+ #
110
+ def invoke_with_call_chain_collector(task_args, new_chain, previous_chain)
111
+ prereqs = invoke_prerequisites_collector(task_args, new_chain)
112
+ parallel = application.parallel
113
+ if needed? or parallel.tasks[self]
114
+ parallel.tasks[self] = [task_args, prereqs]
115
+ unless previous_chain == InvocationChain::EMPTY
116
+ #
117
+ # Touch the parent to propagate 'needed?' upwards. This
118
+ # works because the recursion is depth-first.
119
+ #
120
+ parallel.tasks[previous_chain.value] = true
121
+ end
122
+ end
123
+ end
124
+
125
+ #
126
+ # Dry-run invoke prereqs and return the prereq instances.
127
+ # This also serves to avoid MultiTask#invoke_prerequisites.
128
+ #
129
+ # Called from Task#invoke_with_call_chain_collector.
130
+ #
131
+ def invoke_prerequisites_collector(task_args, invocation_chain)
132
+ @prerequisites.map { |n|
133
+ invoke_prerequisite(n, task_args, invocation_chain)
134
+ }
135
+ end
136
+ end
137
+ end
138
+
139
+ #
140
+ # Error indicating Task#invoke was called inside Task#invoke
141
+ # during parallel execution.
142
+ #
143
+ class InvokeInsideInvoke < StandardError
144
+ def message
145
+ "Cannot call Task#invoke within a task during parallel execution."
146
+ end
42
147
  end
43
148
  end
data/test/filecreation.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  module FileCreation
4
+ ANCIENT_FILE = 'testdata/anc'
4
5
  OLDFILE = "testdata/old"
6
+ MIDDLE_AGED_FILE = "testdata/mid"
5
7
  NEWFILE = "testdata/new"
6
8
 
7
9
  def create_timed_files(oldfile, *newfiles)
@@ -15,6 +17,16 @@ module FileCreation
15
17
  end
16
18
  end
17
19
 
20
+ def create_dispersed_timed_files(*files)
21
+ create_file(files.first)
22
+ (1...files.size).each do |index|
23
+ while create_file(files[index]) <= File.stat(files[index - 1]).mtime
24
+ sleep(0.1)
25
+ File.delete(files[index])
26
+ end
27
+ end
28
+ end
29
+
18
30
  def create_dir(dirname)
19
31
  FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
20
32
  File.stat(dirname).mtime
@@ -0,0 +1,3 @@
1
+ require 'rake'
2
+ Rake.application.options.threads = 8
3
+ puts "- Testing parallel execution"
@@ -7,6 +7,7 @@ rescue LoadError
7
7
  # got no gems
8
8
  end
9
9
 
10
+ require 'thread'
10
11
  require 'flexmock/test_unit'
11
12
 
12
13
  if RUBY_VERSION >= "1.9.0"
@@ -22,3 +23,23 @@ module TestMethods
22
23
  assert_raise(ex, msg, &block)
23
24
  end
24
25
  end
26
+
27
+ class SerializedArray
28
+ def initialize
29
+ @mutex = Mutex.new
30
+ @array = Array.new
31
+ end
32
+
33
+ Array.public_instance_methods.each do |method_name|
34
+ unless method_name =~ %r!\A__! or method_name =~ %r!\A(object_)?id\Z!
35
+ # TODO: jettison 1.8.6; use define_method with |&block|
36
+ eval %{
37
+ def #{method_name}(*args, &block)
38
+ @mutex.synchronize {
39
+ @array.send('#{method_name}', *args, &block)
40
+ }
41
+ end
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ require 'rake'
2
+ Rake.application.options.threads = 1
3
+ puts "- Testing serial execution"
@@ -22,6 +22,7 @@ class TestApplication < Test::Unit::TestCase
22
22
 
23
23
  def setup
24
24
  @app = Rake::Application.new
25
+ @app.options.threads = Rake.application.options.threads
25
26
  @app.options.rakelib = []
26
27
  end
27
28
 
@@ -570,6 +571,7 @@ class TestApplicationOptions < Test::Unit::TestCase
570
571
  def command_line(*options)
571
572
  options.each do |opt| ARGV << opt end
572
573
  @app = Rake::Application.new
574
+ @app.options.threads = Rake.application.options.threads
573
575
  def @app.exit(*args)
574
576
  throw :system_exit, :exit
575
577
  end
@@ -585,6 +587,7 @@ end
585
587
  class TestTaskArgumentParsing < Test::Unit::TestCase
586
588
  def setup
587
589
  @app = Rake::Application.new
590
+ @app.options.threads = Rake.application.options.threads
588
591
  end
589
592
 
590
593
  def test_name_only
@@ -630,6 +633,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
630
633
 
631
634
  def test_terminal_width_using_env
632
635
  app = Rake::Application.new
636
+ app.options.threads = Rake.application.options.threads
633
637
  in_environment('RAKE_COLUMNS' => '1234') do
634
638
  assert_equal 1234, app.terminal_width
635
639
  end
@@ -637,6 +641,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
637
641
 
638
642
  def test_terminal_width_using_stty
639
643
  app = Rake::Application.new
644
+ app.options.threads = Rake.application.options.threads
640
645
  flexmock(app,
641
646
  :unix? => true,
642
647
  :dynamic_width_stty => 1235,
@@ -648,6 +653,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
648
653
 
649
654
  def test_terminal_width_using_tput
650
655
  app = Rake::Application.new
656
+ app.options.threads = Rake.application.options.threads
651
657
  flexmock(app,
652
658
  :unix? => true,
653
659
  :dynamic_width_stty => 0,
@@ -659,6 +665,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
659
665
 
660
666
  def test_terminal_width_using_hardcoded_80
661
667
  app = Rake::Application.new
668
+ app.options.threads = Rake.application.options.threads
662
669
  flexmock(app, :unix? => false)
663
670
  in_environment('RAKE_COLUMNS' => nil) do
664
671
  assert_equal 80, app.terminal_width
@@ -667,6 +674,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
667
674
 
668
675
  def test_terminal_width_with_failure
669
676
  app = Rake::Application.new
677
+ app.options.threads = Rake.application.options.threads
670
678
  flexmock(app).should_receive(:unix?).and_throw(RuntimeError)
671
679
  in_environment('RAKE_COLUMNS' => nil) do
672
680
  assert_equal 80, app.terminal_width
@@ -48,14 +48,18 @@ class TestDefinitions < Test::Unit::TestCase
48
48
  end
49
49
 
50
50
  def test_incremental_definitions
51
- runs = []
51
+ runs = SerializedArray.new
52
52
  task :t1 => [:t2] do runs << "A"; 4321 end
53
53
  task :t1 => [:t3] do runs << "B"; 1234 end
54
54
  task :t1 => [:t3]
55
55
  task :t2
56
56
  task :t3
57
57
  Task[:t1].invoke
58
- assert_equal ["A", "B"], runs
58
+ if Rake.application.options.threads == 1
59
+ assert_equal ["A", "B"], runs
60
+ else
61
+ assert_equal ["A", "B"], runs.sort
62
+ end
59
63
  assert_equal ["t2", "t3"], Task[:t1].prerequisites
60
64
  end
61
65
 
@@ -12,11 +12,17 @@ class TestFileTask < Test::Unit::TestCase
12
12
  include FileCreation
13
13
  include TestMethods
14
14
 
15
+ FILES = [ANCIENT_FILE, OLDFILE, MIDDLE_AGED_FILE, NEWFILE]
16
+
15
17
  def setup
16
18
  Task.clear
17
- @runs = Array.new
18
- FileUtils.rm_f NEWFILE
19
- FileUtils.rm_f OLDFILE
19
+ @runs = SerializedArray.new
20
+ FileUtils.rm_f FILES
21
+ end
22
+
23
+ def test_create_dispersed_timed_files
24
+ create_dispersed_timed_files(*FILES)
25
+ assert_equal FILES, FILES.sort_by { |f| File.stat(f).mtime }
20
26
  end
21
27
 
22
28
  def test_file_need
@@ -73,6 +79,46 @@ class TestFileTask < Test::Unit::TestCase
73
79
  assert_nothing_raised do Task[OLDFILE].invoke end
74
80
  end
75
81
 
82
+ def test_old_file_in_between
83
+ create_dispersed_timed_files(*FILES)
84
+
85
+ file MIDDLE_AGED_FILE => OLDFILE do |t|
86
+ @runs << t.name
87
+ end
88
+ file OLDFILE => NEWFILE do |t|
89
+ @runs << t.name
90
+ touch OLDFILE, :verbose => false
91
+ end
92
+ file NEWFILE do |t|
93
+ @runs << t.name
94
+ end
95
+
96
+ Task[MIDDLE_AGED_FILE].invoke
97
+ assert_equal([OLDFILE, MIDDLE_AGED_FILE], @runs)
98
+ end
99
+
100
+ def test_two_old_files_in_between
101
+ create_dispersed_timed_files(*FILES)
102
+
103
+ file MIDDLE_AGED_FILE => OLDFILE do |t|
104
+ @runs << t.name
105
+ end
106
+ file OLDFILE => ANCIENT_FILE do |t|
107
+ @runs << t.name
108
+ touch OLDFILE, :verbose => false
109
+ end
110
+ file ANCIENT_FILE => NEWFILE do |t|
111
+ @runs << t.name
112
+ touch ANCIENT_FILE, :verbose => false
113
+ end
114
+ file NEWFILE do |t|
115
+ @runs << t.name
116
+ end
117
+
118
+ Task[MIDDLE_AGED_FILE].invoke
119
+ assert_equal([ANCIENT_FILE, OLDFILE, MIDDLE_AGED_FILE], @runs)
120
+ end
121
+
76
122
  # I have currently disabled this test. I'm not convinced that
77
123
  # deleting the file target on failure is always the proper thing to
78
124
  # do. I'm willing to hear input on this topic.
@@ -9,7 +9,7 @@ class TestMultiTask < Test::Unit::TestCase
9
9
 
10
10
  def setup
11
11
  Task.clear
12
- @runs = Array.new
12
+ @runs = SerializedArray.new
13
13
  end
14
14
 
15
15
  def test_running_multitasks
@@ -1,56 +1,195 @@
1
1
 
2
2
  require 'rbconfig'
3
3
  require 'test/unit'
4
+ require 'rake'
5
+ require 'test/rake_test_setup'
4
6
 
5
- PARALLEL_TEST_MESSAGE = <<'EOS'
7
+ if Rake.application.options.threads > 1
8
+ class TestParallel < Test::Unit::TestCase
9
+ VISUALS = false
10
+ TIME_STEP = 0.25
11
+ TIME_EPSILON = 0.05
12
+ MAX_THREADS = 5
6
13
 
14
+ def trace(str)
15
+ if VISUALS
16
+ puts str
17
+ end
18
+ end
7
19
 
8
- Task graph for sample parallel execution:
9
-
10
- default
11
- / \
12
- / \
13
- a b
14
- / \
15
- / \
16
- x y
20
+ def assert_order(expected, actual)
21
+ assert_in_delta(expected*TIME_STEP, actual, TIME_EPSILON)
22
+ end
17
23
 
18
- EOS
19
-
20
- if Rake.application.num_threads > 1
21
- class TestSimpleParallel < Test::Unit::TestCase
22
- def setup
23
- puts PARALLEL_TEST_MESSAGE
24
+ def teardown
25
+ Rake::Task.clear
24
26
  end
25
27
 
26
28
  def test_parallel
27
- here = File.dirname(__FILE__)
28
- rake = File.expand_path("#{here}/../bin/rake")
29
-
30
- ENV["RUBYLIB"] = lambda {
31
- lib = File.expand_path("#{here}/../lib")
32
- current = ENV["RUBYLIB"]
33
- sep = Rake.application.windows? ? ";" : ":"
34
- if current
35
- "#{lib}#{sep}#{current}"
29
+ trace GRAPH
30
+
31
+ data = Hash.new { |hash, key| hash[key] = Hash.new }
32
+
33
+ (1..MAX_THREADS).each { |threads|
34
+ app = Rake::Application.new
35
+ app.options.threads = threads
36
+
37
+ app.define_task Rake::Task, :default => [:a, :b]
38
+ app.define_task Rake::Task, :a => [:x, :y]
39
+ app.define_task Rake::Task, :b
40
+
41
+ mutex = Mutex.new
42
+ STDOUT.sync = true
43
+ start_time = nil
44
+
45
+ %w[default a b x y].each { |task_name|
46
+ app.define_task Rake::Task, task_name.to_sym do
47
+ mutex.synchronize {
48
+ trace "task #{task_name}"
49
+ data[threads][task_name] = Time.now - start_time
50
+ }
51
+ sleep(TIME_STEP)
52
+ end
53
+ }
54
+
55
+ trace "-"*50
56
+ trace "threads: #{threads}"
57
+ start_time = Time.now
58
+ app[:default].invoke
59
+ }
60
+
61
+ assert_order(0, data[1]["x"])
62
+ assert_order(1, data[1]["y"])
63
+ assert_order(2, data[1]["a"])
64
+ assert_order(3, data[1]["b"])
65
+ assert_order(4, data[1]["default"])
66
+
67
+ assert_order(0, data[2]["x"])
68
+ assert_order(0, data[2]["y"])
69
+ assert_order(1, data[2]["a"])
70
+ assert_order(1, data[2]["b"])
71
+ assert_order(2, data[2]["default"])
72
+
73
+ (3..MAX_THREADS).each { |threads|
74
+ assert_order(0, data[threads]["x"])
75
+ assert_order(0, data[threads]["y"])
76
+ assert_order(0, data[threads]["b"])
77
+ assert_order(1, data[threads]["a"])
78
+ assert_order(2, data[threads]["default"])
79
+ }
80
+ end
81
+
82
+ def test_invoke_inside_invoke
83
+ assert_raises(Rake::InvokeInsideInvoke) {
84
+ app = Rake::Application.new
85
+ app.options.threads = 4
86
+ app.define_task Rake::Task, :root do
87
+ app[:stuff].invoke
88
+ end
89
+ app.define_task Rake::Task, :stuff do
90
+ flunk
91
+ end
92
+ app[:root].invoke
93
+ }
94
+ end
95
+
96
+ def test_randomize
97
+ size = 100
98
+ [false, true].each do |randomize|
99
+ memo = SerializedArray.new
100
+ app = Rake::Application.new
101
+ app.define_task Rake::Task, :root
102
+ size.times { |n|
103
+ app.define_task Rake::Task, :root => n.to_s
104
+ app.define_task Rake::Task, n.to_s do
105
+ memo << n
106
+ end
107
+ }
108
+ app.options.randomize = randomize
109
+ app[:root].invoke
110
+ numbers = (0...size).to_a
111
+ if randomize
112
+ assert_not_equal(numbers, memo)
36
113
  else
37
- lib
114
+ assert_equal(numbers, memo)
38
115
  end
39
- }.call
40
-
41
- [
42
- ["Rakefile.simple", true],
43
- ["Rakefile.seq", false],
44
- ].each { |file, disp|
45
- (1..5).each { |n|
46
- args = [rake, "--threads", n.to_s, "-s", "-f", "test/#{file}"]
47
- if disp
48
- puts "\nvisual check: #{n} thread#{n > 1 ? 's' : ''}"
49
- puts args.join(" ")
116
+ end
117
+ end
118
+
119
+ def test_multitask_not_called
120
+ # ensure MultiTask methods are not called by hijacking all of them
121
+
122
+ originals = [
123
+ :private_instance_methods,
124
+ :protected_instance_methods,
125
+ :public_instance_methods,
126
+ ].inject(Hash.new) { |acc, query|
127
+ result = Rake::MultiTask.send(query, false).inject(Hash.new) {
128
+ |sub_acc, method_name|
129
+ sub_acc.merge!(
130
+ method_name => Rake::MultiTask.instance_method(method_name)
131
+ )
132
+ }
133
+ acc.merge!(result)
134
+ }
135
+
136
+ memo = SerializedArray.new
137
+
138
+ Rake::MultiTask.module_eval {
139
+ originals.each_pair { |method_name, method_object|
140
+ remove_method method_name
141
+ define_method method_name do |*args|
142
+ # missing |&block| due to 1.8.6, but not using it anyway
143
+ memo << 'called'
144
+ method_object.bind(self).call(*args)
50
145
  end
51
- assert(ruby(*args))
52
146
  }
53
147
  }
148
+
149
+ begin
150
+ app = Rake::Application.new
151
+
152
+ define = lambda {
153
+ app.define_task Rake::Task, task(:x) { }
154
+ app.define_task Rake::Task, task(:y) { }
155
+ app.define_task Rake::MultiTask, :root => [:x, :y]
156
+ }
157
+
158
+ app.options.threads = 1
159
+ define.call
160
+ app[:root].invoke
161
+ assert_equal ['called'], memo
162
+
163
+ app.clear
164
+ memo.clear
165
+ assert_raises(RuntimeError) { app[:root].invoke }
166
+
167
+ app.options.threads = 4
168
+ define.call
169
+ app[:root].invoke
170
+ assert_equal [], memo
171
+ ensure
172
+ Rake::MultiTask.module_eval {
173
+ originals.each_pair { |method_name, method_object|
174
+ remove_method method_name
175
+ define_method method_name, method_object
176
+ }
177
+ }
178
+ end
54
179
  end
180
+
181
+ GRAPH = <<-'EOS'
182
+
183
+ Task graph for sample parallel execution:
184
+
185
+ default
186
+ / \
187
+ / \
188
+ a b
189
+ / \
190
+ / \
191
+ x y
192
+
193
+ EOS
55
194
  end
56
195
  end