method_timer 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68ce55408cf9543b2a34106b4dc700f6f7c5e49e12e3caa55a14502d06ec4bf4
4
- data.tar.gz: 1782abd41e506211c86e0c02e6c29549354563f9a0404e7eccab10ad3b209add
3
+ metadata.gz: 4b6c0f87fb633ecd9cd510739a6c7fa7a1ae25ed4ec5c58b9f7f6a12ff4e8b6a
4
+ data.tar.gz: 4d06cfd6f2aeb071d995c992d39964221f8a0d8bf693bfd54431d5fd68a3d3e3
5
5
  SHA512:
6
- metadata.gz: 9c760f1a4ffd0788a0c88392793ad3d3879f79f58fe3da4e3f9b85ccf79f8f61e22a17e1abc716c3a166df175ecea068bea135f71ac6d22c6fb8f9452ef75cc9
7
- data.tar.gz: b6f0d33089cd55d60ce0def864aebe13b3c096d4dc564612507201eb3ad76ae0fd6b8464588bcead4dd517520e678752df008a65a5a45d2178200735ea2f2a60
6
+ metadata.gz: '039f1b71e315e945067d16ee974c26aba84541fa1c435282b53aee2535bfaacbdb7bfaed50c1ed72ef63b87749b1ac151fac6adae2277842732ccfafdeb940fb'
7
+ data.tar.gz: ba1a9caba01065d9a16750531bec324b0f0ebbac2c7abc8e8ef4c6c50d8381ff9c24d639a3c01745228567f02bf8a1c1d1943b984cd2adc09a82b010a883895b
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - November 14, 2026
4
+
5
+ ### Added
6
+
7
+ - Select instrumented methods with `install(..., only: [:call])`
8
+ - Profile class and module methods with `singleton: true`, including `extend`, `extend self`,
9
+ `module_function`, and inherited implementations when their owners are installed
10
+ - Sort reports by timing, call counts or allocations, and limit displayed rows without reducing totals
11
+ - Choose `MethodTimer.format = :hash` for on-demand reports with method metrics, totals and optional RSS data
12
+ - Add detailed examples and an API reference, included in the gem package
13
+
14
+ ### Fixed
15
+
16
+ - Remove constructor instrumentation silently on uninstall without changing the console's warning settings
17
+ - Preserve results for classes with overlapping short names across multiple installs
18
+ - Measure SQL with a monotonic clock and avoid counting GC twice with ActiveSupport 7.2+
19
+ - Exclude report output and cleanup from benchmark timing, GC and allocation measurements;
20
+ finish RSS sampling before printing
21
+ - Ignore SQL from other threads during benchmarking
22
+ - Restore the original ActiveRecord logger on uninstall without overwriting a replacement logger
23
+
24
+ ### Changed
25
+
26
+ - Keep one snapshot of the last run or benchmark; previously returned hashes remain independent
27
+ - Re-render saved measurements without collecting new data, keeping their labels and overhead calibration
28
+ - Clear saved measurements on reset or uninstall while preserving the configured report format
29
+ - Reorganize the README around a minimal Ruby example, retaining Rails console installation instructions
30
+
31
+ ## 0.1.0 - October 17, 2026
32
+
33
+ - Initial release with per-method profiling, SQL and GC timing, call counts and allocations
34
+ - Add block benchmarking, optional RSS memory sampling and instrumentation overhead estimates
35
+ - Require Ruby 3.4+
data/README.md CHANGED
@@ -1,43 +1,39 @@
1
1
  # MethodTimer
2
2
 
3
- Per-method timing for Ruby code, for profiling interactively from a console. It instruments every method under a
4
- class or namespace and reports, per method: self and total time, SQL time, GC time, call count and allocations.
5
- Wrappers are allocation-free, and the report includes an estimate of the profiler's own overhead.
3
+ Find out where your Ruby code spends time and allocates objects, one method at a time.
4
+ MethodTimer reports self and total time, SQL time, GC time, calls and allocations, with
5
+ allocation-free wrappers and an estimate of its own overhead.
6
6
 
7
- **This is a console tool: use it from an interactive console session, never from application code.** Running it
8
- in a production console against real data is fine and a common use. Don't call it from code that serves requests or
9
- runs jobs: instrumentation patches classes until you `uninstall`, adds a fixed cost to every
10
- instrumented call, keeps global state and isn't thread-safe. Profile in a throwaway console session (e.g.
11
- `rails console`), then exit.
7
+ **For interactive consoles, not application code.** A production console is a valid place
8
+ to profile, but never call MethodTimer from request handlers or jobs. It patches classes
9
+ and keeps global, non-thread-safe state. Use a throwaway console session, then exit.
12
10
 
13
- Requires Ruby 3.4+: the wrappers forward arguments with `(...)`, which only stopped allocating in Ruby 3.4 (on
14
- older versions every instrumented call would add objects to the `allocs` column). No runtime dependencies; SQL is
15
- tracked automatically when ActiveSupport is loaded (ActiveRecord's `sql.active_record` notifications).
11
+ Requires **Ruby 3.4+**. No runtime dependencies. SQL is tracked automatically when
12
+ ActiveSupport notifications are loaded.
16
13
 
17
14
  ## Installation
18
15
 
19
- Add it with `require: false`, so it's installed everywhere you might open a console but never loaded when the app
20
- boots:
16
+ Add the gem with `require: false`, so it is available in consoles without loading at app boot:
21
17
 
22
18
  ```ruby
23
19
  # Gemfile
24
20
  gem 'method_timer', require: false
25
21
  ```
26
22
 
27
- If you only profile locally, put it in the development group instead.
28
-
29
- Then load it in the console session where you want to profile:
23
+ If you only profile locally, put it in the development group instead. Run `bundle install`,
24
+ then load the gem in your console:
30
25
 
31
26
  ```ruby
32
27
  require 'method_timer'
33
28
  ```
34
29
 
35
- ### Loading it in every Rails console
30
+ ### Load automatically in Rails consoles
36
31
 
37
- Use Rails' `console` hook in `config/application.rb`, which runs only when a console starts (not in the server,
38
- jobs or tests):
32
+ Keep this in `config/application.rb`. Rails runs the `console` hook only when a console
33
+ starts, not in the server, jobs or tests:
39
34
 
40
35
  ```ruby
36
+ # config/application.rb
41
37
  module MyApp
42
38
  class Application < Rails::Application
43
39
  console do
@@ -47,250 +43,150 @@ module MyApp
47
43
  end
48
44
  ```
49
45
 
50
- The `Gem.loaded_specs` guard keeps consoles starting in environments where the gem isn't installed (e.g. when it's
51
- in the development group and you open a production console).
52
-
53
- ## Usage
54
-
55
- MethodTimer works with any Ruby code: plain classes and modules, Rails models and services, or classes from other
56
- gems. Point `install` at a class or a namespace; every instance method defined there (public, protected and
57
- private, plus methods of modules included directly into those classes) gets timed.
58
-
59
- ### Where does the time go?
60
-
61
- ```ruby
62
- MethodTimer.install(MyApp::Billing) # a class or a namespace (all classes under it)
63
- MethodTimer.run { thing_to_profile } # prints the report to stderr, returns the block's value
64
- MethodTimer.run(rss: true) { thing_to_profile } # also samples peak process memory every second
65
- MethodTimer.report(threshold: 0.5) # reprint, showing only methods with total >= 0.5s (default 0.001s)
66
- MethodTimer.stats # the numbers as a hash
67
- MethodTimer.uninstall # remove all wrappers (methods run at full speed again)
68
- ```
69
-
70
- `install` options:
71
-
72
- - `strip_root: false`: keep the full namespace in labels (by default the namespace shared by all instrumented
73
- classes is stripped).
74
- - `silence_sql: false`: keep the ActiveRecord logger (it's silenced by default, as logging every query distorts
75
- timings).
76
-
77
- You can call `install` several times to time a few unrelated classes together. To profile a different piece of
78
- code in the same session, `uninstall` first, so the previous classes stop paying the per-call cost and don't show
79
- up in the next report. (`run` already clears the numbers from the previous run.)
46
+ The guard lets consoles start when the gem is not installed in that environment.
47
+ For a walkthrough using application models and SQL, see the [Rails console example](docs/examples/rails-console.md).
80
48
 
81
- ### Example: plain Ruby
49
+ ## A complete, ordinary Ruby example
82
50
 
83
- A CSV export of 50,000 orders, each with 5 items:
51
+ No options, Rails or extra dependencies are needed. Paste this into a fresh Ruby console:
84
52
 
85
53
  ```ruby
86
- require 'csv'
87
54
  require 'method_timer'
88
55
 
89
- module Shop
90
- Order = Struct.new(:id, :customer, :items)
91
- Item = Struct.new(:name, :price, :quantity)
92
-
93
- class PriceCalculator
94
- TAX = 0.23
95
-
96
- def total(order) = order.items.sum { |item| line_total(item) }
97
-
98
- private
99
-
100
- def line_total(item) = (item.price * item.quantity * (1 + TAX)).round(2)
56
+ class NameFormatter
57
+ def call(names)
58
+ names.map { |name| normalize(name) }
101
59
  end
102
60
 
103
- class CsvExport
104
- def initialize(orders) = @orders = orders
105
-
106
- def call
107
- CSV.generate do |csv|
108
- csv << %w[id customer total]
109
- @orders.each { |order| csv << row(order) }
110
- end
111
- end
112
-
113
- private
114
-
115
- def row(order) = [order.id, order.customer.upcase, calculator.total(order)]
61
+ private
116
62
 
117
- def calculator = @calculator ||= PriceCalculator.new
63
+ def normalize(name)
64
+ name.strip.downcase.gsub(/\s+/, '-')
118
65
  end
119
66
  end
120
67
 
121
- orders = Array.new(50_000) do |i|
122
- Shop::Order.new(i, "customer #{i}", Array.new(5) { |n| Shop::Item.new("item #{n}", 9.99, n + 1) })
123
- end
68
+ names = Array.new(10_000) { |i| " Product #{i} " }
124
69
 
125
- MethodTimer.install(Shop)
126
- MethodTimer.run { Shop::CsvExport.new(orders).call }
127
- ```
70
+ MethodTimer.install(NameFormatter)
71
+ slugs = MethodTimer.run { NameFormatter.new.call(names) }
72
+ MethodTimer.uninstall
128
73
 
74
+ slugs.first # => "product-0"
129
75
  ```
130
- MethodTimer: instrumented 4 classes/modules under Shop (overhead 0.77µs per call).
131
-
132
- method self(ruby) self(sql) self(gc) total calls sql calls allocs
133
- -----------------------------------------------------------------------------------------------------------
134
- PriceCalculator#line_total 0.36301s 0.00000s 0.00000s 0.42104s 250000 0 12
135
- PriceCalculator#total 0.21378s 0.00000s 0.00000s 0.64071s 50000 0 9
136
- CsvExport#row 0.15167s 0.00000s 0.00000s 0.81289s 50000 0 150014
137
- CsvExport#call 0.11139s 0.00000s 0.00000s 0.92428s 1 0 250123
138
- Item#quantity 0.03049s 0.00000s 0.00000s 0.03049s 250000 0 2
139
- Item#price 0.02753s 0.00000s 0.00000s 0.02753s 250000 0 2
140
- CsvExport#calculator 0.00823s 0.00000s 0.00000s 0.00823s 50000 0 6
141
- Order#id 0.00656s 0.00000s 0.00000s 0.00656s 50000 0 2
142
- Order#items 0.00590s 0.00000s 0.00000s 0.00590s 50000 0 2
143
- Order#customer 0.00572s 0.00000s 0.00000s 0.00572s 50000 0 2
144
- -----------------------------------------------------------------------------------------------------------
145
- TOTAL (wall time of run block) 0.92429s
146
- of which captured (sum of self ruby) 0.92428s
147
- of which SQL (sum of self sql / calls) 0.00000s 0
148
- of which GC (sum of self gc) 0.00000s
149
- of which instrumentation overhead (estimated) 0.80849s (1050002 calls x 0.77µs, counted in callers' self)
150
- total allocations (sum of self allocs) 400176
151
- ```
152
-
153
- Two things to notice:
154
-
155
- - `Struct` accessors are real methods, so they're timed too. `allocs` shows where objects are created:
156
- `CsvExport#row` allocates 3 per call, `PriceCalculator` almost nothing.
157
- - The overhead line says 0.81 s of the 0.92 s is the profiler itself: a million calls to tiny methods. When that
158
- happens, instrument only the classes you care about:
159
76
 
160
- ```ruby
161
- MethodTimer.install(Shop::CsvExport)
162
- MethodTimer.install(Shop::PriceCalculator)
163
- MethodTimer.run { Shop::CsvExport.new(orders).call }
164
- # 400,002 instrumented calls instead of 1,050,002; wall time 0.42 s instead of 0.92 s
165
- ```
77
+ `install` wraps existing instance methods, including private helpers. `run` prints the
78
+ report to stderr and returns the block's value. `uninstall` removes the wrappers and clears
79
+ the measurements.
166
80
 
167
- ### Example: a Rails model and service
81
+ Complete stderr output captured on Ruby 4.0.9 (arm64 macOS). Timings, allocations and
82
+ row order can vary between runs; the table and footer below are not reformatted:
168
83
 
169
- In `rails console`, SQL time and query counts are filled in from ActiveRecord's notifications:
84
+ ```text
85
+ MethodTimer: instrumented 1 classes/modules under NameFormatter (overhead 0.61µs per call).
170
86
 
171
- ```ruby
172
- MethodTimer.install(Invoice) # an ActiveRecord model
173
- MethodTimer.install(Billing) # a namespace of service objects, e.g. Billing::MonthlyRun
174
- MethodTimer.run { Billing::MonthlyRun.new(month: Date.current).call }
87
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
88
+ ------------------------------------------------------------------------------------------------------------------------------------------------
89
+ NameFormatter#call 0.00655s 0.00000s 0.00000s 0.01182s 1 0 7
90
+ NameFormatter#normalize 0.00378s 0.00000s 0.00150s 0.00527s 10000 0 80003
91
+ ------------------------------------------------------------------------------------------------------------------------------------------------
92
+ TOTAL (wall time of run block) 0.01183s
93
+ of which captured (sum of self ruby) 0.01033s
94
+ of which SQL (sum of self sql / calls) 0.00000s 0
95
+ of which GC (sum of self gc) 0.00150s
96
+ of which instrumentation overhead (estimated) 0.00607s (10001 calls x 0.61µs, counted in callers' self)
97
+ total allocations (sum of self allocs) 80010
98
+ ------------------------------------------------------------------------------------------------------------------------------------------------
175
99
  ```
176
100
 
177
- `self(sql)` and `sql calls` show which method ran the queries (cached queries are skipped), so an N+1 stands out as
178
- a method with as many `sql calls` as `calls`. Association readers are included (Rails defines them in a named
179
- `GeneratedAssociationMethods` module), e.g. loading 3 records and calling `download.template` on each:
101
+ Here, `normalize` creates most of the objects. `call` includes all helper calls in its
102
+ **total**, but not in its **self** time. The overhead estimate matters: timing 10,000 tiny
103
+ methods can cost more than their useful work.
180
104
 
181
- ```
182
- Download::GeneratedAssociationMethods#template 3 calls 3 sql calls
183
- ```
105
+ For a larger workload and ways to reduce that cost, see the [CSV export walkthrough](docs/examples/plain-ruby.md).
184
106
 
185
- Attribute readers (`invoice.amount`) aren't timed: Rails defines them lazily on first use, after `install` ran.
107
+ ## When you need more control
186
108
 
187
- ### Example: a class from another gem
109
+ Select fewer methods, collect data instead of printing, and sort the saved report:
188
110
 
189
111
  ```ruby
190
- MethodTimer.install(CSV) # CSV and everything under it (CSV::Writer, CSV::Parser, ...)
191
- MethodTimer.run { CSV.generate { |csv| 1_000.times { |i| csv << [i, 'x'] } } }
192
- # CSV::Writer#quote 2000 calls, CSV#<< 1000 calls, ...
193
- ```
112
+ MethodTimer.format = :hash
113
+ MethodTimer.install(NameFormatter, only: [:call])
114
+ MethodTimer.run { NameFormatter.new.call(names) }
194
115
 
195
- ### How long does it really take?
116
+ data = MethodTimer.report(sort: :allocs, limit: 10, threshold: 0)
117
+ # data contains :methods, :totals and :rss
196
118
 
197
- `bench` times a block without instrumenting anything, so it works for any code, including top-level methods and
198
- lambdas that `install` can't reach. Use it to compare two versions:
119
+ MethodTimer.uninstall
120
+ MethodTimer.format = :table
121
+ ```
199
122
 
200
- ```ruby
201
- titles = Array.new(200_000) { |i| "Hello World #{i}!" }
123
+ - `only:` reduces instrumentation overhead, not just the number of displayed rows.
124
+ - `singleton: true` also profiles class and module methods, including `extend`.
125
+ - `report` sorts by self Ruby time by default. Its limit and threshold never change totals.
126
+ - `format = :hash` suppresses automatic reports; `.report` returns data on demand.
127
+ Only the last measurement is kept. Previously returned hashes stay independent.
128
+ - `run(rss: true)` and `bench(rss: true)` also sample process memory.
129
+
130
+ All options, defaults and hash fields are in the [API reference](docs/reference.md).
202
131
 
203
- MethodTimer.bench { titles.map { |t| t.downcase.gsub(/[^a-z0-9]+/, '-').delete_suffix('-') } }
204
- # wall 0.320s
205
- # GC 0.085s
206
- # SQL 0.000s (0 queries)
207
- # allocations 2600038
132
+ ### Measure a block without method instrumentation
208
133
 
209
- MethodTimer.bench { titles.map { |t| t.downcase.tr('^a-z0-9', '-').squeeze('-').delete_suffix('-') } }
210
- # wall 0.150s
211
- # GC 0.043s
212
- # SQL 0.000s (0 queries)
213
- # allocations 1800022
134
+ Use `bench` in a fresh console without `install`, for example to compare implementations:
214
135
 
215
- MethodTimer.bench(rss: true) { thing_to_measure } # plus memory at start, peak (sampled every second) and end
136
+ ```ruby
137
+ MethodTimer.bench { names.map { |name| name.strip.downcase } }
216
138
  ```
217
139
 
218
- When comparing two versions of real code, run `bench` in a fresh process without `install`.
140
+ It reports wall time, GC, SQL and allocations, and still returns the block's value.
141
+ See [benchmarking and memory](docs/examples/benchmarking.md) for a complete comparison.
219
142
 
220
- ### What isn't timed
143
+ ## Reading the columns
221
144
 
222
- - Class (singleton) methods such as `Report.build`: only instance methods are wrapped. Their time counts toward
223
- the instance methods they call, or use `bench`.
224
- - Top-level methods, lambdas and blocks: they aren't methods of an instrumented class.
225
- - Methods defined after `install` (e.g. ActiveRecord attribute readers, which Rails defines on first use) and
226
- methods in anonymous modules: `install` wraps the methods that exist when it runs.
145
+ | Column | Meaning |
146
+ | --- | --- |
147
+ | `self(ruby)` | Own code, excluding instrumented callees, SQL and GC. |
148
+ | `self(sql)` | SQL run directly here, excluding instrumented callees. |
149
+ | `self(gc)` | GC time attributed directly here, excluding instrumented callees. |
150
+ | `total` | Wall time including callees; do not sum this column across methods. |
151
+ | `calls` / `sql calls` | Method calls / uncached, non-schema SQL events. |
152
+ | `allocs` | Objects allocated directly here, excluding instrumented callees. |
227
153
 
228
- ## Reading the report
154
+ The run's wall time includes code outside instrumented methods and the profiler's overhead.
155
+ With ActiveSupport 7.2+, GC during SQL is counted only as GC; older versions cannot separate
156
+ that overlap. See [measurement semantics](docs/reference.md#measurement-semantics).
229
157
 
230
- ```
231
- method self(ruby) self(sql) self(gc) total calls sql calls allocs
232
- ------------------------------------------------------------------------------------------------------------
233
- Supply#build_csv 36.52825s 21.26017s 4.77200s 86.66200s 1 27 52580175
234
- Supply#build_row 22.22469s 0.00000s 0.09000s 24.03738s 4865224 0 4918938
235
- ...
236
- ------------------------------------------------------------------------------------------------------------
237
- TOTAL (wall time of run block) 163.27384s
238
- of which captured (sum of self ruby) 101.23146s
239
- of which SQL (sum of self sql / calls) 49.77568s 250
240
- of which GC (sum of self gc) 12.26600s
241
- of which instrumentation overhead (estimated) 31.75190s (43720945 calls x 0.73µs, counted in callers' self)
242
- total allocations (sum of self allocs) 112497459
243
- ```
158
+ ## Examples and reference
244
159
 
245
- - **self(ruby)**: time spent in the method's own code, excluding instrumented callees, SQL and GC.
246
- - **self(sql)**: SQL run directly by the method (not by instrumented callees). Cached queries are skipped.
247
- - **self(gc)**: GC pauses that started while the method itself was running. Without this column, GC time would
248
- land on whatever method happened to be on the stack.
249
- - **total**: wall time including everything the method called.
250
- - **allocs**: objects allocated directly by the method.
251
- - **wall time of run block** = self(ruby) + self(sql) + self(gc) summed over all methods (plus anything outside
252
- instrumented methods). It **includes the profiler's overhead**.
253
- - **instrumentation overhead (estimated)**: every instrumented call costs a fixed amount (measured when you call
254
- `install`), and that cost shows up in the callers' self(ruby). Subtract it from the wall time to estimate the
255
- code's real run time. A method's `total` includes the overhead of every instrumented call beneath it, so methods
256
- that make millions of calls look relatively more expensive than they are.
257
-
258
- For an exact real run time, use `bench`.
259
-
260
- ## Caveats
261
-
262
- - `uninstall` removes the timing code, but each instrumented class keeps an empty prepended module until the
263
- process exits (Ruby can't un-prepend). It has no per-call cost.
264
- - Only the thread running `run` is profiled: instrumented methods called from other threads run untimed (and
265
- their SQL isn't counted). Run one profiled block at a time.
266
- - With `rss: true`, memory is sampled by a child `sh` process (reading `/proc/<pid>/statm` on Linux, `ps`
267
- elsewhere), so sampling doesn't add allocations or GC to the profiled process.
268
- - Lambdas and blocks aren't methods, so their time is counted in the method that calls them.
269
- - The per-call overhead is about 0.4–0.7µs depending on the process. With tens of millions of calls, expect tens
270
- of seconds of overhead; the report tells you how much.
160
+ - [Plain Ruby: CSV export, allocations and overhead](docs/examples/plain-ruby.md)
161
+ - [Rails console: finding an N+1](docs/examples/rails-console.md)
162
+ - [Class methods, modules, `include`, `extend` and inheritance](docs/examples/class-and-module-methods.md)
163
+ - [Sorting, hashes and the last measurement](docs/examples/reports-as-data.md)
164
+ - [Benchmarking implementations and sampling memory](docs/examples/benchmarking.md)
165
+ - [Historical report from a real export](docs/examples/real-world-export.md)
166
+ - [Full API and limitations](docs/reference.md)
167
+ - [Release notes](CHANGELOG.md)
271
168
 
272
- ## Development
273
-
274
- The code is split into modules extended onto `MethodTimer`, so they share its state and the generated wrappers
275
- call `MethodTimer.enter`/`MethodTimer.leave` without an extra object per call:
169
+ ## Important limits
276
170
 
277
- - `lib/method_timer.rb`: `run` and `bench`
278
- - `lib/method_timer/instrumenter.rb`: `install`, class discovery, labels, wrapper generation, overhead calibration
279
- - `lib/method_timer/recorder.rb`: per-method stats (`enter`/`leave`, `stats`, `reset`)
280
- - `lib/method_timer/reporter.rb`: report output
281
- - `lib/method_timer/sql.rb`: SQL tracking via ActiveSupport notifications
282
- - `lib/method_timer/memory.rb`: RSS sampling
171
+ - Run one measurement at a time. Only the measuring thread's methods and SQL are recorded;
172
+ GC, allocation and RSS counters are process-wide.
173
+ - Installation wraps methods that already exist. Later definitions and anonymous modules
174
+ are not discovered. Inherited implementations need their defining owners installed too.
175
+ - To change an installed owner's method selection, call `uninstall` first.
176
+ - Uninstall leaves empty prepended modules in place because Ruby cannot un-prepend.
177
+ - The block executes normally: profiling does not sandbox or undo its side effects.
283
178
 
284
- Each module has its own spec under `spec/method_timer/`; `spec/method_timer_spec.rb` covers `run` and `bench` end to end.
179
+ ## Development
285
180
 
286
181
  ```sh
287
182
  bundle install
288
183
  bundle exec rspec
289
184
  bundle exec rubocop
290
- bundle exec rake # both
291
185
  gem build method_timer.gemspec
292
186
  ```
293
187
 
188
+ See the [internal layout](docs/reference.md#development) before changing collection or reporting.
189
+
294
190
  ## License
295
191
 
296
192
  MIT, see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,95 @@
1
+ # Comparing implementations and sampling memory
2
+
3
+ [README](../../README.md) · [Benchmark reference](../reference.md#bench)
4
+
5
+ `bench` reports the block's wall time, GC, SQL and allocations without enabling per-method
6
+ recording. Use a fresh process without `install` so inactive wrappers do not add overhead.
7
+
8
+ ## Compare the same workload
9
+
10
+ This standalone example creates its inputs before measurement and checks a few outputs
11
+ before comparing the two implementations:
12
+
13
+ ```ruby
14
+ require 'method_timer'
15
+
16
+ titles = Array.new(200_000) { |i| "Hello World #{i}!" }
17
+ regexp_slug = ->(title) { title.downcase.gsub(/[^a-z0-9]+/, '-').delete_suffix('-') }
18
+ tr_slug = ->(title) { title.downcase.tr('^a-z0-9', '-').squeeze('-').delete_suffix('-') }
19
+
20
+ sample = titles.first(5)
21
+ raise 'Implementations disagree' unless sample.map(&regexp_slug) == sample.map(&tr_slug)
22
+
23
+ MethodTimer.bench { titles.map(&regexp_slug) }
24
+ MethodTimer.bench { titles.map(&tr_slug) }
25
+ ```
26
+
27
+ Complete stderr captured on Ruby 4.0.9 (arm64 macOS), in the same process and order as above.
28
+ The first report is the regexp implementation; the second is the `tr` implementation:
29
+
30
+ ```text
31
+ wall 0.204s
32
+ GC 0.034s
33
+ SQL 0.000s (0 queries)
34
+ allocations 1600006
35
+
36
+ wall 0.102s
37
+ GC 0.023s
38
+ SQL 0.000s (0 queries)
39
+ allocations 1800002
40
+ ```
41
+
42
+ These are individual measurements, not a general speed claim. Compare `wall` for elapsed time,
43
+ `GC` for time spent collecting, and `allocations` for object creation. Lower allocation
44
+ counts can explain a wall-time improvement, but do not measure retained memory.
45
+
46
+ `bench` forces GC before timing; work and GC inside the block are included. Cleanup and
47
+ report rendering are excluded. Its return value is the original block value.
48
+ Inputs, warmup, cache state, retained results and execution order can all affect a comparison;
49
+ for stronger conclusions, repeat in fresh processes with equivalent setup.
50
+
51
+ ## Sample process memory
52
+
53
+ ```ruby
54
+ memory_result = MethodTimer.bench(rss: true) do
55
+ strings = Array.new(100_000) { 'x' * 100 }
56
+ sleep(1.1)
57
+ strings.length
58
+ end
59
+ ```
60
+
61
+ `memory_result` is `100_000`. The pause deliberately keeps the strings alive through a
62
+ sampling interval; it is part of this block's wall time, not a way to benchmark string creation.
63
+ Complete stderr from the same Ruby 4.0.9 session:
64
+
65
+ ```text
66
+ wall 1.115s
67
+ GC 0.000s
68
+ SQL 0.000s (0 queries)
69
+ allocations 200006
70
+
71
+ RSS start 66.9MB
72
+ RSS peak (sampled every 1s) 67.6MB
73
+ RSS end 67.6MB
74
+ RSS growth (peak - start) 784.0KB
75
+ ```
76
+
77
+ The low growth here follows earlier allocation-heavy benchmarks in the same process:
78
+ the Ruby heap already had memory available. This is not a retained-size estimate for the strings.
79
+
80
+ The child process samples every second. A short-lived spike can be missed, and RSS includes
81
+ the entire process, not just Ruby objects. Start and end samples are included in the peak.
82
+ Sampling itself does not allocate in the measured Ruby process.
83
+
84
+ To collect these readings without printing, choose `MethodTimer.format = :hash` before the
85
+ benchmark and then call `.report`; `report[:rss]` contains byte counts. See
86
+ [reports as data](reports-as-data.md).
87
+
88
+ ## Avoid misleading results
89
+
90
+ - Do not compare an instrumented `run` directly with an uninstrumented `bench` as a code optimization.
91
+ - Do not put fixture creation in just one candidate's measured block.
92
+ - Both candidates execute normally; repeating a benchmark can repeat writes or external calls.
93
+ - SQL is optional and counts only events from the measuring thread; query caching can change counts.
94
+ - GC, allocations and RSS are process-wide, so background activity can contaminate results.
95
+ - Run one measurement at a time. Each replaces the saved report, so capture hashes if you need both results.