astrolabe 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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