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.
@@ -0,0 +1,126 @@
1
+ # Class methods, modules and inheritance
2
+
3
+ [README](../../README.md) · [Installation reference](../reference.md#install)
4
+
5
+ Instance methods are profiled by default. Add `singleton: true` to include class and module
6
+ entry points, including implementations reached through `extend`, `extend self` and
7
+ `module_function`.
8
+
9
+ ## A complete example
10
+
11
+ ```ruby
12
+ require 'method_timer'
13
+
14
+ module BillingExample
15
+ module Names
16
+ def normalize(value) = value.strip.downcase
17
+ end
18
+
19
+ class Base
20
+ def self.call(value) = value
21
+ end
22
+
23
+ class Service < Base
24
+ include Names
25
+ extend Names
26
+
27
+ def self.call(value)
28
+ super(normalize(value))
29
+ end
30
+
31
+ def call(value) = normalize(value)
32
+ end
33
+
34
+ module Functions
35
+ def slug(value) = value.downcase.tr(' ', '-')
36
+ module_function :slug
37
+ end
38
+
39
+ module Helpers
40
+ extend self
41
+
42
+ def call(value) = value.upcase
43
+ end
44
+ end
45
+
46
+ MethodTimer.install(BillingExample, singleton: true)
47
+ results = MethodTimer.run do
48
+ [
49
+ BillingExample::Service.call(' Invoice '),
50
+ BillingExample::Service.new.call(' Invoice '),
51
+ BillingExample::Functions.slug('Hello World'),
52
+ BillingExample::Helpers.call('memo')
53
+ ]
54
+ end
55
+ MethodTimer.report(threshold: 0)
56
+ shared_calls = MethodTimer.stats.fetch('Names#normalize')[:calls]
57
+ MethodTimer.uninstall
58
+ ```
59
+
60
+ `results` is `["invoice", "invoice", "hello-world", "MEMO"]`. Reprinting with `threshold: 0`
61
+ reveals even the tiny methods that the default one-millisecond threshold may hide.
62
+
63
+ Complete stderr captured on Ruby 4.0.9 (arm64 macOS). The first table is printed by
64
+ `run` with the default threshold: all methods are too small to appear, but totals still
65
+ include them. The second is the same saved measurement printed by `report(threshold: 0)`.
66
+ Values below five decimal places round to `0.00000s`; that does not mean no work occurred.
67
+
68
+ ```text
69
+ MethodTimer: instrumented 7 classes/modules under BillingExample (overhead 0.63µs per call).
70
+
71
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
72
+ ------------------------------------------------------------------------------------------------------------------------------------------------
73
+ ------------------------------------------------------------------------------------------------------------------------------------------------
74
+ TOTAL (wall time of run block) 0.00002s
75
+ of which captured (sum of self ruby) 0.00001s
76
+ of which SQL (sum of self sql / calls) 0.00000s 0
77
+ of which GC (sum of self gc) 0.00000s
78
+ of which instrumentation overhead (estimated) 0.00000s (7 calls x 0.63µs, counted in callers' self)
79
+ total allocations (sum of self allocs) 43
80
+ ------------------------------------------------------------------------------------------------------------------------------------------------
81
+
82
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
83
+ ------------------------------------------------------------------------------------------------------------------------------------------------
84
+ Service.call 0.00001s 0.00000s 0.00000s 0.00001s 1 0 10
85
+ Names#normalize 0.00000s 0.00000s 0.00000s 0.00000s 2 0 12
86
+ Functions.slug 0.00000s 0.00000s 0.00000s 0.00000s 1 0 8
87
+ Base.call 0.00000s 0.00000s 0.00000s 0.00000s 1 0 3
88
+ Helpers#call 0.00000s 0.00000s 0.00000s 0.00000s 1 0 5
89
+ Service#call 0.00000s 0.00000s 0.00000s 0.00000s 1 0 5
90
+ ------------------------------------------------------------------------------------------------------------------------------------------------
91
+ TOTAL (wall time of run block) 0.00002s
92
+ of which captured (sum of self ruby) 0.00001s
93
+ of which SQL (sum of self sql / calls) 0.00000s 0
94
+ of which GC (sum of self gc) 0.00000s
95
+ of which instrumentation overhead (estimated) 0.00000s (7 calls x 0.63µs, counted in callers' self)
96
+ total allocations (sum of self allocs) 43
97
+ ------------------------------------------------------------------------------------------------------------------------------------------------
98
+ ```
99
+
100
+ ## Why the labels differ
101
+
102
+ - `Service.call` and `Service#call` are different definitions and have separate rows.
103
+ - `super` reaches `Base.call`, which is installed through the same namespace.
104
+ - `Names#normalize` is the shared owner implementation. The class reaches it through
105
+ `extend`; an instance reaches it through `include`. It is wrapped once, not once per receiver.
106
+ - `Functions.slug` is the singleton copy made by `module_function`. Its original instance
107
+ definition remains private.
108
+ - `Helpers#call` belongs to the module used by `extend self`, so it retains its `#` owner label.
109
+
110
+ Self time excludes instrumented children; inclusive total time includes them. Do not add
111
+ `Service.call` and `Base.call` totals to estimate the run's wall time.
112
+
113
+ ## Filtering and scope
114
+
115
+ `only: [:call]` would select both instance and singleton methods named `call`, but not
116
+ `normalize` or `slug`. Their uninstrumented work would remain in the selected callers' self
117
+ time. Change an existing owner's filter only after `uninstall`.
118
+
119
+ Installing a subclass alone does not patch external parent implementations or their mixins.
120
+ Install those parents too, or choose a namespace containing them. Likewise, a shared module
121
+ retains the selection established when it was first instrumented, even if another class later
122
+ uses it through a different path.
123
+
124
+ Private and protected visibility, arguments, keywords and blocks are preserved.
125
+ Later definitions and anonymous mixins are not discovered. See the [full reference](../reference.md)
126
+ for installation and label rules.
@@ -0,0 +1,243 @@
1
+ # Plain Ruby: finding the cost of an export
2
+
3
+ [README](../../README.md) · [API reference](../reference.md)
4
+
5
+ This larger version of the README example exports 50,000 orders with five items each.
6
+ It shows why many cheap calls can make instrumentation itself expensive.
7
+ The original CSV example is retained here. The separate
8
+ [historical application report](real-world-export.md) preserves the real-work excerpt too.
9
+ All current outputs below were captured on Ruby 4.0.9, CSV 3.3.5, arm64 macOS,
10
+ in one process following the code in order. Tables and footers are unabridged;
11
+ timings and allocations vary between runs.
12
+
13
+ ## Setup
14
+
15
+ This example uses the separate `csv` gem. If you use Bundler, declare it in your application's
16
+ Gemfile as well as MethodTimer; Ruby 3.4+ does not make it automatically available to a bundle:
17
+
18
+ ```ruby
19
+ # Gemfile
20
+ gem 'csv'
21
+ gem 'method_timer', require: false
22
+ ```
23
+
24
+ Run `bundle install`, then paste the following into a fresh console. Nothing is written to disk;
25
+ the export is an in-memory string. The prices and tax are just sample data, not financial guidance.
26
+
27
+ ## Profile the whole namespace
28
+
29
+ ```ruby
30
+ require 'csv'
31
+ require 'method_timer'
32
+
33
+ module Shop
34
+ Order = Struct.new(:id, :customer, :items)
35
+ Item = Struct.new(:name, :price, :quantity)
36
+
37
+ class PriceCalculator
38
+ TAX = 0.23
39
+
40
+ def total(order) = order.items.sum { |item| line_total(item) }
41
+
42
+ private
43
+
44
+ def line_total(item) = (item.price * item.quantity * (1 + TAX)).round(2)
45
+ end
46
+
47
+ class CsvExport
48
+ def initialize(orders) = @orders = orders
49
+
50
+ def call
51
+ CSV.generate do |csv|
52
+ csv << %w[id customer total]
53
+ @orders.each { |order| csv << row(order) }
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def row(order) = [order.id, order.customer.upcase, calculator.total(order)]
60
+
61
+ def calculator = @calculator ||= PriceCalculator.new
62
+ end
63
+ end
64
+
65
+ orders = Array.new(50_000) do |i|
66
+ Shop::Order.new(i, "customer #{i}", Array.new(5) { |n| Shop::Item.new("item #{n}", 9.99, n + 1) })
67
+ end
68
+
69
+ MethodTimer.install(Shop)
70
+ csv = MethodTimer.run { Shop::CsvExport.new(orders).call }
71
+ broad_calls = MethodTimer.stats.values.sum { |stats| stats[:calls] }
72
+ ```
73
+
74
+ Data creation is outside the measured block. The export result remains in `csv`; MethodTimer
75
+ does not replace it with a report.
76
+
77
+ Complete stderr:
78
+
79
+ ```text
80
+ MethodTimer: instrumented 4 classes/modules under Shop (overhead 0.62µs per call).
81
+
82
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
83
+ ------------------------------------------------------------------------------------------------------------------------------------------------
84
+ PriceCalculator#line_total 0.28903s 0.00000s 0.00000s 0.34740s 250000 0 12
85
+ PriceCalculator#total 0.16870s 0.00000s 0.00000s 0.52206s 50000 0 10
86
+ CsvExport#row 0.11930s 0.00000s 0.00766s 0.66412s 50000 0 150014
87
+ CsvExport#call 0.08534s 0.00000s 0.00000s 0.74946s 1 0 250127
88
+ Item#price 0.03083s 0.00000s 0.00000s 0.03083s 250000 0 3
89
+ Item#quantity 0.02754s 0.00000s 0.00000s 0.02754s 250000 0 2
90
+ Order#items 0.00596s 0.00000s 0.00000s 0.00596s 50000 0 2
91
+ CsvExport#calculator 0.00527s 0.00000s 0.00000s 0.00527s 50000 0 7
92
+ Order#customer 0.00512s 0.00000s 0.00000s 0.00512s 50000 0 2
93
+ Order#id 0.00470s 0.00000s 0.00000s 0.00470s 50000 0 3
94
+ ------------------------------------------------------------------------------------------------------------------------------------------------
95
+ TOTAL (wall time of run block) 0.74947s
96
+ of which captured (sum of self ruby) 0.74180s
97
+ of which SQL (sum of self sql / calls) 0.00000s 0
98
+ of which GC (sum of self gc) 0.00766s
99
+ of which instrumentation overhead (estimated) 0.65463s (1050002 calls x 0.62µs, counted in callers' self)
100
+ total allocations (sum of self allocs) 400185
101
+ ------------------------------------------------------------------------------------------------------------------------------------------------
102
+ ```
103
+
104
+ `Struct` accessors are real methods, so profiling `Shop` includes them. The complete run has
105
+ 1,050,002 instrumented calls. Check the footer's estimated overhead before treating every
106
+ large `self(ruby)` value as useful application work.
107
+
108
+ Sort another view without rerunning:
109
+
110
+ ```ruby
111
+ MethodTimer.report(sort: :allocs, limit: 5, threshold: 0)
112
+ ```
113
+
114
+ Complete stderr, showing five rows but retaining totals for all 1,050,002 calls:
115
+
116
+ ```text
117
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
118
+ ------------------------------------------------------------------------------------------------------------------------------------------------
119
+ CsvExport#call 0.08534s 0.00000s 0.00000s 0.74946s 1 0 250127
120
+ CsvExport#row 0.11930s 0.00000s 0.00766s 0.66412s 50000 0 150014
121
+ PriceCalculator#line_total 0.28903s 0.00000s 0.00000s 0.34740s 250000 0 12
122
+ PriceCalculator#total 0.16870s 0.00000s 0.00000s 0.52206s 50000 0 10
123
+ CsvExport#calculator 0.00527s 0.00000s 0.00000s 0.00527s 50000 0 7
124
+ ------------------------------------------------------------------------------------------------------------------------------------------------
125
+ TOTAL (wall time of run block) 0.74947s
126
+ of which captured (sum of self ruby) 0.74180s
127
+ of which SQL (sum of self sql / calls) 0.00000s 0
128
+ of which GC (sum of self gc) 0.00766s
129
+ of which instrumentation overhead (estimated) 0.65463s (1050002 calls x 0.62µs, counted in callers' self)
130
+ total allocations (sum of self allocs) 400185
131
+ ------------------------------------------------------------------------------------------------------------------------------------------------
132
+ ```
133
+
134
+ The price calculations create relatively few objects. The row and CSV generation paths
135
+ allocate arrays and strings; these are more useful places to investigate allocation volume.
136
+ Actual allocation totals, GC pauses and timings depend on the Ruby version and process.
137
+
138
+ ## Narrow the scope before drawing conclusions
139
+
140
+ Remove the original wrappers first. Reinstalling selected classes without uninstalling would
141
+ leave the earlier `Struct` instrumentation active.
142
+
143
+ ```ruby
144
+ MethodTimer.uninstall
145
+ MethodTimer.install(Shop::CsvExport)
146
+ MethodTimer.install(Shop::PriceCalculator)
147
+ narrow_csv = MethodTimer.run { Shop::CsvExport.new(orders).call }
148
+ narrow_calls = MethodTimer.stats.values.sum { |stats| stats[:calls] }
149
+ ```
150
+
151
+ Complete stderr, including uninstalling the previous scope and installing the narrower one:
152
+
153
+ ```text
154
+ MethodTimer: instrumented 1 classes/modules under Shop::CsvExport (overhead 0.61µs per call).
155
+ MethodTimer: instrumented 1 classes/modules under Shop::PriceCalculator (overhead 0.60µs per call).
156
+
157
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
158
+ ------------------------------------------------------------------------------------------------------------------------------------------------
159
+ PriceCalculator#total 0.14170s 0.00000s 0.00000s 0.18216s 50000 0 7
160
+ CsvExport#call 0.08192s 0.00000s 0.00000s 0.33683s 1 0 250047
161
+ CsvExport#row 0.06598s 0.00000s 0.00000s 0.25492s 50000 0 150010
162
+ PriceCalculator#line_total 0.04046s 0.00000s 0.00000s 0.04046s 250000 0 4
163
+ CsvExport#calculator 0.00677s 0.00000s 0.00000s 0.00677s 50000 0 3
164
+ ------------------------------------------------------------------------------------------------------------------------------------------------
165
+ TOTAL (wall time of run block) 0.33685s
166
+ of which captured (sum of self ruby) 0.33684s
167
+ of which SQL (sum of self sql / calls) 0.00000s 0
168
+ of which GC (sum of self gc) 0.00000s
169
+ of which instrumentation overhead (estimated) 0.24148s (400002 calls x 0.60µs, counted in callers' self)
170
+ total allocations (sum of self allocs) 400074
171
+ ------------------------------------------------------------------------------------------------------------------------------------------------
172
+ ```
173
+
174
+ This performs the same export with 400,002 instrumented calls rather than 1,050,002.
175
+ The `Item` and `Order` rows disappear because those methods are no longer wrapped.
176
+ Their work still contributes to their instrumented callers.
177
+
178
+ If you only need the export's inclusive cost, select its entry point:
179
+
180
+ ```ruby
181
+ MethodTimer.uninstall
182
+ MethodTimer.install(Shop::CsvExport, only: [:call])
183
+ selected_csv = MethodTimer.run { Shop::CsvExport.new(orders).call }
184
+ selected_calls = MethodTimer.stats.values.sum { |stats| stats[:calls] }
185
+ MethodTimer.uninstall
186
+ ```
187
+
188
+ Complete stderr:
189
+
190
+ ```text
191
+ MethodTimer: instrumented 1 classes/modules under Shop::CsvExport (overhead 0.62µs per call).
192
+
193
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
194
+ ------------------------------------------------------------------------------------------------------------------------------------------------
195
+ CsvExport#call 0.08137s 0.00000s 0.00000s 0.08137s 1 0 400050
196
+ ------------------------------------------------------------------------------------------------------------------------------------------------
197
+ TOTAL (wall time of run block) 0.08138s
198
+ of which captured (sum of self ruby) 0.08137s
199
+ of which SQL (sum of self sql / calls) 0.00000s 0
200
+ of which GC (sum of self gc) 0.00000s
201
+ of which instrumentation overhead (estimated) 0.00000s (1 calls x 0.62µs, counted in callers' self)
202
+ total allocations (sum of self allocs) 400050
203
+ ------------------------------------------------------------------------------------------------------------------------------------------------
204
+ ```
205
+
206
+ Now there is one instrumented call. Helper work is included in `CsvExport#call`'s self time.
207
+ All three exports should be identical; the different scopes change observation cost, not the
208
+ export's business logic. To compare implementations without wrappers, use a fresh process
209
+ and [`bench`](benchmarking.md).
210
+
211
+ ## Profiling another gem
212
+
213
+ If your application already loads CSV, you can also profile its named classes:
214
+
215
+ ```ruby
216
+ MethodTimer.install(CSV)
217
+ MethodTimer.run { CSV.generate { |output| 1_000.times { |i| output << [i, 'x'] } } }
218
+ MethodTimer.uninstall
219
+ ```
220
+
221
+ Complete stderr. The default threshold hides methods shorter than one millisecond;
222
+ the footer still includes their calls and allocations:
223
+
224
+ ```text
225
+ MethodTimer: instrumented 16 classes/modules under CSV (overhead 0.60µs per call).
226
+
227
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
228
+ ------------------------------------------------------------------------------------------------------------------------------------------------
229
+ CSV::Writer#<< 0.00245s 0.00000s 0.00000s 0.00391s 1000 0 3011
230
+ CSV#<< 0.00114s 0.00000s 0.00000s 0.00524s 1000 0 8
231
+ ------------------------------------------------------------------------------------------------------------------------------------------------
232
+ TOTAL (wall time of run block) 0.00585s
233
+ of which captured (sum of self ruby) 0.00526s
234
+ of which SQL (sum of self sql / calls) 0.00000s 0
235
+ of which GC (sum of self gc) 0.00000s
236
+ of which instrumentation overhead (estimated) 0.00480s (8022 calls x 0.60µs, counted in callers' self)
237
+ total allocations (sum of self allocs) 4151
238
+ ------------------------------------------------------------------------------------------------------------------------------------------------
239
+ ```
240
+
241
+ Only methods already loaded at installation time are wrapped. Warm up a lazy library first
242
+ if it defines classes or methods on first use, then install it. Broad instrumentation of
243
+ another gem can have substantial overhead, just like broad instrumentation of application code.
@@ -0,0 +1,185 @@
1
+ # Rails console: finding an N+1
2
+
3
+ [README and console installation](../../README.md#installation) · [API reference](../reference.md)
4
+
5
+ Use your application's `rails console`, never code that serves requests or runs jobs.
6
+ The following assumes an `Invoice` model with a `customer` association. Adapt those names
7
+ to your application. It only reads existing records; it does not create tables or fixtures.
8
+
9
+ ## Reproducing the captured reports without an application
10
+
11
+ The reports below are real captures on Ruby 4.0.9, ActiveRecord / ActiveSupport 8.1.4,
12
+ SQLite3 gem 2.10.2, arm64 macOS. They used 100 invoices, each with a different customer,
13
+ in an isolated in-memory database, not a production application.
14
+
15
+ To reproduce that dataset, start a **fresh plain Ruby process, not your application's console**,
16
+ with `activerecord`, `sqlite3` and `method_timer` installed. Run this setup before the profiling
17
+ code below. It replaces the process's ActiveRecord connection, so do not paste it into an
18
+ existing application session:
19
+
20
+ ```ruby
21
+ require 'active_record'
22
+ require 'sqlite3'
23
+
24
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
25
+ ActiveRecord::Schema.define do
26
+ create_table(:customers) { |t| t.string :name }
27
+ create_table(:invoices) { |t| t.references :customer }
28
+ end
29
+
30
+ class Customer < ActiveRecord::Base
31
+ end
32
+
33
+ class Invoice < ActiveRecord::Base
34
+ belongs_to :customer
35
+ end
36
+
37
+ 100.times do |i|
38
+ Invoice.create!(customer: Customer.create!(name: "Customer #{i}"))
39
+ end
40
+ ```
41
+
42
+ In an existing Rails console, skip this setup and use your own models and records instead.
43
+ Counts and timings will depend on your data and query-cache state.
44
+
45
+ ## Keep queries inside the measured method
46
+
47
+ Passing a lazy relation to the export lets its initial load happen inside `call`.
48
+ Loading records before `run` would exclude those initial queries from the measurement.
49
+
50
+ ```ruby
51
+ require 'method_timer'
52
+
53
+ class ConsoleInvoiceExport
54
+ def call(scope)
55
+ scope.map { |invoice| row(invoice) }
56
+ end
57
+
58
+ private
59
+
60
+ def row(invoice)
61
+ [invoice.id, invoice.customer.name]
62
+ end
63
+ end
64
+
65
+ exporter = ConsoleInvoiceExport.new
66
+ MethodTimer.install(ConsoleInvoiceExport)
67
+ MethodTimer.install(Invoice, only: [:customer])
68
+ rows = MethodTimer.run { exporter.call(Invoice.order(:id).limit(100)) }
69
+ MethodTimer.report(sort: :sql_calls, threshold: 0)
70
+ ```
71
+
72
+ Complete profiling stderr: first the automatic `run` table, then
73
+ `report(sort: :sql_calls, threshold: 0)` for the same measurement:
74
+
75
+ ```text
76
+ MethodTimer: instrumented 1 classes/modules under ConsoleInvoiceExport (overhead 0.60µs per call).
77
+ MethodTimer: instrumented 1 classes/modules under Invoice (overhead 0.60µs per call).
78
+
79
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
80
+ ------------------------------------------------------------------------------------------------------------------------------------------------
81
+ Invoice::GeneratedAssociationMethods#customer 0.00703s 0.00070s 0.00000s 0.00774s 100 100 13646
82
+ ConsoleInvoiceExport#call 0.00032s 0.00008s 0.00000s 0.00837s 1 1 1055
83
+ ConsoleInvoiceExport#row 0.00024s 0.00000s 0.00000s 0.00797s 100 0 214
84
+ ------------------------------------------------------------------------------------------------------------------------------------------------
85
+ TOTAL (wall time of run block) 0.01204s
86
+ of which captured (sum of self ruby) 0.00759s
87
+ of which SQL (sum of self sql / calls) 0.00078s 101
88
+ of which GC (sum of self gc) 0.00000s
89
+ of which instrumentation overhead (estimated) 0.00012s (201 calls x 0.60µs, counted in callers' self)
90
+ total allocations (sum of self allocs) 14915
91
+ ------------------------------------------------------------------------------------------------------------------------------------------------
92
+
93
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
94
+ ------------------------------------------------------------------------------------------------------------------------------------------------
95
+ Invoice::GeneratedAssociationMethods#customer 0.00703s 0.00070s 0.00000s 0.00774s 100 100 13646
96
+ ConsoleInvoiceExport#call 0.00032s 0.00008s 0.00000s 0.00837s 1 1 1055
97
+ ConsoleInvoiceExport#row 0.00024s 0.00000s 0.00000s 0.00797s 100 0 214
98
+ ------------------------------------------------------------------------------------------------------------------------------------------------
99
+ TOTAL (wall time of run block) 0.01204s
100
+ of which captured (sum of self ruby) 0.00759s
101
+ of which SQL (sum of self sql / calls) 0.00078s 101
102
+ of which GC (sum of self gc) 0.00000s
103
+ of which instrumentation overhead (estimated) 0.00012s (201 calls x 0.60µs, counted in callers' self)
104
+ total allocations (sum of self allocs) 14915
105
+ ------------------------------------------------------------------------------------------------------------------------------------------------
106
+ ```
107
+
108
+ `sql.active_record` notifications fill the SQL columns automatically. This capture recorded
109
+ one invoices query and 100 uncached customer queries through the named association-reader
110
+ module. Repeated customers, query caching, empty data or callbacks can change the numbers.
111
+ Cached queries and `SCHEMA` events are excluded. Wall time also includes work outside the
112
+ instrumented frames, such as constructing the relation before entering `call`.
113
+
114
+ Look for many SQL calls on the association reader. Time charged to an instrumented child is
115
+ excluded from its caller's self columns, so `ConsoleInvoiceExport#row` need not carry the SQL
116
+ itself for an N+1 to be present.
117
+
118
+ ## Compare with eager loading
119
+
120
+ Pass a fresh relation and keep preloading inside the same export call:
121
+
122
+ ```ruby
123
+ preloaded_rows = MethodTimer.run do
124
+ exporter.call(Invoice.includes(:customer).order(:id).limit(100))
125
+ end
126
+ MethodTimer.report(sort: :sql_calls, threshold: 0)
127
+ MethodTimer.uninstall
128
+ ```
129
+
130
+ Complete stderr, again showing the automatic table followed by the unfiltered SQL-call view:
131
+
132
+ ```text
133
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
134
+ ------------------------------------------------------------------------------------------------------------------------------------------------
135
+ ConsoleInvoiceExport#call 0.00289s 0.00013s 0.00000s 0.00323s 1 2 3470
136
+ ------------------------------------------------------------------------------------------------------------------------------------------------
137
+ TOTAL (wall time of run block) 0.00328s
138
+ of which captured (sum of self ruby) 0.00310s
139
+ of which SQL (sum of self sql / calls) 0.00013s 2
140
+ of which GC (sum of self gc) 0.00000s
141
+ of which instrumentation overhead (estimated) 0.00012s (201 calls x 0.60µs, counted in callers' self)
142
+ total allocations (sum of self allocs) 3670
143
+ ------------------------------------------------------------------------------------------------------------------------------------------------
144
+
145
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
146
+ ------------------------------------------------------------------------------------------------------------------------------------------------
147
+ ConsoleInvoiceExport#call 0.00289s 0.00013s 0.00000s 0.00323s 1 2 3470
148
+ ConsoleInvoiceExport#row 0.00015s 0.00000s 0.00000s 0.00021s 100 0 200
149
+ Invoice::GeneratedAssociationMethods#customer 0.00006s 0.00000s 0.00000s 0.00006s 100 0 0
150
+ ------------------------------------------------------------------------------------------------------------------------------------------------
151
+ TOTAL (wall time of run block) 0.00328s
152
+ of which captured (sum of self ruby) 0.00310s
153
+ of which SQL (sum of self sql / calls) 0.00013s 2
154
+ of which GC (sum of self gc) 0.00000s
155
+ of which instrumentation overhead (estimated) 0.00012s (201 calls x 0.60µs, counted in callers' self)
156
+ total allocations (sum of self allocs) 3670
157
+ ------------------------------------------------------------------------------------------------------------------------------------------------
158
+ ```
159
+
160
+ The default threshold hides the now-cheap row and association methods in the first table,
161
+ but the second proves they still ran 100 times each. Association SQL fell to zero; both
162
+ remaining queries belong to `ConsoleInvoiceExport#call`.
163
+
164
+ Typically this replaces many association queries with the invoices query and a customer
165
+ preload query, attributed to `ConsoleInvoiceExport#call`. Association-reader SQL counts
166
+ should fall. Do not move `to_a` outside the profiled block and then mistake unmeasured
167
+ preloading work for a free optimization.
168
+
169
+ Each run replaces the saved report. To keep data for both runs, use the
170
+ [hash reporting workflow](reports-as-data.md) and save each returned hash before the next run.
171
+
172
+ ## What to watch
173
+
174
+ - SQL logging is silenced by setting `ActiveRecord::Base.logger = nil` during installation.
175
+ `uninstall` restores the original logger if it is still `nil`; a replacement logger is kept.
176
+ Use `silence_sql: false` if you want to keep the current logger.
177
+ - With ActiveSupport 7.2+, GC during SQL is excluded from SQL time and appears only as GC.
178
+ Older versions cannot separate that overlap.
179
+ - Attribute readers may be defined lazily after `install`; association readers are a different,
180
+ named-module path and are included when present at installation time.
181
+ - A class-method service needs `singleton: true`. Inherited implementations need their
182
+ defining owner installed, not just the leaf class.
183
+ - Queries from other threads are not counted. GC, allocations and RSS remain process-wide.
184
+ - The block runs normally. Profiling does not roll back writes, so inspect application service
185
+ behavior before invoking it against production data. Exit the throwaway console afterward.
@@ -0,0 +1,40 @@
1
+ # Historical report: a real application export
2
+
3
+ [README](../../README.md) · [CSV walkthrough](plain-ruby.md) · [Measurement semantics](../reference.md#measurement-semantics)
4
+
5
+ This report came from real application work and was included in the original README.
6
+ It is preserved verbatim below, not regenerated with the current release. The original
7
+ already omitted rows with `...`; the missing rows and the application's source code are
8
+ not available here. The Ruby version and environment were not recorded with this excerpt.
9
+ For a complete runnable export and current, unabridged reports, see the [CSV walkthrough](plain-ruby.md).
10
+
11
+ ```text
12
+ method self(ruby) self(sql) self(gc) total calls sql calls allocs
13
+ ------------------------------------------------------------------------------------------------------------
14
+ Supply#build_csv 36.52825s 21.26017s 4.77200s 86.66200s 1 27 52580175
15
+ Supply#build_row 22.22469s 0.00000s 0.09000s 24.03738s 4865224 0 4918938
16
+ ...
17
+ ------------------------------------------------------------------------------------------------------------
18
+ TOTAL (wall time of run block) 163.27384s
19
+ of which captured (sum of self ruby) 101.23146s
20
+ of which SQL (sum of self sql / calls) 49.77568s 250
21
+ of which GC (sum of self gc) 12.26600s
22
+ of which instrumentation overhead (estimated) 31.75190s (43720945 calls x 0.73µs, counted in callers' self)
23
+ total allocations (sum of self allocs) 112497459
24
+ ```
25
+
26
+ ## What this exposed
27
+
28
+ - `Supply#build_csv` directly allocated 52,580,175 objects and spent 21.26017 seconds in
29
+ SQL across 27 queries. Its inclusive total also contains instrumented child work.
30
+ - `Supply#build_row` was called 4,865,224 times. It had no direct SQL, but created
31
+ 4,918,938 objects: a repeated Ruby/allocation path rather than a direct-query hotspot.
32
+ - The full run recorded 250 SQL calls, 12.266 seconds of GC and 112,497,459 allocations.
33
+ These totals include the omitted methods; they cannot be reconstructed from the two visible rows.
34
+ - With 43,720,945 instrumented calls, the estimated profiler overhead was 31.75190 seconds.
35
+ That cost is included in measured time, not subtracted automatically. Narrow instrumentation
36
+ before drawing conclusions about tiny methods, and use `bench` in a fresh process for an
37
+ uninstrumented comparison.
38
+
39
+ The report is evidence of one workload, not a performance guarantee or a reproducible
40
+ benchmark. No missing application code, rows or measurements have been invented.