astrolabe 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 46c2d6149556152034a1102c6a17a722068b7d05
4
- data.tar.gz: 89ff885e03757862cd9d5345bfae9127cd5665c0
3
+ metadata.gz: 9b3016609691bcb111bfe66a66ff77da7e533aab
4
+ data.tar.gz: f3314ca723a831d03df024b7e5be1539a5483f76
5
5
  SHA512:
6
- metadata.gz: b76199cb0b7abcab33a57187876733b87eac1e88f0c1db2f96954b99c13b2f707481bfdb71778eac70c02e180bc65a35508b11dceadc46c9bc41aa3be28a7839
7
- data.tar.gz: 3cee02d4725f5ed4a98a3b7ba49818855480a2cdcbd241855dabeb37dea5fe6aaa697544ff8da485f91e5acd9433d24d41a57f435ef9ad9db73b32740a1bdbe0
6
+ metadata.gz: 59af596675bb9815ee963ab92275f636034031c2ec1e245163af0a77b3602a82cdf01072191e958d1faa746d1248a7737fe27907eae741225858d172eaf6c854
7
+ data.tar.gz: c200880804038aa30c87fe8d64f0c50d51823ef5c7c878d7bb3ad6d319238557dd47c2e8585e988efc0a18c8ba3f7a62b451739919ad5a95da1125bb0a4dc3d9
data/Guardfile CHANGED
@@ -7,6 +7,7 @@ group :red_green_refactor, halt_on_fail: true do
7
7
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
8
8
  watch('spec/spec_helper.rb') { 'spec' }
9
9
  watch(%r{^spec/support/.+\.rb$}) { 'spec' }
10
+ watch(%r{^benchmark/.+_spec\.rb$})
10
11
  end
11
12
 
12
13
  guard :rubocop, cli: '--format fuubar' do
data/Rakefile CHANGED
@@ -5,7 +5,20 @@ require 'rspec/core/rake_task'
5
5
  require 'rubocop/rake_task'
6
6
 
7
7
  RSpec::Core::RakeTask.new(:spec)
8
+
9
+ desc 'Run benchmark specs'
10
+ RSpec::Core::RakeTask.new(:benchmark) do |task|
11
+ task.pattern = 'benchmark/**/*_spec.rb'
12
+ task.rspec_opts = '--format documentation'
13
+ end
14
+
8
15
  RuboCop::RakeTask.new(:style)
9
16
 
10
17
  task default: %w(spec style)
11
- task ci: %w(spec style)
18
+
19
+ if RUBY_ENGINE == 'ruby'
20
+ task ci: %w(spec style benchmark)
21
+ else
22
+ # Benchmarks on JRuby and Rubinius are not as stable as CRuby...
23
+ task ci: %w(spec style)
24
+ end
@@ -0,0 +1,5 @@
1
+
2
+ inherit_from: ../.rubocop.yml
3
+
4
+ Documentation:
5
+ Enabled: false
@@ -0,0 +1,107 @@
1
+ # coding: utf-8
2
+
3
+ class Benchmarking
4
+ class << self
5
+ attr_accessor :warm_up
6
+ alias_method :warm_up?, :warm_up
7
+ attr_writer :loop_count
8
+
9
+ def loop_count
10
+ @loop_count ||= 100
11
+ end
12
+
13
+ def pretty_time(time)
14
+ format('%0.05f sec', time)
15
+ end
16
+ end
17
+
18
+ attr_reader :name
19
+
20
+ def initialize(name, &block)
21
+ @name = name
22
+ @process = block
23
+ end
24
+
25
+ def run
26
+ @process.call
27
+ end
28
+
29
+ def time
30
+ return @time if @time
31
+
32
+ self.class.loop_count.times { run } if self.class.warm_up?
33
+ GC.start # https://github.com/ruby/ruby/blob/v2_1_2/lib/benchmark.rb#L265
34
+
35
+ beginning = Time.now
36
+ self.class.loop_count.times { run }
37
+ ending = Time.now
38
+
39
+ @time = ending - beginning
40
+ end
41
+
42
+ def pretty_time
43
+ self.class.pretty_time(time)
44
+ end
45
+
46
+ def inspect
47
+ "#{name} (#{pretty_time})"
48
+ end
49
+
50
+ alias_method :to_s, :inspect
51
+ end
52
+
53
+ RSpec::Matchers.define :be_faster_than do |other|
54
+ match do |subject|
55
+ if @times
56
+ subject.time < (other.time / @times)
57
+ else
58
+ subject.time < other.time
59
+ end
60
+ end
61
+
62
+ {
63
+ twice: 2,
64
+ three_times: 3,
65
+ four_times: 4,
66
+ five_times: 5,
67
+ six_times: 6,
68
+ seven_times: 7
69
+ }.each do |name, value|
70
+ chain name do
71
+ @times = value
72
+ end
73
+ end
74
+
75
+ failure_message do |subject|
76
+ other_label = other.name
77
+ other_label << " / #{@times}" if @times
78
+
79
+ label_width = [subject.name, other_label].map { |label| label.length }.max
80
+
81
+ message = "#{subject.name.rjust(label_width)}: #{subject.pretty_time}\n"
82
+
83
+ if @times
84
+ message << "#{other_label.rjust(label_width)}: "
85
+ shortened_other_time = Benchmarking.pretty_time(other.time / @times)
86
+ message << "#{shortened_other_time} (#{other.pretty_time} / #{@times})"
87
+ else
88
+ message << "#{other_label.rjust(label_width)}: #{other.pretty_time}"
89
+ end
90
+ end
91
+ end
92
+
93
+ RSpec::Matchers.define :be_as_fast_as do |other|
94
+ margin = 1.2
95
+
96
+ match do |subject|
97
+ subject.time < (other.time * margin)
98
+ end
99
+
100
+ failure_message do |subject|
101
+ label_width = [subject, other].map { |b| b.name.length }.max
102
+
103
+ [subject, other].map do |benchmark|
104
+ "#{benchmark.name.rjust(label_width)}: #{benchmark.pretty_time}"
105
+ end.join("\n")
106
+ end
107
+ end
@@ -0,0 +1,342 @@
1
+ # coding: utf-8
2
+
3
+ require_relative 'benchmark_helper'
4
+ require 'astrolabe/node'
5
+ require 'parser/current'
6
+
7
+ describe 'performance' do
8
+ Benchmarking.warm_up = true
9
+ Benchmarking.loop_count = 1000
10
+
11
+ def generate_source(max, current_nest_level = 1)
12
+ <<-END
13
+ class SomeClass#{current_nest_level}
14
+ def some_method(foo)
15
+ do_something do |bar|
16
+ end
17
+ end
18
+
19
+ #{generate_source(max, current_nest_level + 1) if current_nest_level < max}
20
+ end
21
+ END
22
+ end
23
+
24
+ let(:source) { generate_source(100) }
25
+
26
+ def create_builder_class(node_class)
27
+ Class.new(Parser::Builders::Default) do
28
+ define_method(:n) do |type, children, source_map|
29
+ node_class.new(type, children, location: source_map)
30
+ end
31
+ end
32
+ end
33
+
34
+ def ast_with_node_class(node_class)
35
+ buffer = Parser::Source::Buffer.new('(string)')
36
+ buffer.source = source
37
+ builder = create_builder_class(node_class).new
38
+ parser = Parser::CurrentRuby.new(builder)
39
+ parser.parse(buffer)
40
+ end
41
+
42
+ describe 'Node#each_descendant' do
43
+ class EachDescendantBenchmark < Benchmarking
44
+ def initialize(name, root_node)
45
+ @name = name
46
+ @root_node = root_node
47
+ end
48
+
49
+ def run
50
+ count = 0
51
+
52
+ @root_node.each_descendant do
53
+ count += 1
54
+ end
55
+
56
+ fail if count == 0
57
+ end
58
+ end
59
+
60
+ let(:current_implementation) do
61
+ EachDescendantBenchmark.new('current implementation', ast_with_node_class(Astrolabe::Node))
62
+ end
63
+
64
+ describe 'nested-yield vs. block-pass vs. proc-pass' do
65
+ let(:nested_yield) do
66
+ node_class = Class.new(Astrolabe::Node) do
67
+ def each_descendant
68
+ return to_enum(__method__) unless block_given?
69
+
70
+ each_child_node do |child_node|
71
+ yield child_node
72
+ child_node.each_descendant { |node| yield node }
73
+ end
74
+ end
75
+ end
76
+
77
+ EachDescendantBenchmark.new('nested-yield', ast_with_node_class(node_class))
78
+ end
79
+
80
+ let(:block_pass) do
81
+ node_class = Class.new(Astrolabe::Node) do
82
+ def each_descendant(&block)
83
+ return to_enum(__method__) unless block_given?
84
+
85
+ each_child_node do |child_node|
86
+ yield child_node
87
+ child_node.each_descendant(&block)
88
+ end
89
+ end
90
+ end
91
+
92
+ EachDescendantBenchmark.new('block-pass', ast_with_node_class(node_class))
93
+ end
94
+
95
+ let(:proc_pass) do
96
+ node_class = Class.new(Astrolabe::Node) do
97
+ # Yeah, this is ugly interface, but just for reference.
98
+ def each_descendant(proc_object = nil, &block)
99
+ return to_enum(__method__) if !block_given? && !proc_object
100
+
101
+ proc_object ||= block
102
+
103
+ each_child_node do |child_node|
104
+ proc_object.call(child_node)
105
+ child_node.each_descendant(proc_object)
106
+ end
107
+ end
108
+ end
109
+
110
+ EachDescendantBenchmark.new('proc-pass', ast_with_node_class(node_class))
111
+ end
112
+
113
+ specify 'block-pass is obviously (at least 5 times) faster than nested-yield' do
114
+ expect(block_pass).to be_faster_than(nested_yield).five_times
115
+ end
116
+
117
+ specify 'block-pass is a bit faster than proc-pass' do
118
+ expect(block_pass).to be_faster_than(proc_pass)
119
+ end
120
+
121
+ describe 'current implementation' do
122
+ it 'is as fast as block-pass' do
123
+ expect(current_implementation).to be_as_fast_as(block_pass)
124
+ end
125
+ end
126
+ end
127
+
128
+ describe 'inline code vs. delegation to #each_child_node' do
129
+ let(:inline_code) do
130
+ node_class = Class.new(Astrolabe::Node) do
131
+ def each_descendant(&block)
132
+ return to_enum(__method__) unless block_given?
133
+
134
+ children.each do |child|
135
+ next unless child.is_a?(self.class)
136
+ yield child
137
+ child.each_descendant(&block)
138
+ end
139
+
140
+ self
141
+ end
142
+ end
143
+
144
+ EachDescendantBenchmark.new('inline code', ast_with_node_class(node_class))
145
+ end
146
+
147
+ let(:delegation) do
148
+ node_class = Class.new(Astrolabe::Node) do
149
+ def each_descendant(&block)
150
+ return to_enum(__method__) unless block_given?
151
+
152
+ each_child_node do |child_node|
153
+ yield child_node
154
+ child_node.each_descendant(&block)
155
+ end
156
+ end
157
+ end
158
+
159
+ EachDescendantBenchmark.new('delegation', ast_with_node_class(node_class))
160
+ end
161
+
162
+ specify 'inline code is relatively faster than delegation' do
163
+ expect(inline_code).to be_faster_than(delegation)
164
+ end
165
+
166
+ describe 'current implementation' do
167
+ it 'is as fast as inline code' do
168
+ expect(current_implementation).to be_as_fast_as(inline_code)
169
+ end
170
+ end
171
+ end
172
+
173
+ describe 'Array#each vs. for-loop' do
174
+ let(:array_each) do
175
+ node_class = Class.new(Astrolabe::Node) do
176
+ def each_descendant(&block)
177
+ return to_enum(__method__) unless block_given?
178
+
179
+ children.each do |child|
180
+ next unless child.is_a?(self.class)
181
+ yield child
182
+ child.each_descendant(&block)
183
+ end
184
+
185
+ self
186
+ end
187
+ end
188
+
189
+ EachDescendantBenchmark.new('Array#each', ast_with_node_class(node_class))
190
+ end
191
+
192
+ let(:for_loop) do
193
+ node_class = Class.new(Astrolabe::Node) do
194
+ def each_descendant(&block)
195
+ return to_enum(__method__) unless block_given?
196
+
197
+ for child in children # rubocop:disable For
198
+ next unless child.is_a?(self.class)
199
+ yield child
200
+ child.each_descendant(&block)
201
+ end
202
+
203
+ self
204
+ end
205
+ end
206
+
207
+ EachDescendantBenchmark.new('for-loop', ast_with_node_class(node_class))
208
+ end
209
+
210
+ specify "there's no obvious difference between them" do
211
+ expect(array_each).to be_as_fast_as(for_loop)
212
+ end
213
+
214
+ describe 'current implementation' do
215
+ it 'is as fast as Array#each' do
216
+ expect(current_implementation).to be_as_fast_as(array_each)
217
+ end
218
+ end
219
+ end
220
+
221
+ describe 'pure recursion vs. combination of entry and recursion methods' do
222
+ let(:pure_recursion) do
223
+ node_class = Class.new(Astrolabe::Node) do
224
+ def each_descendant(&block)
225
+ return to_enum(__method__) unless block_given?
226
+
227
+ children.each do |child|
228
+ next unless child.is_a?(self.class)
229
+ yield child
230
+ child.each_descendant(&block)
231
+ end
232
+
233
+ self
234
+ end
235
+ end
236
+
237
+ EachDescendantBenchmark.new('pure recursion', ast_with_node_class(node_class))
238
+ end
239
+
240
+ let(:combination) do
241
+ node_class = Class.new(Astrolabe::Node) do
242
+ def each_descendant(&block)
243
+ return to_enum(__method__) unless block_given?
244
+ visit_descendants(&block)
245
+ self
246
+ end
247
+
248
+ def visit_descendants(&block)
249
+ children.each do |child|
250
+ next unless child.is_a?(self.class)
251
+ yield child
252
+ child.visit_descendants(&block)
253
+ end
254
+ end
255
+ end
256
+
257
+ EachDescendantBenchmark.new('combination', ast_with_node_class(node_class))
258
+ end
259
+
260
+ specify 'combination is faster than pure recursion' do
261
+ expect(combination).to be_faster_than(pure_recursion)
262
+ end
263
+
264
+ describe 'current implementation' do
265
+ it 'is as fast as combination' do
266
+ expect(current_implementation).to be_as_fast_as(combination)
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ describe 'Node#each_ancestor' do
273
+ class EachAncestorBenchmark < Benchmarking
274
+ def initialize(name, root_node)
275
+ @name = name
276
+ @deepest_node = root_node.each_node.max_by { |node| node.each_ancestor.count }
277
+ end
278
+
279
+ def run
280
+ count = 0
281
+
282
+ @deepest_node.each_ancestor do
283
+ count += 1
284
+ end
285
+
286
+ fail if count == 0
287
+ end
288
+ end
289
+
290
+ let(:current_implementation) do
291
+ EachAncestorBenchmark.new('current implementation', ast_with_node_class(Astrolabe::Node))
292
+ end
293
+
294
+ describe 'recursion vs. while-loop' do
295
+ let(:while_loop) do
296
+ node_class = Class.new(Astrolabe::Node) do
297
+ def each_ancestor
298
+ return to_enum(__method__) unless block_given?
299
+
300
+ last_node = self
301
+
302
+ while (current_node = last_node.parent)
303
+ yield current_node
304
+ last_node = current_node
305
+ end
306
+
307
+ self
308
+ end
309
+ end
310
+
311
+ EachAncestorBenchmark.new('while-loop', ast_with_node_class(node_class))
312
+ end
313
+
314
+ let(:recursion) do
315
+ node_class = Class.new(Astrolabe::Node) do
316
+ def each_ancestor(&block)
317
+ return to_enum(__method__) unless block_given?
318
+
319
+ if parent
320
+ yield parent
321
+ parent.each_ancestor(&block)
322
+ end
323
+
324
+ self
325
+ end
326
+ end
327
+
328
+ EachAncestorBenchmark.new('recursion', ast_with_node_class(node_class))
329
+ end
330
+
331
+ specify 'while-loop is faster than recursion' do
332
+ expect(while_loop).to be_faster_than(recursion)
333
+ end
334
+
335
+ describe 'current implementation' do
336
+ it 'is as fast as while-loop' do
337
+ expect(current_implementation).to be_as_fast_as(while_loop)
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -69,12 +69,14 @@ module Astrolabe
69
69
  # @yieldparam [Node] node each ancestor node
70
70
  # @return [self] if a block is given
71
71
  # @return [Enumerator] if no block is given
72
- def each_ancestor(&block)
72
+ def each_ancestor
73
73
  return to_enum(__method__) unless block_given?
74
74
 
75
- if parent
76
- yield parent
77
- parent.each_ancestor(&block)
75
+ last_node = self
76
+
77
+ while (current_node = last_node.parent)
78
+ yield current_node
79
+ last_node = current_node
78
80
  end
79
81
 
80
82
  self
@@ -108,11 +110,8 @@ module Astrolabe
108
110
  # @return [Enumerator] if no block is given
109
111
  def each_descendant(&block)
110
112
  return to_enum(__method__) unless block_given?
111
-
112
- each_child_node do |child_node|
113
- yield child_node
114
- child_node.each_descendant(&block)
115
- end
113
+ visit_descendants(&block)
114
+ self
116
115
  end
117
116
 
118
117
  # Calls the given block for the receiver and each descendant node with depth first order.
@@ -129,5 +128,15 @@ module Astrolabe
129
128
  yield self
130
129
  each_descendant(&block)
131
130
  end
131
+
132
+ protected
133
+
134
+ def visit_descendants(&block)
135
+ children.each do |child|
136
+ next unless child.is_a?(Node)
137
+ yield child
138
+ child.visit_descendants(&block)
139
+ end
140
+ end
132
141
  end
133
142
  end
@@ -5,7 +5,7 @@ module Astrolabe
5
5
  # http://semver.org/
6
6
  module Version
7
7
  MAJOR = 0
8
- MINOR = 3
8
+ MINOR = 4
9
9
  PATCH = 0
10
10
 
11
11
  def self.to_s
@@ -17,7 +17,7 @@ module Astrolabe
17
17
  # (arg :arg_b))
18
18
  # (send nil :do_something))
19
19
 
20
- context 'when the node has parent' do
20
+ context 'with a non-root node' do
21
21
  let(:target_node) { root_node.each_node.find(&:args_type?) }
22
22
 
23
23
  it 'returns the parent node' do
@@ -25,7 +25,7 @@ module Astrolabe
25
25
  end
26
26
  end
27
27
 
28
- context 'when the node does not have parent' do
28
+ context 'with a root node' do
29
29
  it 'returns nil' do
30
30
  expect(root_node.parent).to be_nil
31
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: astrolabe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Nakayama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-04 00:00:00.000000000 Z
11
+ date: 2014-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -188,6 +188,9 @@ files:
188
188
  - README.md
189
189
  - Rakefile
190
190
  - astrolabe.gemspec
191
+ - benchmark/.rubocop.yml
192
+ - benchmark/benchmark_helper.rb
193
+ - benchmark/performance_spec.rb
191
194
  - lib/astrolabe.rb
192
195
  - lib/astrolabe/builder.rb
193
196
  - lib/astrolabe/node.rb