profile-tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2ea7a642f74758aef7887554315fac1c6141058a668f9c42bd94cb90db90d0b8
4
+ data.tar.gz: 1d4f88fa199c5c6adf6162d39b221b41ed4fc9679b9d727e99b3c84950e0a3d4
5
+ SHA512:
6
+ metadata.gz: 95b41c270ca78f611e60863f046d0a0141107bc620e2ca93c726ed806dae98b03c0a9143ed0356de794a8c6d047d151a98529d38e293444d38be4f49a4a5af7a
7
+ data.tar.gz: 415571c41e649b9548b1e4011245b90a1fd30328c90f5428469d6dea11ee7d00b775e19b8511bb914680187f2fcc8c40f1bad0c61c313bfbcadc65fd700b3ac1
data/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
51
+ *~
52
+ *.yaml
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ Naming/FileName:
2
+ Enabled: false
3
+ Metrics/LineLength:
4
+ Max: 120
5
+ Metrics/ModuleLength:
6
+ Max: 120
7
+ Metrics/MethodLength:
8
+ Max: 20
9
+ AllCops:
10
+ Exclude:
11
+ - 'spec/spec_helper.rb'
12
+ - 'spec/**/*_spec.rb'
13
+ - 'spec/support/*.rb'
14
+ - 'script/test'
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ profile-tools
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.3
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ gem 'activesupport'
6
+
7
+ group :development do
8
+ gem 'rake'
9
+ gem 'rubocop'
10
+ end
11
+
12
+ group :spec do
13
+ gem 'rspec'
14
+ gem 'simplecov'
15
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,67 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (6.0.0)
5
+ concurrent-ruby (~> 1.0, >= 1.0.2)
6
+ i18n (>= 0.7, < 2)
7
+ minitest (~> 5.1)
8
+ tzinfo (~> 1.1)
9
+ zeitwerk (~> 2.1, >= 2.1.8)
10
+ ast (2.4.0)
11
+ concurrent-ruby (1.1.5)
12
+ diff-lcs (1.3)
13
+ docile (1.3.2)
14
+ i18n (1.6.0)
15
+ concurrent-ruby (~> 1.0)
16
+ jaro_winkler (1.5.3)
17
+ json (2.2.0)
18
+ minitest (5.11.3)
19
+ parallel (1.17.0)
20
+ parser (2.6.3.0)
21
+ ast (~> 2.4.0)
22
+ rainbow (3.0.0)
23
+ rake (12.3.3)
24
+ rspec (3.8.0)
25
+ rspec-core (~> 3.8.0)
26
+ rspec-expectations (~> 3.8.0)
27
+ rspec-mocks (~> 3.8.0)
28
+ rspec-core (3.8.2)
29
+ rspec-support (~> 3.8.0)
30
+ rspec-expectations (3.8.4)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.8.0)
33
+ rspec-mocks (3.8.1)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.8.0)
36
+ rspec-support (3.8.2)
37
+ rubocop (0.74.0)
38
+ jaro_winkler (~> 1.5.1)
39
+ parallel (~> 1.10)
40
+ parser (>= 2.6)
41
+ rainbow (>= 2.2.2, < 4.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 1.4.0, < 1.7)
44
+ ruby-progressbar (1.10.1)
45
+ simplecov (0.17.0)
46
+ docile (~> 1.1)
47
+ json (>= 1.8, < 3)
48
+ simplecov-html (~> 0.10.0)
49
+ simplecov-html (0.10.2)
50
+ thread_safe (0.3.6)
51
+ tzinfo (1.2.5)
52
+ thread_safe (~> 0.1)
53
+ unicode-display_width (1.6.0)
54
+ zeitwerk (2.1.9)
55
+
56
+ PLATFORMS
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ activesupport
61
+ rake
62
+ rspec
63
+ rubocop
64
+ simplecov
65
+
66
+ BUNDLED WITH
67
+ 1.17.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 dougyouch
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # profile-tools
2
+ Ruby profiling tools
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ProfileTools is used to instrument specific methods. Provides feedback about method execution
4
+ # time and number of objects created.
5
+ class ProfileTools
6
+ autoload :Collector, 'profile_tools/collector'
7
+ autoload :LogSubscriber, 'profile_tools/log_subscriber'
8
+ autoload :Profiler, 'profile_tools/profiler'
9
+
10
+ EVENT = 'profile.profile_tools'
11
+
12
+ @profiled_methods = []
13
+ class << self
14
+ attr_reader :profiled_methods
15
+ end
16
+
17
+ def self.add_method(display_name)
18
+ profiled_methods << display_name
19
+ end
20
+
21
+ def self.delete_method(display_name)
22
+ profiled_methods.delete_if { |method| method == display_name }
23
+ end
24
+
25
+ def initialize
26
+ ObjectSpace.count_objects
27
+ end
28
+
29
+ def profile_instance_method(class_name, method_name)
30
+ profile_method(Object.const_get(class_name), method_name, "#{class_name}##{method_name}")
31
+ end
32
+
33
+ def profile_class_method(class_name, method_name)
34
+ profile_method(Object.const_get(class_name).singleton_class, method_name, "#{class_name}.#{method_name}")
35
+ end
36
+
37
+ def remove_profiled_instance_method(class_name, method_name)
38
+ remove_profiling(Object.const_get(class_name), method_name, "#{class_name}##{method_name}")
39
+ end
40
+
41
+ def remove_profiled_class_method(class_name, method_name)
42
+ remove_profiling(Object.const_get(class_name).singleton_class, method_name, "#{class_name}.#{method_name}")
43
+ end
44
+
45
+ def self.load(yaml_file)
46
+ require 'yaml'
47
+ profile(YAML.load_file(yaml_file))
48
+ end
49
+
50
+ def self.profile(classes)
51
+ profile_tools = new
52
+
53
+ classes.each do |class_name, methods|
54
+ methods.each do |method_name|
55
+ if method_name =~ /\A\./
56
+ profile_tools.profile_class_method(class_name, method_name[1, method_name.size])
57
+ else
58
+ profile_tools.profile_instance_method(class_name, method_name)
59
+ end
60
+ end
61
+ end
62
+
63
+ profile_tools
64
+ end
65
+
66
+ def self.stop_profiling(methods)
67
+ profile_tools = new
68
+
69
+ methods.each do |method|
70
+ if method =~ /#/
71
+ profile_tools.remove_profiled_instance_method(*method.split('#', 2))
72
+ elsif method =~ /\./
73
+ profile_tools.remove_profiled_class_method(*method.split('.', 2))
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.stop_profiling!
79
+ stop_profiling(profiled_methods.dup)
80
+ end
81
+
82
+ def self.profiler
83
+ Thread.current[:profile_tools_profiler] ||= Profiler.new
84
+ end
85
+
86
+ def self.instrument
87
+ profiler.instrument do
88
+ yield
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def profile_method(kls, method_name, display_name)
95
+ self.class.add_method(display_name)
96
+
97
+ method_name_without_profiling = generate_method_name(method_name.to_s, 'without_profiling')
98
+ method_name_with_profiling = generate_method_name(method_name.to_s, 'with_profiling')
99
+
100
+ kls.class_eval(
101
+ <<-STR, __FILE__, __LINE__ + 1
102
+ def #{method_name_with_profiling}(*args)
103
+ ::ProfileTools.profiler.instrument('#{display_name}') do
104
+ #{method_name_without_profiling}(*args)
105
+ end
106
+ end
107
+ STR
108
+ )
109
+
110
+ kls.alias_method(method_name_without_profiling, method_name)
111
+ kls.alias_method(method_name, method_name_with_profiling)
112
+ end
113
+
114
+ def generate_method_name(method_name, suffix)
115
+ punctuation =
116
+ if method_name =~ /(\?|!)$/
117
+ $1
118
+ end
119
+
120
+ method_name = method_name.sub(punctuation, '') if punctuation
121
+
122
+ "#{method_name}_#{suffix}#{punctuation}"
123
+ end
124
+
125
+ def remove_profiling(kls, method_name, display_name)
126
+ self.class.delete_method(display_name)
127
+
128
+ method_name_without_profiling = generate_method_name(method_name.to_s, 'without_profiling')
129
+ method_name_with_profiling = generate_method_name(method_name.to_s, 'with_profiling')
130
+
131
+ kls.alias_method(method_name, method_name_without_profiling)
132
+ kls.send(:remove_method, method_name_with_profiling)
133
+ kls.send(:remove_method, method_name_without_profiling)
134
+ end
135
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ class ProfileTools
6
+ # Collects stats around method calls
7
+ class Collector
8
+ attr_reader :methods,
9
+ :total_collection_calls
10
+
11
+ def initialize
12
+ @methods = {}
13
+ @total_collection_calls = 0
14
+ @sort_order = 0
15
+ end
16
+
17
+ def init_method(method)
18
+ @methods[method] = {
19
+ method: method,
20
+ duration: 0.0,
21
+ calls: 0,
22
+ count_objects: Hash.new(0),
23
+ num_collection_calls: 0,
24
+ sort_order: nil
25
+ }
26
+ end
27
+
28
+ def called_methods
29
+ @methods
30
+ .values
31
+ .reject { |info| info[:calls].zero? }
32
+ .sort { |a, b| a[:sort_order] <=> b[:sort_order] }
33
+ end
34
+
35
+ def instrument(method)
36
+ current_collection_calls = @total_collection_calls
37
+ result = nil
38
+ duration = nil
39
+ @methods[method][:sort_order] ||= (@sort_order += 1)
40
+ count_objects = count_objects_around do
41
+ started_at = now
42
+ result = yield
43
+ duration = now - started_at
44
+ end
45
+ add(
46
+ method,
47
+ duration * 1000.0,
48
+ count_objects,
49
+ @total_collection_calls - current_collection_calls
50
+ )
51
+ result
52
+ end
53
+
54
+ private
55
+
56
+ def add(method, duration, count_object_changes, num_collection_calls)
57
+ @total_collection_calls += 1
58
+ @methods[method][:calls] += 1
59
+ @methods[method][:duration] += duration
60
+ @methods[method][:num_collection_calls] = num_collection_calls
61
+ add_object_changes(@methods[method][:count_objects], count_object_changes)
62
+ adjust_count_objects(@methods[method][:count_objects], num_collection_calls)
63
+ end
64
+
65
+ def add_object_changes(current_objects, new_objects)
66
+ new_objects.each do |name, cnt|
67
+ current_objects[name] += cnt
68
+ end
69
+ current_objects
70
+ end
71
+
72
+ def adjust_count_objects(count_objects, num_collection_calls)
73
+ return if num_collection_calls.zero?
74
+
75
+ count_objects[:T_STRING] -= (1 * num_collection_calls)
76
+ count_objects[:T_ARRAY] -= (1 * num_collection_calls)
77
+ count_objects[:T_HASH] -= (2 * num_collection_calls)
78
+ end
79
+
80
+ def now
81
+ Concurrent.monotonic_time
82
+ end
83
+
84
+ def count_objects_changes(starting_objects, new_objects)
85
+ new_objects.each do |name, _|
86
+ new_objects[name] -= starting_objects[name]
87
+ new_objects[name] -= 1 if name == :T_HASH
88
+ end
89
+ end
90
+
91
+ def count_objects_around
92
+ starting_objects = ObjectSpace.count_objects
93
+ yield
94
+ count_objects_changes(starting_objects, ObjectSpace.count_objects)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProfileTools
4
+ # Logs the collector stats
5
+ class LogSubscriber < ActiveSupport::LogSubscriber
6
+ def profile(event)
7
+ event.payload[:collector].called_methods.each do |info|
8
+ duration = info[:duration].round(5)
9
+ count_objects = display_count_objects(info[:count_objects])
10
+ logger.info "method #{info[:method]} took #{duration}ms, called #{info[:calls]}, objects: #{count_objects}"
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def display_count_objects(count_objects)
17
+ count_objects.reject! { |_, cnt| cnt.zero? }
18
+ count_objects.delete(:FREE)
19
+ count_objects.to_a.map { |k, v| "#{k}: #{v}" }.join(', ')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProfileTools
4
+ # Aggregates profile stats into the collector
5
+ class Profiler
6
+ attr_reader :collector
7
+
8
+ def initialize
9
+ @call_depth = 0
10
+ end
11
+
12
+ def instrument(class_and_method_name = 'ProfileTools::Profiler#instrument')
13
+ result = nil
14
+ if increment_call_depth == 1
15
+ @collector = new_collector
16
+ @collector.init_method(class_and_method_name)
17
+ instrument_with_notifications(class_and_method_name) do
18
+ result = yield
19
+ end
20
+ else
21
+ instrument_with_collector(class_and_method_name) do
22
+ result = yield
23
+ end
24
+ end
25
+ decrement_call_depth
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ def instrument_with_notifications(class_and_method_name)
32
+ ActiveSupport::Notifications.instrument(EVENT, collector: @collector) do
33
+ instrument_with_collector(class_and_method_name) do
34
+ yield
35
+ end
36
+ end
37
+ end
38
+
39
+ def instrument_with_collector(class_and_method_name)
40
+ @collector.instrument(class_and_method_name) do
41
+ yield
42
+ end
43
+ end
44
+
45
+ def increment_call_depth
46
+ @call_depth += 1
47
+ end
48
+
49
+ def decrement_call_depth
50
+ @call_depth -= 1
51
+ end
52
+
53
+ def new_collector
54
+ ::ProfileTools::Collector.new.tap do |collector|
55
+ ::ProfileTools.profiled_methods.each { |display_name| collector.init_method(display_name) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'profile-tools'
5
+ s.version = '0.1.0'
6
+ s.licenses = ['MIT']
7
+ s.summary = 'Profile tools'
8
+ s.description = 'Dynamically add method profiling to any class. Collects method times and objects created.'
9
+ s.authors = ['Doug Youch']
10
+ s.email = 'dougyouch@gmail.com'
11
+ s.homepage = 'https://github.com/dougyouch/profile-tools'
12
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|examples)/}) }
13
+ end
data/script/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
5
+
6
+ require 'active_support/notifications'
7
+ require 'active_support/log_subscriber'
8
+ require 'logger'
9
+ require 'concurrent'
10
+ require 'profile-tools'
11
+ require 'irb'
12
+
13
+ ActiveSupport::LogSubscriber.logger = Logger.new($stdout)
14
+ ProfileTools::LogSubscriber.attach_to :profile_tools
15
+
16
+ IRB.start(__FILE__)
data/script/test ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
5
+
6
+ require 'active_support/notifications'
7
+ require 'active_support/log_subscriber'
8
+ require 'logger'
9
+ require 'concurrent'
10
+ require 'profile-tools'
11
+ require 'irb'
12
+
13
+ ActiveSupport::LogSubscriber.logger = Logger.new($stdout)
14
+ ProfileTools::LogSubscriber.attach_to :profile_tools
15
+
16
+ class Foo
17
+ def bar
18
+ bar2
19
+ end
20
+
21
+ def bar2
22
+ puts "HERE #{1} too"
23
+ bar3
24
+ end
25
+
26
+ def bar3
27
+ puts "HERE #{1}"
28
+ end
29
+
30
+ def bar4
31
+ bar2
32
+ 5.times { bar3 }
33
+ end
34
+
35
+ def bar5
36
+ 5.times { 1 }
37
+ end
38
+
39
+ def bar6
40
+ bar4
41
+ end
42
+ end
43
+
44
+ ProfileTools.new.tap do |t|
45
+ t.profile_instance_method(:Foo, :bar)
46
+ t.profile_instance_method(:Foo, :bar2)
47
+ t.profile_instance_method(:Foo, :bar3)
48
+ t.profile_instance_method(:Foo, :bar4)
49
+ t.profile_instance_method(:Foo, :bar5)
50
+ t.profile_instance_method(:Foo, :bar6)
51
+ end
52
+
53
+ IRB.start(__FILE__)
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: profile-tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Doug Youch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Dynamically add method profiling to any class. Collects method times
14
+ and objects created.
15
+ email: dougyouch@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rubocop.yml"
22
+ - ".ruby-gemset"
23
+ - ".ruby-version"
24
+ - Gemfile
25
+ - Gemfile.lock
26
+ - LICENSE
27
+ - README.md
28
+ - lib/profile-tools.rb
29
+ - lib/profile_tools/collector.rb
30
+ - lib/profile_tools/log_subscriber.rb
31
+ - lib/profile_tools/profiler.rb
32
+ - profile-tools.gemspec
33
+ - script/console
34
+ - script/test
35
+ homepage: https://github.com/dougyouch/profile-tools
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.7.10
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Profile tools
59
+ test_files: []