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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +109 -213
- data/docs/examples/benchmarking.md +95 -0
- data/docs/examples/class-and-module-methods.md +126 -0
- data/docs/examples/plain-ruby.md +243 -0
- data/docs/examples/rails-console.md +185 -0
- data/docs/examples/real-world-export.md +40 -0
- data/docs/examples/reports-as-data.md +124 -0
- data/docs/reference.md +272 -0
- data/lib/method_timer/instrumenter.rb +50 -24
- data/lib/method_timer/memory.rb +6 -0
- data/lib/method_timer/recorder.rb +13 -11
- data/lib/method_timer/reporter.rb +76 -44
- data/lib/method_timer/sql.rb +8 -2
- data/lib/method_timer/version.rb +1 -1
- data/lib/method_timer.rb +22 -10
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b6c0f87fb633ecd9cd510739a6c7fa7a1ae25ed4ec5c58b9f7f6a12ff4e8b6a
|
|
4
|
+
data.tar.gz: 4d06cfd6f2aeb071d995c992d39964221f8a0d8bf693bfd54431d5fd68a3d3e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
14
|
-
|
|
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
|
|
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
|
-
###
|
|
30
|
+
### Load automatically in Rails consoles
|
|
36
31
|
|
|
37
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
49
|
+
## A complete, ordinary Ruby example
|
|
82
50
|
|
|
83
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
def normalize(name)
|
|
64
|
+
name.strip.downcase.gsub(/\s+/, '-')
|
|
118
65
|
end
|
|
119
66
|
end
|
|
120
67
|
|
|
121
|
-
|
|
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(
|
|
126
|
-
MethodTimer.run {
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
```text
|
|
85
|
+
MethodTimer: instrumented 1 classes/modules under NameFormatter (overhead 0.61µs per call).
|
|
170
86
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
107
|
+
## When you need more control
|
|
186
108
|
|
|
187
|
-
|
|
109
|
+
Select fewer methods, collect data instead of printing, and sort the saved report:
|
|
188
110
|
|
|
189
111
|
```ruby
|
|
190
|
-
MethodTimer.
|
|
191
|
-
MethodTimer.
|
|
192
|
-
|
|
193
|
-
```
|
|
112
|
+
MethodTimer.format = :hash
|
|
113
|
+
MethodTimer.install(NameFormatter, only: [:call])
|
|
114
|
+
MethodTimer.run { NameFormatter.new.call(names) }
|
|
194
115
|
|
|
195
|
-
|
|
116
|
+
data = MethodTimer.report(sort: :allocs, limit: 10, threshold: 0)
|
|
117
|
+
# data contains :methods, :totals and :rss
|
|
196
118
|
|
|
197
|
-
|
|
198
|
-
|
|
119
|
+
MethodTimer.uninstall
|
|
120
|
+
MethodTimer.format = :table
|
|
121
|
+
```
|
|
199
122
|
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
```ruby
|
|
137
|
+
MethodTimer.bench { names.map { |name| name.strip.downcase } }
|
|
216
138
|
```
|
|
217
139
|
|
|
218
|
-
|
|
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
|
-
|
|
143
|
+
## Reading the columns
|
|
221
144
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
|
|
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
|
-
##
|
|
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
|
-
-
|
|
278
|
-
|
|
279
|
-
-
|
|
280
|
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
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
|
-
|
|
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(®exp_slug) == sample.map(&tr_slug)
|
|
22
|
+
|
|
23
|
+
MethodTimer.bench { titles.map(®exp_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.
|