class-profiler 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: 3cc114dfe22c12351314f0374fec2f6caa62c781ec643b8ee4b9f8df53d38bb0
4
+ data.tar.gz: bd6c43d1ce630abf9389a0cc4b4048cc0b09a26e263e9bf6fc240b55de038894
5
+ SHA512:
6
+ metadata.gz: 132477ca1e6db7f33c42833fac920bb175f1725701932f9b0991ca9895d1f1f176b79a754277c302d81a61da442807ed0c985575471654d5f720f19f0dd538ad
7
+ data.tar.gz: bd324e3ea404c8256bc1c8e7192d3484e4265fa4f1e2a447e727eedad1a205aeff30729bd8d300570d0f81aac48cccf4156705c3ec2940eaf9801a7130080fba
@@ -0,0 +1,27 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ strategy:
15
+ matrix:
16
+ ruby:
17
+ - '3.2.1'
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+ - name: Run the default task
27
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,38 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: single_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: single_quotes
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - spec/**/*.rb
16
+ - ./* # for Guardfile, gemspec, etc.
17
+
18
+ Style/BlockComments:
19
+ Enabled: false
20
+
21
+ Metrics/AbcSize:
22
+ Enabled: false
23
+
24
+ Metrics/MethodLength:
25
+ Enabled: false
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ Enabled: false
29
+
30
+ Layout/LineLength:
31
+ Enabled: false
32
+
33
+ Style/AccessModifierDeclarations:
34
+ EnforcedStyle: inline
35
+ Enabled: true
36
+
37
+ Metrics/PerceivedComplexity:
38
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,157 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ class-profiler (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activesupport (8.0.2.1)
10
+ base64
11
+ benchmark (>= 0.3)
12
+ bigdecimal
13
+ concurrent-ruby (~> 1.0, >= 1.3.1)
14
+ connection_pool (>= 2.2.5)
15
+ drb
16
+ i18n (>= 1.6, < 2)
17
+ logger (>= 1.4.2)
18
+ minitest (>= 5.1)
19
+ securerandom (>= 0.3)
20
+ tzinfo (~> 2.0, >= 2.0.5)
21
+ uri (>= 0.13.1)
22
+ ast (2.4.2)
23
+ base64 (0.3.0)
24
+ benchmark (0.4.0)
25
+ bigdecimal (3.2.2)
26
+ binding_of_caller (1.0.1)
27
+ debug_inspector (>= 1.2.0)
28
+ byebug (11.1.3)
29
+ coderay (1.1.3)
30
+ concurrent-ruby (1.3.5)
31
+ connection_pool (2.4.1)
32
+ debug_inspector (1.2.0)
33
+ diff-lcs (1.5.1)
34
+ drb (2.2.1)
35
+ ffi (1.17.2-aarch64-linux-gnu)
36
+ ffi (1.17.2-arm-linux-gnu)
37
+ ffi (1.17.2-arm64-darwin)
38
+ ffi (1.17.2-x86-linux-gnu)
39
+ ffi (1.17.2-x86_64-darwin)
40
+ ffi (1.17.2-x86_64-linux-gnu)
41
+ formatador (1.2.0)
42
+ reline
43
+ guard (2.19.1)
44
+ formatador (>= 0.2.4)
45
+ listen (>= 2.7, < 4.0)
46
+ logger (~> 1.6)
47
+ lumberjack (>= 1.0.12, < 2.0)
48
+ nenv (~> 0.1)
49
+ notiffany (~> 0.0)
50
+ ostruct (~> 0.6)
51
+ pry (>= 0.13.0)
52
+ shellany (~> 0.0)
53
+ thor (>= 0.18.1)
54
+ guard-compat (1.2.1)
55
+ guard-rspec (4.7.3)
56
+ guard (~> 2.1)
57
+ guard-compat (~> 1.1)
58
+ rspec (>= 2.99.0, < 4.0)
59
+ i18n (1.14.7)
60
+ concurrent-ruby (~> 1.0)
61
+ io-console (0.8.1)
62
+ json (2.10.1)
63
+ language_server-protocol (3.17.0.4)
64
+ lint_roller (1.1.0)
65
+ listen (3.9.0)
66
+ rb-fsevent (~> 0.10, >= 0.10.3)
67
+ rb-inotify (~> 0.9, >= 0.9.10)
68
+ logger (1.7.0)
69
+ lumberjack (1.4.1)
70
+ method_source (1.1.0)
71
+ minitest (5.25.5)
72
+ nenv (0.3.0)
73
+ notiffany (0.1.3)
74
+ nenv (~> 0.1)
75
+ shellany (~> 0.0)
76
+ ostruct (0.6.3)
77
+ parallel (1.26.3)
78
+ parser (3.3.7.1)
79
+ ast (~> 2.4.1)
80
+ racc
81
+ pry (0.14.2)
82
+ coderay (~> 1.1)
83
+ method_source (~> 1.0)
84
+ pry-byebug (3.10.1)
85
+ byebug (~> 11.0)
86
+ pry (>= 0.13, < 0.15)
87
+ pry-stack_explorer (0.6.1)
88
+ binding_of_caller (~> 1.0)
89
+ pry (~> 0.13)
90
+ racc (1.8.1)
91
+ rainbow (3.1.1)
92
+ rake (13.2.1)
93
+ rb-fsevent (0.11.2)
94
+ rb-inotify (0.11.1)
95
+ ffi (~> 1.0)
96
+ regexp_parser (2.10.0)
97
+ reline (0.6.2)
98
+ io-console (~> 0.5)
99
+ rspec (3.13.0)
100
+ rspec-core (~> 3.13.0)
101
+ rspec-expectations (~> 3.13.0)
102
+ rspec-mocks (~> 3.13.0)
103
+ rspec-core (3.13.0)
104
+ rspec-support (~> 3.13.0)
105
+ rspec-expectations (3.13.2)
106
+ diff-lcs (>= 1.2.0, < 2.0)
107
+ rspec-support (~> 3.13.0)
108
+ rspec-mocks (3.13.1)
109
+ diff-lcs (>= 1.2.0, < 2.0)
110
+ rspec-support (~> 3.13.0)
111
+ rspec-support (3.13.1)
112
+ rubocop (1.73.1)
113
+ json (~> 2.3)
114
+ language_server-protocol (~> 3.17.0.2)
115
+ lint_roller (~> 1.1.0)
116
+ parallel (~> 1.10)
117
+ parser (>= 3.3.0.2)
118
+ rainbow (>= 2.2.2, < 4.0)
119
+ regexp_parser (>= 2.9.3, < 3.0)
120
+ rubocop-ast (>= 1.38.0, < 2.0)
121
+ ruby-progressbar (~> 1.7)
122
+ unicode-display_width (>= 2.4.0, < 4.0)
123
+ rubocop-ast (1.38.1)
124
+ parser (>= 3.3.1.0)
125
+ ruby-progressbar (1.13.0)
126
+ securerandom (0.4.1)
127
+ shellany (0.0.1)
128
+ thor (1.4.0)
129
+ tzinfo (2.0.6)
130
+ concurrent-ruby (~> 1.0)
131
+ unicode-display_width (3.1.4)
132
+ unicode-emoji (~> 4.0, >= 4.0.4)
133
+ unicode-emoji (4.0.4)
134
+ uri (1.0.2)
135
+
136
+ PLATFORMS
137
+ aarch64-linux
138
+ arm-linux
139
+ arm64-darwin
140
+ x86-linux
141
+ x86_64-darwin
142
+ x86_64-linux
143
+
144
+ DEPENDENCIES
145
+ activesupport
146
+ class-profiler!
147
+ guard
148
+ guard-rspec
149
+ pry
150
+ pry-byebug
151
+ pry-stack_explorer
152
+ rake
153
+ rspec
154
+ rubocop
155
+
156
+ BUNDLED WITH
157
+ 2.5.17
data/Guardfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ # Feel free to open issues for suggestions and improvements
8
+
9
+ # RSpec files
10
+ rspec = dsl.rspec
11
+ watch(rspec.spec_helper) { rspec.spec_dir }
12
+ watch(rspec.spec_support) { rspec.spec_dir }
13
+ watch(rspec.spec_files)
14
+
15
+ # Ruby files
16
+ ruby = dsl.ruby
17
+ dsl.watch_spec_files_for(ruby.lib_files)
18
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Matthew Greenfield
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,173 @@
1
+ ClassProfiler: track performance and memory allocations in Ruby classes
2
+
3
+ ### Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'class-profiler'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ ```bash
14
+ bundle install
15
+ ```
16
+
17
+ Or install it yourself as:
18
+
19
+ ```bash
20
+ gem install class-profiler
21
+ ```
22
+
23
+ ### Usage
24
+
25
+ Include `ClassProfiler` in your class. Use the unified class-level helpers to wrap methods.
26
+
27
+ ```ruby
28
+ class Report
29
+ include ClassProfiler
30
+
31
+ def fetch
32
+ sleep 0.01
33
+ 'ok'
34
+ end
35
+
36
+ def allocate
37
+ Array.new(1000) { 'x' * 10 }
38
+ end
39
+
40
+ # Track non-inherited public/protected/private instance methods (default)
41
+ track_performance
42
+ track_memory
43
+ end
44
+
45
+ r = Report.new
46
+ r.fetch
47
+ r.allocate
48
+
49
+ # Per-method metrics
50
+ r.performance # => { fetch: { time: 0.0102, total: 0.0306 } }
51
+ r.memory # => { allocate: { allocated_objects: 1005, malloc_increase_bytes: 8192 } }
52
+
53
+ # Combined view (performance + memory)
54
+ r.profile # => { fetch: { time: 0.0102 }, allocate: { objects: 1005, bytes: 8192 } }
55
+
56
+ # Reports
57
+ r.performance_report # prints a table of last time and total time
58
+ r.memory_report # prints a table of allocations/bytes
59
+ r.profile_report # prints a combined table
60
+ ```
61
+
62
+ Selection is controlled via flags on the unified helpers:
63
+
64
+ ```ruby
65
+ # Include inherited methods?
66
+ track_performance inherited: true
67
+ track_memory inherited: true
68
+
69
+ # Choose visibilities (public/protected/private default to true)
70
+ track_performance protected: false, private: false
71
+ track_memory public: true, protected: false, private: false
72
+ ```
73
+
74
+ ### API
75
+
76
+ - `track_performance(inherited: false, public: true, protected: true, private: true)`
77
+ - `track_memory(inherited: false, public: true, protected: true, private: true)`
78
+ - `performance` (instance): Hash of method name → `{ time: Float, total: Float }`
79
+ - `memory` (instance): Hash of method name → allocation deltas
80
+ - `allocated_objects` (Integer)
81
+ - `malloc_increase_bytes` (Integer)
82
+ - `profile` (instance): Combined Hash of method name → `{ time:, objects:, bytes: }`
83
+ - `performance_report`, `memory_report`, `profile_report` (instance): print tabular reports
84
+
85
+ ### Composing with wrap_method
86
+
87
+ You can compose custom behaviors using the built-in `wrap_method` helper from `ClassProfiler::Methods`.
88
+ This is the same mechanism used by the Performance and Memory modules under the hood.
89
+
90
+ ```ruby
91
+ class Widget
92
+ include ClassProfiler
93
+
94
+ def compute(x, y)
95
+ x + y
96
+ end
97
+
98
+ # Add custom logging around the original implementation
99
+ wrap_method :compute do |original, *args|
100
+ profiler_logger.info("compute called with #{args.inspect}")
101
+ result = original.bind(self).call(*args)
102
+ profiler_logger.info("compute returned #{result}")
103
+ result
104
+ end
105
+
106
+ # You can still benchmark or profile the same method
107
+ track_performance
108
+ # track_memory
109
+ end
110
+
111
+ w = Widget.new
112
+ w.compute(2, 3)
113
+ w.performance[:compute] # => { time:, total: }
114
+ ```
115
+
116
+ ### Requirements
117
+
118
+ - Ruby >= 3.1
119
+
120
+ ### Development
121
+
122
+ - Run `bin/setup` to install dependencies
123
+ - Run `rake` to execute tests and RuboCop
124
+ - Run `bin/console` to experiment in Pry
125
+
126
+ ### Examples (speed vs memory trade-offs)
127
+
128
+ Explore the gem with runnable examples that contrast time vs memory usage for common problems.
129
+
130
+ Run all examples:
131
+
132
+ ```bash
133
+ bundle exec rake examples
134
+ ```
135
+
136
+ What you'll see:
137
+
138
+ ```text
139
+ === Two Sum: brute_force (low memory, slower) vs hashmap (higher memory, faster) ===
140
+ Input SIZE=5000
141
+
142
+ Summary (lower time is better; lower allocations usually better):
143
+ Method Time (s) Allocated Objects Malloc +bytes
144
+ ----------------------------------------------------------------------
145
+ brute_force 0.456789 12345 0
146
+ hashmap 0.012345 45678 8192
147
+
148
+ Speed: hashmap is 37.00x faster than brute_force
149
+ Memory: hashmap allocates 3.70x more objects than brute_force
150
+
151
+ Raw reports:
152
+ ...
153
+ ```
154
+
155
+ You can run individual examples and tune sizes:
156
+
157
+ ```bash
158
+ # Two Sum: brute force vs hash map
159
+ SIZE=10000 bundle exec ruby examples/two_sum.rb
160
+
161
+ # Primes: trial division vs sieve of Eratosthenes
162
+ N=50000 bundle exec ruby examples/primes.rb
163
+ ```
164
+
165
+ Each example prints a concise table plus a human-readable conclusion highlighting the trade-off (e.g. "sieve is 10x faster but allocates 4x more objects").
166
+
167
+ ### Contributing
168
+
169
+ Bug reports and pull requests are welcome on GitHub at `https://github.com/omgreenfield/class-profiler`.
170
+
171
+ ### License
172
+
173
+ The gem is available as open source under the terms of the MIT License.
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'class_profiler'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require 'pry'
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/class_profiler'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = ClassProfiler::NAME
7
+ gem.version = ClassProfiler::VERSION
8
+ gem.authors = ['Matthew Greenfield']
9
+ gem.email = ['mattgreenfield1@gmail.com']
10
+
11
+ gem.summary = 'Benchmark speed and profile memory of class methods'
12
+ gem.description = %(
13
+ Quickly benchmark execution time and profile memory allocations for specific
14
+ or all instance methods within a class. Include ClassProfiler to get
15
+ `benchmark_methods` and `profile_methods` helpers and collect results via
16
+ `benchmarked` and `profiled_memory`.
17
+ )
18
+ gem.homepage = 'https://github.com/omgreenfield/class-profiler'
19
+ gem.license = 'MIT'
20
+
21
+ gem.files = if File.exist?(File.expand_path('.git'))
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec/|examples/)}) }
23
+ else
24
+ Dir['lib/**/*.rb'] + Dir['*.md'] + Dir['LICENSE*']
25
+ end
26
+ gem.require_paths = ['lib']
27
+
28
+ gem.required_ruby_version = '>= 3.1.0'
29
+
30
+ gem.add_development_dependency 'activesupport'
31
+ gem.add_development_dependency 'guard'
32
+ gem.add_development_dependency 'guard-rspec'
33
+ gem.add_development_dependency 'pry'
34
+ gem.add_development_dependency 'pry-byebug'
35
+ gem.add_development_dependency 'pry-stack_explorer'
36
+ gem.add_development_dependency 'rake'
37
+ gem.add_development_dependency 'rspec'
38
+ gem.add_development_dependency 'rubocop'
39
+ end
data/config.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ version: 0.1.0
3
+ name: class-profiler
4
+ authors:
5
+ - Matthew Greenfield
6
+ emails:
7
+ - mattgreenfield1@gmail.com
8
+ summary: Quickly benchmark speed and profile memory of specific or all methods within a class
9
+ description: |
10
+ Quickly benchmark execution time and profile memory allocations for specific or
11
+ all instance methods within a class. Include ClassProfiler to get helper methods
12
+ and collect results via `benchmarked` and `profiled_memory`.
13
+ homepage: https://github.com/omgreenfield/class-profiler
14
+ license: MIT
15
+ required_ruby_version: '>= 3.1.0'
16
+ metadata:
17
+ homepage_uri: https://github.com/omgreenfield/class-profiler
18
+ rubygems_mfa_required: 'true'
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassProfiler
4
+ module Logging
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.include(InstanceMethods)
8
+ end
9
+
10
+ module InstanceMethods
11
+ def profiler_logger
12
+ self.class.profiler_logger
13
+ end
14
+
15
+ def print_report(entries, headers: [], include_zero: true, sort_index: 0)
16
+ rows = entries.entries
17
+ rows.filter! { |(_, time)| include_zero || time.to_f > 0.0 }
18
+ rows.sort_by! do |row|
19
+ value = row[sort_index]
20
+ if value.is_a?(Float)
21
+ value.to_f
22
+ else
23
+ value.to_s
24
+ end
25
+ end
26
+
27
+ rows = [headers] + rows
28
+
29
+ column_widths = rows.transpose.map do |column|
30
+ column.map { |value| value.to_s.length }.max
31
+ end
32
+
33
+ rows.each do |row|
34
+ row_text = row.map.with_index do |cell, index|
35
+ cell = format('%.6f', cell) if cell.is_a?(Float)
36
+ cell.to_s.ljust(column_widths[index])
37
+ end.join(' | ')
38
+
39
+ profiler_logger.info(row_text)
40
+ end
41
+
42
+ entries
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ # Simple fan-out logger that forwards to multiple underlying loggers
48
+ class MultiLogger
49
+ def initialize(*loggers)
50
+ @loggers = loggers.compact
51
+ end
52
+
53
+ def add(severity, message = nil, progname = nil)
54
+ @loggers.each { |logger| logger.add(severity, message, progname) }
55
+ end
56
+
57
+ def level=(level)
58
+ @loggers.each { |logger| logger.level = level }
59
+ end
60
+
61
+ def level
62
+ @loggers.map(&:level).min || Logger::INFO
63
+ end
64
+ end
65
+
66
+ def profiler_logger
67
+ @profiler_logger ||= begin
68
+ require 'logger'
69
+ logger = Logger.new($stdout)
70
+ logger.level = Logger::INFO
71
+ logger
72
+ end
73
+ end
74
+
75
+ def profiler_logger=(logger)
76
+ @profiler_logger = logger
77
+ end
78
+
79
+ # Configure logging to stdout
80
+ def enable_profiler_logging_to_stdout(level: Logger::INFO)
81
+ require 'logger'
82
+ self.profiler_logger = Logger.new($stdout).tap { |l| l.level = level }
83
+ end
84
+
85
+ # Configure logging to a file, optionally also to stdout
86
+ def enable_profiler_logging_to_file(path, level: Logger::INFO, shift_age: 0, shift_size: 1_048_576, also_stdout: false)
87
+ require 'logger'
88
+ file_logger = Logger.new(path, shift_age, shift_size)
89
+ file_logger.level = level
90
+ self.profiler_logger = if also_stdout
91
+ stdout_logger = Logger.new($stdout)
92
+ stdout_logger.level = level
93
+ MultiLogger.new(file_logger, stdout_logger)
94
+ else
95
+ file_logger
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassProfiler
4
+ module Memory
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.extend(Methods::ClassMethods)
8
+ base.include(InstanceMethods)
9
+ base.include(ClassProfiler::Logging)
10
+ end
11
+
12
+ module InstanceMethods
13
+ # Stores per-method memory results
14
+ # { method_name => { allocated_objects: Integer, malloc_increase_bytes: Integer } }
15
+ def memory
16
+ @memory ||= {}
17
+ end
18
+
19
+ # Prints report of each method's allocated objects and bytes used
20
+ #
21
+ # @param include_zero [Boolean] include rows with zero deltas (default: true)
22
+ # @param sort_index [Integer] 0 for method, 1 for objects, 2 for bytes
23
+ # @return [Hash] memory entries
24
+ def memory_report(include_zero: true, sort_index: 0)
25
+ headers = %w[Method Objects Bytes]
26
+ entries = memory.map { |method, values| [method] + values.values }
27
+ print_report(entries, headers: headers, include_zero:, sort_index:)
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ # Unified API to select and track instance methods' memory
33
+ #
34
+ # @param inherited [Boolean] include inherited instance methods
35
+ # @param public [Boolean] include public methods
36
+ # @param protected [Boolean] include protected methods
37
+ # @param private [Boolean] include private methods
38
+ def track_memory(inherited: false, public: true, protected: true, private: true)
39
+ include_public = binding.local_variable_get(:public)
40
+ include_protected = binding.local_variable_get(:protected)
41
+ include_private = binding.local_variable_get(:private)
42
+
43
+ names = []
44
+ names |= select_instance_methods(visibility: :public, include_inherited: inherited) if include_public
45
+ names |= select_instance_methods(visibility: :protected, include_inherited: inherited) if include_protected
46
+ names |= select_instance_methods(visibility: :private, include_inherited: inherited) if include_private
47
+ measure_memory_for_methods(*names)
48
+ end
49
+ # (unified) Use track_memory to configure which methods are wrapped
50
+
51
+ # Wraps each method and records allocation deltas per call
52
+ #
53
+ # @param method_names [Array<Symbol>]
54
+ private def measure_memory_for_methods(*method_names)
55
+ require 'objspace'
56
+
57
+ method_names.each do |method_name|
58
+ wrap_method method_name do |original, *args, &block|
59
+ before_alloc_objects = GC.stat[:total_allocated_objects]
60
+ before_malloc_bytes = GC.stat[:malloc_increase_bytes] || 0
61
+
62
+ result = original.bind(self).call(*args, &block)
63
+
64
+ after_alloc_objects = GC.stat[:total_allocated_objects]
65
+ after_malloc_bytes = GC.stat[:malloc_increase_bytes] || 0
66
+
67
+ memory[method_name] = {
68
+ allocated_objects: after_alloc_objects - before_alloc_objects,
69
+ malloc_increase_bytes: after_malloc_bytes - before_malloc_bytes
70
+ }
71
+
72
+ result
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassProfiler
4
+ module Methods
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.include(InstanceMethods)
8
+ end
9
+
10
+ module InstanceMethods; end
11
+
12
+ module ClassMethods
13
+ # Helper: exclude helper methods defined by ClassProfiler modules to avoid recursion/noise
14
+ def helper_owner_module?(owner)
15
+ owner_name = owner.respond_to?(:name) ? owner.name : nil
16
+ !!owner_name&.start_with?('ClassProfiler::')
17
+ end
18
+
19
+ # Methods that should never be wrapped on instances to avoid recursion/footguns
20
+ RESERVED_INSTANCE_METHODS = %i[
21
+ instance_exec instance_eval send __send__ method public_method
22
+ respond_to? object_id __id__ class inspect to_s
23
+ ].freeze
24
+
25
+ # Methods that should never be wrapped on the singleton to avoid recursion/footguns
26
+ RESERVED_SINGLETON_METHODS = %i[
27
+ send public_send method singleton_method define_singleton_method
28
+ instance_eval class_eval method_missing respond_to? allocate new superclass
29
+ inspect to_s name ancestors inherited extend include prepend
30
+ alias_method remove_method undef_method autoload autoload?
31
+ object_id __id__
32
+ ].freeze
33
+
34
+ # (internal) Helper: obtain singleton class
35
+ def singleton_class_of(klass)
36
+ class << klass; self; end
37
+ end
38
+
39
+ # Select instance method names by visibility and inheritance, excluding helpers/reserved
40
+ #
41
+ # @param visibility [Symbol] :public, :protected, :private, :all
42
+ # @param include_inherited [Boolean]
43
+ # @return [Array<Symbol>]
44
+ def select_instance_methods(visibility: :public, include_inherited: true)
45
+ names = case visibility
46
+ when :public then public_instance_methods(include_inherited)
47
+ when :protected then protected_instance_methods(include_inherited)
48
+ when :private then private_instance_methods(include_inherited)
49
+ when :all then (instance_methods(include_inherited) + protected_instance_methods(include_inherited) + private_instance_methods(include_inherited)).uniq
50
+ else public_instance_methods(include_inherited)
51
+ end
52
+
53
+ names.reject { |m| RESERVED_INSTANCE_METHODS.include?(m) || m.to_s.start_with?('_') || helper_owner_module?(instance_method(m).owner) }
54
+ end
55
+
56
+ # Select class method names by visibility and inheritance, excluding helpers/reserved
57
+ # @param visibility [Symbol] :public, :protected, :private, :all
58
+ # @param include_inherited [Boolean]
59
+ # @return [Array<Symbol>]
60
+ def select_class_methods(visibility: :public, include_inherited: true)
61
+ singleton = singleton_class_of(self)
62
+ inherit = include_inherited || false
63
+
64
+ names = case visibility
65
+ when :public then singleton.public_instance_methods(inherit)
66
+ when :protected then singleton.protected_instance_methods(inherit)
67
+ when :private then singleton.private_instance_methods(inherit)
68
+ when :all then (singleton.instance_methods(inherit) + singleton.protected_instance_methods(inherit) + singleton.private_instance_methods(inherit)).uniq
69
+ else singleton.public_instance_methods(inherit)
70
+ end
71
+
72
+ names.reject { |m| RESERVED_SINGLETON_METHODS.include?(m) || m.to_s.start_with?('_') || helper_owner_module?(singleton.instance_method(m).owner) }
73
+ end
74
+
75
+ # Wraps a method by creating an alias to the original method creating a new
76
+ # method in its place, executing the passed in block and returning the result
77
+ # The given block is executed in the INSTANCE context via instance_exec.
78
+ #
79
+ # @yield [original_method, *args] wrapper executed in instance context
80
+ def wrap_method(method_name, *_args, prefix: '_', &wrapper)
81
+ wrapped_method_name = "#{prefix}#{method_name}".to_sym
82
+ alias_method wrapped_method_name, method_name
83
+ wrapped_method = instance_method(wrapped_method_name)
84
+
85
+ define_method(method_name) do |*args|
86
+ instance_exec(wrapped_method, *args, &wrapper)
87
+ end
88
+ end
89
+
90
+ # Wrap a class method on the singleton class
91
+ def wrap_class_method(method_name, *_args, prefix: '_', &wrapper)
92
+ singleton = singleton_class_of(self)
93
+ wrapped_method_name = "#{prefix}#{method_name}".to_sym
94
+ singleton.alias_method wrapped_method_name, method_name
95
+ singleton.define_method(method_name) do |*args, &block|
96
+ start_time = Time.now
97
+ result = send(wrapped_method_name, *args, &block)
98
+ # let wrapper decide what to record/return
99
+ # wrapper executed in singleton context
100
+ wrapper.call(start_time, result, method_name)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClassProfiler
4
+ module Performance
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ # Ensure the including class has access to wrap_method
8
+ base.extend(Methods::ClassMethods)
9
+ base.include(InstanceMethods)
10
+ end
11
+
12
+ module InstanceMethods
13
+ def performance
14
+ @performance ||= {}
15
+ end
16
+
17
+ # Prints all performance-measured instance methods and the time taken per method (seconds)
18
+ #
19
+ # @param include_zero [Boolean] include methods with 0.0s measurements
20
+ # @param sort_index [Integer] 0 for method, 1 for time, 2 for total
21
+ #
22
+ # @return [Hash]
23
+ def performance_report(include_zero: false, sort_index: 1)
24
+ headers = %w[Method Time Total]
25
+ entries = performance.map do |method, values|
26
+ if values.is_a?(Hash)
27
+ [method, values[:time], values[:total]]
28
+ else
29
+ [method, values, values]
30
+ end
31
+ end
32
+ print_report(entries, headers: headers, include_zero: include_zero, sort_index: sort_index)
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ include Methods::ClassMethods
38
+
39
+ # Unified API to select and track instance methods' performance
40
+ #
41
+ # @param inherited [Boolean] include inherited instance methods
42
+ # @param public [Boolean] include public methods
43
+ # @param protected [Boolean] include protected methods
44
+ # @param private [Boolean] include private methods
45
+ def track_performance(inherited: false, public: true, protected: true, private: true)
46
+ include_public = binding.local_variable_get(:public)
47
+ include_protected = binding.local_variable_get(:protected)
48
+ include_private = binding.local_variable_get(:private)
49
+
50
+ names = []
51
+ names |= select_instance_methods(visibility: :public, include_inherited: inherited) if include_public
52
+ names |= select_instance_methods(visibility: :protected, include_inherited: inherited) if include_protected
53
+ names |= select_instance_methods(visibility: :private, include_inherited: inherited) if include_private
54
+ measure_performance_for_methods(*names)
55
+ end
56
+
57
+ # Measures non-inherited instance methods
58
+ #
59
+ # @param visibility [Symbol] :public, :protected, :private, :all
60
+ def performance_instance_methods(visibility: :public)
61
+ names = select_instance_methods(visibility: visibility, include_inherited: false)
62
+ performance_methods(*names)
63
+ end
64
+
65
+ # Records how long each method call takes and saves it to `performance` hash
66
+ #
67
+ # @param method_names [Array<Symbol>] the names of the methods to measure
68
+ private def measure_performance_for_methods(*method_names)
69
+ method_names.each do |method_name|
70
+ wrap_method method_name do |original, *args, &block|
71
+ start_time = Time.now
72
+ result = original.bind(self).call(*args, &block)
73
+ end_time = Time.now
74
+ duration = end_time - start_time
75
+ performance[method_name] ||= { total: 0 }
76
+ performance[method_name][:total] += duration
77
+ performance[method_name][:time] = duration
78
+ result
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'class_profiler/methods'
4
+ require_relative 'class_profiler/performance'
5
+ require_relative 'class_profiler/logging'
6
+ require_relative 'class_profiler/memory'
7
+
8
+ module ClassProfiler
9
+ NAME = 'class-profiler'
10
+ VERSION = '0.1.0'
11
+
12
+ def self.included(base)
13
+ base.include(ClassProfiler::Performance)
14
+ base.include(ClassProfiler::Memory)
15
+ base.include(ClassProfiler::Logging)
16
+ base.include(InstanceMethods)
17
+ end
18
+
19
+ module InstanceMethods
20
+ def profile
21
+ perf = performance.transform_values do |value|
22
+ if value.is_a?(Hash)
23
+ { time: value[:time] }
24
+ else
25
+ { time: value }
26
+ end
27
+ end
28
+ mem = memory.transform_values { |values| { objects: values[:allocated_objects], bytes: values[:malloc_increase_bytes] } }
29
+ perf.merge(mem) { |_k, a, b| a.merge(b) }
30
+ end
31
+
32
+ def profile_report(include_zero: true, sort_index: 0)
33
+ headers = %w[Method Time Objects Bytes]
34
+ entries = profile.map { |method, values| [method, values[:time], values[:objects], values[:bytes]] }
35
+ print_report(entries, headers: headers, include_zero:, sort_index:)
36
+ end
37
+ end
38
+ end
data/rakefile.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
13
+
14
+ desc 'Run all available examples'
15
+ task :examples do
16
+ examples_dir = File.expand_path('examples', __dir__)
17
+ scripts = Dir[File.join(examples_dir, '*.rb')].sort
18
+ if scripts.empty?
19
+ puts 'No scripts found in examples/'
20
+ next
21
+ end
22
+
23
+ scripts.each do |script|
24
+ puts "\n\n=== Running: #{File.basename(script)} ==="
25
+ system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, RbConfig.ruby, script)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: class-profiler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Greenfield
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-09-04 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: guard
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: guard-rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: pry
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: pry-byebug
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: pry-stack_explorer
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rubocop
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ description: "\n Quickly benchmark execution time and profile memory allocations
139
+ for specific\n or all instance methods within a class. Include ClassProfiler
140
+ to get\n `benchmark_methods` and `profile_methods` helpers and collect results
141
+ via\n `benchmarked` and `profiled_memory`.\n "
142
+ email:
143
+ - mattgreenfield1@gmail.com
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".github/workflows/main.yml"
149
+ - ".gitignore"
150
+ - ".rspec"
151
+ - ".rubocop.yml"
152
+ - Gemfile
153
+ - Gemfile.lock
154
+ - Guardfile
155
+ - LICENSE.txt
156
+ - README.md
157
+ - bin/console
158
+ - bin/setup
159
+ - class-profiler.gemspec
160
+ - config.yml
161
+ - lib/class_profiler.rb
162
+ - lib/class_profiler/logging.rb
163
+ - lib/class_profiler/memory.rb
164
+ - lib/class_profiler/methods.rb
165
+ - lib/class_profiler/performance.rb
166
+ - rakefile.rb
167
+ homepage: https://github.com/omgreenfield/class-profiler
168
+ licenses:
169
+ - MIT
170
+ metadata: {}
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: 3.1.0
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubygems_version: 3.6.2
186
+ specification_version: 4
187
+ summary: Benchmark speed and profile memory of class methods
188
+ test_files: []