Skip to content

Commit b18321d

Browse files
authored
Merge pull request #96 from ruby-syntax-tree/parallel
Parallel CLI execution
2 parents 830cbae + 009c570 commit b18321d

File tree

2 files changed

+120
-53
lines changed

2 files changed

+120
-53
lines changed

lib/syntax_tree.rb

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "etc"
34
require "json"
45
require "pp"
56
require "prettier_print"

lib/syntax_tree/cli.rb

+119-53
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,41 @@ def self.yellow(value)
3434
end
3535
end
3636

37+
# An item of work that corresponds to a file to be processed.
38+
class FileItem
39+
attr_reader :filepath
40+
41+
def initialize(filepath)
42+
@filepath = filepath
43+
end
44+
45+
def handler
46+
HANDLERS[File.extname(filepath)]
47+
end
48+
49+
def source
50+
handler.read(filepath)
51+
end
52+
end
53+
54+
# An item of work that corresponds to the stdin content.
55+
class STDINItem
56+
def handler
57+
HANDLERS[".rb"]
58+
end
59+
60+
def filepath
61+
:stdin
62+
end
63+
64+
def source
65+
$stdin.read
66+
end
67+
end
68+
3769
# The parent action class for the CLI that implements the basics.
3870
class Action
39-
def run(handler, filepath, source)
71+
def run(item)
4072
end
4173

4274
def success
@@ -48,8 +80,8 @@ def failure
4880

4981
# An action of the CLI that prints out the AST for the given source.
5082
class AST < Action
51-
def run(handler, _filepath, source)
52-
pp handler.parse(source)
83+
def run(item)
84+
pp item.handler.parse(item.source)
5385
end
5486
end
5587

@@ -59,10 +91,11 @@ class Check < Action
5991
class UnformattedError < StandardError
6092
end
6193

62-
def run(handler, filepath, source)
63-
raise UnformattedError if source != handler.format(source)
94+
def run(item)
95+
source = item.source
96+
raise UnformattedError if source != item.handler.format(source)
6497
rescue StandardError
65-
warn("[#{Color.yellow("warn")}] #{filepath}")
98+
warn("[#{Color.yellow("warn")}] #{item.filepath}")
6699
raise
67100
end
68101

@@ -81,9 +114,11 @@ class Debug < Action
81114
class NonIdempotentFormatError < StandardError
82115
end
83116

84-
def run(handler, filepath, source)
85-
warning = "[#{Color.yellow("warn")}] #{filepath}"
86-
formatted = handler.format(source)
117+
def run(item)
118+
handler = item.handler
119+
120+
warning = "[#{Color.yellow("warn")}] #{item.filepath}"
121+
formatted = handler.format(item.source)
87122

88123
raise NonIdempotentFormatError if formatted != handler.format(formatted)
89124
rescue StandardError
@@ -102,53 +137,56 @@ def failure
102137

103138
# An action of the CLI that prints out the doc tree IR for the given source.
104139
class Doc < Action
105-
def run(handler, _filepath, source)
140+
def run(item)
141+
source = item.source
142+
106143
formatter = Formatter.new(source, [])
107-
handler.parse(source).format(formatter)
144+
item.handler.parse(source).format(formatter)
108145
pp formatter.groups.first
109146
end
110147
end
111148

112149
# An action of the CLI that formats the input source and prints it out.
113150
class Format < Action
114-
def run(handler, _filepath, source)
115-
puts handler.format(source)
151+
def run(item)
152+
puts item.handler.format(item.source)
116153
end
117154
end
118155

119156
# An action of the CLI that converts the source into its equivalent JSON
120157
# representation.
121158
class Json < Action
122-
def run(handler, _filepath, source)
123-
object = Visitor::JSONVisitor.new.visit(handler.parse(source))
159+
def run(item)
160+
object = Visitor::JSONVisitor.new.visit(item.handler.parse(item.source))
124161
puts JSON.pretty_generate(object)
125162
end
126163
end
127164

128165
# An action of the CLI that outputs a pattern-matching Ruby expression that
129166
# would match the input given.
130167
class Match < Action
131-
def run(handler, _filepath, source)
132-
puts handler.parse(source).construct_keys
168+
def run(item)
169+
puts item.handler.parse(item.source).construct_keys
133170
end
134171
end
135172

136173
# An action of the CLI that formats the input source and writes the
137174
# formatted output back to the file.
138175
class Write < Action
139-
def run(handler, filepath, source)
140-
print filepath
176+
def run(item)
177+
filepath = item.filepath
141178
start = Time.now
142179

143-
formatted = handler.format(source)
180+
source = item.source
181+
formatted = item.handler.format(source)
144182
File.write(filepath, formatted) if filepath != :stdin
145183

146184
color = source == formatted ? Color.gray(filepath) : filepath
147185
delta = ((Time.now - start) * 1000).round
148186

149-
puts "\r#{color} #{delta}ms"
187+
puts "#{color} #{delta}ms"
150188
rescue StandardError
151-
puts "\r#{filepath}"
189+
puts filepath
152190
raise
153191
end
154192
end
@@ -258,24 +296,41 @@ def run(argv)
258296
plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
259297
end
260298

261-
# Track whether or not there are any errors from any of the files that
262-
# we take action on so that we can properly clean up and exit.
263-
errored = false
264-
265-
each_file(arguments) do |handler, filepath, source|
266-
action.run(handler, filepath, source)
267-
rescue Parser::ParseError => error
268-
warn("Error: #{error.message}")
269-
highlight_error(error, source)
270-
errored = true
271-
rescue Check::UnformattedError, Debug::NonIdempotentFormatError
272-
errored = true
273-
rescue StandardError => error
274-
warn(error.message)
275-
warn(error.backtrace)
276-
errored = true
299+
# We're going to build up a queue of items to process.
300+
queue = Queue.new
301+
302+
# If we're reading from stdin, then we'll just add the stdin object to
303+
# the queue. Otherwise, we'll add each of the filepaths to the queue.
304+
if $stdin.tty? || arguments.any?
305+
arguments.each do |pattern|
306+
Dir
307+
.glob(pattern)
308+
.each do |filepath|
309+
queue << FileItem.new(filepath) if File.file?(filepath)
310+
end
311+
end
312+
else
313+
queue << STDINItem.new
277314
end
278315

316+
# At the end, we're going to return whether or not this worker ever
317+
# encountered an error.
318+
errored =
319+
with_workers(queue) do |item|
320+
action.run(item)
321+
false
322+
rescue Parser::ParseError => error
323+
warn("Error: #{error.message}")
324+
highlight_error(error, item.source)
325+
true
326+
rescue Check::UnformattedError, Debug::NonIdempotentFormatError
327+
true
328+
rescue StandardError => error
329+
warn(error.message)
330+
warn(error.backtrace)
331+
true
332+
end
333+
279334
if errored
280335
action.failure
281336
1
@@ -287,22 +342,33 @@ def run(argv)
287342

288343
private
289344

290-
def each_file(arguments)
291-
if $stdin.tty? || arguments.any?
292-
arguments.each do |pattern|
293-
Dir
294-
.glob(pattern)
295-
.each do |filepath|
296-
next unless File.file?(filepath)
297-
298-
handler = HANDLERS[File.extname(filepath)]
299-
source = handler.read(filepath)
300-
yield handler, filepath, source
301-
end
345+
def with_workers(queue)
346+
# If the queue is just 1 item, then we're not going to bother going
347+
# through the whole ceremony of parallelizing the work.
348+
return yield queue.shift if queue.size == 1
349+
350+
workers =
351+
Etc.nprocessors.times.map do
352+
Thread.new do
353+
# Propagate errors in the worker threads up to the parent thread.
354+
Thread.current.abort_on_exception = true
355+
356+
# Track whether or not there are any errors from any of the files
357+
# that we take action on so that we can properly clean up and
358+
# exit.
359+
errored = false
360+
361+
# While there is still work left to do, shift off the queue and
362+
# process the item.
363+
(errored ||= yield queue.shift) until queue.empty?
364+
365+
# At the end, we're going to return whether or not this worker
366+
# ever encountered an error.
367+
errored
368+
end
302369
end
303-
else
304-
yield HANDLERS[".rb"], :stdin, $stdin.read
305-
end
370+
371+
workers.inject(false) { |accum, thread| accum || thread.value }
306372
end
307373

308374
# Highlights a snippet from a source and parse error.

0 commit comments

Comments
 (0)