active_record_query_counter 2.3.0 → 3.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +17 -0
- data/VERSION +1 -1
- data/active_record_query_counter.gemspec +2 -2
- data/lib/active_record_query_counter/connection_adapter_extension.rb +40 -14
- data/lib/active_record_query_counter/transaction_extension.rb +52 -0
- data/lib/active_record_query_counter.rb +99 -26
- metadata +7 -12
- data/lib/active_record_query_counter/transaction_manager_extension.rb +0 -40
- data/lib/active_record_query_counter/version.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1966d4714e8fc7a3d77b86be07c8c930470d2af298dbd8d88d4bee4043a4da84
|
|
4
|
+
data.tar.gz: 8ad2b534042042259038f72daff078b6302b552f6a742b4f38f0504afe9a8919
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13a2c8c037ea0b47ea11384e133c294d047a74b34f89564a49adb0c339e2e489a1d61ef8a48c76d31b7a7abbd9afca6caf9ebfea011dcd703dbfec19fa65e46f
|
|
7
|
+
data.tar.gz: 6ba58ac495883fa98d20e7cda7ae363444f279db7a543016a6ffcdacf1f4f2d7404c8f7f7d06425c9f159d004788c73bde3fb857ce3e63a218380d6d00e359fd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ 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.1.0
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Transaction time now ends when the COMMIT or ROLLBACK statement completes rather than before it is sent, so it includes the commit itself. Time spent in after commit and after rollback callbacks is not included since those run after the database transaction is over.
|
|
12
|
+
- The counter is now stored in `ActiveSupport::IsolatedExecutionState` when available (Rails 7+) so that query counting follows the application's configured thread or fiber isolation level. On Rails 6.x the counter remains fiber-local.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- A transaction whose COMMIT statement fails (e.g. a deadlock or serialization failure detected at commit time) is now counted as a rollback. Previously it was recorded as a successful commit and the rollback count was not incremented.
|
|
17
|
+
- `ActiveRecordQueryCounter.last_transaction_end_time` now returns the latest transaction end time when transactions on multiple connections overlap. Previously it returned the end time of the transaction that started last.
|
|
18
|
+
- Cached queries for ignored statements (`SCHEMA`, `EXPLAIN`) are no longer counted in the cached query count.
|
|
19
|
+
- Setting up the query cache subscription in `ActiveRecordQueryCounter.enable!` is now thread safe, so concurrent calls can no longer create duplicate subscriptions that would double count cached queries.
|
|
20
|
+
|
|
21
|
+
## 3.0.0
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- 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.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- Dropped support for Ruby versions older than 3.1 (required for `GC.total_time`).
|
|
34
|
+
|
|
7
35
|
## 2.3.0
|
|
8
36
|
|
|
9
37
|
### Added
|
data/README.md
CHANGED
|
@@ -55,6 +55,15 @@ ActiveRecordQueryCounter.count_queries do
|
|
|
55
55
|
end
|
|
56
56
|
```
|
|
57
57
|
|
|
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
|
+
|
|
58
67
|
### Middleware Integration
|
|
59
68
|
|
|
60
69
|
For **Rails** and **Sidekiq**, middleware is included to enable query counting in web requests and workers.
|
|
@@ -106,6 +115,11 @@ Triggered when a query exceeds the query_time threshold with the payload:
|
|
|
106
115
|
- `:binds` - The bind parameters that were used.
|
|
107
116
|
- `:row_count` - The number of rows returned.
|
|
108
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).
|
|
121
|
+
|
|
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`.
|
|
109
123
|
|
|
110
124
|
##### 2. active_record_query_counter.row_count notification
|
|
111
125
|
|
|
@@ -115,6 +129,9 @@ Triggered when a query exceeds the row_count threshold with the payload:
|
|
|
115
129
|
- `:binds` - The bind parameters that were used.
|
|
116
130
|
- `:row_count` - The number of rows returned.
|
|
117
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).
|
|
118
135
|
|
|
119
136
|
##### 3. active_record_query_counter.transaction_time notification
|
|
120
137
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.1.0
|
|
@@ -34,9 +34,9 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
|
|
35
35
|
spec.require_paths = ["lib"]
|
|
36
36
|
|
|
37
|
-
spec.add_dependency "activerecord", ">=
|
|
37
|
+
spec.add_dependency "activerecord", ">= 6.0"
|
|
38
38
|
|
|
39
39
|
spec.add_development_dependency "bundler"
|
|
40
40
|
|
|
41
|
-
spec.required_ruby_version = ">=
|
|
41
|
+
spec.required_ruby_version = ">= 3.1"
|
|
42
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 = [], ...)
|
|
56
|
+
ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
|
|
57
|
+
end
|
|
26
58
|
end
|
|
27
59
|
|
|
28
60
|
module InternalExecQuery
|
|
29
|
-
def internal_exec_query(sql, name = nil, binds = [],
|
|
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
|
|
61
|
+
def internal_exec_query(sql, name = nil, binds = [], ...)
|
|
62
|
+
ConnectionAdapterExtension.measure_query(sql, name, binds) { super }
|
|
37
63
|
end
|
|
38
64
|
end
|
|
39
65
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordQueryCounter
|
|
4
|
+
# Extension to ActiveRecord::ConnectionAdapters::RealTransaction to count transactions.
|
|
5
|
+
# Real transactions are always the outermost database transaction; savepoint transactions
|
|
6
|
+
# nested inside them are considered part of the transaction and are not counted separately.
|
|
7
|
+
module TransactionExtension
|
|
8
|
+
class << self
|
|
9
|
+
def inject(transaction_class)
|
|
10
|
+
unless transaction_class.include?(self)
|
|
11
|
+
transaction_class.prepend(self)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(...)
|
|
17
|
+
super
|
|
18
|
+
@active_record_query_counter_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def commit(...)
|
|
22
|
+
# The transaction is only recorded after the COMMIT succeeds. If it raises, the start
|
|
23
|
+
# time is left in place so the rollback that Rails performs next is counted as a
|
|
24
|
+
# rollback rather than a successful commit. Recording here rather than in the
|
|
25
|
+
# transaction manager keeps time spent in after commit callbacks (which run after this
|
|
26
|
+
# method returns) out of the transaction time.
|
|
27
|
+
retval = super
|
|
28
|
+
active_record_query_counter_record_transaction
|
|
29
|
+
retval
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def rollback(...)
|
|
33
|
+
super
|
|
34
|
+
ensure
|
|
35
|
+
# Recorded even if the ROLLBACK itself raises (e.g. the connection died) since the
|
|
36
|
+
# transaction is over either way.
|
|
37
|
+
active_record_query_counter_record_transaction(rollback: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def active_record_query_counter_record_transaction(rollback: false)
|
|
43
|
+
start_time = @active_record_query_counter_start_time
|
|
44
|
+
return unless start_time
|
|
45
|
+
|
|
46
|
+
@active_record_query_counter_start_time = nil
|
|
47
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
+
ActiveRecordQueryCounter.add_transaction(start_time, end_time)
|
|
49
|
+
ActiveRecordQueryCounter.increment_rollbacks if rollback
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
require_relative "active_record_query_counter/connection_adapter_extension"
|
|
4
6
|
require_relative "active_record_query_counter/counter"
|
|
5
7
|
require_relative "active_record_query_counter/thresholds"
|
|
6
8
|
require_relative "active_record_query_counter/transaction_info"
|
|
7
|
-
require_relative "active_record_query_counter/
|
|
9
|
+
require_relative "active_record_query_counter/transaction_extension"
|
|
8
10
|
|
|
9
11
|
# Everything you need to count ActiveRecord queries and row counts within a block.
|
|
10
12
|
#
|
|
@@ -16,13 +18,17 @@ require_relative "active_record_query_counter/transaction_manager_extension"
|
|
|
16
18
|
# puts ActiveRecordQueryCounter.row_count
|
|
17
19
|
# end
|
|
18
20
|
module ActiveRecordQueryCounter
|
|
21
|
+
VERSION = File.read(File.join(__dir__, "..", "VERSION")).strip
|
|
22
|
+
|
|
19
23
|
autoload :RackMiddleware, "active_record_query_counter/rack_middleware"
|
|
20
24
|
autoload :SidekiqMiddleware, "active_record_query_counter/sidekiq_middleware"
|
|
21
|
-
autoload :VERSION, "active_record_query_counter/version"
|
|
22
25
|
|
|
23
26
|
IGNORED_STATEMENTS = %w[SCHEMA EXPLAIN].freeze
|
|
24
27
|
private_constant :IGNORED_STATEMENTS
|
|
25
28
|
|
|
29
|
+
@lock = Mutex.new
|
|
30
|
+
@default_thresholds = Thresholds.new
|
|
31
|
+
|
|
26
32
|
class << self
|
|
27
33
|
# Enable query counting within a block.
|
|
28
34
|
#
|
|
@@ -37,8 +43,8 @@ module ActiveRecordQueryCounter
|
|
|
37
43
|
|
|
38
44
|
transaction_count = counter.transaction_count
|
|
39
45
|
if transaction_count > 0
|
|
40
|
-
transaction_threshold =
|
|
41
|
-
if transaction_threshold
|
|
46
|
+
transaction_threshold = counter.thresholds.transaction_count || -1
|
|
47
|
+
if transaction_threshold.between?(0, transaction_count)
|
|
42
48
|
send_notification("transaction_count", counter.first_transaction_start_time, counter.last_transaction_end_time, transactions: counter.transactions)
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -65,32 +71,51 @@ module ActiveRecordQueryCounter
|
|
|
65
71
|
|
|
66
72
|
# Increment the query counters.
|
|
67
73
|
#
|
|
74
|
+
# The reported query time is the wall clock time spent executing the query with the GC
|
|
75
|
+
# time and Ruby thread CPU time subtracted out so that it reflects the time actually
|
|
76
|
+
# spent waiting on the database as closely as possible (see {.database_query_time}). This
|
|
77
|
+
# query time, rather than the raw wall clock time, is what is accumulated, compared against
|
|
78
|
+
# the threshold, and used as the duration of the emitted notification.
|
|
79
|
+
#
|
|
80
|
+
# @param sql [String] the SQL statement that was executed
|
|
81
|
+
# @param name [String, nil] the name of the query
|
|
82
|
+
# @param binds [Array] the bind parameters
|
|
68
83
|
# @param row_count [Integer] the number of rows returned by the query
|
|
69
|
-
# @param
|
|
84
|
+
# @param start_time [Float] the monotonic time when the query started
|
|
85
|
+
# @param end_time [Float] the monotonic time when the query ended
|
|
86
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
87
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
70
88
|
# @return [void]
|
|
71
89
|
# @api private
|
|
72
|
-
def add_query(sql, name, binds, row_count, start_time, end_time)
|
|
90
|
+
def add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time)
|
|
73
91
|
return if IGNORED_STATEMENTS.include?(name)
|
|
74
92
|
|
|
75
93
|
counter = current_counter
|
|
76
94
|
return unless counter.is_a?(Counter)
|
|
77
95
|
|
|
78
96
|
elapsed_time = end_time - start_time
|
|
97
|
+
query_time = database_query_time(elapsed_time, gc_time, cpu_time)
|
|
79
98
|
counter.query_count += 1
|
|
80
99
|
counter.row_count += row_count
|
|
81
|
-
counter.query_time +=
|
|
100
|
+
counter.query_time += query_time
|
|
101
|
+
|
|
102
|
+
# The notification duration is the database query time, so the event ends that long after
|
|
103
|
+
# it started rather than at the raw wall clock end time.
|
|
104
|
+
notification_end_time = start_time + query_time
|
|
82
105
|
|
|
83
106
|
trace = nil
|
|
84
|
-
query_time_threshold =
|
|
85
|
-
if query_time_threshold
|
|
107
|
+
query_time_threshold = counter.thresholds.query_time || -1
|
|
108
|
+
if query_time_threshold.between?(0, query_time)
|
|
86
109
|
trace = backtrace
|
|
87
|
-
|
|
110
|
+
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)
|
|
111
|
+
send_notification("query_time", start_time, notification_end_time, **payload)
|
|
88
112
|
end
|
|
89
113
|
|
|
90
|
-
row_count_threshold =
|
|
91
|
-
if row_count_threshold
|
|
114
|
+
row_count_threshold = counter.thresholds.row_count || -1
|
|
115
|
+
if row_count_threshold.between?(0, row_count)
|
|
92
116
|
trace ||= backtrace
|
|
93
|
-
|
|
117
|
+
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)
|
|
118
|
+
send_notification("row_count", start_time, notification_end_time, **payload)
|
|
94
119
|
end
|
|
95
120
|
end
|
|
96
121
|
|
|
@@ -107,8 +132,8 @@ module ActiveRecordQueryCounter
|
|
|
107
132
|
trace = backtrace
|
|
108
133
|
counter.add_transaction(trace: trace, start_time: start_time, end_time: end_time)
|
|
109
134
|
|
|
110
|
-
transaction_time_threshold =
|
|
111
|
-
if transaction_time_threshold
|
|
135
|
+
transaction_time_threshold = counter.thresholds.transaction_time || -1
|
|
136
|
+
if transaction_time_threshold.between?(0, end_time - start_time)
|
|
112
137
|
send_notification("transaction_time", start_time, end_time, trace: backtrace)
|
|
113
138
|
end
|
|
114
139
|
end
|
|
@@ -194,7 +219,7 @@ module ActiveRecordQueryCounter
|
|
|
194
219
|
# @return [Float, nil] the monotonic time when the last transaction ended,
|
|
195
220
|
def last_transaction_end_time
|
|
196
221
|
counter = current_counter
|
|
197
|
-
counter.
|
|
222
|
+
counter.last_transaction_end_time if counter.is_a?(Counter)
|
|
198
223
|
end
|
|
199
224
|
|
|
200
225
|
# Return an array of transaction information for any transactions that have been counted
|
|
@@ -240,9 +265,7 @@ module ActiveRecordQueryCounter
|
|
|
240
265
|
# thresholds are used as the default values.
|
|
241
266
|
#
|
|
242
267
|
# @return [ActiveRecordQueryCounter::Thresholds]
|
|
243
|
-
|
|
244
|
-
@default_thresholds ||= Thresholds.new
|
|
245
|
-
end
|
|
268
|
+
attr_reader :default_thresholds
|
|
246
269
|
|
|
247
270
|
# Get the current local notification thresholds. These thresholds are only used within
|
|
248
271
|
# the current `count_queries` block.
|
|
@@ -257,25 +280,39 @@ module ActiveRecordQueryCounter
|
|
|
257
280
|
def enable!(connection_class)
|
|
258
281
|
ActiveSupport.on_load(:active_record) do
|
|
259
282
|
ConnectionAdapterExtension.inject(connection_class)
|
|
260
|
-
|
|
283
|
+
TransactionExtension.inject(ActiveRecord::ConnectionAdapters::RealTransaction)
|
|
261
284
|
end
|
|
262
285
|
|
|
263
|
-
@
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
@lock.synchronize do
|
|
287
|
+
@cache_subscription ||= ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start_time, _end_time, _id, payload|
|
|
288
|
+
if payload[:cached] && !IGNORED_STATEMENTS.include?(payload[:name])
|
|
289
|
+
counter = current_counter
|
|
290
|
+
counter.cached_query_count += 1 if counter.is_a?(Counter)
|
|
291
|
+
end
|
|
267
292
|
end
|
|
268
293
|
end
|
|
269
294
|
end
|
|
270
295
|
|
|
271
296
|
private
|
|
272
297
|
|
|
298
|
+
# The counter is stored in ActiveSupport::IsolatedExecutionState when available so that
|
|
299
|
+
# it follows the application's configured isolation level (thread or fiber). The fallback
|
|
300
|
+
# for ActiveSupport 6.x uses Thread.current, which is fiber-local, so on those versions
|
|
301
|
+
# queries executed in a fiber spawned inside the block are not counted.
|
|
273
302
|
def current_counter
|
|
274
|
-
|
|
303
|
+
if defined?(ActiveSupport::IsolatedExecutionState)
|
|
304
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter]
|
|
305
|
+
else
|
|
306
|
+
Thread.current[:active_record_query_counter]
|
|
307
|
+
end
|
|
275
308
|
end
|
|
276
309
|
|
|
277
310
|
def current_counter=(counter)
|
|
278
|
-
|
|
311
|
+
if defined?(ActiveSupport::IsolatedExecutionState)
|
|
312
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter] = counter
|
|
313
|
+
else
|
|
314
|
+
Thread.current[:active_record_query_counter] = counter
|
|
315
|
+
end
|
|
279
316
|
end
|
|
280
317
|
|
|
281
318
|
def send_notification(name, start_time, end_time, payload = {})
|
|
@@ -283,6 +320,42 @@ module ActiveRecordQueryCounter
|
|
|
283
320
|
ActiveSupport::Notifications.publish("active_record_query_counter.#{name}", start_time, end_time, id, payload)
|
|
284
321
|
end
|
|
285
322
|
|
|
323
|
+
def notification_payload(sql:, binds:, row_count:, trace:, elapsed_time:, gc_time:, cpu_time:)
|
|
324
|
+
{
|
|
325
|
+
sql: sql,
|
|
326
|
+
binds: binds,
|
|
327
|
+
row_count: row_count,
|
|
328
|
+
trace: trace,
|
|
329
|
+
elapsed_time: (elapsed_time * 1000.0).round(6),
|
|
330
|
+
gc_time: (gc_time * 1000.0).round(6),
|
|
331
|
+
cpu_time: (cpu_time * 1000.0).round(6)
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Estimate the time spent waiting on the database by subtracting the GC time and thread CPU
|
|
336
|
+
# time from the wall clock time the query took.
|
|
337
|
+
#
|
|
338
|
+
# The GC time and CPU time normally measure distinct, non-overlapping intervals: a GC pause
|
|
339
|
+
# triggered by another thread happens while this thread is parked waiting on the database
|
|
340
|
+
# (off CPU, so it does not count as CPU time), while CPU time covers the Ruby work of
|
|
341
|
+
# building the result. They only overlap when the query's own thread triggers a GC, which
|
|
342
|
+
# runs on that thread and so counts as both GC time and CPU time. When that overlap is large
|
|
343
|
+
# enough to drive the result negative, only the larger of the two is subtracted so the shared
|
|
344
|
+
# interval is removed once. The result is clamped so it never exceeds the wall clock time and
|
|
345
|
+
# is never negative.
|
|
346
|
+
#
|
|
347
|
+
# @param elapsed_time [Float] the wall clock time the query took in seconds
|
|
348
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
349
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
350
|
+
# @return [Float] the estimated database time in seconds
|
|
351
|
+
def database_query_time(elapsed_time, gc_time, cpu_time)
|
|
352
|
+
return 0.0 if elapsed_time <= 0.0
|
|
353
|
+
|
|
354
|
+
query_time = elapsed_time - (gc_time + cpu_time)
|
|
355
|
+
query_time = elapsed_time - [gc_time, cpu_time].max if query_time.negative?
|
|
356
|
+
query_time.clamp(0.0, elapsed_time)
|
|
357
|
+
end
|
|
358
|
+
|
|
286
359
|
def backtrace
|
|
287
360
|
caller.reject { |line| line.start_with?(__dir__) }
|
|
288
361
|
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.1.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: []
|
|
@@ -56,9 +54,8 @@ files:
|
|
|
56
54
|
- lib/active_record_query_counter/rack_middleware.rb
|
|
57
55
|
- lib/active_record_query_counter/sidekiq_middleware.rb
|
|
58
56
|
- lib/active_record_query_counter/thresholds.rb
|
|
57
|
+
- lib/active_record_query_counter/transaction_extension.rb
|
|
59
58
|
- lib/active_record_query_counter/transaction_info.rb
|
|
60
|
-
- lib/active_record_query_counter/transaction_manager_extension.rb
|
|
61
|
-
- lib/active_record_query_counter/version.rb
|
|
62
59
|
homepage: https://github.com/bdurand/active_record_query_counter
|
|
63
60
|
licenses:
|
|
64
61
|
- MIT
|
|
@@ -66,7 +63,6 @@ metadata:
|
|
|
66
63
|
homepage_uri: https://github.com/bdurand/active_record_query_counter
|
|
67
64
|
source_code_uri: https://github.com/bdurand/active_record_query_counter
|
|
68
65
|
changelog_uri: https://github.com/bdurand/active_record_query_counter/blob/main/CHANGELOG.md
|
|
69
|
-
post_install_message:
|
|
70
66
|
rdoc_options: []
|
|
71
67
|
require_paths:
|
|
72
68
|
- lib
|
|
@@ -74,15 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
74
70
|
requirements:
|
|
75
71
|
- - ">="
|
|
76
72
|
- !ruby/object:Gem::Version
|
|
77
|
-
version: '
|
|
73
|
+
version: '3.1'
|
|
78
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
75
|
requirements:
|
|
80
76
|
- - ">="
|
|
81
77
|
- !ruby/object:Gem::Version
|
|
82
78
|
version: '0'
|
|
83
79
|
requirements: []
|
|
84
|
-
rubygems_version:
|
|
85
|
-
signing_key:
|
|
80
|
+
rubygems_version: 4.0.3
|
|
86
81
|
specification_version: 4
|
|
87
82
|
summary: Provides detailed insights into how your code interacts with the database
|
|
88
83
|
by hooking into ActiveRecord.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveRecordQueryCounter
|
|
4
|
-
# Extension to ActiveRecord::ConnectionAdapters::TransactionManager to count transactions.
|
|
5
|
-
module TransactionManagerExtension
|
|
6
|
-
class << self
|
|
7
|
-
def inject(transaction_manager_class)
|
|
8
|
-
unless transaction_manager_class.include?(self)
|
|
9
|
-
transaction_manager_class.prepend(self)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def begin_transaction(*args, **kwargs)
|
|
15
|
-
if open_transactions == 0
|
|
16
|
-
@active_record_query_counter_transaction_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
17
|
-
end
|
|
18
|
-
super
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def commit_transaction(*args)
|
|
22
|
-
if @active_record_query_counter_transaction_start_time && open_transactions == 1
|
|
23
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
-
ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
|
|
25
|
-
@active_record_query_counter_transaction_start_time = nil
|
|
26
|
-
end
|
|
27
|
-
super
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def rollback_transaction(*args)
|
|
31
|
-
if @active_record_query_counter_transaction_start_time && open_transactions == 1
|
|
32
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
|
-
ActiveRecordQueryCounter.add_transaction(@active_record_query_counter_transaction_start_time, end_time)
|
|
34
|
-
ActiveRecordQueryCounter.increment_rollbacks
|
|
35
|
-
@active_record_query_counter_transaction_start_time = nil
|
|
36
|
-
end
|
|
37
|
-
super
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|