benchmarkable 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,54 @@
1
+ BENCHMARKABLE
2
+ by Aman King
3
+
4
+ http://bitbucket.org/amanking/benchmarkable/
5
+
6
+ A module that allows a class to mention which instance methods to benchmark, and then allows retrieving the benchmark report in csv format from the class's instances. The csv includes the method invocation timestamp, some context description (if provided), the method name along with arguments passed for the invocation, and the number of seconds the method took to execute.
7
+
8
+ Eg:
9
+ class Gateway
10
+ # ...
11
+ def make_request(url)
12
+ url = URI.parse(url)
13
+ Net::HTTP.start(url.host, url.port) {|http|
14
+ http.get('/index.html')
15
+ }
16
+ rescue => error
17
+ puts "Unexpected error: #{error.message}"
18
+ end
19
+ # ...
20
+
21
+ include Benchmarkable
22
+ benchmark :make_request
23
+ end
24
+
25
+ gateway = Gateway.new
26
+ gateway.make_request('http://www.google.com/')
27
+ gateway.make_request('http://www.yahoo.com/')
28
+ gateway.make_request('http://www.wikyblog.com/AmanKing/')
29
+ File.open('./gateway_performance_report.csv', 'a+') do |file|
30
+ file.puts gateway.benchmark_report.to_csv(:starting_context => 'gateway')
31
+ end
32
+
33
+ Output (in gateway_performance_report.csv):
34
+ "2009-09-10T20:13:59+05:30","gateway","make_request http://www.google.com/","0.306514978408813"
35
+ "2009-09-10T20:13:59+05:30","gateway","make_request http://www.yahoo.com/","1.08063411712646"
36
+ "2009-09-10T20:14:00+05:30","gateway","make_request http://www.wikyblog.com/AmanKing/","2.1521680355072"
37
+
38
+ (see samples/ajaxy_blog_spec.rb for an example of how to use Benchmarkable to monitor ajax performance using selenium, and look at spec/benchmarkable_spec.rb for more details about the api)
39
+
40
+ License
41
+
42
+ Copyright 2009 Aman King
43
+
44
+ Licensed under the Apache License, Version 2.0 (the "License");
45
+ you may not use this file except in compliance with the License.
46
+ You may obtain a copy of the License at
47
+
48
+ http://www.apache.org/licenses/LICENSE-2.0
49
+
50
+ Unless required by applicable law or agreed to in writing, software
51
+ distributed under the License is distributed on an "AS IS" BASIS,
52
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
53
+ See the License for the specific language governing permissions and
54
+ limitations under the License.
@@ -0,0 +1,141 @@
1
+ =begin
2
+ Copyright 2009 Aman King
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ =end
16
+
17
+ require 'benchmark'
18
+ require 'date'
19
+ require 'faster_csv'
20
+
21
+ module Benchmarkable
22
+ def self.included(klass)
23
+ klass.extend(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods
27
+ def benchmark(*methods_with_description)
28
+ methods_with_description.each do |method_with_description|
29
+ if method_with_description.kind_of?(Hash)
30
+ method_with_description.each_pair {|method_name, description| decorate_for_benchmarking(method_name, description) }
31
+ else
32
+ decorate_for_benchmarking(method_with_description, method_with_description)
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+ def decorate_for_benchmarking(method_name, description)
39
+ return if method_name.to_s =~ /(=|\+|-|\[\]|\*|\/|<<|>>|<|>)/ || method_name.to_s =~ /with(out)?_benchmarking$/
40
+
41
+ with_feature_method_name, without_feature_method_name = with_and_without_feature_method_names(method_name, "benchmarking")
42
+ return if instance_methods.include?(with_feature_method_name)
43
+
44
+ class_eval <<-EOS
45
+ def #{with_feature_method_name}(*args, &block)
46
+ benchmark_report.add_entry_for("#{description} \#{args.join(', ')}".strip) do
47
+ #{without_feature_method_name}(*args, &block)
48
+ end
49
+ end
50
+ EOS
51
+ alias_method_chain method_name, :benchmarking
52
+ end
53
+
54
+ unless self.respond_to?(:alias_method_chain)
55
+ def alias_method_chain(method_name, feature)
56
+ with_feature_method_name, without_feature_method_name = with_and_without_feature_method_names(method_name, feature)
57
+ alias_method without_feature_method_name, method_name
58
+ alias_method method_name, with_feature_method_name
59
+ end
60
+ end
61
+
62
+ def with_and_without_feature_method_names(method_name, feature)
63
+ method_name = method_name.to_s
64
+ if method_name =~ /(!|\?)$/
65
+ return ["#{method_name[0...-1]}_with_#{feature}#{$1}", "#{method_name[0...-1]}_without_#{feature}#{$1}"]
66
+ end
67
+ ["#{method_name}_with_#{feature}", "#{method_name}_without_#{feature}"]
68
+ end
69
+ end
70
+
71
+ def benchmark_report
72
+ @benchmark_report ||= BenchmarkReport.new
73
+ end
74
+
75
+ class BenchmarkReport
76
+ def add_entry_for(description)
77
+ result = nil
78
+ call_stack << []
79
+ data = Data.new(description) do
80
+ result = yield
81
+ end
82
+ call_stack.pop.each {|nested_call_data| data << nested_call_data }
83
+ call_stack.last << data
84
+ result
85
+ end
86
+
87
+ def to_csv(options = {})
88
+ csv_options = {:force_quotes => true, :encoding => 'U'}.merge(options.delete(:csv_options) || {})
89
+ FasterCSV.generate(csv_options) do |csv|
90
+ entries(options).each do |entry|
91
+ csv << [entry[:timestamp].to_s, entry[:context], entry[:execution_time].real].flatten
92
+ end
93
+ end
94
+ end
95
+
96
+ def entries(options = {})
97
+ starting_context = options[:starting_context].to_a || []
98
+ nesting_limit = options[:nesting_limit] == :none ? call_stack.first.collect {|data| data.nested_levels }.max : options[:nesting_limit] || 0
99
+ call_stack.first.collect {|data| data.to_entry(starting_context, nesting_limit) }.flatten
100
+ end
101
+
102
+ private
103
+ def call_stack
104
+ scope_for_top_level_calls = []
105
+ @call_stack ||= [scope_for_top_level_calls]
106
+ end
107
+
108
+ class Data
109
+ def initialize(description)
110
+ @description = description
111
+ @timestamp = DateTime.now
112
+ @overall_time = Benchmark.measure do
113
+ yield
114
+ end
115
+ end
116
+
117
+ def <<(data_for_nested_call)
118
+ data_for_nested_calls << data_for_nested_call
119
+ end
120
+
121
+ def to_entry(context_so_far = [], nesting_limit = 0)
122
+ current_context = context_so_far + [@description]
123
+ empty_context_columns = nesting_limit > 0 ? ['-'] * [nesting_limit, nested_levels].max : []
124
+ current_entry = {:timestamp => @timestamp, :context => current_context + empty_context_columns, :execution_time => @overall_time}
125
+ nested_entries = nesting_limit == 0 ? [] :
126
+ data_for_nested_calls.collect {|nested_call_data| nested_call_data.to_entry(current_context, nesting_limit - 1)}.flatten
127
+ [current_entry] + nested_entries
128
+ end
129
+
130
+ def nested_levels
131
+ return 0 if data_for_nested_calls.empty?
132
+ 1 + data_for_nested_calls.collect {|nested_call_data| nested_call_data.nested_levels }.max
133
+ end
134
+
135
+ protected
136
+ def data_for_nested_calls
137
+ @data_for_nested_calls ||= []
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,270 @@
1
+ =begin
2
+ Copyright 2009 Aman King
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ =end
16
+
17
+ require 'spec'
18
+ require File.join(File.dirname(__FILE__), '../lib/benchmarkable')
19
+
20
+ describe Benchmarkable do
21
+ before(:each) do
22
+ @klass = Class.new { include Benchmarkable }
23
+ end
24
+
25
+ describe 'alias methods' do
26
+ it 'should be created for a method with a normal name' do
27
+ @klass.class_eval do
28
+ def some_method(*args); end
29
+ benchmark :some_method
30
+ end
31
+
32
+ @klass.new.should respond_to(:some_method)
33
+ @klass.new.should respond_to(:some_method_with_benchmarking)
34
+ @klass.new.should respond_to(:some_method_without_benchmarking)
35
+ end
36
+
37
+ it 'should be created for a method with a ! suffix' do
38
+ @klass.class_eval do
39
+ def some_method!(*args); end
40
+ benchmark :some_method!
41
+ end
42
+
43
+ @klass.new.should respond_to(:some_method!)
44
+ @klass.new.should respond_to(:some_method_with_benchmarking!)
45
+ @klass.new.should respond_to(:some_method_without_benchmarking!)
46
+ end
47
+
48
+ it 'should be created for a method with a ? suffix' do
49
+ @klass.class_eval do
50
+ def some_method?(*args); end
51
+ benchmark :some_method?
52
+ end
53
+
54
+ @klass.new.should respond_to(:some_method?)
55
+ @klass.new.should respond_to(:some_method_with_benchmarking?)
56
+ @klass.new.should respond_to(:some_method_without_benchmarking?)
57
+ end
58
+
59
+ it 'should not be created for a method with a = suffix' do
60
+ @klass.class_eval do
61
+ def some_method=(*args); end
62
+ benchmark :some_method=
63
+ end
64
+
65
+ @klass.new.should respond_to(:some_method=)
66
+ @klass.new.should_not respond_to(:some_method_with_benchmarking=)
67
+ @klass.new.should_not respond_to(:some_method_without_benchmarking=)
68
+ end
69
+
70
+ it 'should not be created for operators' do
71
+ @klass.class_eval do
72
+ operators = [:==, :+, :-, :[], :*, :/, :<<, :>>, :<, :>]
73
+ operators.each do |operator|
74
+ define_method(operator) {}
75
+ end
76
+ benchmark *operators
77
+ end
78
+
79
+ [:==, :+, :-, :[], :*, :/, :<<, :>>, :<, :>].each do |operator|
80
+ @klass.new.should respond_to(operator)
81
+ @klass.new.should_not respond_to("#{operator}_with_benchmarking")
82
+ @klass.new.should_not respond_to("#{operator}_without_benchmarking")
83
+ end
84
+ end
85
+
86
+ it 'should not be created for a decorating method (with a with_benchmarking or without_benchmarking suffix)' do
87
+ @klass.class_eval do
88
+ def some_method_with_benchmarking(*args); end
89
+ def some_method_without_benchmarking(*args); end
90
+ benchmark :some_method_with_benchmarking, :some_method_without_benchmarking
91
+ end
92
+
93
+ [:some_method_with_benchmarking, :some_method_without_benchmarking].each do |method_name|
94
+ @klass.new.should respond_to(method_name)
95
+ @klass.new.should_not respond_to("#{method_name}_with_benchmarking")
96
+ @klass.new.should_not respond_to("#{method_name}_without_benchmarking")
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ it 'should benchmark a method using the method name as context' do
103
+ @klass.class_eval do
104
+ def some_method(*args); 'something'; end
105
+ benchmark :some_method
106
+ end
107
+
108
+ DateTime.stub!(:now).and_return(:time1)
109
+ Benchmark.should_receive(:measure).and_yield.and_return(:measured_time)
110
+ instance = @klass.new
111
+ instance.some_method('arg1', 'arg2').should == 'something'
112
+ instance.benchmark_report.entries.should == [{:timestamp => :time1, :context =>['some_method arg1, arg2'], :execution_time => :measured_time }]
113
+ end
114
+
115
+ it 'should not interfere with the functionality of the original method' do
116
+ @klass.class_eval do
117
+ def some_method(*args); 'something'; end
118
+ benchmark :some_method
119
+ end
120
+
121
+ @klass.new.some_method('arg1', 'arg2').should == 'something'
122
+ end
123
+
124
+ it 'should not decorate a method for benchmarking more than once' do
125
+ @klass.class_eval do
126
+ def some_method(*args); 'something'; end
127
+ benchmark :some_method, :some_method
128
+ end
129
+
130
+ lambda { @klass.new.some_method('arg1', 'arg2') }.should_not raise_error
131
+ end
132
+
133
+ it 'should benchmark a method using the specified context message' do
134
+ @klass.class_eval do
135
+ def some_method(*args); 'something'; end
136
+ benchmark :some_method => 'context 1'
137
+ end
138
+
139
+ DateTime.stub!(:now).and_return(:time1)
140
+ Benchmark.should_receive(:measure).and_yield.and_return(:measured_time)
141
+ instance = @klass.new
142
+ instance.some_method('arg1', 'arg2')
143
+ instance.benchmark_report.entries.should == [{:timestamp => :time1, :context =>['context 1 arg1, arg2'], :execution_time => :measured_time }]
144
+ end
145
+
146
+ it 'should benchmark a method using the specified context message after the starting context' do
147
+ @klass.class_eval do
148
+ def some_method(*args); 'something'; end
149
+ benchmark :some_method => 'context 1'
150
+ end
151
+
152
+ DateTime.stub!(:now).and_return(:time1)
153
+ Benchmark.should_receive(:measure).and_yield.and_return(:measured_time)
154
+ instance = @klass.new
155
+ instance.some_method('arg1', 'arg2')
156
+
157
+ instance.benchmark_report.entries(:starting_context => 'my context').should ==
158
+ [{:timestamp => :time1, :context =>['my context', 'context 1 arg1, arg2'], :execution_time => :measured_time }]
159
+
160
+ instance.benchmark_report.entries(:starting_context => ['my context1', 'my context2']).should ==
161
+ [{:timestamp => :time1, :context =>['my context1', 'my context2', 'context 1 arg1, arg2'], :execution_time => :measured_time }]
162
+ end
163
+
164
+ it 'should benchmark multiple methods' do
165
+ @klass.class_eval do
166
+ def some_method(*args); 'something'; end
167
+ def another_method; 'another'; end
168
+ benchmark :another_method, :some_method => 'context 1'
169
+ end
170
+
171
+ DateTime.stub!(:now).and_return(:time1, :time2)
172
+ Benchmark.should_receive(:measure).twice.and_yield.and_return(:measured_time1, :measured_time2)
173
+ instance = @klass.new
174
+ instance.some_method('arg1', 'arg2')
175
+ instance.another_method
176
+ instance.benchmark_report.entries.should ==
177
+ [
178
+ {:timestamp => :time1, :context =>['context 1 arg1, arg2'], :execution_time => :measured_time1 },
179
+ {:timestamp => :time2, :context =>['another_method'], :execution_time => :measured_time2 }
180
+ ]
181
+ end
182
+
183
+ describe 'with nested context' do
184
+ before(:each) do
185
+ @klass.class_eval do
186
+ def some_method(*args); 'something'; end
187
+ def another_method; 'another'; end
188
+ def yet_another_method; some_method('blah'); 'yet ' + another_method; end
189
+ def yet_another_yet_another_method; 'yet ' + yet_another_method; end
190
+ benchmark :another_method => 'another', :some_method => 'some', :yet_another_method => 'yet', :yet_another_yet_another_method => 'yet yet'
191
+ end
192
+
193
+ DateTime.stub!(:now).and_return(*Array.new(9) {|i| "time#{i + 1}".to_sym})
194
+ Benchmark.should_receive(:measure).twice.and_yield.and_return(*Array.new(9) {|i| "measured_time#{i + 1}".to_sym})
195
+ @instance = @klass.new
196
+ @instance.some_method('arg1', 'arg2')
197
+ @instance.another_method
198
+ @instance.yet_another_method
199
+ @instance.yet_another_yet_another_method
200
+ end
201
+
202
+ it 'should benchmark methods without any nesting level' do
203
+ @instance.benchmark_report.entries.should ==
204
+ [
205
+ {:timestamp => :time1, :context =>['some arg1, arg2'], :execution_time => :measured_time1 },
206
+ {:timestamp => :time2, :context =>['another'], :execution_time => :measured_time2 },
207
+ {:timestamp => :time3, :context =>['yet'], :execution_time => :measured_time5 },
208
+ {:timestamp => :time6, :context =>['yet yet'], :execution_time => :measured_time9 },
209
+ ]
210
+ end
211
+
212
+ it 'should benchmark methods till two nesting levels' do
213
+ @instance.benchmark_report.entries(:nesting_limit => 2).should ==
214
+ [
215
+ {:timestamp => :time1, :context =>['some arg1, arg2', '-', '-'], :execution_time => :measured_time1 },
216
+ {:timestamp => :time2, :context =>['another', '-', '-'], :execution_time => :measured_time2 },
217
+ {:timestamp => :time3, :context =>['yet', '-', '-'], :execution_time => :measured_time5 },
218
+ {:timestamp => :time4, :context =>['yet', 'some blah', '-'], :execution_time => :measured_time3 },
219
+ {:timestamp => :time5, :context =>['yet', 'another', '-'], :execution_time => :measured_time4 },
220
+ {:timestamp => :time6, :context =>['yet yet', '-', '-'], :execution_time => :measured_time9 },
221
+ {:timestamp => :time7, :context =>['yet yet', 'yet', '-'], :execution_time => :measured_time8 },
222
+ {:timestamp => :time8, :context =>['yet yet', 'yet', 'some blah'], :execution_time => :measured_time6 },
223
+ {:timestamp => :time9, :context =>['yet yet', 'yet', 'another'], :execution_time => :measured_time7 }
224
+ ]
225
+ end
226
+
227
+ it 'should benchmark methods till all nesting levels' do
228
+ @instance.benchmark_report.entries(:nesting_limit => :none).should ==
229
+ [
230
+ {:timestamp => :time1, :context =>['some arg1, arg2', '-', '-'], :execution_time => :measured_time1 },
231
+ {:timestamp => :time2, :context =>['another', '-', '-'], :execution_time => :measured_time2 },
232
+ {:timestamp => :time3, :context =>['yet', '-', '-'], :execution_time => :measured_time5 },
233
+ {:timestamp => :time4, :context =>['yet', 'some blah', '-'], :execution_time => :measured_time3 },
234
+ {:timestamp => :time5, :context =>['yet', 'another', '-'], :execution_time => :measured_time4 },
235
+ {:timestamp => :time6, :context =>['yet yet', '-', '-'], :execution_time => :measured_time9 },
236
+ {:timestamp => :time7, :context =>['yet yet', 'yet', '-'], :execution_time => :measured_time8 },
237
+ {:timestamp => :time8, :context =>['yet yet', 'yet', 'some blah'], :execution_time => :measured_time6 },
238
+ {:timestamp => :time9, :context =>['yet yet', 'yet', 'another'], :execution_time => :measured_time7 }
239
+ ]
240
+ end
241
+ end
242
+
243
+ describe 'exporting as csv' do
244
+ it 'should generate csv output for entries' do
245
+ @klass.class_eval do
246
+ def some_method(*args); 'something'; end
247
+ def another_method; 'another'; end
248
+ def yet_another_method; some_method('blah'); 'yet ' + another_method; end
249
+ def yet_another_yet_another_method; 'yet ' + yet_another_method; end
250
+ benchmark :another_method => 'another', :some_method => 'some', :yet_another_method => 'yet', :yet_another_yet_another_method => 'yet yet'
251
+ end
252
+
253
+ times = (1..5).collect{ |i| DateTime.parse("2009-07-29T10:0#{i}:00+05:30") }
254
+ DateTime.stub!(:now).and_return(*times)
255
+ Benchmark.should_receive(:measure).twice.and_yield.and_return(*Array.new(5) {|i| stub("measured_time#{i + 1}", :real => i + 1) })
256
+ instance = @klass.new
257
+ instance.some_method('arg1', 'arg2')
258
+ instance.yet_another_yet_another_method
259
+
260
+ instance.benchmark_report.to_csv(:nesting_limit => :none, :starting_context => 'initial context', :csv_options => {:row_sep => "\n"}).should ==
261
+ <<EOS
262
+ "2009-07-29T10:01:00+05:30","initial context","some arg1, arg2","-","-","1"
263
+ "2009-07-29T10:02:00+05:30","initial context","yet yet","-","-","5"
264
+ "2009-07-29T10:03:00+05:30","initial context","yet yet","yet","-","4"
265
+ "2009-07-29T10:04:00+05:30","initial context","yet yet","yet","some blah","2"
266
+ "2009-07-29T10:05:00+05:30","initial context","yet yet","yet","another","3"
267
+ EOS
268
+ end
269
+ end
270
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: benchmarkable
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Aman King
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-14 00:00:00 +05:30
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Benchmarkable allows monitoring the performance of instance methods and reporting those in csv format
17
+ email: amanking@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README
26
+ - lib/benchmarkable.rb
27
+ - spec/benchmarkable_spec.rb
28
+ has_rdoc: true
29
+ homepage: http://bitbucket.org/amanking/benchmarkable/
30
+ licenses: []
31
+
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.3.5
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: Benchmarkable allows monitoring the performance of instance methods and reporting those in csv format
56
+ test_files:
57
+ - spec/benchmarkable_spec.rb