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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 752c1c799ab4ee023a24f4f8c3550da35831fdcebf39e71c9bb945bbf4ca7ff9
4
- data.tar.gz: ecd642c76f010996f40d0a6d791db96c06515137b2e1393d5655c1feae314b23
3
+ metadata.gz: 89a919244edbce9bf36bdaa232078b04470f1bacc952469eab4a4fffc2416e08
4
+ data.tar.gz: 97a66d47fda85ad15b832da3e390f8cc035a374983c64f53e4d673094b54d8b9
5
5
  SHA512:
6
- metadata.gz: b4a88b4283290565e8352d35f6c0b5483515dbcacbd17fb223fc90398b94d50ac2984519f84c6a188a576468cb1f9ea373edfcc6855301e78522676558511e9d
7
- data.tar.gz: ff360b2f495c953f79668a27520722c425c94d9cafe6607dcae40d25b65d10dd8f38f7b0d478771495927a5229ecf1e6948328b6adb87be5d6d186c77727d4f2
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
  [![Continuous Integration](https://github.com/bdurand/active_record_query_counter/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/active_record_query_counter/actions/workflows/continuous_integration.yml)
4
- [![Regression Test](https://github.com/bdurand/active_record_query_counter/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/active_record_query_counter/actions/workflows/regression_test.yml)
5
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
5
  [![Gem Version](https://badge.fury.io/rb/active_record_query_counter.svg)](https://badge.fury.io/rb/active_record_query_counter)
7
6
 
8
- This gem injects itself into ActiveRecord to give you insight into how your code is using the database.
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
- Within a block of code, it will count:
9
+ It measures database usage within a block of code, including:
11
10
 
12
- - the number of queries
13
- - the number of rows returned
14
- - the amount of time spent on queries
15
- - the number of transactions used
16
- - the amount of time spent inside transactions
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
- The intended use is to gather instrumentation stats for finding hot spots in your code that produce a lot of queries or slow queries or queries that return a lot of rows. It can also be used to find code that is not using transactions when making multiple updates to the database.
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
- The behavior must be enabled on your database connection adapter from within an initializer.
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
- Postgres:
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
- Next you must specify the blocks where you want to count queries.
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
- This gem includes middleware for both Rack and Sidekiq that will enable query counting on web requests and in workers. If you are using Rails with Sidekiq, you can enable both with an initializer.
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
- If you want to disable query counting within a block of code, you can use the `disable` method.
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 in this block.
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
- You can also subscribe to ActiveSupport notifications to get notified when query thresholds are exceeded.
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
- #### active_record_query_counter.query_time notification
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
- #### active_record_query_counter.row_count notification
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
- This notification is triggered when a query returns more rows than the `row_count` threshold. The payload contains the following keys:
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
- #### active_record_query_counter.transaction_time notification
136
+ ##### 3. active_record_query_counter.transaction_time notification
100
137
 
101
- This notification is triggered when a transaction takes longer than the `transaction_time` threshold. The payload contains the following keys:
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
- #### active_record_query_counter.transaction_count notification
142
+ ##### 4. active_record_query_counter.transaction_count notification
106
143
 
107
- This notification is triggered when a transaction takes longer than the `transaction_count` threshold. The payload contains the following keys:
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
- The thresholds for triggering notifications can be set globally in an initializer:
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
- They can be set locally inside a `count_queries` block with the `thresholds` object. Local thresholds will override the global thresholds only inside the block and will not change any global state.
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
- You can pass thresholds to individual Sidekiq workers via the `sidekiq_options` on the worker.
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
- You can set separate thresholds on the Rack middleware when you install it.
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 Notification Subscriptions
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
- 2.2.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 = "Count total number of ActiveRecord queries and row counts inside a block"
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", ">= 5.1"
37
+ spec.add_dependency "activerecord", ">= 6.0"
32
38
 
33
39
  spec.add_development_dependency "bundler"
34
40
 
35
- spec.required_ruby_version = ">= 2.5"
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.instance_methods.include?(:internal_exec_query) ? InternalExecQuery : ExecQuery)
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
- module ExecQuery
17
- def exec_query(sql, name = nil, binds = [], *args, **kwargs)
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 = super
34
+ result = yield
20
35
  if result.is_a?(ActiveRecord::Result)
21
36
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
- ActiveRecordQueryCounter.add_query(sql, name, binds, result.length, start_time, end_time)
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
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
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 elapsed_time [Float] the time spent executing the query
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 += elapsed_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 && elapsed_time >= query_time_threshold
102
+ if query_time_threshold >= 0 && query_time >= query_time_threshold
86
103
  trace = backtrace
87
- send_notification("query_time", start_time, end_time, sql: sql, binds: binds, row_count: row_count, trace: trace)
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
- send_notification("row_count", start_time, end_time, sql: sql, binds: binds, row_count: row_count, trace: trace)
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: 2.2.1
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: 2024-04-03 00:00:00.000000000 Z
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: '5.1'
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: '5.1'
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
- post_install_message:
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: '2.5'
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.4.12
82
- signing_key:
81
+ rubygems_version: 3.6.9
83
82
  specification_version: 4
84
- summary: Count total number of ActiveRecord queries and row counts inside a block
83
+ summary: Provides detailed insights into how your code interacts with the database
84
+ by hooking into ActiveRecord.
85
85
  test_files: []