active_record_tracer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []