frequent 0.1

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.
@@ -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