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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/lib/active_record_tracer/report.rb +209 -0
- data/lib/active_record_tracer/reporter.rb +79 -0
- data/lib/active_record_tracer/version.rb +5 -0
- data/lib/active_record_tracer.rb +82 -0
- metadata +67 -0
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
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
|
+
[](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,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: []
|