active_record_query_counter 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +73 -32
- data/VERSION +1 -1
- data/active_record_query_counter.gemspec +9 -3
- data/lib/active_record_query_counter/connection_adapter_extension.rb +39 -13
- data/lib/active_record_query_counter/counter.rb +2 -1
- data/lib/active_record_query_counter/transaction_manager_extension.rb +1 -0
- data/lib/active_record_query_counter.rb +83 -7
- metadata +12 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89a919244edbce9bf36bdaa232078b04470f1bacc952469eab4a4fffc2416e08
|
|
4
|
+
data.tar.gz: 97a66d47fda85ad15b832da3e390f8cc035a374983c64f53e4d673094b54d8b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5c657cf312deb55e21063b5d08f824cd762e42050704b06fc0610e00f938aacac8fc37050c60d293452521841ca614430cb50cd3bf10d3bca794df9d0dee6d0
|
|
7
|
+
data.tar.gz: d81cb599eea68c911fad417a902848f1b1f2d0f29f4d095b70d5af8ef2f6366dac0e2b2de0fe534ed51fcbffb3d696147edfc53e64b549862a4dfe78a156a54d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 3.0.0
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Query time now excludes GC pause time and Ruby thread CPU time so that it more closely reflects the time actually spent waiting on the database. This is what is now reported as the event duration in the `query_time` and `row_count` notifications.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `:elapsed_time` (the raw wall clock time), `:gc_time`, and `:cpu_time` (all in milliseconds) to the `query_time` and `row_count` notification payloads.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- Dropped support for Ruby versions older than 3.1 (required for `GC.total_time`).
|
|
20
|
+
|
|
21
|
+
## 2.3.0
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Added count of rollbacks from transactions
|
|
26
|
+
|
|
7
27
|
## 2.2.1
|
|
8
28
|
|
|
9
29
|
### Changed
|
data/README.md
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
# ActiveRecordQueryCounter
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bdurand/active_record_query_counter/actions/workflows/continuous_integration.yml)
|
|
4
|
-
[](https://github.com/bdurand/active_record_query_counter/actions/workflows/regression_test.yml)
|
|
5
4
|
[](https://github.com/testdouble/standard)
|
|
6
5
|
[](https://badge.fury.io/rb/active_record_query_counter)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
**ActiveRecordQueryCounter** is a ruby gem that provides detailed insights into how your code interacts with the database by hooking into ActiveRecord.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
It measures database usage within a block of code, including:
|
|
11
10
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
11
|
+
- The number of queries executed
|
|
12
|
+
- The number of rows returned
|
|
13
|
+
- The total time spent on queries
|
|
14
|
+
- The number of transactions used
|
|
15
|
+
- The total time spent inside transactions
|
|
16
|
+
- The number of transactions that were rolled back
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
This gem is designed to help you:
|
|
19
|
+
|
|
20
|
+
- Identify "hot spots" in your code that generate excessive or slow queries.
|
|
21
|
+
- Spot queries returning unexpectedly large result sets.
|
|
22
|
+
- Detect areas where transactions are underutilized, especially when performing multiple database updates.
|
|
19
23
|
|
|
20
24
|
## Usage
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
### Enabling The Gem
|
|
27
|
+
|
|
28
|
+
To use **ActiveRecordQueryCounter**, you first need to enable it on your database connection adapter. Add the following to an initializer:
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
For **PostgreSQL**:
|
|
25
31
|
|
|
26
32
|
```ruby
|
|
27
33
|
ActiveRecordQueryCounter.enable!(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
|
28
34
|
```
|
|
29
35
|
|
|
30
|
-
MySQL
|
|
36
|
+
For **MySQL**:
|
|
31
37
|
|
|
32
38
|
```ruby
|
|
33
39
|
ActiveRecordQueryCounter.enable!(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
### Counting Queries
|
|
43
|
+
|
|
44
|
+
To measure database activity, wrap the code you want to monitor inside a `count_queries` block:
|
|
37
45
|
|
|
38
46
|
```ruby
|
|
39
47
|
ActiveRecordQueryCounter.count_queries do
|
|
@@ -43,18 +51,34 @@ ActiveRecordQueryCounter.count_queries do
|
|
|
43
51
|
puts "Query Time: #{ActiveRecordQueryCounter.query_time}"
|
|
44
52
|
puts "Transactions: #{ActiveRecordQueryCounter.transaction_count}"
|
|
45
53
|
puts "Transaction Time: #{ActiveRecordQueryCounter.transaction_time}"
|
|
54
|
+
puts "Rollbacks: #{ActiveRecordQueryCounter.rollback_count}"
|
|
46
55
|
end
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
### Query Time
|
|
59
|
+
|
|
60
|
+
The query time (`ActiveRecordQueryCounter.query_time` and the duration reported by the notifications) is **not** the raw wall clock time a query took. The wall clock time includes time the thread was not actually waiting on the database, such as GC pauses (which can be triggered by other threads and stop the world) and the Ruby CPU work of building the result objects. On a busy, multi-threaded server these can add up to seconds, making a trivial query look pathologically slow.
|
|
61
|
+
|
|
62
|
+
To report the time actually spent waiting on the database as closely as possible, the GC time and thread CPU time that elapsed while the query ran are subtracted from the wall clock time. The raw wall clock time is still available as `:elapsed_time` in the notification payloads.
|
|
63
|
+
|
|
64
|
+
> [!NOTE]
|
|
65
|
+
> Measuring GC time requires Ruby's GC total time measurement, which is enabled by default (`GC.measure_total_time`). Thread CPU time is measured via `Process::CLOCK_THREAD_CPUTIME_ID`; on platforms that do not provide it, CPU time is treated as zero.
|
|
66
|
+
|
|
67
|
+
### Middleware Integration
|
|
68
|
+
|
|
69
|
+
For **Rails** and **Sidekiq**, middleware is included to enable query counting in web requests and workers.
|
|
70
|
+
|
|
71
|
+
Add the following to an initializer:
|
|
50
72
|
|
|
51
73
|
```ruby
|
|
52
74
|
ActiveSupport.on_load(:active_record) do
|
|
53
75
|
ActiveRecordQueryCounter.enable!(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
|
54
76
|
end
|
|
55
77
|
|
|
78
|
+
# Enable Rack Middleware
|
|
56
79
|
Rails.application.config.middleware.use(ActiveRecordQueryCounter::RackMiddleware)
|
|
57
80
|
|
|
81
|
+
# Enable Sidekiq Middleware
|
|
58
82
|
Sidekiq.configure_server do |config|
|
|
59
83
|
config.server_middleware do |chain|
|
|
60
84
|
chain.add ActiveRecordQueryCounter::SidekiqMiddleware
|
|
@@ -62,13 +86,15 @@ Sidekiq.configure_server do |config|
|
|
|
62
86
|
end
|
|
63
87
|
```
|
|
64
88
|
|
|
65
|
-
|
|
89
|
+
### Disabling Query Counting
|
|
90
|
+
|
|
91
|
+
You can temporarily disable query counting within a block using `disable`:
|
|
66
92
|
|
|
67
93
|
```ruby
|
|
68
94
|
ActiveRecordQueryCounter.count_queries do
|
|
69
95
|
do_something
|
|
70
96
|
ActiveRecordQueryCounter.disable do
|
|
71
|
-
# Queries will not be counted
|
|
97
|
+
# Queries in this block will not be counted.
|
|
72
98
|
do_something_else
|
|
73
99
|
end
|
|
74
100
|
end
|
|
@@ -76,43 +102,54 @@ end
|
|
|
76
102
|
|
|
77
103
|
### Notifications
|
|
78
104
|
|
|
79
|
-
|
|
105
|
+
**ActiveRecordQueryCounter** supports ActiveSupport notifications when certain query thresholds are exceeded.
|
|
106
|
+
|
|
107
|
+
#### Available Notifications
|
|
108
|
+
|
|
109
|
+
##### 1. active_record_query_counter.query_time notification
|
|
80
110
|
|
|
81
|
-
|
|
111
|
+
Triggered when a query exceeds the query_time threshold with the payload:
|
|
82
112
|
|
|
83
|
-
This notification is triggered when a query takes longer than the `query_time` threshold. The payload contains the following keys:
|
|
84
113
|
|
|
85
114
|
- `:sql` - The SQL statement that was executed.
|
|
86
115
|
- `:binds` - The bind parameters that were used.
|
|
87
116
|
- `:row_count` - The number of rows returned.
|
|
88
117
|
- `:trace` - The stack trace of where the query was executed.
|
|
118
|
+
- `:elapsed_time` - The raw wall clock time the query took (in milliseconds).
|
|
119
|
+
- `:gc_time` - The GC time that elapsed while the query ran (in milliseconds).
|
|
120
|
+
- `:cpu_time` - The thread CPU time spent while the query ran (in milliseconds).
|
|
89
121
|
|
|
90
|
-
|
|
122
|
+
The duration of the notification event is the query time: the wall clock time with the GC time and CPU time subtracted out (see [Query Time](#query-time)). The raw wall clock time is still available as `:elapsed_time`.
|
|
91
123
|
|
|
92
|
-
|
|
124
|
+
##### 2. active_record_query_counter.row_count notification
|
|
125
|
+
|
|
126
|
+
Triggered when a query exceeds the row_count threshold with the payload:
|
|
93
127
|
|
|
94
128
|
- `:sql` - The SQL statement that was executed.
|
|
95
129
|
- `:binds` - The bind parameters that were used.
|
|
96
130
|
- `:row_count` - The number of rows returned.
|
|
97
131
|
- `:trace` - The stack trace of where the query was executed.
|
|
132
|
+
- `:elapsed_time` - The raw wall clock time the query took (in milliseconds).
|
|
133
|
+
- `:gc_time` - The GC time that elapsed while the query ran (in milliseconds).
|
|
134
|
+
- `:cpu_time` - The thread CPU time spent while the query ran (in milliseconds).
|
|
98
135
|
|
|
99
|
-
|
|
136
|
+
##### 3. active_record_query_counter.transaction_time notification
|
|
100
137
|
|
|
101
|
-
|
|
138
|
+
Triggered when a transaction exceeds the transaction_time threshold with the payload:
|
|
102
139
|
|
|
103
140
|
- `:trace` - The stack trace of where the transaction was completed.
|
|
104
141
|
|
|
105
|
-
|
|
142
|
+
##### 4. active_record_query_counter.transaction_count notification
|
|
106
143
|
|
|
107
|
-
|
|
144
|
+
Triggered when transactions exceed the transaction_count threshold with the payload:
|
|
108
145
|
|
|
109
146
|
- `:transactions` - An array of `ActiveRecordQueryCounter::TransactionInfo` objects.
|
|
110
147
|
|
|
111
148
|
The duration of the notification event is the time between when the first transaction was started and the last transaction was completed.
|
|
112
149
|
|
|
113
|
-
#### Thresholds
|
|
150
|
+
#### Setting Thresholds
|
|
114
151
|
|
|
115
|
-
|
|
152
|
+
Thresholds can be configured **globally** in an initializer:
|
|
116
153
|
|
|
117
154
|
```ruby
|
|
118
155
|
ActiveRecordQueryCounter.default_thresholds.set(
|
|
@@ -123,7 +160,7 @@ ActiveRecordQueryCounter.default_thresholds.set(
|
|
|
123
160
|
)
|
|
124
161
|
```
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
Or locally within a block:
|
|
127
164
|
|
|
128
165
|
```ruby
|
|
129
166
|
ActiveRecordQueryCounter.count_queries do
|
|
@@ -136,7 +173,8 @@ ActiveRecordQueryCounter.count_queries do
|
|
|
136
173
|
end
|
|
137
174
|
```
|
|
138
175
|
|
|
139
|
-
|
|
176
|
+
#### Sidekiq Worker Thresholds
|
|
177
|
+
Thresholds for individual Sidekiq workers can be set using `sidekiq_options`:
|
|
140
178
|
|
|
141
179
|
```ruby
|
|
142
180
|
class MyWorker
|
|
@@ -152,7 +190,6 @@ class MyWorker
|
|
|
152
190
|
}
|
|
153
191
|
}
|
|
154
192
|
)
|
|
155
|
-
# You can disable thresholds for the worker by setting `thresholds: false`.
|
|
156
193
|
|
|
157
194
|
def perform
|
|
158
195
|
do_something
|
|
@@ -160,7 +197,11 @@ class MyWorker
|
|
|
160
197
|
end
|
|
161
198
|
```
|
|
162
199
|
|
|
163
|
-
|
|
200
|
+
To disable thresholds for a worker, set `thresholds: false`.
|
|
201
|
+
|
|
202
|
+
#### Rack Middleware Thresholds
|
|
203
|
+
|
|
204
|
+
You can configure separate thresholds for the Rack middleware:
|
|
164
205
|
|
|
165
206
|
```ruby
|
|
166
207
|
Rails.application.config.middleware.use(ActiveRecordQueryCounter::RackMiddleware, thresholds: {
|
|
@@ -171,7 +212,7 @@ Rails.application.config.middleware.use(ActiveRecordQueryCounter::RackMiddleware
|
|
|
171
212
|
})
|
|
172
213
|
```
|
|
173
214
|
|
|
174
|
-
#### Example
|
|
215
|
+
#### Example: Subscribing to Notifications
|
|
175
216
|
|
|
176
217
|
```ruby
|
|
177
218
|
ActiveRecordQueryCounter.default_thresholds.query_time = 1.0
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.0
|
|
@@ -4,10 +4,16 @@ Gem::Specification.new do |spec|
|
|
|
4
4
|
spec.authors = ["Brian Durand"]
|
|
5
5
|
spec.email = ["bbdurand@gmail.com"]
|
|
6
6
|
|
|
7
|
-
spec.summary = "
|
|
7
|
+
spec.summary = "Provides detailed insights into how your code interacts with the database by hooking into ActiveRecord."
|
|
8
8
|
spec.homepage = "https://github.com/bdurand/active_record_query_counter"
|
|
9
9
|
spec.license = "MIT"
|
|
10
10
|
|
|
11
|
+
spec.metadata = {
|
|
12
|
+
"homepage_uri" => spec.homepage,
|
|
13
|
+
"source_code_uri" => spec.homepage,
|
|
14
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
# Specify which files should be added to the gem when it is released.
|
|
12
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
13
19
|
ignore_files = %w[
|
|
@@ -28,9 +34,9 @@ Gem::Specification.new do |spec|
|
|
|
28
34
|
|
|
29
35
|
spec.require_paths = ["lib"]
|
|
30
36
|
|
|
31
|
-
spec.add_dependency "activerecord", ">=
|
|
37
|
+
spec.add_dependency "activerecord", ">= 6.0"
|
|
32
38
|
|
|
33
39
|
spec.add_development_dependency "bundler"
|
|
34
40
|
|
|
35
|
-
spec.required_ruby_version = ">=
|
|
41
|
+
spec.required_ruby_version = ">= 3.1"
|
|
36
42
|
end
|
|
@@ -3,37 +3,63 @@
|
|
|
3
3
|
module ActiveRecordQueryCounter
|
|
4
4
|
# Module to prepend to the connection adapter to inject the counting behavior.
|
|
5
5
|
module ConnectionAdapterExtension
|
|
6
|
+
# Clock used to measure the CPU time consumed by the current thread while a query runs.
|
|
7
|
+
# It is not available on every platform (e.g. Windows), in which case CPU time is not
|
|
8
|
+
# measured and is treated as zero.
|
|
9
|
+
CPU_CLOCK_ID = (Process::CLOCK_THREAD_CPUTIME_ID if defined?(Process::CLOCK_THREAD_CPUTIME_ID))
|
|
10
|
+
|
|
6
11
|
class << self
|
|
7
12
|
def inject(connection_class)
|
|
8
13
|
# Rails 7.1+ uses internal_exec_query instead of exec_query.
|
|
9
|
-
mod = (connection_class.
|
|
14
|
+
mod = (connection_class.method_defined?(:internal_exec_query) ? InternalExecQuery : ExecQuery)
|
|
10
15
|
unless connection_class.include?(mod)
|
|
11
16
|
connection_class.prepend(mod)
|
|
12
17
|
end
|
|
13
18
|
end
|
|
14
|
-
end
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
# Measure a query by wrapping its execution. In addition to the wall clock time, the GC
|
|
21
|
+
# time and thread CPU time spent while the query runs are captured so that the time
|
|
22
|
+
# actually spent waiting on the database can be isolated from time lost to garbage
|
|
23
|
+
# collection and Ruby VM work.
|
|
24
|
+
#
|
|
25
|
+
# @param sql [String] the SQL statement being executed
|
|
26
|
+
# @param name [String, nil] the name of the query
|
|
27
|
+
# @param binds [Array] the bind parameters
|
|
28
|
+
# @yield executes the query and returns its result
|
|
29
|
+
# @return [Object] the result of the query
|
|
30
|
+
def measure_query(sql, name, binds)
|
|
31
|
+
gc_start = GC.total_time
|
|
32
|
+
cpu_start = current_cpu_time
|
|
18
33
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
-
result =
|
|
34
|
+
result = yield
|
|
20
35
|
if result.is_a?(ActiveRecord::Result)
|
|
21
36
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
-
|
|
37
|
+
cpu_time = current_cpu_time - cpu_start
|
|
38
|
+
gc_time = (GC.total_time - gc_start) / 1_000_000_000.0
|
|
39
|
+
ActiveRecordQueryCounter.add_query(sql, name, binds, result.length, start_time, end_time, gc_time, cpu_time)
|
|
23
40
|
end
|
|
24
41
|
result
|
|
25
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# The current thread CPU time in seconds, or 0.0 when the platform does not support it.
|
|
47
|
+
#
|
|
48
|
+
# @return [Float]
|
|
49
|
+
def current_cpu_time
|
|
50
|
+
CPU_CLOCK_ID ? Process.clock_gettime(CPU_CLOCK_ID) : 0.0
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module ExecQuery
|
|
55
|
+
def exec_query(sql, name = nil, binds = [], *args, **kwargs)
|
|
56
|
+
ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
|
|
57
|
+
end
|
|
26
58
|
end
|
|
27
59
|
|
|
28
60
|
module InternalExecQuery
|
|
29
61
|
def internal_exec_query(sql, name = nil, binds = [], *args, **kwargs)
|
|
30
|
-
|
|
31
|
-
result = super
|
|
32
|
-
if result.is_a?(ActiveRecord::Result)
|
|
33
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
34
|
-
ActiveRecordQueryCounter.add_query(sql, name, binds, result.length, start_time, end_time)
|
|
35
|
-
end
|
|
36
|
-
result
|
|
62
|
+
ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
|
|
37
63
|
end
|
|
38
64
|
end
|
|
39
65
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ActiveRecordQueryCounter
|
|
4
4
|
# Data structure for storing query information encountered within a block.
|
|
5
5
|
class Counter
|
|
6
|
-
attr_accessor :query_count, :row_count, :query_time, :cached_query_count
|
|
6
|
+
attr_accessor :query_count, :row_count, :query_time, :cached_query_count, :rollback_count
|
|
7
7
|
attr_reader :thresholds
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
@@ -13,6 +13,7 @@ module ActiveRecordQueryCounter
|
|
|
13
13
|
@cached_query_count = 0
|
|
14
14
|
@transactions_hash = {}
|
|
15
15
|
@thresholds = ActiveRecordQueryCounter.default_thresholds.dup
|
|
16
|
+
@rollback_count = 0
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Return the percentage of queries that used the query cache instead of going to the database.
|
|
@@ -31,6 +31,7 @@ module ActiveRecordQueryCounter
|
|
|
31
31
|
if @active_record_query_counter_transaction_start_time && open_transactions == 1
|
|
32
32
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
33
|
ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
|
|
34
|
+
ActiveRecordQueryCounter.increment_rollbacks
|
|
34
35
|
@active_record_query_counter_transaction_start_time = nil
|
|
35
36
|
end
|
|
36
37
|
super
|
|
@@ -65,32 +65,51 @@ module ActiveRecordQueryCounter
|
|
|
65
65
|
|
|
66
66
|
# Increment the query counters.
|
|
67
67
|
#
|
|
68
|
+
# The reported query time is the wall clock time spent executing the query with the GC
|
|
69
|
+
# time and Ruby thread CPU time subtracted out so that it reflects the time actually
|
|
70
|
+
# spent waiting on the database as closely as possible (see {.database_query_time}). This
|
|
71
|
+
# query time, rather than the raw wall clock time, is what is accumulated, compared against
|
|
72
|
+
# the threshold, and used as the duration of the emitted notification.
|
|
73
|
+
#
|
|
74
|
+
# @param sql [String] the SQL statement that was executed
|
|
75
|
+
# @param name [String, nil] the name of the query
|
|
76
|
+
# @param binds [Array] the bind parameters
|
|
68
77
|
# @param row_count [Integer] the number of rows returned by the query
|
|
69
|
-
# @param
|
|
78
|
+
# @param start_time [Float] the monotonic time when the query started
|
|
79
|
+
# @param end_time [Float] the monotonic time when the query ended
|
|
80
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
81
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
70
82
|
# @return [void]
|
|
71
83
|
# @api private
|
|
72
|
-
def add_query(sql, name, binds, row_count, start_time, end_time)
|
|
84
|
+
def add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time)
|
|
73
85
|
return if IGNORED_STATEMENTS.include?(name)
|
|
74
86
|
|
|
75
87
|
counter = current_counter
|
|
76
88
|
return unless counter.is_a?(Counter)
|
|
77
89
|
|
|
78
90
|
elapsed_time = end_time - start_time
|
|
91
|
+
query_time = database_query_time(elapsed_time, gc_time, cpu_time)
|
|
79
92
|
counter.query_count += 1
|
|
80
93
|
counter.row_count += row_count
|
|
81
|
-
counter.query_time +=
|
|
94
|
+
counter.query_time += query_time
|
|
95
|
+
|
|
96
|
+
# The notification duration is the database query time, so the event ends that long after
|
|
97
|
+
# it started rather than at the raw wall clock end time.
|
|
98
|
+
notification_end_time = start_time + query_time
|
|
82
99
|
|
|
83
100
|
trace = nil
|
|
84
101
|
query_time_threshold = (counter.thresholds.query_time || -1)
|
|
85
|
-
if query_time_threshold >= 0 &&
|
|
102
|
+
if query_time_threshold >= 0 && query_time >= query_time_threshold
|
|
86
103
|
trace = backtrace
|
|
87
|
-
|
|
104
|
+
payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time)
|
|
105
|
+
send_notification("query_time", start_time, notification_end_time, **payload)
|
|
88
106
|
end
|
|
89
107
|
|
|
90
108
|
row_count_threshold = (counter.thresholds.row_count || -1)
|
|
91
109
|
if row_count_threshold >= 0 && row_count >= row_count_threshold
|
|
92
110
|
trace ||= backtrace
|
|
93
|
-
|
|
111
|
+
payload = notification_payload(sql: sql, binds: binds, row_count: row_count, trace: trace, elapsed_time: elapsed_time, gc_time: gc_time, cpu_time: cpu_time)
|
|
112
|
+
send_notification("row_count", start_time, notification_end_time, **payload)
|
|
94
113
|
end
|
|
95
114
|
end
|
|
96
115
|
|
|
@@ -113,6 +132,17 @@ module ActiveRecordQueryCounter
|
|
|
113
132
|
end
|
|
114
133
|
end
|
|
115
134
|
|
|
135
|
+
# Return the number of rollbacks that have been counted within the current block.
|
|
136
|
+
# Returns nil if not inside a block where queries are being counted.
|
|
137
|
+
#
|
|
138
|
+
# @return [Integer, nil]
|
|
139
|
+
def increment_rollbacks
|
|
140
|
+
counter = current_counter
|
|
141
|
+
return unless counter.is_a?(Counter)
|
|
142
|
+
|
|
143
|
+
counter.rollback_count += 1
|
|
144
|
+
end
|
|
145
|
+
|
|
116
146
|
# Return the number of queries that have been counted within the current block.
|
|
117
147
|
# Returns nil if not inside a block where queries are being counted.
|
|
118
148
|
#
|
|
@@ -195,6 +225,15 @@ module ActiveRecordQueryCounter
|
|
|
195
225
|
counter.transactions if counter.is_a?(Counter)
|
|
196
226
|
end
|
|
197
227
|
|
|
228
|
+
# Return the number of transactions that have rolled back within the current block.
|
|
229
|
+
# Returns nil if not inside a block where queries are being counted.
|
|
230
|
+
#
|
|
231
|
+
# @return [Integer, nil]
|
|
232
|
+
def rollback_count
|
|
233
|
+
counter = current_counter
|
|
234
|
+
counter.rollback_count if counter.is_a?(Counter)
|
|
235
|
+
end
|
|
236
|
+
|
|
198
237
|
# Return the query info as a hash with keys :query_count, :row_count, :query_time
|
|
199
238
|
# :transaction_count, and :transaction_type or nil if not inside a block where queries
|
|
200
239
|
# are being counted.
|
|
@@ -210,7 +249,8 @@ module ActiveRecordQueryCounter
|
|
|
210
249
|
cached_query_count: counter.cached_query_count,
|
|
211
250
|
cache_hit_rate: counter.cache_hit_rate,
|
|
212
251
|
transaction_count: counter.transaction_count,
|
|
213
|
-
transaction_time: counter.transaction_time
|
|
252
|
+
transaction_time: counter.transaction_time,
|
|
253
|
+
rollback_count: counter.rollback_count
|
|
214
254
|
}
|
|
215
255
|
end
|
|
216
256
|
end
|
|
@@ -262,6 +302,42 @@ module ActiveRecordQueryCounter
|
|
|
262
302
|
ActiveSupport::Notifications.publish("active_record_query_counter.#{name}", start_time, end_time, id, payload)
|
|
263
303
|
end
|
|
264
304
|
|
|
305
|
+
def notification_payload(sql:, binds:, row_count:, trace:, elapsed_time:, gc_time:, cpu_time:)
|
|
306
|
+
{
|
|
307
|
+
sql: sql,
|
|
308
|
+
binds: binds,
|
|
309
|
+
row_count: row_count,
|
|
310
|
+
trace: trace,
|
|
311
|
+
elapsed_time: (elapsed_time * 1000.0).round(6),
|
|
312
|
+
gc_time: (gc_time * 1000.0).round(6),
|
|
313
|
+
cpu_time: (cpu_time * 1000.0).round(6)
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Estimate the time spent waiting on the database by subtracting the GC time and thread CPU
|
|
318
|
+
# time from the wall clock time the query took.
|
|
319
|
+
#
|
|
320
|
+
# The GC time and CPU time normally measure distinct, non-overlapping intervals: a GC pause
|
|
321
|
+
# triggered by another thread happens while this thread is parked waiting on the database
|
|
322
|
+
# (off CPU, so it does not count as CPU time), while CPU time covers the Ruby work of
|
|
323
|
+
# building the result. They only overlap when the query's own thread triggers a GC, which
|
|
324
|
+
# runs on that thread and so counts as both GC time and CPU time. When that overlap is large
|
|
325
|
+
# enough to drive the result negative, only the larger of the two is subtracted so the shared
|
|
326
|
+
# interval is removed once. The result is clamped so it never exceeds the wall clock time and
|
|
327
|
+
# is never negative.
|
|
328
|
+
#
|
|
329
|
+
# @param elapsed_time [Float] the wall clock time the query took in seconds
|
|
330
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
331
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
332
|
+
# @return [Float] the estimated database time in seconds
|
|
333
|
+
def database_query_time(elapsed_time, gc_time, cpu_time)
|
|
334
|
+
return 0.0 if elapsed_time <= 0.0
|
|
335
|
+
|
|
336
|
+
query_time = elapsed_time - (gc_time + cpu_time)
|
|
337
|
+
query_time = elapsed_time - [gc_time, cpu_time].max if query_time.negative?
|
|
338
|
+
query_time.clamp(0.0, elapsed_time)
|
|
339
|
+
end
|
|
340
|
+
|
|
265
341
|
def backtrace
|
|
266
342
|
caller.reject { |line| line.start_with?(__dir__) }
|
|
267
343
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record_query_counter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '6.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '6.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: bundler
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,7 +37,6 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- bbdurand@gmail.com
|
|
44
42
|
executables: []
|
|
@@ -62,8 +60,10 @@ files:
|
|
|
62
60
|
homepage: https://github.com/bdurand/active_record_query_counter
|
|
63
61
|
licenses:
|
|
64
62
|
- MIT
|
|
65
|
-
metadata:
|
|
66
|
-
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/bdurand/active_record_query_counter
|
|
65
|
+
source_code_uri: https://github.com/bdurand/active_record_query_counter
|
|
66
|
+
changelog_uri: https://github.com/bdurand/active_record_query_counter/blob/main/CHANGELOG.md
|
|
67
67
|
rdoc_options: []
|
|
68
68
|
require_paths:
|
|
69
69
|
- lib
|
|
@@ -71,15 +71,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
74
|
+
version: '3.1'
|
|
75
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
requirements:
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
79
|
version: '0'
|
|
80
80
|
requirements: []
|
|
81
|
-
rubygems_version: 3.
|
|
82
|
-
signing_key:
|
|
81
|
+
rubygems_version: 3.6.9
|
|
83
82
|
specification_version: 4
|
|
84
|
-
summary:
|
|
83
|
+
summary: Provides detailed insights into how your code interacts with the database
|
|
84
|
+
by hooking into ActiveRecord.
|
|
85
85
|
test_files: []
|