active_record_query_counter 3.1.1 → 3.3.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 +18 -4
- data/VERSION +1 -1
- data/active_record_query_counter.gemspec +1 -3
- data/lib/active_record_query_counter/connection_adapter_extension.rb +43 -30
- data/lib/active_record_query_counter/counter.rb +7 -2
- data/lib/active_record_query_counter/query_info.rb +28 -0
- data/lib/active_record_query_counter/transaction_details.rb +74 -0
- data/lib/active_record_query_counter/transaction_extension.rb +13 -1
- data/lib/active_record_query_counter/transaction_info.rb +13 -4
- data/lib/active_record_query_counter.rb +197 -34
- metadata +5 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 499ad971fe53532f23bfa674961133b6c39f850b39d89b102d959be760ba386c
|
|
4
|
+
data.tar.gz: 0f2eb66f8d2d274ddd2b173fce6f21ba35a7069d398f0f72e03052905f0b9221
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ead36fbb297cfb37af6ccb6c5b50903c123224c5c0ff72f96cb9ccf4caeac02da8e12b34858f91d7ef4ea153383babe0c6eac37e59bace9491707542ccac48b
|
|
7
|
+
data.tar.gz: d3042dca0d9c1b0624338367461c0277360a676061dd62dd4f2ba101cfdf7831c67a1382637703e4d91d7da51bfc90e0829b2792ff2b20fecfbcee892b48fff4
|
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.3.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `TransactionInfo#query_count` reports the number of queries executed within the transaction.
|
|
12
|
+
- `TransactionInfo#gc_time` and `TransactionInfo#cpu_time` report the GC time and thread CPU time in seconds that elapsed while the transaction was open.
|
|
13
|
+
- `TransactionInfo#idle_time` estimates the time the transaction was open but not spent executing queries, running Ruby code, or paused for garbage collection. A large value indicates the process was waiting on something other than the database (an HTTP request, a sleep, a lock, etc.) while it held the transaction open.
|
|
14
|
+
- The `transaction_time` notification payload now includes `:queries` along with `:gc_time`, `:cpu_time`, and `:idle_time` in milliseconds. The queries are an array of new `ActiveRecordQueryCounter::QueryInfo` objects; each includes the SQL statement, query name, row count, start and end times, and the GC, CPU, and connection setup times measured for the query. Bind parameter values are not included.
|
|
15
|
+
- The full query details for a transaction only live in a transient `ActiveRecordQueryCounter::TransactionDetails` object that populates the `transaction_time` notification payload and is then released. The `TransactionInfo` objects retained for the duration of a `count_queries` block hold only counts and timings, so memory stays flat even when a process creates many implicit transactions (for example, many saves without an explicit transaction).
|
|
16
|
+
|
|
17
|
+
## 3.2.0
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Connection setup time (time spent in the adapter's `connect!`, `reconnect!`, and `verify!` methods while running a query) is now measured and subtracted from the reported query time, so a query that triggers a reconnect after an idle period or a database failover is no longer reported as an inexplicably slow query. The time is reported separately as `:connection_time` in the `query_time` and `row_count` notification payloads.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- Support for ActiveRecord versions prior to 7.1.
|
|
26
|
+
|
|
7
27
|
## 3.1.1
|
|
8
28
|
|
|
9
29
|
### Fixed
|
data/README.md
CHANGED
|
@@ -14,6 +14,9 @@ It measures database usage within a block of code, including:
|
|
|
14
14
|
- The number of transactions used
|
|
15
15
|
- The total time spent inside transactions
|
|
16
16
|
- The number of transactions that were rolled back
|
|
17
|
+
- The queries executed inside each transaction, with the time each one took
|
|
18
|
+
- The GC time and CPU time spent inside each transaction
|
|
19
|
+
- The idle time inside each transaction spent waiting on things other than the database
|
|
17
20
|
|
|
18
21
|
This gem is designed to help you:
|
|
19
22
|
|
|
@@ -57,9 +60,11 @@ end
|
|
|
57
60
|
|
|
58
61
|
### Query Time
|
|
59
62
|
|
|
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)
|
|
63
|
+
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), the Ruby CPU work of building the result objects, and the time spent establishing or re-establishing the database connection. On a busy, multi-threaded server these can add up to seconds, making a trivial query look pathologically slow.
|
|
61
64
|
|
|
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.
|
|
65
|
+
To report the time actually spent waiting on the database as closely as possible, the connection setup time, 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.
|
|
66
|
+
|
|
67
|
+
Connection setup time is the wall clock time spent inside the adapter's `connect!`, `reconnect!`, and `verify!` methods while running the query. ActiveRecord (re)establishes and verifies connections lazily, from within the query execution path, so when a connection has gone stale — after an idle period, or a database failover (common with clustered databases such as Amazon Aurora) — the reconnect (DNS resolution, TCP connect, TLS handshake, and authentication) happens on the next query and is otherwise charged to it. This is reported separately as `:connection_time` in the notification payloads so these events are diagnosable rather than appearing as inexplicably slow queries.
|
|
63
68
|
|
|
64
69
|
> [!NOTE]
|
|
65
70
|
> 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.
|
|
@@ -118,8 +123,9 @@ Triggered when a query exceeds the query_time threshold with the payload:
|
|
|
118
123
|
- `:elapsed_time` - The raw wall clock time the query took (in milliseconds).
|
|
119
124
|
- `:gc_time` - The GC time that elapsed while the query ran (in milliseconds).
|
|
120
125
|
- `:cpu_time` - The thread CPU time spent while the query ran (in milliseconds).
|
|
126
|
+
- `:connection_time` - The time spent establishing, verifying, or reconnecting the database connection while the query ran (in milliseconds).
|
|
121
127
|
|
|
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`.
|
|
128
|
+
The duration of the notification event is the query time: the wall clock time with the connection setup time, GC time, and CPU time subtracted out (see [Query Time](#query-time)). The raw wall clock time is still available as `:elapsed_time`.
|
|
123
129
|
|
|
124
130
|
##### 2. active_record_query_counter.row_count notification
|
|
125
131
|
|
|
@@ -132,21 +138,29 @@ Triggered when a query exceeds the row_count threshold with the payload:
|
|
|
132
138
|
- `:elapsed_time` - The raw wall clock time the query took (in milliseconds).
|
|
133
139
|
- `:gc_time` - The GC time that elapsed while the query ran (in milliseconds).
|
|
134
140
|
- `:cpu_time` - The thread CPU time spent while the query ran (in milliseconds).
|
|
141
|
+
- `:connection_time` - The time spent establishing, verifying, or reconnecting the database connection while the query ran (in milliseconds).
|
|
135
142
|
|
|
136
143
|
##### 3. active_record_query_counter.transaction_time notification
|
|
137
144
|
|
|
138
145
|
Triggered when a transaction exceeds the transaction_time threshold with the payload:
|
|
139
146
|
|
|
140
147
|
- `:trace` - The stack trace of where the transaction was completed.
|
|
148
|
+
- `:queries` - An array of `ActiveRecordQueryCounter::QueryInfo` objects for the queries executed within the transaction. Each object includes the SQL statement, query name, row count, and timing details. Bind parameter values are not included.
|
|
149
|
+
- `:gc_time` - The GC time that elapsed while the transaction was open (in milliseconds).
|
|
150
|
+
- `:cpu_time` - The thread CPU time spent while the transaction was open (in milliseconds).
|
|
151
|
+
- `:idle_time` - The estimated time the transaction was open but not spent executing queries, running Ruby code, or paused for garbage collection (in milliseconds). A large value indicates the process was waiting on something other than the database (an HTTP request, a sleep, a lock, etc.) while it held the transaction open.
|
|
141
152
|
|
|
142
153
|
##### 4. active_record_query_counter.transaction_count notification
|
|
143
154
|
|
|
144
155
|
Triggered when transactions exceed the transaction_count threshold with the payload:
|
|
145
156
|
|
|
146
|
-
- `:transactions` - An array of `ActiveRecordQueryCounter::TransactionInfo` objects.
|
|
157
|
+
- `:transactions` - An array of `ActiveRecordQueryCounter::TransactionInfo` objects. Each object includes the number of queries executed within the transaction (`query_count`), the GC time (`gc_time`) and thread CPU time (`cpu_time`) in seconds spent while the transaction was open, and the estimated idle time (`idle_time`) in seconds spent waiting on things other than the database.
|
|
147
158
|
|
|
148
159
|
The duration of the notification event is the time between when the first transaction was started and the last transaction was completed.
|
|
149
160
|
|
|
161
|
+
> [!NOTE]
|
|
162
|
+
> The full query details for a transaction are only available in the `transaction_time` notification payload. They are released after the notification fires; the `TransactionInfo` objects retained for the duration of a `count_queries` block hold only counts and timings so that memory stays flat even when many transactions are created. Query details are also only collected while queries are being counted inside a `count_queries` block (or the bundled middleware).
|
|
163
|
+
|
|
150
164
|
#### Setting Thresholds
|
|
151
165
|
|
|
152
166
|
Thresholds can be configured **globally** in an initializer:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.0
|
|
@@ -34,9 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
|
|
35
35
|
spec.require_paths = ["lib"]
|
|
36
36
|
|
|
37
|
-
spec.add_dependency "activerecord", ">=
|
|
38
|
-
|
|
39
|
-
spec.add_development_dependency "bundler"
|
|
37
|
+
spec.add_dependency "activerecord", ">= 7.1"
|
|
40
38
|
|
|
41
39
|
spec.required_ruby_version = ">= 3.1"
|
|
42
40
|
end
|
|
@@ -3,63 +3,76 @@
|
|
|
3
3
|
module ActiveRecordQueryCounter
|
|
4
4
|
# Module to prepend to the connection adapter to inject the counting behavior.
|
|
5
5
|
module ConnectionAdapterExtension
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
6
|
+
# Connection adapter methods that establish, verify, or reconnect the underlying database
|
|
7
|
+
# connection. When these run inside a query (for example when a stale connection is
|
|
8
|
+
# re-established after an idle period or a database failover), the wall clock time they
|
|
9
|
+
# consume is spent setting up the connection rather than executing the query. It is measured
|
|
10
|
+
# separately so it can be subtracted from the reported query time.
|
|
11
|
+
CONNECTION_SETUP_METHODS = %i[connect! reconnect! verify!].freeze
|
|
10
12
|
|
|
11
13
|
class << self
|
|
12
14
|
def inject(connection_class)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
unless connection_class.include?(InternalExecQuery)
|
|
16
|
+
connection_class.prepend(InternalExecQuery)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless connection_class.include?(ConnectionSetupExtension)
|
|
20
|
+
connection_class.prepend(ConnectionSetupExtension)
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# Measure a query by wrapping its execution. In addition to the wall clock time, the GC
|
|
21
|
-
# time
|
|
22
|
-
# actually spent waiting on the database can be isolated from time lost to
|
|
23
|
-
# collection
|
|
25
|
+
# time, thread CPU time, and connection setup time spent while the query runs are captured
|
|
26
|
+
# so that the time actually spent waiting on the database can be isolated from time lost to
|
|
27
|
+
# garbage collection, Ruby VM work, and (re)establishing the database connection.
|
|
24
28
|
#
|
|
25
29
|
# @param sql [String] the SQL statement being executed
|
|
26
30
|
# @param name [String, nil] the name of the query
|
|
27
31
|
# @param binds [Array] the bind parameters
|
|
32
|
+
# @param connection [Object, nil] the connection adapter the query is being executed on
|
|
28
33
|
# @yield executes the query and returns its result
|
|
29
34
|
# @return [Object] the result of the query
|
|
30
|
-
def measure_query(sql, name, binds)
|
|
35
|
+
def measure_query(sql, name, binds, connection = nil)
|
|
31
36
|
gc_start = GC.total_time
|
|
32
|
-
cpu_start = current_cpu_time
|
|
37
|
+
cpu_start = ActiveRecordQueryCounter.current_cpu_time
|
|
38
|
+
previous_timer = ActiveRecordQueryCounter.start_connection_timer
|
|
33
39
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
34
|
-
|
|
40
|
+
begin
|
|
41
|
+
result = yield
|
|
42
|
+
ensure
|
|
43
|
+
connection_time = ActiveRecordQueryCounter.stop_connection_timer(previous_timer)
|
|
44
|
+
end
|
|
35
45
|
if result.is_a?(ActiveRecord::Result)
|
|
36
46
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
37
|
-
cpu_time = current_cpu_time - cpu_start
|
|
47
|
+
cpu_time = ActiveRecordQueryCounter.current_cpu_time - cpu_start
|
|
38
48
|
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)
|
|
49
|
+
ActiveRecordQueryCounter.add_query(sql, name, binds, result.length, start_time, end_time, gc_time, cpu_time, connection_time, connection: connection)
|
|
40
50
|
end
|
|
41
51
|
result
|
|
42
52
|
end
|
|
53
|
+
end
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
# @return [Float]
|
|
49
|
-
def current_cpu_time
|
|
50
|
-
CPU_CLOCK_ID ? Process.clock_gettime(CPU_CLOCK_ID) : 0.0
|
|
55
|
+
module InternalExecQuery
|
|
56
|
+
def internal_exec_query(sql, name = nil, binds = [], **kwargs)
|
|
57
|
+
ConnectionAdapterExtension.measure_query(sql, name, binds, self) { super }
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
# Module prepended to the connection adapter to measure the wall clock time spent
|
|
62
|
+
# establishing, verifying, or reconnecting the database connection while a query is running
|
|
63
|
+
# (see {CONNECTION_SETUP_METHODS}). The measured time is accumulated on the current query's
|
|
64
|
+
# connection timer so it can be subtracted from the reported query time.
|
|
65
|
+
module ConnectionSetupExtension
|
|
66
|
+
def connect!(...)
|
|
67
|
+
ActiveRecordQueryCounter.measure_connection_setup { super }
|
|
57
68
|
end
|
|
58
|
-
end
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
def reconnect!(...)
|
|
71
|
+
ActiveRecordQueryCounter.measure_connection_setup { super }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def verify!(...)
|
|
75
|
+
ActiveRecordQueryCounter.measure_connection_setup { super }
|
|
63
76
|
end
|
|
64
77
|
end
|
|
65
78
|
end
|
|
@@ -41,9 +41,14 @@ module ActiveRecordQueryCounter
|
|
|
41
41
|
# @param trace [Array<String>] the trace of the transaction
|
|
42
42
|
# @param start_time [Float] the monotonic time when the transaction began
|
|
43
43
|
# @param end_time [Float] the monotonic time when the transaction ended
|
|
44
|
+
# @param query_count [Integer] the number of queries executed within the transaction
|
|
45
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the transaction was open
|
|
46
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the transaction was open
|
|
47
|
+
# @param idle_time [Float] the time in seconds the transaction was open but not spent on
|
|
48
|
+
# queries, Ruby code, or garbage collection
|
|
44
49
|
# @return [void]
|
|
45
50
|
# @api private
|
|
46
|
-
def add_transaction(trace:, start_time:, end_time:)
|
|
51
|
+
def add_transaction(trace:, start_time:, end_time:, query_count: 0, gc_time: 0.0, cpu_time: 0.0, idle_time: 0.0)
|
|
47
52
|
trace_transactions = @transactions_hash[trace]
|
|
48
53
|
if trace_transactions
|
|
49
54
|
# Memory optimization so that we don't store duplicate traces for every transaction in a loop.
|
|
@@ -53,7 +58,7 @@ module ActiveRecordQueryCounter
|
|
|
53
58
|
@transactions_hash[trace] = trace_transactions
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
trace_transactions << TransactionInfo.new(start_time: start_time, end_time: end_time, trace: trace)
|
|
61
|
+
trace_transactions << TransactionInfo.new(start_time: start_time, end_time: end_time, trace: trace, query_count: query_count, gc_time: gc_time, cpu_time: cpu_time, idle_time: idle_time)
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
# Return the number of transactions that have been tracked by the counter.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordQueryCounter
|
|
4
|
+
# Data structure for storing information about a query executed within a transaction.
|
|
5
|
+
# Note that the start and end times are monotonic time and not wall clock time. Bind
|
|
6
|
+
# parameter values are deliberately not stored since they can contain sensitive data.
|
|
7
|
+
class QueryInfo
|
|
8
|
+
attr_reader :sql, :name, :row_count, :start_time, :end_time, :gc_time, :cpu_time, :connection_time
|
|
9
|
+
|
|
10
|
+
def initialize(sql:, name:, row_count:, start_time:, end_time:, gc_time: 0.0, cpu_time: 0.0, connection_time: 0.0)
|
|
11
|
+
@sql = sql
|
|
12
|
+
@name = name
|
|
13
|
+
@row_count = row_count
|
|
14
|
+
@start_time = start_time
|
|
15
|
+
@end_time = end_time
|
|
16
|
+
@gc_time = gc_time
|
|
17
|
+
@cpu_time = cpu_time
|
|
18
|
+
@connection_time = connection_time
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Return the wall clock time spent executing the query.
|
|
22
|
+
#
|
|
23
|
+
# @return [Float]
|
|
24
|
+
def elapsed_time
|
|
25
|
+
end_time - start_time
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordQueryCounter
|
|
4
|
+
# Data structure with the full details about a transaction, including the queries executed
|
|
5
|
+
# within it. Note that the start and end times are monotonic time and not wall clock time.
|
|
6
|
+
#
|
|
7
|
+
# This object is transient. It is created when a transaction is recorded to build the
|
|
8
|
+
# `transaction_time` notification payload and to derive the retained
|
|
9
|
+
# {ActiveRecordQueryCounter::TransactionInfo}, and is then released so the query details
|
|
10
|
+
# do not accumulate in memory.
|
|
11
|
+
class TransactionDetails
|
|
12
|
+
attr_reader :start_time, :end_time, :trace, :queries, :gc_time, :cpu_time
|
|
13
|
+
|
|
14
|
+
def initialize(start_time:, end_time:, trace:, queries: [], gc_time: 0.0, cpu_time: 0.0)
|
|
15
|
+
@start_time = start_time
|
|
16
|
+
@end_time = end_time
|
|
17
|
+
@trace = trace
|
|
18
|
+
@queries = queries
|
|
19
|
+
@gc_time = gc_time
|
|
20
|
+
@cpu_time = cpu_time
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Return the time spent in the transaction.
|
|
24
|
+
#
|
|
25
|
+
# @return [Float]
|
|
26
|
+
def elapsed_time
|
|
27
|
+
end_time - start_time
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the number of queries executed within the transaction.
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
def query_count
|
|
34
|
+
queries.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Estimate the time the transaction was open but not spent executing queries, running Ruby
|
|
38
|
+
# code, or paused for garbage collection. A large value indicates the process was waiting
|
|
39
|
+
# on something other than the database (an HTTP request, a sleep, a lock, etc.) while it
|
|
40
|
+
# held the transaction open.
|
|
41
|
+
#
|
|
42
|
+
# The wall clock time of each query is subtracted first since it already includes any GC
|
|
43
|
+
# and CPU time that occurred while the query ran. Only the GC and CPU time that occurred
|
|
44
|
+
# outside of queries is then subtracted from the remainder. GC and CPU time normally cover
|
|
45
|
+
# distinct intervals, but they overlap when this thread triggers a GC; when subtracting
|
|
46
|
+
# both would drive the result negative, only the larger of the two is subtracted so the
|
|
47
|
+
# shared interval is removed once (the same heuristic used for the query time). The result
|
|
48
|
+
# is clamped so it is never negative and never exceeds the elapsed time.
|
|
49
|
+
#
|
|
50
|
+
# @return [Float]
|
|
51
|
+
def idle_time
|
|
52
|
+
return 0.0 if elapsed_time <= 0.0
|
|
53
|
+
|
|
54
|
+
queries_elapsed_time = 0.0
|
|
55
|
+
queries_gc_time = 0.0
|
|
56
|
+
queries_cpu_time = 0.0
|
|
57
|
+
queries.each do |query|
|
|
58
|
+
queries_elapsed_time += query.elapsed_time
|
|
59
|
+
queries_gc_time += query.gc_time
|
|
60
|
+
queries_cpu_time += query.cpu_time
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
remaining_time = elapsed_time - queries_elapsed_time
|
|
64
|
+
return 0.0 if remaining_time <= 0.0
|
|
65
|
+
|
|
66
|
+
other_gc_time = (gc_time - queries_gc_time).clamp(0.0, remaining_time)
|
|
67
|
+
other_cpu_time = (cpu_time - queries_cpu_time).clamp(0.0, remaining_time)
|
|
68
|
+
|
|
69
|
+
idle_time = remaining_time - (other_gc_time + other_cpu_time)
|
|
70
|
+
idle_time = remaining_time - [other_gc_time, other_cpu_time].max if idle_time.negative?
|
|
71
|
+
idle_time.clamp(0.0, remaining_time)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -16,6 +16,9 @@ module ActiveRecordQueryCounter
|
|
|
16
16
|
def initialize(...)
|
|
17
17
|
super
|
|
18
18
|
@active_record_query_counter_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
@active_record_query_counter_gc_start = GC.total_time
|
|
20
|
+
@active_record_query_counter_cpu_start = ActiveRecordQueryCounter.current_cpu_time
|
|
21
|
+
@active_record_query_counter_queries = ActiveRecordQueryCounter.register_transaction_queries(connection)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def commit(...)
|
|
@@ -44,8 +47,17 @@ module ActiveRecordQueryCounter
|
|
|
44
47
|
return unless start_time
|
|
45
48
|
|
|
46
49
|
@active_record_query_counter_start_time = nil
|
|
50
|
+
ActiveRecordQueryCounter.unregister_transaction_queries(connection)
|
|
47
51
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
-
|
|
52
|
+
gc_time = (GC.total_time - @active_record_query_counter_gc_start) / 1_000_000_000.0
|
|
53
|
+
cpu_time = ActiveRecordQueryCounter.current_cpu_time - @active_record_query_counter_cpu_start
|
|
54
|
+
ActiveRecordQueryCounter.add_transaction(
|
|
55
|
+
start_time,
|
|
56
|
+
end_time,
|
|
57
|
+
queries: @active_record_query_counter_queries,
|
|
58
|
+
gc_time: gc_time,
|
|
59
|
+
cpu_time: cpu_time
|
|
60
|
+
)
|
|
49
61
|
ActiveRecordQueryCounter.increment_rollbacks if rollback
|
|
50
62
|
end
|
|
51
63
|
end
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveRecordQueryCounter
|
|
4
|
-
# Data structure for storing
|
|
5
|
-
# times are monotonic time and not wall clock time.
|
|
4
|
+
# Data structure for storing the counts and timings of a transaction. Note that the start
|
|
5
|
+
# and end times are monotonic time and not wall clock time.
|
|
6
|
+
#
|
|
7
|
+
# This structure deliberately does not hold the queries executed within the transaction so
|
|
8
|
+
# that retaining it for the duration of a `count_queries` block stays cheap. The full query
|
|
9
|
+
# details are only available transiently in the `transaction_time` notification payload
|
|
10
|
+
# (see {ActiveRecordQueryCounter::TransactionDetails}).
|
|
6
11
|
class TransactionInfo
|
|
7
|
-
attr_reader :start_time, :end_time, :trace
|
|
12
|
+
attr_reader :start_time, :end_time, :trace, :query_count, :gc_time, :cpu_time, :idle_time
|
|
8
13
|
|
|
9
|
-
def initialize(start_time:, end_time:, trace:)
|
|
14
|
+
def initialize(start_time:, end_time:, trace:, query_count: 0, gc_time: 0.0, cpu_time: 0.0, idle_time: 0.0)
|
|
10
15
|
@start_time = start_time
|
|
11
16
|
@end_time = end_time
|
|
12
17
|
@trace = trace
|
|
18
|
+
@query_count = query_count
|
|
19
|
+
@gc_time = gc_time
|
|
20
|
+
@cpu_time = cpu_time
|
|
21
|
+
@idle_time = idle_time
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
# Return the time spent in the transaction.
|
|
@@ -4,7 +4,9 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
require_relative "active_record_query_counter/connection_adapter_extension"
|
|
6
6
|
require_relative "active_record_query_counter/counter"
|
|
7
|
+
require_relative "active_record_query_counter/query_info"
|
|
7
8
|
require_relative "active_record_query_counter/thresholds"
|
|
9
|
+
require_relative "active_record_query_counter/transaction_details"
|
|
8
10
|
require_relative "active_record_query_counter/transaction_info"
|
|
9
11
|
require_relative "active_record_query_counter/transaction_extension"
|
|
10
12
|
|
|
@@ -26,6 +28,10 @@ module ActiveRecordQueryCounter
|
|
|
26
28
|
IGNORED_STATEMENTS = %w[SCHEMA EXPLAIN].freeze
|
|
27
29
|
private_constant :IGNORED_STATEMENTS
|
|
28
30
|
|
|
31
|
+
# Clock used to measure the CPU time consumed by the current thread. It is not available on
|
|
32
|
+
# every platform (e.g. Windows), in which case CPU time is not measured and is treated as zero.
|
|
33
|
+
CPU_CLOCK_ID = (Process::CLOCK_THREAD_CPUTIME_ID if defined?(Process::CLOCK_THREAD_CPUTIME_ID))
|
|
34
|
+
|
|
29
35
|
@lock = Mutex.new
|
|
30
36
|
@default_thresholds = Thresholds.new
|
|
31
37
|
|
|
@@ -72,10 +78,11 @@ module ActiveRecordQueryCounter
|
|
|
72
78
|
# Increment the query counters.
|
|
73
79
|
#
|
|
74
80
|
# The reported query time is the wall clock time spent executing the query with the GC
|
|
75
|
-
# time
|
|
76
|
-
# spent waiting on the database as closely as possible (see
|
|
77
|
-
# query time, rather than the raw wall clock time, is what is
|
|
78
|
-
# the threshold, and used as the duration of the emitted
|
|
81
|
+
# time, Ruby thread CPU time, and connection setup time subtracted out so that it reflects
|
|
82
|
+
# the time actually spent waiting on the database as closely as possible (see
|
|
83
|
+
# {.database_query_time}). This query time, rather than the raw wall clock time, is what is
|
|
84
|
+
# accumulated, compared against the threshold, and used as the duration of the emitted
|
|
85
|
+
# notification.
|
|
79
86
|
#
|
|
80
87
|
# @param sql [String] the SQL statement that was executed
|
|
81
88
|
# @param name [String, nil] the name of the query
|
|
@@ -85,20 +92,38 @@ module ActiveRecordQueryCounter
|
|
|
85
92
|
# @param end_time [Float] the monotonic time when the query ended
|
|
86
93
|
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
87
94
|
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
95
|
+
# @param connection_time [Float] the time in seconds spent establishing, verifying, or
|
|
96
|
+
# reconnecting the database connection while the query ran
|
|
97
|
+
# @param connection [Object, nil] the connection adapter the query was executed on; used to
|
|
98
|
+
# attach the query to the transaction currently open on that connection, if any
|
|
88
99
|
# @return [void]
|
|
89
100
|
# @api private
|
|
90
|
-
def add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time)
|
|
101
|
+
def add_query(sql, name, binds, row_count, start_time, end_time, gc_time, cpu_time, connection_time = 0.0, connection: nil)
|
|
91
102
|
return if IGNORED_STATEMENTS.include?(name)
|
|
92
103
|
|
|
93
104
|
counter = current_counter
|
|
94
105
|
return unless counter.is_a?(Counter)
|
|
95
106
|
|
|
96
107
|
elapsed_time = end_time - start_time
|
|
97
|
-
query_time = database_query_time(elapsed_time, gc_time, cpu_time)
|
|
108
|
+
query_time = database_query_time(elapsed_time, gc_time, cpu_time, connection_time)
|
|
98
109
|
counter.query_count += 1
|
|
99
110
|
counter.row_count += row_count
|
|
100
111
|
counter.query_time += query_time
|
|
101
112
|
|
|
113
|
+
queries = transaction_queries(connection)
|
|
114
|
+
if queries
|
|
115
|
+
queries << QueryInfo.new(
|
|
116
|
+
sql: sql,
|
|
117
|
+
name: name,
|
|
118
|
+
row_count: row_count,
|
|
119
|
+
start_time: start_time,
|
|
120
|
+
end_time: end_time,
|
|
121
|
+
gc_time: gc_time,
|
|
122
|
+
cpu_time: cpu_time,
|
|
123
|
+
connection_time: connection_time
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
102
127
|
# The notification duration is the database query time, so the event ends that long after
|
|
103
128
|
# it started rather than at the raw wall clock end time.
|
|
104
129
|
notification_end_time = start_time + query_time
|
|
@@ -107,14 +132,14 @@ module ActiveRecordQueryCounter
|
|
|
107
132
|
query_time_threshold = counter.thresholds.query_time || -1
|
|
108
133
|
if query_time_threshold.between?(0, query_time)
|
|
109
134
|
trace = backtrace
|
|
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)
|
|
135
|
+
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, connection_time: connection_time)
|
|
111
136
|
send_notification("query_time", start_time, notification_end_time, **payload)
|
|
112
137
|
end
|
|
113
138
|
|
|
114
139
|
row_count_threshold = counter.thresholds.row_count || -1
|
|
115
140
|
if row_count_threshold.between?(0, row_count)
|
|
116
141
|
trace ||= backtrace
|
|
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)
|
|
142
|
+
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, connection_time: connection_time)
|
|
118
143
|
send_notification("row_count", start_time, notification_end_time, **payload)
|
|
119
144
|
end
|
|
120
145
|
end
|
|
@@ -123,18 +148,50 @@ module ActiveRecordQueryCounter
|
|
|
123
148
|
#
|
|
124
149
|
# @param start_time [Float] the time the transaction started
|
|
125
150
|
# @param end_time [Float] the time the transaction ended
|
|
151
|
+
# @param queries [Array<ActiveRecordQueryCounter::QueryInfo>] the queries executed within
|
|
152
|
+
# the transaction
|
|
153
|
+
# @param gc_time [Float] the GC time in seconds that elapsed while the transaction was open
|
|
154
|
+
# @param cpu_time [Float] the thread CPU time in seconds spent while the transaction was open
|
|
126
155
|
# @return [void]
|
|
127
156
|
# @api private
|
|
128
|
-
def add_transaction(start_time, end_time)
|
|
157
|
+
def add_transaction(start_time, end_time, queries: [], gc_time: 0.0, cpu_time: 0.0)
|
|
129
158
|
counter = current_counter
|
|
130
159
|
return unless counter.is_a?(Counter)
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
counter
|
|
161
|
+
# The details object holds the full query information. Only the counts and timings
|
|
162
|
+
# derived from it are retained on the counter; the details are released when this
|
|
163
|
+
# method returns so the query data does not accumulate in memory.
|
|
164
|
+
details = TransactionDetails.new(
|
|
165
|
+
start_time: start_time,
|
|
166
|
+
end_time: end_time,
|
|
167
|
+
trace: backtrace,
|
|
168
|
+
queries: queries,
|
|
169
|
+
gc_time: gc_time,
|
|
170
|
+
cpu_time: cpu_time
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
counter.add_transaction(
|
|
174
|
+
trace: details.trace,
|
|
175
|
+
start_time: start_time,
|
|
176
|
+
end_time: end_time,
|
|
177
|
+
query_count: details.query_count,
|
|
178
|
+
gc_time: gc_time,
|
|
179
|
+
cpu_time: cpu_time,
|
|
180
|
+
idle_time: details.idle_time
|
|
181
|
+
)
|
|
134
182
|
|
|
135
183
|
transaction_time_threshold = counter.thresholds.transaction_time || -1
|
|
136
184
|
if transaction_time_threshold.between?(0, end_time - start_time)
|
|
137
|
-
send_notification(
|
|
185
|
+
send_notification(
|
|
186
|
+
"transaction_time",
|
|
187
|
+
start_time,
|
|
188
|
+
end_time,
|
|
189
|
+
trace: details.trace,
|
|
190
|
+
queries: details.queries,
|
|
191
|
+
gc_time: (gc_time * 1000.0).round(6),
|
|
192
|
+
cpu_time: (cpu_time * 1000.0).round(6),
|
|
193
|
+
idle_time: (details.idle_time * 1000.0).round(6)
|
|
194
|
+
)
|
|
138
195
|
end
|
|
139
196
|
end
|
|
140
197
|
|
|
@@ -149,6 +206,91 @@ module ActiveRecordQueryCounter
|
|
|
149
206
|
counter.rollback_count += 1
|
|
150
207
|
end
|
|
151
208
|
|
|
209
|
+
# The current thread CPU time in seconds, or 0.0 when the platform does not support it.
|
|
210
|
+
#
|
|
211
|
+
# @return [Float]
|
|
212
|
+
# @api private
|
|
213
|
+
def current_cpu_time
|
|
214
|
+
CPU_CLOCK_ID ? Process.clock_gettime(CPU_CLOCK_ID) : 0.0
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Register a new query list for a transaction that was opened on a connection. Queries
|
|
218
|
+
# executed on that connection will be appended to the list until it is unregistered.
|
|
219
|
+
# Registering a connection always replaces any list already registered for it.
|
|
220
|
+
#
|
|
221
|
+
# The list is bound to the counter that is active when the transaction is opened. Queries
|
|
222
|
+
# counted by a different counter (e.g. a nested `count_queries` block) are not appended,
|
|
223
|
+
# so the recorded query count for the transaction stays consistent with the counter that
|
|
224
|
+
# recorded the transaction.
|
|
225
|
+
#
|
|
226
|
+
# @param connection [Object] the connection adapter the transaction was opened on
|
|
227
|
+
# @return [Array<ActiveRecordQueryCounter::QueryInfo>] the registered query list
|
|
228
|
+
# @api private
|
|
229
|
+
def register_transaction_queries(connection)
|
|
230
|
+
registry = ActiveSupport::IsolatedExecutionState[:active_record_query_counter_transaction_queries] ||= {}
|
|
231
|
+
queries = []
|
|
232
|
+
registry[connection] = {counter: current_counter, queries: queries}
|
|
233
|
+
queries
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Remove the registered query list for a connection when its transaction has ended.
|
|
237
|
+
#
|
|
238
|
+
# @param connection [Object] the connection adapter the transaction was opened on
|
|
239
|
+
# @return [void]
|
|
240
|
+
# @api private
|
|
241
|
+
def unregister_transaction_queries(connection)
|
|
242
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter_transaction_queries]&.delete(connection)
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Begin measuring the time spent establishing, verifying, or reconnecting the database
|
|
247
|
+
# connection for a single query. Returns the timer that was previously in effect so it can
|
|
248
|
+
# be restored by {.stop_connection_timer}; this keeps nested queries (should they ever
|
|
249
|
+
# occur) from leaking connection time into one another.
|
|
250
|
+
#
|
|
251
|
+
# @return [Object, nil] the previous connection timer
|
|
252
|
+
# @api private
|
|
253
|
+
def start_connection_timer
|
|
254
|
+
previous_timer = connection_timer
|
|
255
|
+
self.connection_timer = {elapsed: 0.0, measuring: false}
|
|
256
|
+
previous_timer
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Finish measuring connection setup time for the current query and restore the previously
|
|
260
|
+
# active timer.
|
|
261
|
+
#
|
|
262
|
+
# @param previous_timer [Object, nil] the timer returned by {.start_connection_timer}
|
|
263
|
+
# @return [Float] the connection setup time in seconds accumulated for the query
|
|
264
|
+
# @api private
|
|
265
|
+
def stop_connection_timer(previous_timer)
|
|
266
|
+
timer = connection_timer
|
|
267
|
+
self.connection_timer = previous_timer
|
|
268
|
+
timer ? timer[:elapsed] : 0.0
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Measure the wall clock time a connection setup operation (connect, reconnect, or verify)
|
|
272
|
+
# takes and accumulate it onto the current query's connection timer. When no query is being
|
|
273
|
+
# measured, or when a connection setup operation is already being measured (for example when
|
|
274
|
+
# `verify!` delegates to `reconnect!`), the block is yielded without recording so the
|
|
275
|
+
# interval is only counted once.
|
|
276
|
+
#
|
|
277
|
+
# @yield the connection setup operation
|
|
278
|
+
# @return [Object] the result of the block
|
|
279
|
+
# @api private
|
|
280
|
+
def measure_connection_setup
|
|
281
|
+
timer = connection_timer
|
|
282
|
+
return yield if timer.nil? || timer[:measuring]
|
|
283
|
+
|
|
284
|
+
timer[:measuring] = true
|
|
285
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
286
|
+
begin
|
|
287
|
+
yield
|
|
288
|
+
ensure
|
|
289
|
+
timer[:elapsed] += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
290
|
+
timer[:measuring] = false
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
152
294
|
# Return the number of queries that have been counted within the current block.
|
|
153
295
|
# Returns nil if not inside a block where queries are being counted.
|
|
154
296
|
#
|
|
@@ -295,24 +437,34 @@ module ActiveRecordQueryCounter
|
|
|
295
437
|
|
|
296
438
|
private
|
|
297
439
|
|
|
298
|
-
# The counter is stored in ActiveSupport::IsolatedExecutionState
|
|
299
|
-
#
|
|
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.
|
|
440
|
+
# The counter is stored in ActiveSupport::IsolatedExecutionState so that it follows the
|
|
441
|
+
# application's configured isolation level (thread or fiber).
|
|
302
442
|
def current_counter
|
|
303
|
-
|
|
304
|
-
ActiveSupport::IsolatedExecutionState[:active_record_query_counter]
|
|
305
|
-
else
|
|
306
|
-
Thread.current[:active_record_query_counter]
|
|
307
|
-
end
|
|
443
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter]
|
|
308
444
|
end
|
|
309
445
|
|
|
310
446
|
def current_counter=(counter)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
447
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter] = counter
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# The connection timer accumulates the connection setup time for the query currently being
|
|
451
|
+
# measured. It is stored with the same isolation as the counter (see {#current_counter}).
|
|
452
|
+
def connection_timer
|
|
453
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter_connection_timer]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def connection_timer=(timer)
|
|
457
|
+
ActiveSupport::IsolatedExecutionState[:active_record_query_counter_connection_timer] = timer
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# The query list registered for the transaction currently open on a connection, or nil
|
|
461
|
+
# when the connection is unknown, has no open transaction, or the transaction was opened
|
|
462
|
+
# under a different counter than the current one.
|
|
463
|
+
def transaction_queries(connection)
|
|
464
|
+
return nil if connection.nil?
|
|
465
|
+
|
|
466
|
+
entry = ActiveSupport::IsolatedExecutionState[:active_record_query_counter_transaction_queries]&.[](connection)
|
|
467
|
+
entry[:queries] if entry && entry[:counter].equal?(current_counter)
|
|
316
468
|
end
|
|
317
469
|
|
|
318
470
|
def send_notification(name, start_time, end_time, payload = {})
|
|
@@ -320,7 +472,7 @@ module ActiveRecordQueryCounter
|
|
|
320
472
|
ActiveSupport::Notifications.publish("active_record_query_counter.#{name}", start_time, end_time, id, payload)
|
|
321
473
|
end
|
|
322
474
|
|
|
323
|
-
def notification_payload(sql:, binds:, row_count:, trace:, elapsed_time:, gc_time:, cpu_time:)
|
|
475
|
+
def notification_payload(sql:, binds:, row_count:, trace:, elapsed_time:, gc_time:, cpu_time:, connection_time:)
|
|
324
476
|
{
|
|
325
477
|
sql: sql,
|
|
326
478
|
binds: binds,
|
|
@@ -328,12 +480,18 @@ module ActiveRecordQueryCounter
|
|
|
328
480
|
trace: trace,
|
|
329
481
|
elapsed_time: (elapsed_time * 1000.0).round(6),
|
|
330
482
|
gc_time: (gc_time * 1000.0).round(6),
|
|
331
|
-
cpu_time: (cpu_time * 1000.0).round(6)
|
|
483
|
+
cpu_time: (cpu_time * 1000.0).round(6),
|
|
484
|
+
connection_time: (connection_time * 1000.0).round(6)
|
|
332
485
|
}
|
|
333
486
|
end
|
|
334
487
|
|
|
335
|
-
# Estimate the time spent waiting on the database by subtracting the
|
|
336
|
-
# time from the wall clock time the query took.
|
|
488
|
+
# Estimate the time spent waiting on the database by subtracting the connection setup time,
|
|
489
|
+
# GC time, and thread CPU time from the wall clock time the query took.
|
|
490
|
+
#
|
|
491
|
+
# The connection setup time is a measured sub-interval of the wall clock time that was spent
|
|
492
|
+
# establishing, verifying, or reconnecting the database connection rather than executing the
|
|
493
|
+
# query, so it is removed first. This is the time that inflates a trivial query into a
|
|
494
|
+
# multi-second one after an idle period or a database failover.
|
|
337
495
|
#
|
|
338
496
|
# The GC time and CPU time normally measure distinct, non-overlapping intervals: a GC pause
|
|
339
497
|
# triggered by another thread happens while this thread is parked waiting on the database
|
|
@@ -347,13 +505,18 @@ module ActiveRecordQueryCounter
|
|
|
347
505
|
# @param elapsed_time [Float] the wall clock time the query took in seconds
|
|
348
506
|
# @param gc_time [Float] the GC time in seconds that elapsed while the query ran
|
|
349
507
|
# @param cpu_time [Float] the thread CPU time in seconds spent while the query ran
|
|
508
|
+
# @param connection_time [Float] the time in seconds spent establishing, verifying, or
|
|
509
|
+
# reconnecting the database connection while the query ran
|
|
350
510
|
# @return [Float] the estimated database time in seconds
|
|
351
|
-
def database_query_time(elapsed_time, gc_time, cpu_time)
|
|
511
|
+
def database_query_time(elapsed_time, gc_time, cpu_time, connection_time = 0.0)
|
|
352
512
|
return 0.0 if elapsed_time <= 0.0
|
|
353
513
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
514
|
+
wait_time = (elapsed_time - connection_time).clamp(0.0, elapsed_time)
|
|
515
|
+
return 0.0 if wait_time <= 0.0
|
|
516
|
+
|
|
517
|
+
query_time = wait_time - (gc_time + cpu_time)
|
|
518
|
+
query_time = wait_time - [gc_time, cpu_time].max if query_time.negative?
|
|
519
|
+
query_time.clamp(0.0, wait_time)
|
|
357
520
|
end
|
|
358
521
|
|
|
359
522
|
def backtrace
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record_query_counter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -15,28 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '7.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: bundler
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
25
|
+
version: '7.1'
|
|
40
26
|
email:
|
|
41
27
|
- bbdurand@gmail.com
|
|
42
28
|
executables: []
|
|
@@ -51,9 +37,11 @@ files:
|
|
|
51
37
|
- lib/active_record_query_counter.rb
|
|
52
38
|
- lib/active_record_query_counter/connection_adapter_extension.rb
|
|
53
39
|
- lib/active_record_query_counter/counter.rb
|
|
40
|
+
- lib/active_record_query_counter/query_info.rb
|
|
54
41
|
- lib/active_record_query_counter/rack_middleware.rb
|
|
55
42
|
- lib/active_record_query_counter/sidekiq_middleware.rb
|
|
56
43
|
- lib/active_record_query_counter/thresholds.rb
|
|
44
|
+
- lib/active_record_query_counter/transaction_details.rb
|
|
57
45
|
- lib/active_record_query_counter/transaction_extension.rb
|
|
58
46
|
- lib/active_record_query_counter/transaction_info.rb
|
|
59
47
|
homepage: https://github.com/bdurand/active_record_query_counter
|