active_record_tracer 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: 68b33954ad5db638e0861cf22af151123e584e4706201aff3d46aa08a6722cf5
4
+ data.tar.gz: 4175dc8fb627ea54dcac0ae19ce236a0bbe67bdc6cee1c4fc8ae08f4bf717d84
5
+ SHA512:
6
+ metadata.gz: 64a10ad4f761c88e3c7d474a4a2d34b400fab04ad26de5f3be7a0704317fe84f70301cea4b2938b8824808da284707c90f468c51e3f0e3855d0c5329d6300f88
7
+ data.tar.gz: 9847110a4de17861950dda79b2ce18cfea85e7431b390c019ec1844c5e3e7fc441bb6d8bf6f87959a510243eb42b56a0a8fcf1cae053d40556b26f5e4b297927
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## master (unreleased)
2
+
3
+ ## 0.1.0 (2024-07-10)
4
+
5
+ - First release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 fatkodima
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # ActiveRecordTracer
2
+
3
+ A tracer for Active Record queries
4
+
5
+ [![Build Status](https://github.com/fatkodima/active_record_tracer/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/fatkodima/active_record_tracer/actions/workflows/test.yml)
6
+
7
+ You identified (or suspect) that the reason for the slowness of some code is Active Record,
8
+ specifically lots of queries and/or loaded records. How do you easily detect which queries,
9
+ which records are loaded the most, and the sources of those? This tool to the rescue!
10
+
11
+ ## Requirements
12
+
13
+ - ruby 3.1+
14
+ - activerecord 7.0+
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "active_record_tracer", group: [:development, :test]
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```sh
27
+ $ bundle
28
+ ```
29
+
30
+ Or install it yourself as:
31
+
32
+ ```sh
33
+ $ gem install active_record_tracer
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```ruby
39
+ require "active_record_tracer"
40
+ report = ActiveRecordTracer.report do
41
+ # run your code here
42
+ end
43
+
44
+ report.pretty_print
45
+ ```
46
+
47
+ Or, you can use the `.start`/`.stop` API as well:
48
+
49
+ ```ruby
50
+ require "active_record_tracer"
51
+
52
+ ActiveRecordTracer.start
53
+
54
+ # run your code
55
+
56
+ report = ActiveRecordTracer.stop
57
+ report.pretty_print
58
+ ```
59
+
60
+ **NOTE**: `.start`/`.stop` can only be run once per report, and `.stop` will
61
+ be the only time you can retrieve the report using this API.
62
+
63
+ ### Tracing tests
64
+
65
+ To trace tests, use the folloding somewhere in the `rails_helper.rb`/`test_helper.rb`:
66
+
67
+ ```ruby
68
+ ActiveRecordTracer.start
69
+ at_exit do
70
+ report = ActiveRecordTracer.stop
71
+ report.pretty_print(to_file: "tmp/active_record_tracer-tests.txt")
72
+ end
73
+ ```
74
+
75
+ ## Options
76
+
77
+ ### `report`
78
+
79
+ The `report` method can take a few options:
80
+
81
+ * `top`: maximum number of entries to display in a report (default is `50`)
82
+ * `backtrace_lines`: maximum number of backtrace lines to include in the report (default is `5`)
83
+ * `ignore_cached_queries`: whether to ignore cached queries (default is `false`)
84
+ * `ignore_schema_queries`: whether to ignore schema queries (default is `true`)
85
+
86
+ Check out `Reporter#new` for more details.
87
+
88
+ ### `pretty_print`
89
+
90
+ The `pretty_print` method can take a few options:
91
+
92
+ * `to_file`: a path to your log file - can be given a String
93
+ * `detailed_report`: whether to include detailed information - can be given a Boolean
94
+
95
+ Check out `Report#pretty_print` for more details.
96
+
97
+ ## Example output
98
+
99
+ ```
100
+ Total runtime: 181.36s
101
+ Total SQL queries: 8936
102
+ Total loaded records: 2648
103
+
104
+ Top SQL queries
105
+ -----------------------------------
106
+ 857 SAVEPOINT active_record_1
107
+
108
+ 856 RELEASE SAVEPOINT active_record_1
109
+
110
+ 382 SELECT "user_roles".* FROM "user_roles" WHERE "user_roles"."id" = $1 LIMIT $2
111
+
112
+ 362 SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2
113
+
114
+ 301 INSERT INTO "accounts" ("username", "domain", "private_key") VALUES ($1, $2, $3) RETURNING "id"
115
+
116
+ 219 SELECT "settings".* FROM "settings" WHERE "settings"."thing_type" IS NULL AND "settings"."thing_id" IS NULL AND "settings"."var" = $1 LIMIT $2
117
+
118
+ 217 INSERT INTO "conversations" ("uri", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"
119
+
120
+ 201 SELECT "statuses".* FROM "statuses" WHERE "statuses"."deleted_at" IS NULL AND "statuses"."id" = $1 ORDER BY "statuses"."id" DESC LIMIT $2
121
+
122
+ 175 BEGIN
123
+
124
+ 174 ROLLBACK
125
+
126
+ 169 SELECT "account_stats".* = $1 LIMIT $2
127
+
128
+ 158 SELECT 1 AS one FROM "instances" WHERE "instances"."domain" = $1 LIMIT $2
129
+
130
+ 155 SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2
131
+
132
+ 152 SELECT "domain_blocks".* FROM "domain_blocks" WHERE "domain_blocks"."domain" IN ($1, $2) ORDER BY CHAR_LENGTH(domain) DESC LIMIT $3
133
+ ...
134
+
135
+ SQL queries by location
136
+ -----------------------------------
137
+ 586 app/validators/unique_username_validator.rb:12
138
+ 391 app/models/user_role.rb:112
139
+ 314 app/models/concerns/account/counters.rb:54
140
+ 253 app/models/concerns/account/interactions.rb:116
141
+ 217 app/models/setting.rb:80
142
+ 215 app/models/concerns/status/safe_reblog_insert.rb:19
143
+ 168 app/models/concerns/account/counters.rb:48
144
+ 165 app/models/domain_block.rb:73
145
+ 158 app/models/concerns/domain_materializable.rb:13
146
+ 140 app/models/email_domain_block.rb:61
147
+ 137 app/models/concerns/database_view_record.rb:8
148
+ 123 app/lib/activitypub/activity/create.rb:86
149
+ 122 app/lib/activitypub/tag_manager.rb:185
150
+ 120 app/models/status.rb:400
151
+ 110 app/models/account.rb:375
152
+ 98 app/models/concerns/account/finder_concern.rb:32
153
+ 98 app/models/concerns/account/finder_concern.rb:16
154
+ 87 app/models/status.rb:377
155
+ 78 app/models/status.rb:289
156
+ 74 app/models/account.rb:150
157
+ 68 app/models/follow_request.rb:38
158
+ 64 app/services/activitypub/fetch_featured_collection_service.rb:76
159
+ 63 app/services/activitypub/process_status_update_service.rb:163
160
+ 63 app/models/account.rb:265
161
+ 62 app/models/status.rb:371
162
+ ...
163
+
164
+ SQL queries by file
165
+ -----------------------------------
166
+ 586 app/validators/unique_username_validator.rb
167
+ 563 app/models/concerns/account/counters.rb
168
+ 495 app/models/status.rb
169
+ 392 app/models/user_role.rb
170
+ 376 app/models/concerns/account/interactions.rb
171
+ 340 app/models/account.rb
172
+ 337 app/services/activitypub/process_status_update_service.rb
173
+ 241 app/models/setting.rb
174
+ 217 app/models/concerns/status/safe_reblog_insert.rb
175
+ 213 app/lib/activitypub/activity/create.rb
176
+ 196 app/models/concerns/account/finder_concern.rb
177
+ 166 app/services/fan_out_on_write_service.rb
178
+ 165 app/models/domain_block.rb
179
+ 158 app/models/concerns/domain_materializable.rb
180
+ 155 app/models/email_domain_block.rb
181
+ 137 app/models/concerns/database_view_record.rb
182
+ 134 app/lib/activitypub/tag_manager.rb
183
+ 107 app/models/follow_request.rb
184
+ 106 app/lib/feed_manager.rb
185
+ ...
186
+
187
+ SQL queries by backtrace
188
+ -----------------------------------
189
+ 539 app/validators/unique_username_validator.rb:12:in `validate'
190
+
191
+ 306 app/models/user_role.rb:112:in `everyone'
192
+ app/models/user.rb:160:in `role'
193
+ app/models/user.rb:486:in `sanitize_role'
194
+
195
+ 168 app/models/concerns/account/interactions.rb:116:in `follow!'
196
+
197
+ 140 app/models/email_domain_block.rb:61:in `blocking?'
198
+ app/models/email_domain_block.rb:49:in `match?'
199
+ app/models/email_domain_block.rb:94:in `requires_approval?'
200
+ app/models/user.rb:470:in `sign_up_email_requires_approval?'
201
+ app/models/user.rb:416:in `set_approved'
202
+
203
+ 137 app/models/concerns/domain_materializable.rb:13:in `refresh_instances_view'
204
+
205
+ 124 app/models/concerns/account/counters.rb:54:in `updated_account_stat'
206
+ app/models/concerns/account/counters.rb:38:in `update_count!'
207
+ app/models/concerns/account/counters.rb:24:in `increment_count!'
208
+ app/models/status.rb:455:in `increment_counter_caches'
209
+ ...
210
+
211
+ Loaded records by model
212
+ -----------------------------------
213
+ 533 Account
214
+ 390 UserRole
215
+ 287 Status
216
+ 101 AccountStat
217
+ 70 Setting
218
+ 64 User
219
+ 29 Follow
220
+ 24 AccountDeletionRequest
221
+ 21 MediaAttachment
222
+ 20 Conversation
223
+ 17 FollowRequest
224
+ 17 Tag
225
+ ...
226
+
227
+ Loaded records by location
228
+ -----------------------------------
229
+ 381 app/models/user_role.rb:112
230
+ 98 app/models/concerns/account/finder_concern.rb:16
231
+ 65 app/models/concerns/account/finder_concern.rb:32
232
+ 64 app/models/setting.rb:80
233
+ 61 app/models/concerns/account/counters.rb:48
234
+ 53 app/lib/activitypub/tag_manager.rb:185
235
+ 46 app/models/concerns/rate_limitable.rb:23
236
+ 45 app/workers/distribution_worker.rb:10
237
+ 45 app/services/fan_out_on_write_service.rb:14
238
+ ...
239
+
240
+ Loaded records by file
241
+ -----------------------------------
242
+ 385 app/models/user_role.rb
243
+ 163 app/models/concerns/account/finder_concern.rb
244
+ 97 app/models/concerns/account/counters.rb
245
+ 70 app/models/setting.rb
246
+ 68 app/models/account.rb
247
+ 57 app/services/fan_out_on_write_service.rb
248
+ 53 app/lib/activitypub/tag_manager.rb
249
+ ...
250
+
251
+ Loaded records by backtrace
252
+ -----------------------------------
253
+ 298 app/models/user_role.rb:112:in `everyone'
254
+ app/models/user.rb:160:in `role'
255
+ app/models/user.rb:486:in `sanitize_role'
256
+
257
+ 61 app/models/setting.rb:80:in `block in []'
258
+ app/models/setting.rb:79:in `[]'
259
+ app/models/setting.rb:65:in `method_missing'
260
+ app/models/user.rb:474:in `open_registrations?'
261
+ app/models/user.rb:419:in `set_approved'
262
+
263
+ 45 app/services/fan_out_on_write_service.rb:14:in `call'
264
+ app/workers/distribution_worker.rb:10:in `block in perform'
265
+ app/models/concerns/lockable.rb:12:in `block (2 levels) in with_redis_lock'
266
+ app/models/concerns/lockable.rb:10:in `block in with_redis_lock'
267
+ app/lib/redis_configuration.rb:10:in `with'
268
+ ...
269
+ ```
270
+
271
+ ## Development
272
+
273
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake test` to run the tests. This project uses multiple Gemfiles to test against multiple versions of Active Record; you can run the tests against the specific version with `BUNDLE_GEMFILE=gemfiles/activerecord_70.gemfile bundle exec rake test`.
274
+
275
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
276
+
277
+ ## Contributing
278
+
279
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fatkodima/active_record_tracer.
280
+
281
+ ## License
282
+
283
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordTracer
4
+ class Report
5
+ # @private
6
+ def initialize(total_runtime, queries_counts, backtrace_queries, loaded_records, loaded_records_backtraces, top:)
7
+ @total_runtime = total_runtime
8
+ @queries_counts = queries_counts
9
+ @backtrace_queries = backtrace_queries
10
+ @loaded_records = loaded_records
11
+ @loaded_records_backtraces = loaded_records_backtraces
12
+ @top = top
13
+ end
14
+
15
+ # Output the results of the report
16
+ #
17
+ # @param to_file [String] a path to your log file
18
+ # @param detailed_report [Boolean] should the report include detailed information
19
+ #
20
+ def pretty_print(io = $stdout, to_file: nil, detailed_report: true)
21
+ # Handle the special case that Ruby PrettyPrint expects `pretty_print`
22
+ # to be a customized pretty printing function for a class.
23
+ return io.pp_object(self) if defined?(PP) && io.is_a?(PP)
24
+
25
+ io = File.open(to_file, "w") if to_file
26
+
27
+ # Summary stats.
28
+ io.puts("Total runtime: #{@total_runtime.round(2)}s")
29
+ io.puts("Total SQL queries: #{@queries_counts.values.sum}")
30
+ io.puts("Total loaded records: #{total_loaded_records}")
31
+ io.puts
32
+
33
+ if detailed_report != false
34
+ # Queries stats.
35
+ print_queries_by_count(io)
36
+ if @backtrace_queries.any?
37
+ print_queries_by_location(io)
38
+ print_queries_by_file(io)
39
+ print_queries_by_backtrace(io)
40
+ end
41
+
42
+ # Loaded records stats.
43
+ print_records_by_model(io)
44
+ if @loaded_records_backtraces.any?
45
+ print_records_by_location(io)
46
+ print_records_by_file(io)
47
+ print_records_by_backtrace(io)
48
+ end
49
+ end
50
+
51
+ nil
52
+ ensure
53
+ io.close if io.is_a?(File)
54
+ end
55
+
56
+ private
57
+ def total_loaded_records
58
+ @loaded_records.values.sum
59
+ end
60
+
61
+ def print_queries_by_count(io)
62
+ print_title(io, "Top SQL queries")
63
+
64
+ top_query_counts = @queries_counts.sort_by { |_k, v| -v }.take(@top)
65
+
66
+ top_query_counts.each do |query, count|
67
+ query.lines.each_with_index do |line, index|
68
+ if index == 0
69
+ io.puts(count.to_s.rjust(8) + " #{line}")
70
+ else
71
+ io.puts(" #{line}")
72
+ end
73
+ end
74
+ io.puts
75
+ end
76
+ end
77
+
78
+ def print_queries_by_location(io)
79
+ print_title(io, "SQL queries by location")
80
+
81
+ location_queries = Hash.new(0)
82
+ @backtrace_queries.each do |backtrace, queries|
83
+ location = extract_location(backtrace[0]) || "(internal)"
84
+ location_queries[location] += queries.size
85
+ end
86
+
87
+ top_location_queries = location_queries.sort_by { |_k, v| -v }.take(@top)
88
+
89
+ top_location_queries.each do |location, queries_count|
90
+ io.puts(queries_count.to_s.rjust(8) + " #{location}")
91
+ end
92
+ io.puts
93
+ end
94
+
95
+ def print_queries_by_file(io)
96
+ print_title(io, "SQL queries by file")
97
+
98
+ file_queries = Hash.new(0)
99
+ @backtrace_queries.each do |backtrace, queries|
100
+ file = extract_file(backtrace[0]) || "(internal)"
101
+ file_queries[file] += queries.size
102
+ end
103
+
104
+ top_file_queries = file_queries.sort_by { |_k, v| -v }.take(@top)
105
+
106
+ top_file_queries.each do |file, queries_count|
107
+ io.puts(queries_count.to_s.rjust(8) + " #{file}")
108
+ end
109
+ io.puts
110
+ end
111
+
112
+ def print_queries_by_backtrace(io)
113
+ print_title(io, "SQL queries by backtrace")
114
+
115
+ backtrace_queries = @backtrace_queries.sort_by { |_k, v| -v.size }.take(@top)
116
+ backtrace_queries.each do |backtrace, queries|
117
+ # Backtrace can be empty if it does not contain
118
+ # custom user code and was cleaned.
119
+ next if backtrace.empty?
120
+
121
+ print_backtrace(io, queries.size, backtrace)
122
+ io.puts
123
+ end
124
+ end
125
+
126
+ def print_records_by_model(io)
127
+ print_title(io, "Loaded records by model")
128
+
129
+ model_records = @loaded_records.sort_by { |_k, v| -v }.take(@top)
130
+ model_records.each do |model, records_count|
131
+ io.puts(records_count.to_s.rjust(8) + " #{model}")
132
+ end
133
+ io.puts
134
+ end
135
+
136
+ def print_records_by_location(io)
137
+ print_title(io, "Loaded records by location")
138
+
139
+ location_records = Hash.new(0)
140
+ @loaded_records_backtraces.each do |backtrace, records_count|
141
+ location = extract_location(backtrace[0]) || "(internal)"
142
+ location_records[location] += records_count
143
+ end
144
+
145
+ top_location_records = location_records.sort_by { |_k, v| -v }.take(@top)
146
+
147
+ top_location_records.each do |location, records_count|
148
+ io.puts(records_count.to_s.rjust(8) + " #{location}")
149
+ end
150
+ io.puts
151
+ end
152
+
153
+ def print_records_by_file(io)
154
+ print_title(io, "Loaded records by file")
155
+
156
+ file_records = Hash.new(0)
157
+ @loaded_records_backtraces.each do |backtrace, records_count|
158
+ file = extract_file(backtrace[0]) || "(internal)"
159
+ file_records[file] += records_count
160
+ end
161
+
162
+ top_file_records = file_records.sort_by { |_k, v| -v }.take(@top)
163
+
164
+ top_file_records.each do |file, records_count|
165
+ io.puts(records_count.to_s.rjust(8) + " #{file}")
166
+ end
167
+ io.puts
168
+ end
169
+
170
+ def print_records_by_backtrace(io)
171
+ print_title(io, "Loaded records by backtrace")
172
+
173
+ backtrace_records = @loaded_records_backtraces.sort_by { |_k, v| -v }.take(@top)
174
+ backtrace_records.each do |backtrace, records_count|
175
+ # Backtrace can be empty if it does not contain
176
+ # custom user code and was cleaned.
177
+ next if backtrace.empty?
178
+
179
+ print_backtrace(io, records_count, backtrace)
180
+ io.puts
181
+ end
182
+ end
183
+
184
+ def print_title(io, title)
185
+ io.puts(title)
186
+ io.puts("-----------------------------------")
187
+ end
188
+
189
+ def print_backtrace(io, prefix, backtrace)
190
+ backtrace.each_with_index do |line, index|
191
+ if index == 0
192
+ io.puts(prefix.to_s.rjust(8) + " #{line}")
193
+ else
194
+ io.puts(" #{line}")
195
+ end
196
+ end
197
+ end
198
+
199
+ def extract_location(backtrace_line)
200
+ /\A(?<location>.+:\d+):in.+/ =~ backtrace_line
201
+ location || backtrace_line
202
+ end
203
+
204
+ def extract_file(backtrace_line)
205
+ /\A(?<file>.+):\d+:in.+/ =~ backtrace_line
206
+ file || backtrace_line
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordTracer
4
+ # @private
5
+ class Reporter
6
+ class << self
7
+ attr_accessor :current
8
+ end
9
+
10
+ def initialize(top: 50, backtrace_lines: 5, ignore_cached_queries: false, ignore_schema_queries: true)
11
+ @top = top
12
+ @backtrace_lines = backtrace_lines
13
+ @ignore_cached_queries = ignore_cached_queries
14
+ @ignore_schema_queries = ignore_schema_queries
15
+
16
+ @total_runtime = 0.0
17
+ @queries_counts = Hash.new(0)
18
+ @backtrace_queries = Hash.new { |h, k| h[k] = [] }
19
+ @loaded_records = Hash.new(0)
20
+ @loaded_records_backtraces = Hash.new(0)
21
+ @subscriber1 = nil
22
+ @subscriber2 = nil
23
+
24
+ @backtraces_cache = {}
25
+ end
26
+
27
+ def start
28
+ @subscriber1 = ActiveSupport::Notifications.monotonic_subscribe("sql.active_record") do |_name, start, finish, _id, payload|
29
+ next if payload[:cached] && @ignore_cached_queries
30
+ next if payload[:name] == "SCHEMA" && @ignore_schema_queries
31
+
32
+ runtime = finish - start
33
+ @total_runtime += runtime
34
+
35
+ sql = payload[:sql].strip
36
+ @queries_counts[sql] += 1
37
+
38
+ if @backtrace_lines && @backtrace_lines > 0
39
+ backtrace = query_backtrace(caller(1), @backtrace_lines)
40
+ @backtrace_queries[backtrace] << sql
41
+ end
42
+ end
43
+
44
+ @subscriber2 = ActiveSupport::Notifications.subscribe("instantiation.active_record") do |*, payload|
45
+ record_count = payload[:record_count]
46
+
47
+ # Active Record erroneously emits notifications even when
48
+ # there are no records https://github.com/rails/rails/pull/52272.
49
+ if record_count > 0
50
+ @loaded_records[payload[:class_name]] += record_count
51
+
52
+ if @backtrace_lines && @backtrace_lines > 0
53
+ backtrace = query_backtrace(caller(1), @backtrace_lines)
54
+ @loaded_records_backtraces[backtrace] += record_count
55
+ end
56
+ end
57
+ end
58
+ true
59
+ end
60
+
61
+ def stop
62
+ ActiveSupport::Notifications.unsubscribe(@subscriber1)
63
+ ActiveSupport::Notifications.unsubscribe(@subscriber2)
64
+
65
+ Report.new(@total_runtime, @queries_counts, @backtrace_queries, @loaded_records,
66
+ @loaded_records_backtraces, top: @top)
67
+ end
68
+
69
+ private
70
+ def query_backtrace(backtrace, limit)
71
+ @backtraces_cache[backtrace] ||=
72
+ if limit && limit > 0
73
+ ActiveRecordTracer.backtrace_cleaner.call(backtrace.lazy).take(limit).to_a
74
+ else
75
+ ActiveRecordTracer.backtrace_cleaner.call(backtrace)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordTracer
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record_tracer/report"
4
+ require_relative "active_record_tracer/reporter"
5
+ require_relative "active_record_tracer/version"
6
+
7
+ module ActiveRecordTracer
8
+ # Helper for running against block and generating a report.
9
+ #
10
+ # @option options [Integer] :top (50) max number of entries to output
11
+ # @option options [Integer, nil] :backtrace_lines (5) max number of backtrace lines
12
+ # to print. Generating backtrace-related reports is not always needed and can be
13
+ # costly - set it to '0' or 'nil' to skip it.
14
+ # @option options [Boolean] :ignore_cached_queries (false) whether to ignore cached queries
15
+ # @option options [Boolean] :ignore_schema_queries (true) whether to ignore schema queries
16
+ #
17
+ # @return [ActiveRecordTracer::Report]
18
+ #
19
+ # @example
20
+ # report = ActiveRecordTracer.report(top: 20) do
21
+ # # ... generate SQL queries ...
22
+ # end
23
+ # report.pretty_print
24
+ #
25
+ def self.report(**options)
26
+ start(**options)
27
+ yield
28
+ stop
29
+ ensure
30
+ stop
31
+ end
32
+
33
+ # Start collecting data for the report.
34
+ #
35
+ # @see .report
36
+ # @return [void]
37
+ #
38
+ def self.start(**options)
39
+ unless Reporter.current
40
+ Reporter.current = Reporter.new(**options)
41
+ Reporter.current.start
42
+ end
43
+ end
44
+
45
+ # Stop collecting data for the report.
46
+ #
47
+ # @return [ActiveRecordTracer::Report]
48
+ #
49
+ def self.stop
50
+ Reporter.current&.stop
51
+ ensure
52
+ Reporter.current = nil
53
+ end
54
+
55
+ # Backtrace cleaner to use when cleaning backtraces.
56
+ #
57
+ # It will use 'Rails.backtrace_cleaner' by default if it is available.
58
+ #
59
+ # @return [Proc]
60
+ #
61
+ def self.backtrace_cleaner
62
+ @backtrace_cleaner ||=
63
+ if defined?(Rails.backtrace_cleaner)
64
+ ->(backtrace) { Rails.backtrace_cleaner.clean(backtrace) }
65
+ else
66
+ ->(backtrace) { backtrace }
67
+ end
68
+ end
69
+
70
+ # Set backtrace cleaner to be used when cleaning backtraces.
71
+ #
72
+ # @param value [Proc, ActiveSupport::BacktraceCleaner]
73
+ #
74
+ def self.backtrace_cleaner=(value)
75
+ @backtrace_cleaner =
76
+ if value.is_a?(Proc)
77
+ value
78
+ else
79
+ ->(backtrace) { value.clean(backtrace) }
80
+ end
81
+ end
82
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_tracer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - fatkodima
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ description:
28
+ email:
29
+ - fatkodima123@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE.txt
36
+ - README.md
37
+ - lib/active_record_tracer.rb
38
+ - lib/active_record_tracer/report.rb
39
+ - lib/active_record_tracer/reporter.rb
40
+ - lib/active_record_tracer/version.rb
41
+ homepage: https://github.com/fatkodima/active_record_tracer
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/fatkodima/active_record_tracer
46
+ source_code_uri: https://github.com/fatkodima/active_record_tracer
47
+ changelog_uri: https://github.com/fatkodima/active_record_tracer/blob/master/CHANGELOG.md
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.1.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.4.19
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: A tracer for Active Record queries
67
+ test_files: []