rbenchmarker 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2877e9a63034a510f81700591df732ac237b2f7f30e7a09ac34edee5d2315faf
4
+ data.tar.gz: 282590fe246b95a914dd91e3bafb9985b3cd9877d29272373af2790b389391d3
5
+ SHA512:
6
+ metadata.gz: 3e06e196167f76bb8a9bfef4e55bf81f02779fe3b842e969518dbb2df0978b216b0001b0567aa3595bc5a2ae15f22fc577429e6462c3c418fdaf99216d5c2c40
7
+ data.tar.gz: 7a9178fa2069857b395f3c92d3768b3ff08176c82d2dc3a43007df73220dd91fb460d639c593b940c3712efa6aa953f4753a0ed4fb78c3b11b7ad413e5b8342c
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Rbenchmarker Changelog
2
+
3
+ Doing our best at supporting [SemVer](http://semver.org/) with a nice looking [Changelog](http://keepachangelog.com).
4
+
5
+ ## Version 0.1.1 <sub><sup>2021-02-05</sub></sup>
6
+
7
+ - Corrected incorrect description
8
+
9
+ ## Version 0.1.0 <sub><sup>2020-02-05</sub></sup>
10
+
11
+ - Initial version
12
+ - Class methods benchmark
13
+ - Module methods benchmark
14
+ - Options to select the target
15
+ - Options to choose where to place the logs
16
+ - Benchmark option to select the number of executions
17
+ - Option to select dry run
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 daiki shibata
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,364 @@
1
+ # Rbenchmarker
2
+
3
+ Rbenchmarker is a gem that allows you to automatically benchmark the execution time of a method defined in a Ruby class and module.
4
+ Benchmark module (https://docs.ruby-lang.org/ja/latest/class/Benchmark.html) is used inside Rbenchmarker, and bm method is automatically applied to all target methods.
5
+
6
+ However, method itself to which Rbenchmarker is applied remains unchanged, takes the same arguments as before, and returns the same return value as before.
7
+
8
+ So you don't have to change the methods yourself, and you don't have to benchmark the methods one by one.
9
+ Just launch the application as before and will automatically benchmark all targeted methods!
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ # Gemfile
17
+
18
+ gem 'rbenchmarker'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle install
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install rbenchmarker
28
+
29
+ ## Add the launch process to your Ruby project
30
+
31
+ Add the following code to any file in your Ruby project.
32
+
33
+ ```ruby
34
+ # Note that you need to run `Rbenchmarker.setup` after all the files have been read.
35
+ Rbenchmarker.setup switch: 'on',
36
+ output_log_path: nil,
37
+ except_classes: [],
38
+ except_modules: []
39
+ ```
40
+
41
+ Or run it directly from console
42
+
43
+ ```
44
+ irb(main):001:0> Rbenchmarker.setup switch: 'on', output_log_path: nil, except_classes: [], except_modules: []
45
+ => true
46
+ ```
47
+
48
+ The `setup` method executes the process of adding the benchmark function to all the methods in the specified class and module.
49
+
50
+ Note that you need to run `Rbenchmarker.setup` after all the files (classes, modules) have been read.
51
+
52
+ [Details of setup options](https://github.com/shibatadaiki/Rbenchmarker#about-setup-options)
53
+
54
+ ### For Ruby on Rails projects, add the following settings to your `config`
55
+
56
+ ```ruby
57
+ # config/environments/development.rb
58
+
59
+ config.eager_load = true # Please note that this setting is mandatory!
60
+
61
+ config.after_initialize do
62
+ Rbenchmarker.setup switch: 'on', output_log_path: nil, except_classes: [], except_modules: []
63
+ end
64
+ ```
65
+
66
+ Please note that when using the Rbenchmark feature, the server will need to be restarted for the file changes to take effect.
67
+
68
+ [Details of config.after_initialize and config.eager_load](https://guides.rubyonrails.org/configuring.html#rails-general-configuration)
69
+
70
+ ## Add rbenchmarker to your Class
71
+
72
+ ```ruby
73
+ # app/models/sample_class.rb
74
+
75
+ class SampleClass
76
+ rbenchmarker all: __FILE__
77
+ end
78
+ ```
79
+
80
+ Your Class is now a benchmarker!
81
+
82
+ When the method in the class in which rbenchmarker is set is executed, the following log will be output.
83
+
84
+ Logs are placed directly under the './log' directory if './log' directory exists, or directly under the current directory if does not exist.
85
+
86
+ `rbenchmarker.log`
87
+
88
+ ```log
89
+ # Logfile created on 2020-12-22 16:24:06 +0900 by logger.rb/v1.4.2
90
+ I, [2020-12-22T16:24:06.327445 #54558] INFO -- : == Start recording Rbenchmarker ==
91
+
92
+ I, [2020-12-22T16:24:12.848277 #54558] INFO -- :
93
+ report def test_method1 class method: current time
94
+ user: 0.00000900, system: 0.00000700, total: 0.00001600, real: 0.00000700
95
+ report def test_method1 class method: total time for 1 times called
96
+ user: 0.00000900, system: 0.00000700, total: 0.00001600, real: 0.00000700
97
+ report def test_method1 class method: avarage time
98
+ user: 0.00000900, system: 0.00000700, total: 0.00001600, real: 0.00000700
99
+
100
+ I, [2020-12-22T16:24:14.009972 #54558] INFO -- :
101
+ report def test_method1 class method: current time
102
+ user: 0.00000500, system: 0.00000200, total: 0.00000700, real: 0.00000400
103
+ report def test_method1 class method: total time for 2 times called
104
+ user: 0.00001400, system: 0.00000900, total: 0.00002300, real: 0.00001100
105
+ report def test_method1 class method: avarage time
106
+ user: 0.00000700, system: 0.00000450, total: 0.00001150, real: 0.00000550
107
+
108
+ I, [2020-12-22T16:24:29.969068 #54558] INFO -- :
109
+ report def test_method2 instance method: current time
110
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000500
111
+ report def test_method2 instance method: total time for 1 times called
112
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000500
113
+ report def test_method2 instance method: avarage time
114
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000500
115
+
116
+ I, [2020-12-22T16:24:30.545224 #54558] INFO -- :
117
+ report def test_method2 instance method: current time
118
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000500
119
+ report def test_method2 instance method: total time for 2 times called
120
+ user: 0.00001200, system: 0.00000400, total: 0.00001600, real: 0.00001000
121
+ report def test_method2 instance method: avarage time
122
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000500
123
+
124
+ I, [2020-12-22T16:24:31.185216 #54558] INFO -- :
125
+ report def test_method2 instance method: current time
126
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000400
127
+ report def test_method2 instance method: total time for 3 times called
128
+ user: 0.00001800, system: 0.00000600, total: 0.00002400, real: 0.00001400
129
+ report def test_method2 instance method: avarage time
130
+ user: 0.00000600, system: 0.00000200, total: 0.00000800, real: 0.00000467
131
+ ```
132
+
133
+ If repeat the same method, the total execution time and the average execution time of the number of executions will be output to the log.
134
+
135
+ ## Add rbenchmarker to your Module
136
+
137
+ Can do the same with class as with module, but with module, additional work is required.
138
+
139
+ ```ruby
140
+ # lib/sample_module.rb
141
+
142
+ module SampleModule
143
+ extend Rbenchmarker::ClassMethods # for module, add this sentence before launching rbenchmarker
144
+ rbenchmarker all: __FILE__
145
+ end
146
+ ```
147
+
148
+ Then add the following options in the class that is using that module.
149
+
150
+ ```ruby
151
+ # app/models/class_include_module.rb
152
+
153
+ class ClassIncludeModule
154
+ include SampleModule
155
+ rbenchmarker all: __FILE__, include: [SampleModule] # In the `include` option, specify all included modules in array format
156
+ end
157
+ ```
158
+
159
+ ```ruby
160
+ # app/models/class_extend_module.rb
161
+
162
+ class ClassExtendModule
163
+ extend SampleModule
164
+ rbenchmarker all: __FILE__, extend: [SampleModule] # In the `extend` option, specify all extended modules in array format
165
+ end
166
+ ```
167
+
168
+ ```ruby
169
+ # app/models/class_prepend_module.rb
170
+
171
+ class ClassPrependModule
172
+ prepend SampleModule
173
+ rbenchmarker all: __FILE__, prepend: [SampleModule] # In the `prepend` option, specify all prepended modules in array format
174
+ end
175
+ ```
176
+
177
+ The same is true for modules that use modules.
178
+
179
+ ```ruby
180
+ # lib/module_include_module.rb
181
+
182
+ module DoIncludeModule
183
+ include SampleModule
184
+ extend Rbenchmarker::ClassMethods
185
+ rbenchmarker all: __FILE__, include: [SampleModule]
186
+ end
187
+ ```
188
+
189
+ ```ruby
190
+ # lib/module_extend_module.rb
191
+
192
+ module DoExtendModule
193
+ extend SampleModule
194
+ extend Rbenchmarker::ClassMethods
195
+ rbenchmarker all: __FILE__, extend: [SampleModule]
196
+ end
197
+ ```
198
+
199
+ ```ruby
200
+ # lib/module_prepend_module.rb
201
+
202
+ module DoPrependModule
203
+ prepend SampleModule
204
+ extend Rbenchmarker::ClassMethods
205
+ rbenchmarker all: __FILE__, prepend: [SampleModule]
206
+ end
207
+ ```
208
+
209
+ Your Module is now a benchmarker!
210
+
211
+ ## About `rbenchmarker` options
212
+
213
+ List of all possible options
214
+
215
+ | Options | Description | Exsample |
216
+ | ------------- | ------------- | ------------- |
217
+ | `all` | `all` option performs static analysis to identify the target method, so put `__FILE__` in the argument. Setting this option will measure all methods listed in the file. normally, set this option. | `rbenchmarker all: __FILE__` |
218
+ | `only` | method specified in `only` only will be benchmarked. | `rbenchmarker only: [:sample_method1, :sample_method2]` |
219
+ | `except` | method specified in `except` will not be benchmarked. | `rbenchmarker except: [:sample_method1, :sample_method2]` |
220
+ | `added` | method specified by `added` is added to the benchmark after the `only` and `except` method filtering is done. | `rbenchmarker added: [:sample_method1, :sample_method2]` |
221
+ | `label_width` | `label_width` specifies the width of the benchmark label. | `rbenchmarker label_width: 25` |
222
+ | `times` | benchmark is measured by repeatedly executing the benchmarked method for the number of times specified by `times`. (Keep in mind that code such as SQL queries will also be executed repeatedly.) | `rbenchmarker times: 5` |
223
+ | `require_hidden_method` | If `require_hidden_method` option is set to true, methods dynamically created by metaprogramming, methods added to another file, etc. are also included. | `rbenchmarker require_hidden_method: true` |
224
+ | `include` | in the `include` option, specify the module that you are including. Arrange the option array in the loading order of the modules to be included. | `rbenchmarker include: [SampleModule1, SampleModule2]` |
225
+ | `extend` | in the `extend` option, specify the module that you are extending. Arrange the option array in the loading order of the modules to be extended. | `rbenchmarker extend: [SampleModule1, SampleModule2]` |
226
+ | `prepend` | in the `prepend` option, specify the module that you are prepending. Arrange the option array in the loading order of the modules to be prepended. | `rbenchmarker prepend: [SampleModule1, SampleModule2]` |
227
+
228
+ Can set multiple options as follows.
229
+
230
+ ```ruby
231
+ # app/models/sample_class.rb
232
+
233
+ class SampleClass
234
+ rbenchmarker all: __FILE__, only: [:sample_method1, :sample_method2], label_width: 25
235
+ end
236
+ ```
237
+
238
+ Of course, also possible with modules.
239
+
240
+ ```ruby
241
+ # lib/sample_module.rb
242
+
243
+ module SampleModule
244
+ extend Rbenchmarker::ClassMethods
245
+ rbenchmarker all: __FILE__, only: [:sample_method1, :sample_method2], label_width: 25
246
+ end
247
+ ```
248
+
249
+ ## About `require_hidden_method` option
250
+
251
+ Maybe, `require_hidden_method` option may often be used in Ruby on Rails projects.
252
+
253
+ For example, in the case of the following Ruby on Rails code
254
+
255
+ ```ruby
256
+ # app/models/sample_class.rb
257
+
258
+ class SampleClass
259
+ rbenchmarker all: __FILE__
260
+
261
+ belongs_to :sample_parent
262
+ has_many :sample_children, dependent: :destroy
263
+ scope :has_sample_parent, -> { where.not(sample_parent_id: nil) }
264
+
265
+ def my_name_length
266
+ name.length
267
+ end
268
+ end
269
+ ```
270
+
271
+ Ruby on Rails adds methods to the class with various options, but usually it's not written in a file.
272
+ However, the `all: __FILE__` option can only track methods defined directly in the file. (In this case, only `my_name_length` is tracked)
273
+
274
+ In such cases, use the `require_hidden_method` option to track invisible methods.
275
+
276
+ ```ruby
277
+ # app/models/sample_class.rb
278
+
279
+ class SampleClass
280
+ rbenchmarker require_hidden_method: true # the `all` option is not required when using the `require_hidden_method` option
281
+
282
+ belongs_to :sample_parent
283
+ has_many :sample_children, dependent: :destroy
284
+ scope :has_sample_parent, -> { where.not(sample_parent_id: nil) }
285
+
286
+ def my_name_length
287
+ name.length
288
+ end
289
+ end
290
+ ```
291
+
292
+ The same applies in the following cases.
293
+
294
+ ```ruby
295
+ # lib/module_include_module.rb
296
+
297
+ module IncludedModule
298
+ extend ActiveSupport::Concern
299
+ included do
300
+ def sample_module_method
301
+ puts 'sample_module_method!'
302
+ end
303
+ end
304
+ end
305
+
306
+ # app/models/sample_class.rb
307
+
308
+ class SampleClass
309
+ include IncludedModule
310
+ rbenchmarker require_hidden_method: true # Requires the `require_hidden_method` option to track the IncludedModule methods
311
+ end
312
+ ```
313
+
314
+ Set of `extend ActiveSupport::Concern` and `included do ~`, which is often seen when using Ruby on Rails modules, adds a method to the class itself by internally executing `class_eval` to the target class
315
+ (Rather than using module methods).
316
+ Therefore, want Rbenchmarker to track the methods added in this way, need the `require_hidden_method` option.
317
+
318
+ However, `require_hidden_method` option can be annoying as it keeps track of all hidden methods. In that case, adjust using options such as `only`, `except`, and `added`.
319
+
320
+ ## About `setup` options
321
+
322
+ | Options | Description | Exsample |
323
+ | ------------- | ------------- | ------------- |
324
+ | `switch` | In case of `off` or `OFF`, the processing of rbenchmarker will not be executed. | `Rbenchmarker.setup switch: 'off'` |
325
+ | `output_log_path` | Specify the output destination of the measurement result log output by rbenchmark. | `Rbenchmarker.setup output_log_path: '/Users/daiki.shibata/xxx/workspace'` |
326
+ | `except_classes` | Specify the class that does not give rbenchmark processing. | `Rbenchmarker.setup except_classes: [Class1, Class2, Class3]` |
327
+ | `except_modules` | Specify the module that does not give rbenchmark processing. | `Rbenchmarker.setup except_modules: [Module1, Module2, Module3]` |
328
+
329
+ Can set multiple options as follows.
330
+
331
+ ```ruby
332
+ # rbenchmarker_setup.rb
333
+
334
+ Rbenchmarker.setup switch: 'on',
335
+ output_log_path: '/Users/daiki.shibata/xxx/workspace',
336
+ except_classes: [Class1, Class2, Class3],
337
+ except_modules: [Module1, Module2, Module3]
338
+ ```
339
+
340
+ ## License
341
+
342
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
343
+
344
+ ## Code of Conduct
345
+
346
+ Everyone interacting in the Rbenchmarker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/shibatadaiki/Rbenchmarker/blob/main/CODE_OF_CONDUCT.md).
347
+
348
+ ## Contributing
349
+
350
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
351
+
352
+ - [Report bugs](https://github.com/shibatadaiki/Rbenchmarker/issues)
353
+ - Fix bugs and [submit pull requests](https://github.com/shibatadaiki/Rbenchmarker/pulls)
354
+ - Write, clarify, or fix documentation
355
+ - Suggest or add new features
356
+
357
+ To get started with development:
358
+
359
+ ```sh
360
+ git clone https://github.com/shibatadaiki/Rbenchmarker.git
361
+ cd Rbenchmarker
362
+ bundle install
363
+ bundle exec rake test
364
+ ```
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rbenchmarker/version'
4
+ require_relative 'rbenchmarker/class_methods'
5
+ require_relative 'rbenchmarker/prepend_modules'
6
+ require_relative 'rbenchmarker/rbenchmarker_log'
7
+ require_relative 'rbenchmarker/exceptions'
8
+
9
+ module Rbenchmarker
10
+ LOG_DIRECTORY = 'log'
11
+ LOG_FILE_NAME = 'rbenchmark.log'
12
+
13
+ @setup_executed = false
14
+ @rbench_classes = []
15
+ @rbench_modules = []
16
+ @object_with_has_modules = []
17
+ @module_with_has_methods_lists = {}
18
+ @bm_reports = {}
19
+ @rbenchmarker_log_file_path = if Dir.exist? "#{Dir.pwd}/#{LOG_DIRECTORY}"
20
+ "#{Dir.pwd}/#{LOG_DIRECTORY}/#{LOG_FILE_NAME}"
21
+ else
22
+ "#{Dir.pwd}/#{LOG_FILE_NAME}"
23
+ end
24
+
25
+ def self.setup_executed?
26
+ @setup_executed
27
+ end
28
+
29
+ def self.setup_executed!
30
+ @setup_executed = true
31
+ end
32
+
33
+ def self.setup_no_executed!
34
+ @setup_executed = false
35
+ end
36
+
37
+ def self.tracking_classes
38
+ @rbench_classes
39
+ end
40
+
41
+ def self.add_class(value)
42
+ @rbench_classes << value
43
+ end
44
+
45
+ def self.init_tracking_classes
46
+ @rbench_classes = []
47
+ end
48
+
49
+ def self.tracking_modules
50
+ @rbench_modules
51
+ end
52
+
53
+ def self.add_module(value)
54
+ @rbench_modules << value
55
+ end
56
+
57
+ def self.init_tracking_modules
58
+ @rbench_modules = []
59
+ end
60
+
61
+ def self.object_with_has_modules
62
+ @object_with_has_modules
63
+ end
64
+
65
+ def self.add_object_with_modules(value)
66
+ @object_with_has_modules << value
67
+ end
68
+
69
+ def self.init_object_with_has_modules
70
+ @object_with_has_modules = []
71
+ end
72
+
73
+ def self.module_with_has_methods_lists
74
+ @module_with_has_methods_lists
75
+ end
76
+
77
+ def self.add_module_with_has_methods_list(key, value)
78
+ @module_with_has_methods_lists[key] = value
79
+ end
80
+
81
+ def self.init_module_with_has_methods_lists
82
+ @module_with_has_methods_lists = {}
83
+ end
84
+
85
+ def self.tracking_reports
86
+ @bm_reports
87
+ end
88
+
89
+ def self.add_report(key, value)
90
+ @bm_reports[key] = value
91
+ callbacks_self_add_report(key, value)
92
+ end
93
+
94
+ def self.init_tracking_reports
95
+ @bm_reports = {}
96
+ end
97
+
98
+ def self.output_log_file_path
99
+ @rbenchmarker_log_file_path
100
+ end
101
+
102
+ def self.change_output_log_file_path(path_text)
103
+ @rbenchmarker_log_file_path = path_text
104
+ end
105
+
106
+ def self.init_log_file_path
107
+ @rbenchmarker_log_file_path = if Dir.exist? "#{Dir.pwd}/#{LOG_DIRECTORY}"
108
+ "#{Dir.pwd}/#{LOG_DIRECTORY}/#{LOG_FILE_NAME}"
109
+ else
110
+ "#{Dir.pwd}/#{LOG_FILE_NAME}"
111
+ end
112
+ end
113
+
114
+ # This method must read (execute) after reading all files except this configuration file.
115
+ def self.setup(switch: 'on', output_log_path: nil, except_classes: [], except_modules: [])
116
+ return if %w[off OFF].include?(switch)
117
+ return puts 'setup has already been executed.' if setup_executed?
118
+
119
+ setup_validation_check(except_classes, except_modules, output_log_path)
120
+
121
+ change_output_log_file_path "#{output_log_path}/#{LOG_FILE_NAME}" if output_log_path
122
+
123
+ tracking_classes.each do |benchmark_target_class, options|
124
+ next if except_classes.include?(benchmark_target_class)
125
+
126
+ benchmark_target_class.call_register_rbenchmarker_methods(options)
127
+ end
128
+
129
+ tracking_modules.each do |benchmark_target_module, options|
130
+ next if except_modules.include?(benchmark_target_module)
131
+
132
+ benchmark_target_module.call_register_rbenchmarker_methods(options)
133
+ end
134
+
135
+ Rbenchmarker::PrependModules.register_rbenchmarker_methods_to_module(
136
+ object_with_has_modules, module_with_has_methods_lists, except_modules
137
+ )
138
+
139
+ Rbenchmarker::RbenchmarkerLog.init_log
140
+ setup_executed!
141
+ end
142
+
143
+ # private
144
+
145
+ def self.setup_validation_check(except_classes, except_modules, output_log_path)
146
+ raise ExceptClassDesignationError unless except_classes.is_a?(Array)
147
+ raise ExceptClassDesignationError unless except_classes.all? { |obj| obj.class.to_s == 'Class' }
148
+ raise ExceptModuleDesignationError unless except_modules.is_a?(Array)
149
+ raise ExceptModuleDesignationError unless except_modules.all? { |obj| obj.class.to_s == 'Module' }
150
+ raise TargetDirPathError if output_log_path && !Dir.exist?(output_log_path)
151
+ end
152
+
153
+ def self.callbacks_self_add_report(key, value)
154
+ Rbenchmarker::RbenchmarkerLog.puts_log(key, value)
155
+ end
156
+
157
+ private_class_method :setup_validation_check
158
+ private_class_method :callbacks_self_add_report
159
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'exceptions'
4
+ require_relative 'register_rbenchmarker_methods'
5
+
6
+ module Rbenchmarker
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def rbenchmarker(**options)
13
+ rbenchmarker_validation_check(options)
14
+
15
+ options[:only] = options[:only].map(&:to_sym) if options[:only]
16
+ options[:except] = options[:except].map(&:to_sym) if options[:except]
17
+ options[:added] = options[:added].map(&:to_sym) if options[:added]
18
+
19
+ granted_modules = {}
20
+ granted_modules[:prepend] = options.delete(:prepend) if options[:prepend]
21
+ granted_modules[:include] = options.delete(:include) if options[:include]
22
+ granted_modules[:extend] = options.delete(:extend) if options[:extend]
23
+
24
+ if instance_of?(Module)
25
+ options[:object_type] = 'Module'
26
+ Rbenchmarker.add_module [self, options]
27
+ else
28
+ options[:object_type] = 'Class'
29
+ Rbenchmarker.add_class [self, options]
30
+ end
31
+
32
+ Rbenchmarker.add_object_with_modules([self, granted_modules])
33
+ end
34
+
35
+ def call_register_rbenchmarker_methods(options)
36
+ options[:target_obj] = self
37
+ Rbenchmarker::RegisterRbenchmarkerMethods.register_rbenchmarker_methods(**options)
38
+ end
39
+
40
+ private # not strictly concealed because can be called from unspecified object.
41
+
42
+ def rbenchmarker_validation_check(options)
43
+ raise Rbenchmarker::TargetFilePathError if options[:all] && !options[:all].is_a?(String)
44
+ raise Rbenchmarker::TargetFilePathError if options[:all] && !File.file?(options[:all])
45
+ raise Rbenchmarker::OnlyMethodDesignationError if options[:only] && !options[:only].is_a?(Array)
46
+ raise Rbenchmarker::OnlyMethodDesignationError if options[:only] && !options[:only].all? do |method_name|
47
+ method_name.is_a?(String) || method_name.is_a?(Symbol)
48
+ end
49
+ raise Rbenchmarker::ExceptMethodDesignationError if options[:except] && !options[:except].is_a?(Array)
50
+ raise Rbenchmarker::ExceptMethodDesignationError if options[:except] && !options[:except].all? do |method_name|
51
+ method_name.is_a?(String) || method_name.is_a?(Symbol)
52
+ end
53
+ raise Rbenchmarker::AddedMethodDesignationError if options[:added] && !options[:added].is_a?(Array)
54
+ raise Rbenchmarker::AddedMethodDesignationError if options[:added] && !options[:added].all? do |method_name|
55
+ method_name.is_a?(String) || method_name.is_a?(Symbol)
56
+ end
57
+ raise Rbenchmarker::PrependModuleDesignationError if options[:prepend] && !options[:prepend].is_a?(Array)
58
+ raise Rbenchmarker::PrependModuleDesignationError if options[:prepend] && !options[:prepend].all? do |obj|
59
+ obj.class.to_s == 'Module'
60
+ end
61
+ raise Rbenchmarker::IncludeModuleDesignationError if options[:include] && !options[:include].is_a?(Array)
62
+ raise Rbenchmarker::IncludeModuleDesignationError if options[:include] && !options[:include].all? do |obj|
63
+ obj.class.to_s == 'Module'
64
+ end
65
+ raise Rbenchmarker::ExtendModuleDesignationError if options[:extend] && !options[:extend].is_a?(Array)
66
+ raise Rbenchmarker::ExtendModuleDesignationError if options[:extend] && !options[:extend].all? do |obj|
67
+ obj.class.to_s == 'Module'
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ Object.send :include, Rbenchmarker::ClassMethods
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbenchmarker
4
+ class ExceptClassDesignationError < ArgumentError
5
+ def initialize
6
+ super('"except_classes" option must be an array containing only "Class".')
7
+ end
8
+ end
9
+
10
+ class ExceptModuleDesignationError < ArgumentError
11
+ def initialize
12
+ super('"except_modules" option must be an array containing only the defined "Module".')
13
+ end
14
+ end
15
+
16
+ class TargetDirPathError < ArgumentError
17
+ def initialize
18
+ super('In the argument of "output_log_file_path", specify the path of the directory where you want to place the log.')
19
+ end
20
+ end
21
+
22
+ class OnlyMethodDesignationError < ArgumentError
23
+ def initialize
24
+ super('"Only" option must be an array containing only "String" or "Symbol".')
25
+ end
26
+ end
27
+
28
+ class ExceptMethodDesignationError < ArgumentError
29
+ def initialize
30
+ super('"Except" option must be an array containing only "String" or "Symbol".')
31
+ end
32
+ end
33
+
34
+ class AddedMethodDesignationError < ArgumentError
35
+ def initialize
36
+ super('"Added" option must be an array containing only "String" or "Symbol".')
37
+ end
38
+ end
39
+
40
+ class PrependModuleDesignationError < ArgumentError
41
+ def initialize
42
+ super('"prepend" option must be an array containing only the defined "Module".')
43
+ end
44
+ end
45
+
46
+ class IncludeModuleDesignationError < ArgumentError
47
+ def initialize
48
+ super('"include" option must be an array containing only the defined "Module".')
49
+ end
50
+ end
51
+
52
+ class ExtendModuleDesignationError < ArgumentError
53
+ def initialize
54
+ super('"extend" option must be an array containing only the defined "Module".')
55
+ end
56
+ end
57
+
58
+ class TargetFilePathError < ArgumentError
59
+ def initialize
60
+ super('Must be specify an existing file path. Unless there is a special reason, specify the return value of the "__FILE__" method in the all argument.')
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbenchmarker
4
+ module PrependModules
5
+ def self.register_rbenchmarker_methods_to_module(object_with_has_modules, module_with_has_methods_lists, except_modules)
6
+ object_with_has_modules.each do |obj, granted_modules|
7
+ if granted_modules[:prepend].is_a?(Array)
8
+ methods_setup(granted_modules, :prepend, obj, except_modules, module_with_has_methods_lists)
9
+ end
10
+
11
+ if granted_modules[:include].is_a?(Array)
12
+ methods_setup(granted_modules, :include, obj, except_modules, module_with_has_methods_lists)
13
+ end
14
+
15
+ if granted_modules[:extend].is_a?(Array)
16
+ methods_setup(granted_modules, :extend, obj, except_modules, module_with_has_methods_lists)
17
+ end
18
+ end
19
+ end
20
+
21
+ # private
22
+
23
+ def self.methods_setup(granted_modules, include_type, obj, except_modules, module_with_has_methods_lists)
24
+ granted_modules[include_type].each do |benchmark_module|
25
+ next if except_modules.include?(benchmark_module)
26
+ next unless module_with_has_methods_lists[benchmark_module.to_s.to_sym]
27
+
28
+ case include_type
29
+ when :prepend
30
+ obj.prepend module_with_has_methods_lists[benchmark_module.to_s.to_sym]
31
+ when :include
32
+ obj.include module_with_has_methods_lists[benchmark_module.to_s.to_sym]
33
+ when :extend
34
+ obj.extend module_with_has_methods_lists[benchmark_module.to_s.to_sym]
35
+ end
36
+ end
37
+ end
38
+
39
+ private_class_method :methods_setup
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Rbenchmarker
6
+ module RbenchmarkerLog
7
+ def self.init_log
8
+ @logger = Logger.new(Rbenchmarker.output_log_file_path)
9
+ @logger.info("== Start recording Rbenchmarker == \n")
10
+ end
11
+
12
+ def self.puts_log(label, report, number_of_digits: 8)
13
+ return unless defined? @logger
14
+
15
+ log_text = "\n"
16
+ log_text += "#{label}: current time\n"
17
+ log_text += "user: #{format("%.#{number_of_digits}f", report[:utime][-1])}, "
18
+ log_text += "system: #{format("%.#{number_of_digits}f", report[:stime][-1])}, "
19
+ log_text += "total: #{format("%.#{number_of_digits}f", report[:total][-1])}, "
20
+ log_text += "real: #{format("%.#{number_of_digits}f", report[:real][-1])}\n"
21
+
22
+ log_text += "#{label}: total time for #{report[:number_of_executions]} times called\n"
23
+ log_text += "user: #{format("%.#{number_of_digits}f", report[:utime].sum)}, "
24
+ log_text += "system: #{format("%.#{number_of_digits}f", report[:stime].sum)}, "
25
+ log_text += "total: #{format("%.#{number_of_digits}f", report[:total].sum)}, "
26
+ log_text += "real: #{format("%.#{number_of_digits}f", report[:real].sum)}\n"
27
+
28
+ log_text += "#{label}: avarage time\n"
29
+ log_text += "user: #{format("%.#{number_of_digits}f", (report[:utime].sum / report[:number_of_executions]))}, "
30
+ log_text += "system: #{format("%.#{number_of_digits}f", (report[:stime].sum / report[:number_of_executions]))}, "
31
+ log_text += "total: #{format("%.#{number_of_digits}f", (report[:total].sum / report[:number_of_executions]))}, "
32
+ log_text += "real: #{format("%.#{number_of_digits}f", (report[:real].sum / report[:number_of_executions]))}\n"
33
+
34
+ @logger.info(log_text)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Rbenchmarker
6
+ module RegisterRbenchmarkerMethods
7
+ DEFAULT_REPORT_LENGTH = 30
8
+ TIMES_THE_METHOD_WAS_CALLED = 1
9
+
10
+ # "rbenchmarker" needs to start processing after all the methods of the target class(file) have been read,
11
+ # Rbenchmarker uses the benchmark library (https://docs.ruby-lang.org/ja/latest/class/Benchmark.html) internally.
12
+ #
13
+ # "all" option performs static analysis to identify the target method, so put "__FILE__" in the argument. normally, set this option.
14
+ #
15
+ # method specified in "only" will not be benchmarked.
16
+ # method specified in "except" will not be benchmarked.
17
+ # method specified by "added" is added to the benchmark after the "only" and "except" method filtering is done.
18
+ #
19
+ # "label_width" specifies the width of the benchmark label.
20
+ #
21
+ # benchmark is measured by repeatedly executing the benchmarked method for the number of times specified by "times".
22
+ # Keep in mind that code such as SQL queries will also be executed repeatedly.
23
+ #
24
+ # "require_hidden_method" option is set to true, methods created dynamically by metaprogramming will also be targeted.
25
+ #
26
+ # in the "include" option, specify the module that you are including. Arrange the option array in the loading order of the modules to be included.
27
+ # in the "extend" option, specify the module that you are extending. Arrange the option array in the loading order of the modules to be extended.
28
+ # in the "prepend" option, specify the module that you are prepending. Arrange the option array in the loading order of the modules to be prepended.
29
+ def self.register_rbenchmarker_methods(
30
+ all: nil,
31
+ only: [],
32
+ except: [],
33
+ added: [],
34
+ label_width: 0,
35
+ times: 0,
36
+ require_hidden_method: false,
37
+ object_type: nil,
38
+ target_obj: nil
39
+ )
40
+ # Collect the method names to be benchmarked.
41
+ target_instance_method_names, target_class_method_names = collect_target_methods(
42
+ target_obj, all, require_hidden_method, only, except, added
43
+ )
44
+
45
+ # Add benchmark function to the method to be benchmarked.
46
+ instance_method_codes_with_benchmark_function = generate_benchmarking_codes(
47
+ target_instance_method_names, label_width, times, (object_type == 'Module' ? 'module' : 'instance')
48
+ )
49
+ class_method_codes_with_benchmark_function = generate_benchmarking_codes(
50
+ target_class_method_names, label_width, times, (object_type == 'Module' ? 'module' : 'class')
51
+ )
52
+
53
+ # Override the original method with the benchmark functionality.
54
+ prepended_instance_methods = prepend_benchmarking_methods(instance_method_codes_with_benchmark_function)
55
+ prepended_class_methods = prepend_benchmarking_methods(class_method_codes_with_benchmark_function)
56
+
57
+ case object_type
58
+ when 'Module'
59
+ Rbenchmarker.add_module_with_has_methods_list(target_obj.to_s.to_sym, prepended_instance_methods)
60
+ when 'Class'
61
+ target_obj.prepend prepended_instance_methods
62
+ end
63
+
64
+ target_obj.singleton_class.prepend prepended_class_methods
65
+ end
66
+
67
+ # private
68
+
69
+ def self.collect_target_methods(target_obj, target_file, require_hidden_method, only, except, added)
70
+ if require_hidden_method
71
+ instance_method_name_candidates = target_obj.instance_methods(false) + target_obj.private_instance_methods(false)
72
+ class_method_name_candidates = target_obj.singleton_methods(false)
73
+ elsif target_file
74
+ instance_method_name_candidates, class_method_name_candidates =
75
+ statically_analyze_and_extract_target_method_names(target_obj, target_file)
76
+ else
77
+ instance_method_name_candidates = []
78
+ class_method_name_candidates = []
79
+ end
80
+
81
+ filtered_instance_method_names =
82
+ filter_by_only_except_and_added(instance_method_name_candidates, only, except, added, target_obj, 'instance_method')
83
+ filtered_class_method_names =
84
+ filter_by_only_except_and_added(class_method_name_candidates, only, except, added, target_obj, 'class_method')
85
+
86
+ [filtered_instance_method_names, filtered_class_method_names]
87
+ end
88
+
89
+ def self.statically_analyze_and_extract_target_method_names(target_obj, target_file)
90
+ instance_method_name_candidates = []
91
+ class_method_name_candidates = []
92
+ words = []
93
+
94
+ all_class_methods = target_obj.singleton_methods(false)
95
+ all_instance_methods = target_obj.instance_methods(false) + target_obj.private_instance_methods(false)
96
+ File.foreach(target_file) { |line| words << line[/def\ (.*?)[ ;|\(\n]/, 1] }
97
+ user_described_all_method_names = words.compact.map { |word| word.match(/^self\./) ? word.gsub(/^self\./, '') : word }
98
+
99
+ user_described_all_method_names.each do |def_name|
100
+ class_method_name_candidates << def_name.to_sym if all_class_methods.include?(def_name.to_sym)
101
+ instance_method_name_candidates << def_name.to_sym if all_instance_methods.include?(def_name.to_sym)
102
+ end
103
+
104
+ [instance_method_name_candidates.uniq, class_method_name_candidates.uniq]
105
+ end
106
+
107
+ def self.filter_by_only_except_and_added(method_names, only, except, added, target_obj, method_type)
108
+ has_methods = case method_type
109
+ when 'instance_method'
110
+ target_obj.instance_methods(false) + target_obj.private_instance_methods(false)
111
+ when 'class_method'
112
+ target_obj.singleton_methods(false)
113
+ end
114
+
115
+ added = added.select { |method_name| has_methods.include?(method_name) }
116
+
117
+ if !only.empty?
118
+ ((method_names & only) + added).uniq
119
+ elsif !except.empty?
120
+ (method_names - except + added).uniq
121
+ else
122
+ (method_names + added).uniq
123
+ end
124
+ end
125
+
126
+ def self.generate_benchmarking_codes(method_names, width, times, class_type)
127
+ method_names.map { |method_name| define_benchmarking_code(method_name, width, times, class_type) }
128
+ end
129
+
130
+ def self.define_benchmarking_code(method_name, width, times, class_type)
131
+ times_to_i = times.to_i.nonzero?
132
+ generate_report_function_code = generate_report_method_text('bm_result')
133
+
134
+ <<-RUBY_EVAL
135
+ def #{method_name}(*)
136
+ origin_return_value = nil
137
+ bm_result = # 'bm_result' is Arguments of 'generate_report_method_text'
138
+ Benchmark.bm #{width.to_i.nonzero? || method_name.length + DEFAULT_REPORT_LENGTH} do |r|
139
+ r.report "report def #{method_name}#{"(#{times_to_i}" if times_to_i}#{' loops)' if times_to_i} #{class_type} method" do
140
+ #{times_to_i ? "#{times_to_i}.times{ origin_return_value = super }" : 'origin_return_value = super'}
141
+ end
142
+ end
143
+ #{generate_report_function_code}
144
+ origin_return_value
145
+ end
146
+ RUBY_EVAL
147
+ end
148
+
149
+ def self.generate_report_method_text(bm_report_name)
150
+ <<-RUBY_EVAL
151
+ new_report = #{bm_report_name}.first
152
+ bm_reports = Rbenchmarker.tracking_reports
153
+ if bm_reports[new_report.label.to_sym]
154
+ previous_report = bm_reports[new_report.label.to_sym]
155
+ key = new_report.label.to_sym
156
+ value = {
157
+ utime: previous_report[:utime] << new_report.utime,
158
+ stime: previous_report[:stime] << new_report.stime,
159
+ total: previous_report[:total] << new_report.total,
160
+ real: previous_report[:real] << new_report.real,
161
+ number_of_executions: previous_report[:number_of_executions] + 1
162
+ }
163
+ Rbenchmarker.add_report(key, value)
164
+ else
165
+ key = new_report.label.to_sym
166
+ value = {
167
+ utime: [new_report.utime],
168
+ stime: [new_report.stime],
169
+ total: [new_report.total],
170
+ real: [new_report.real],
171
+ number_of_executions: TIMES_THE_METHOD_WAS_CALLED
172
+ }
173
+ Rbenchmarker.add_report(key, value)
174
+ end
175
+ RUBY_EVAL
176
+ end
177
+
178
+ def self.prepend_benchmarking_methods(benchmarking_codes)
179
+ Module.new do
180
+ benchmarking_codes.each { |code| class_eval code, __FILE__, __LINE__ + 1 }
181
+ end
182
+ end
183
+
184
+ private_class_method :collect_target_methods
185
+ private_class_method :statically_analyze_and_extract_target_method_names
186
+ private_class_method :filter_by_only_except_and_added
187
+ private_class_method :generate_benchmarking_codes
188
+ private_class_method :define_benchmarking_code
189
+ private_class_method :generate_report_method_text
190
+ private_class_method :prepend_benchmarking_methods
191
+ end
192
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rbenchmarker
4
+ VERSION = '0.1.1'
5
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbenchmarker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - daiki shibata
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Rbenchmarker is a gem that allows you to automatically benchmark the execution time of a method defined in a Ruby class and module. Benchmark module (https://docs.ruby-lang.org/ja/latest/class/Benchmark.html) is used inside Rbenchmarker, and bm method is automatically applied to all target methods.
15
+ However, method itself to which Rbenchmarker is applied remains unchanged, takes the same arguments as before, and returns the same return value as before.
16
+ So you don't have to change the methods yourself, and you don't have to benchmark the methods one by one. Just launch the application as before and will automatically benchmark all targeted methods!
17
+ email:
18
+ - shibatadaiki92@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - CHANGELOG.md
24
+ - MIT-LICENSE
25
+ - README.md
26
+ - lib/rbenchmarker.rb
27
+ - lib/rbenchmarker/class_methods.rb
28
+ - lib/rbenchmarker/exceptions.rb
29
+ - lib/rbenchmarker/prepend_modules.rb
30
+ - lib/rbenchmarker/rbenchmarker_log.rb
31
+ - lib/rbenchmarker/register_rbenchmarker_methods.rb
32
+ - lib/rbenchmarker/version.rb
33
+ homepage: https://github.com/shibatadaiki/Rbenchmarker
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/shibatadaiki/Rbenchmarker
38
+ source_code_uri: https://github.com/shibatadaiki/Rbenchmarker/
39
+ changelog_uri: https://github.com/shibatadaiki/Rbenchmarker/blob/master/CHANGELOG.md
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.3.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.2.3
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Automatically log benchmarks for all methods
59
+ test_files: []