frequent 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ .bundle
3
+ vendor
4
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tape_deck.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2012 Ben Weintraub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,153 @@
1
+ # Frequent
2
+
3
+ Frequent is a little Ruby metaprogramming demo gem that can keep track of what's
4
+ happening during your Ruby program's execution - specifically, how many times a
5
+ targeted method is called.
6
+
7
+ ## Usage
8
+
9
+ To use frequent, install the gem, then `require 'frequent'` in one of your source
10
+ files and set the `COUNT_CALLS_TO` environment variable to the name of the
11
+ method you'd like to count calls to. Like this:
12
+
13
+ ```
14
+ require 'frequent'
15
+
16
+ 100.times do
17
+ puts "hello, frequent".split(',').inspect
18
+ end
19
+ ```
20
+
21
+ ```
22
+ $ COUNT_CALLS_TO='String#split' ruby test.rb
23
+ ...
24
+ String#split called 100 times
25
+ ```
26
+
27
+ Following Ruby conventions, instance methods are identified with a hash
28
+ (e.g. `String#split`), and class methods are identified with a period
29
+ (e.g. `File.join`).
30
+
31
+ ### API
32
+
33
+ You can also use the `Frequent` module to manually place probes in your code:
34
+
35
+ ```
36
+ probe = Frequent.instrument('MyClass#instance_method')
37
+ ...
38
+ probe.calls # number of calls to target since instrumentation
39
+ ```
40
+
41
+ If you're only interested in capturing calls during a specific part of your
42
+ code's execution, you can pass a block to `instrument`, during which
43
+ instrumentation will be enabled:
44
+
45
+ ```
46
+ probe = Frequent.instrument('MyClass.method') do
47
+ ...
48
+ end
49
+ probe.calls # number of calls to target that happened in the block
50
+ ```
51
+
52
+ Your targetd class/module and method need not be defined yet at the time of
53
+ instrumentation, but see the performance section below for notes about the
54
+ performance implementations of instrumenting not-yet-defined targets.
55
+
56
+ ## Internals
57
+
58
+ Frequent works by overwriting the original implementation of the targeted method
59
+ with an instrumented version that keeps a call count, using Ruby's `class_eval`,
60
+ `alias_method`, and `define_method` facilities. The original version of the
61
+ instrumented method is saved so that it can be optionally restored after
62
+ instrumentation.
63
+
64
+ ## Performance
65
+
66
+ Frequent is relatively low-overhead, but there are a few things to keep in mind
67
+ regarding performance:
68
+
69
+ 1. You'll get better performance if you place your probes *after* your target
70
+ method has been defined. Frequent will attempt to place your probe immediately
71
+ upon calls to `Frequent.instrument`, but if it ever encounters a probe that it
72
+ cannot place yet due to a missing host class/module or method, it will make
73
+ use of the `method_added`, `singleton_method_added`, and `included` hooks in
74
+ Ruby.
75
+
76
+ These hooks will cause a bit of code to execute each time a new method is
77
+ added to a Ruby module in your process, and each time a module is included
78
+ in a new class. Frequent uses these hooks so that it has a chance to place
79
+ instrumentation as soon as your targeted class/module and method become
80
+ available.
81
+
82
+ Most Ruby processes don't add many additional methods or create additional
83
+ classes after an initial start-up sequence, meaning that the performance
84
+ impact of using these hooks should be generally limited to a slightly higher
85
+ fixed start-up cost for your process.
86
+
87
+ 2. Calls to instrumented methods will be slower than calls to uninstrumented
88
+ methods. Only methods that are actually instrumented will be subject to this
89
+ added overhead.
90
+
91
+ The degree to which this affects your program's performance in practice will
92
+ depend heavily on how hot your instrumented method is.
93
+ Instrumentation of a method called in a tight loop many times during your
94
+ program's execution will be more expensive overall than instrumentation of a
95
+ method only called a few times during the life of your program.
96
+
97
+ The benchmark in `spec/frequent_spec.rb` (run with `bundle exec rake test BENCH=1`)
98
+ attempts to measure the overhead incurred by instrumentation of a method with
99
+ Frequent. The benchmark compares times to call empty instrumented and
100
+ uninstrumented methods (both class and instance methods).
101
+
102
+ When interpreting the results, keep in mind that most methods worth
103
+ instrumenting are not empty -- that is, they do some non-trivial work that will
104
+ help amortize the per-call overhead.
105
+
106
+ Benchmark results will vary from machine to machine (and between different Ruby
107
+ implementations), but on the author's machine, the overhead introduced by
108
+ instrumenting a method is ~0.5 μs / call. This represents a slowdown of about
109
+ 2.5x - 7x for a completely empty method call (depending on whether a class or
110
+ instance method is being instrumented).
111
+
112
+ ## Caveats
113
+
114
+ ### method_missing
115
+
116
+ In Ruby, it's fairly common to encounter methods that aren't explicitly defined,
117
+ but are instead dynamically implemented using Ruby's `method_missing` facility.
118
+ Frequent will not work for methods defined in this way.
119
+
120
+ The main challenge in dealing with methods defined through `method_missing` is
121
+ knowing when to place instrumentation. In order for instrumentation of
122
+ `method_missing` methods to work, probes must be placed immediately upon
123
+ creation of the targeted class or module - `method_added` is of no use here,
124
+ since the target method is never actually added.
125
+
126
+ There's unfortunately no `const_added` hook available in Ruby (though there was
127
+ discussion on the mailing list of adding one a while ago), and the author has
128
+ not found any alternatives for detecting module / class creation that he finds
129
+ sufficiently robust, simple, and performant.
130
+
131
+ Alternatives include using `set_trace_func` temporarily until the target class
132
+ or method has been created and then disabling it (slow and complex) and looking
133
+ for placeable probes after each `require` or `load` (doesn't work for
134
+ dynamically-created classes).
135
+
136
+ ### Metaprogramming
137
+
138
+ Frequent isn't magic - it relies on the same hooks and facilities that are made
139
+ available by the Ruby interpreter to any code in your program. That means it's
140
+ probably possible to break or trick it in many ways.
141
+
142
+ ### Instrumenting the same target multiple times
143
+
144
+ This won't work, and should probably raise an exception, but currently just
145
+ fails.
146
+
147
+ ## License
148
+
149
+ MIT license. See LICENSE for details.
150
+
151
+ ## Author
152
+
153
+ Ben Weintraub - benweint@gmail.com
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'spec'
7
+ test.pattern = 'spec/**/*_spec.rb'
8
+ test.verbose = true
9
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["Ben Weintraub"]
5
+ gem.email = ["benweint@gmail.com"]
6
+ gem.description = %q{Ruby method instrumentation demo}
7
+ gem.summary = %q{Ruby method instrumentation demo}
8
+ gem.homepage = ""
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.test_files = gem.files.grep(%r{^(spec)/})
12
+ gem.name = "frequent"
13
+ gem.require_paths = ["lib"]
14
+ gem.version = '0.1'
15
+
16
+ gem.add_development_dependency('rake')
17
+ gem.add_development_dependency('minitest')
18
+ gem.add_development_dependency('minitest-matchers')
19
+ end
@@ -0,0 +1,147 @@
1
+ module Frequent
2
+ VERSION = 0.1
3
+
4
+ ProbeNameError = Class.new(StandardError)
5
+
6
+ def self.instrument(name)
7
+ probe = Frequent::Probe.new(name)
8
+ probes[name] = probe
9
+ Frequent::Deferred.enable! unless probe.enabled?
10
+ if block_given?
11
+ yield
12
+ probe.disable!
13
+ probes.delete(name)
14
+ end
15
+ probe
16
+ end
17
+
18
+ def self.constantize(name)
19
+ names = name.split('::')
20
+ names.shift if names.first == ''
21
+ constant = Object
22
+ until names.empty?
23
+ n = names.shift
24
+ return nil unless constant.const_defined?(n)
25
+ constant = constant.const_get(n, false)
26
+ end
27
+ constant
28
+ end
29
+
30
+ def self.probes
31
+ @probes ||= {}
32
+ end
33
+ end
34
+
35
+ module Frequent
36
+ class Probe
37
+ attr_reader :name, :calls, :class_name, :method_name, :original_implementation
38
+ alias_method :to_s, :name
39
+
40
+ def initialize(name)
41
+ @calls = 0
42
+ @name = name
43
+ @enabled = false
44
+ parse_name(name)
45
+ enable! if ready?
46
+ end
47
+
48
+ def increment
49
+ @calls += 1
50
+ end
51
+
52
+ def enabled?
53
+ @enabled
54
+ end
55
+
56
+ def ready?
57
+ !enabled? && target_defined?
58
+ end
59
+
60
+ def method_owner
61
+ owner = Frequent.constantize(class_name)
62
+ return unless owner
63
+ @type == :instance ? owner : owner.singleton_class
64
+ end
65
+
66
+ def target_defined?
67
+ owner = method_owner
68
+ owner && (
69
+ owner.method_defined?(method_name) ||
70
+ owner.private_instance_methods.include?(method_name)
71
+ )
72
+ end
73
+
74
+ def enable!
75
+ unless @enabling
76
+ @enabling = true
77
+ @original_implementation = method_owner.instance_method(method_name)
78
+ probe = self
79
+ aliased_name = self.aliased_name
80
+ method_owner.class_eval do
81
+ alias_method aliased_name, probe.method_name
82
+ define_method(probe.method_name) do |*args, &blk|
83
+ probe.increment
84
+ send(aliased_name, *args, &blk)
85
+ end
86
+ end
87
+ @enabled = true
88
+ @enabling = false
89
+ end
90
+ end
91
+
92
+ def disable!
93
+ if enabled?
94
+ probe = self
95
+ method_owner.class_eval do
96
+ define_method(probe.method_name, probe.original_implementation)
97
+ remove_method(probe.aliased_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ def aliased_name
103
+ "__frequent_original_#{method_name}".to_sym
104
+ end
105
+
106
+ def parse_name(name)
107
+ md = name.match(/(.*)(\.|\#)(.*)/)
108
+ raise ProbeNameError.new("Failed to parse probe name '#{name}'") unless md
109
+ class_name, sep, method_name = md[1..3]
110
+ @class_name = class_name
111
+ @type = (sep == '#') ? :instance : :class
112
+ @method_name = method_name.to_sym
113
+ end
114
+ end
115
+ end
116
+
117
+ module Frequent
118
+ module Deferred
119
+ def self.place_by_name(name)
120
+ p = Frequent.probes[name]
121
+ p.enable! if p && p.ready?
122
+ end
123
+
124
+ def self.enable!
125
+ return if @enabled
126
+ ::Module.class_eval do
127
+ def method_added(m)
128
+ Frequent::Deferred.place_by_name("#{self}##{m}")
129
+ end
130
+
131
+ def singleton_method_added(m)
132
+ Frequent::Deferred.place_by_name("#{self}.#{m}")
133
+ end
134
+
135
+ def included(host)
136
+ Frequent.probes.values.select(&:ready?).each(&:enable!)
137
+ end
138
+ end
139
+ @enabled = true
140
+ end
141
+ end
142
+ end
143
+
144
+ if ENV['COUNT_CALLS_TO']
145
+ probe = Frequent.instrument(ENV['COUNT_CALLS_TO'])
146
+ at_exit { puts "#{probe} called #{probe.calls} times" }
147
+ end
@@ -0,0 +1,221 @@
1
+ require 'spec_helper'
2
+
3
+ describe Frequent do
4
+ before :each do
5
+ class Potato
6
+ def instance_method; end
7
+ def block_method(x); yield x; end
8
+ def self.class_method(*args); end
9
+
10
+ def self.recursive_class_method(n)
11
+ return if n == 1
12
+ recursive_class_method(n-1)
13
+ end
14
+
15
+ def self.respond_to?(method)
16
+ method.to_s == 'class_missing_method' ? true : super
17
+ end
18
+
19
+ def overridden_method; end
20
+
21
+ protected
22
+ def protected_instance_method; end
23
+
24
+ private
25
+ def private_instance_method; end
26
+ end
27
+
28
+ class RedPotato < Potato
29
+ def overridden_method; end
30
+ end
31
+ end
32
+
33
+ describe 'probe name parsing' do
34
+ it 'should raise ProbeNameError on invalid probe name' do
35
+ proc { Frequent.instrument("Lava$monster") }.must_raise(Frequent::ProbeNameError)
36
+ end
37
+ end
38
+
39
+ describe 'instance method instrumentation' do
40
+ it 'should support simple method call counting' do
41
+ p = Frequent.instrument('Potato#instance_method')
42
+ 11.times { Potato.new.instance_method }
43
+ p.calls.must_equal(11)
44
+ end
45
+
46
+ it 'should apply to already-created instances' do
47
+ instance = Potato.new
48
+ p = Frequent.instrument('Potato#instance_method')
49
+ 3.times { instance.instance_method }
50
+ p.calls.must_equal(3)
51
+ end
52
+
53
+ it 'should not count calls identically-named methods from parent class' do
54
+ p0 = Frequent.instrument('Potato#overridden_method')
55
+ p1 = Frequent.instrument('RedPotato#overridden_method')
56
+ 2.times { Potato.new.overridden_method }
57
+ 3.times { RedPotato.new.overridden_method }
58
+ p0.calls.must_equal(2)
59
+ p1.calls.must_equal(3)
60
+ end
61
+
62
+ it 'should support private / protected methods' do
63
+ p0 = Frequent.instrument('Potato#private_instance_method')
64
+ p1 = Frequent.instrument('Potato#protected_instance_method')
65
+ 3.times { Potato.new.send(:private_instance_method) }
66
+ 4.times { Potato.new.send(:protected_instance_method) }
67
+ p0.calls.must_equal(3)
68
+ p1.calls.must_equal(4)
69
+ end
70
+
71
+ it 'should support instrumenting methods on Object' do
72
+ p = Frequent.instrument('Object#tainted?') do
73
+ Object.new.tainted?
74
+ end
75
+ p.calls.must_equal(1)
76
+ end
77
+
78
+ it 'should pass-thru blocks and args' do
79
+ v = nil
80
+ p = Frequent.instrument('Potato#block_method')
81
+ Potato.new.block_method(42) { |n| v = n }
82
+ v.must_equal(42)
83
+ p.calls.must_equal(1)
84
+ end
85
+ end
86
+
87
+ describe 'instrumentation of class methods' do
88
+ it 'should support simple call counting' do
89
+ p = Frequent.instrument('Potato.class_method')
90
+ 9.times { Potato.class_method }
91
+ p.calls.must_equal(9)
92
+ end
93
+
94
+ it 'should support recursive methods' do
95
+ p = Frequent.instrument('Potato.recursive_class_method')
96
+ Potato.recursive_class_method(7)
97
+ p.calls.must_equal(7)
98
+ end
99
+ end
100
+
101
+ describe 'probe removal' do
102
+ it 'should remove probes on instance methods' do
103
+ p = Frequent.instrument('Potato#instance_method')
104
+ Potato.new.instance_method
105
+ p.disable!
106
+ Potato.new.instance_method
107
+ p.calls.must_equal(1)
108
+ end
109
+
110
+ it 'should remove probes on class methods' do
111
+ p = Frequent.instrument('Potato.class_method')
112
+ Potato.class_method
113
+ p.disable!
114
+ Potato.class_method
115
+ p.calls.must_equal(1)
116
+ end
117
+
118
+ it 'should instrument scoped to block' do
119
+ p = Frequent.instrument('Potato#instance_method') do
120
+ 5.times { Potato.new.instance_method }
121
+ end
122
+ 3.times { Potato.new.instance_method }
123
+ p.calls.must_equal(5)
124
+ end
125
+ end
126
+
127
+ describe 'instrumentation of modules' do
128
+ it 'should catch module methods even if included after instrumentation' do
129
+ module Mod1
130
+ def demo; end
131
+ end
132
+
133
+ p = Frequent.instrument('Mod1#demo')
134
+
135
+ class Dummy1
136
+ include Mod1
137
+ end
138
+ Dummy1.new.demo
139
+
140
+ p.calls.must_equal(1)
141
+ end
142
+
143
+ it 'should work if probe placed before module/class definition' do
144
+ p = Frequent.instrument('Dummy3#foo')
145
+
146
+ module Dummy2; def foo; end; end
147
+ class Dummy3; include Dummy2; end
148
+
149
+ 10.times { Dummy3.new.foo }
150
+
151
+ p.calls.must_equal(10)
152
+ end
153
+
154
+ it 'should work for module methods' do
155
+ p = Frequent.instrument('Dummy5.foo')
156
+ module Dummy5; def self.foo; end; end
157
+ 5.times { Dummy5.foo }
158
+ p.calls.must_equal(5)
159
+ end
160
+
161
+ it 'should work with nested modules' do
162
+ p = Frequent.instrument('Dummies::Dummy6.foo')
163
+
164
+ module Dummies
165
+ module Dummy6; def self.foo; end; end
166
+ end
167
+
168
+ 5.times { Dummies::Dummy6.foo }
169
+ p.calls.must_equal(5)
170
+ end
171
+ end
172
+
173
+ it 'should work if method is added to class after instrumentation' do
174
+ class Dummy7; end
175
+
176
+ p = Frequent.instrument('Dummy7#foo')
177
+
178
+ class Dummy7; def foo; end; end
179
+
180
+ 3.times { Dummy7.new.foo }
181
+ p.calls.must_equal(3)
182
+ end
183
+
184
+ it 'should work for dynamically-created classes' do
185
+ p = Frequent.instrument('Dummy8.foo')
186
+
187
+ eval "class Dummy8; def self.foo; end; end; 5.times { Dummy8.foo }"
188
+
189
+ p.calls.must_equal(5)
190
+ end
191
+
192
+ if ENV['BENCH']
193
+ it 'should be fast' do
194
+ p = Potato.new
195
+ n = 1000000
196
+
197
+ puts "Trials: #{n}"
198
+ Benchmark.bmbm do |rpt|
199
+ rpt.report("Uninstrumented instance method") do
200
+ n.times { p.instance_method }
201
+ end
202
+
203
+ rpt.report("Instrumented instance method") do
204
+ Frequent.instrument('Potato#instance_method') do
205
+ n.times { p.instance_method }
206
+ end
207
+ end
208
+
209
+ rpt.report("Uninstrumented class method") do
210
+ n.times { Potato.class_method }
211
+ end
212
+
213
+ rpt.report("Instrumented class method") do
214
+ Frequent.instrument("Potato.class_method") do
215
+ n.times { Potato.class_method }
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,6 @@
1
+ require 'frequent'
2
+
3
+ require 'benchmark'
4
+ require 'minitest/autorun'
5
+ require 'minitest/matchers'
6
+ require 'minitest/benchmark' if ENV['BENCH']
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frequent
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Weintraub
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-24 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70106089255840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70106089255840
25
+ - !ruby/object:Gem::Dependency
26
+ name: minitest
27
+ requirement: &70106089255400 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70106089255400
36
+ - !ruby/object:Gem::Dependency
37
+ name: minitest-matchers
38
+ requirement: &70106089254980 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70106089254980
47
+ description: Ruby method instrumentation demo
48
+ email:
49
+ - benweint@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - frequent.gemspec
60
+ - lib/frequent.rb
61
+ - spec/frequent_spec.rb
62
+ - spec/spec_helper.rb
63
+ homepage: ''
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.10
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Ruby method instrumentation demo
87
+ test_files:
88
+ - spec/frequent_spec.rb
89
+ - spec/spec_helper.rb