mysql_genius-core 0.6.0 → 0.7.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: 01d397f796109a83914008b801549df18cec6c6b9363c7878f0aaa5b04b5a160
4
- data.tar.gz: 4bdd6cfbee56cffd41feffcadc287ae104b5d5f71fbb2d59a4e85dbcb0da567e
3
+ metadata.gz: 3ff33401fa2eda90123c44ab9e7dc63f685d44a65df87cef083052f05abe2f93
4
+ data.tar.gz: 995386078c2ec80cbf95e3002e9cf23b6a7b51fda4a6519496600de07cae1cb9
5
5
  SHA512:
6
- metadata.gz: bc74723d3f13499a57b115d857732f3061787d1b442c509ba89b690e2614522b3a921a5c39f3b52e2e420a2f0513de1dc41acb7b34613445b85e30f9c30730e6
7
- data.tar.gz: ec977e844a1fd46189560c332a6d46e5b9482c2e83f9f6acd2646ae5e4eedf631847b34360f8ded330832f2ae1f0aa92f01247b168e9d33b27f2db9ac6a87e6a
6
+ metadata.gz: b7421f6cdac811fbadff2c9faf72b46bd513e51a5b1156c0723a228b45ecb19e8b02a0dfc1d49758deb71515b7899affbc808003ba3a224161f2f80d2a69486d
7
+ data.tar.gz: 5f413a66dfda47ea3bab363ccf49d10e65a1dbf0b1f4a76fa4c0df3544c04aae8282afcf629591b6cb6acbfbe7f5c48f33c444cc43bd420da5295bb994285efa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Added
6
+ - `MysqlGenius::Core::Analysis::StatsHistory` — thread-safe in-memory ring buffer storing per-digest query performance snapshots. Supports `record`, `series_for`, `digests`, `clear`. Max 1440 samples per digest (24 hours at 60-second intervals).
7
+ - `MysqlGenius::Core::Analysis::StatsCollector` — background thread that samples `performance_schema.events_statements_summary_by_digest` at a configurable interval, computes per-interval deltas, and records them into a `StatsHistory` instance. Handles server restarts (negative deltas clamped to 0) and performance_schema unavailability (stops gracefully).
8
+ - `MysqlGenius::Core::Analysis::QueryStats` now includes `digest:` (the `DIGEST` hex hash from performance_schema) in its return value for stable URL keys.
9
+ - `capability?(name)` template helper contract — shared templates gate Redis-backed UI via `<% if capability?(:slow_queries) %>` guards. Rails adapter returns `true` for all names; the desktop sidecar returns `true` only for `:ai`.
10
+ - Query detail shared template (`query_detail.html.erb`) with SQL display, Explain button, stats cards, and inline SVG time-series charts.
11
+ - Query Stats dashboard tab now renders SQL cells as clickable links to `/queries/:digest`.
12
+
3
13
  ## 0.6.0
4
14
 
5
15
  No functional changes in `mysql_genius-core`. Version bumped to maintain lockstep with `mysql_genius 0.6.0`, which drops Rails 5.2 support from the Rails adapter. See the root `CHANGELOG.md` for the full change list.
@@ -42,6 +42,7 @@ module MysqlGenius
42
42
  def build_sql(order_clause, limit)
43
43
  <<~SQL
44
44
  SELECT
45
+ DIGEST,
45
46
  DIGEST_TEXT,
46
47
  COUNT_STAR AS calls,
47
48
  ROUND(SUM_TIMER_WAIT / 1000000000, 1) AS total_time_ms,
@@ -77,6 +78,7 @@ module MysqlGenius
77
78
  rows_sent = (row["rows_sent"] || row["ROWS_SENT"] || 0).to_i
78
79
 
79
80
  {
81
+ digest: (row["DIGEST"] || row["digest"] || "").to_s,
80
82
  sql: truncate(digest, 500),
81
83
  calls: calls,
82
84
  total_time_ms: (row["total_time_ms"] || 0).to_f,
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Analysis
6
+ # Background sampler that periodically queries performance_schema for the
7
+ # top 50 digests by SUM_TIMER_WAIT, computes per-interval deltas, and
8
+ # records snapshots into a StatsHistory ring buffer.
9
+ #
10
+ # The +connection_provider+ is a callable (lambda/proc) that returns a
11
+ # Core::Connection on each invocation. This allows each adapter to supply
12
+ # its own connection strategy:
13
+ #
14
+ # Rails: -> { ActiveRecordAdapter.new(ActiveRecord::Base.connection) }
15
+ # Desktop: -> { session.checkout { |c| c } }
16
+ class StatsCollector
17
+ DEFAULT_INTERVAL = 60
18
+ STOP_JOIN_TIMEOUT = 5
19
+ TOP_N = 50
20
+
21
+ def initialize(connection_provider:, history:, interval: DEFAULT_INTERVAL)
22
+ @connection_provider = connection_provider
23
+ @history = history
24
+ @interval = interval
25
+ @mutex = Mutex.new
26
+ @cv = ConditionVariable.new
27
+ @stop_signal = false
28
+ @running = false
29
+ @thread = nil
30
+ @previous = {}
31
+ end
32
+
33
+ def start
34
+ return self if @running
35
+
36
+ @stop_signal = false
37
+ @running = true
38
+ @thread = Thread.new { run_loop }
39
+ self
40
+ end
41
+
42
+ def stop
43
+ @mutex.synchronize do
44
+ @stop_signal = true
45
+ @cv.signal
46
+ end
47
+ @thread&.join(STOP_JOIN_TIMEOUT)
48
+ @thread = nil
49
+ end
50
+
51
+ def running?
52
+ @running
53
+ end
54
+
55
+ private
56
+
57
+ def run_loop
58
+ loop do
59
+ tick
60
+ break if wait_or_stop(@interval)
61
+ end
62
+ rescue StandardError => e
63
+ warn("[MysqlGenius] StatsCollector stopped: #{e.message}")
64
+ ensure
65
+ @running = false
66
+ end
67
+
68
+ def tick
69
+ connection = @connection_provider.call
70
+ result = connection.exec_query(build_sql(connection))
71
+ current = {}
72
+
73
+ result.to_hashes.each do |row|
74
+ digest_text = (row["DIGEST_TEXT"] || row["digest_text"]).to_s
75
+ next if digest_text.empty?
76
+
77
+ calls = (row["COUNT_STAR"] || row["count_star"]).to_i
78
+ total_time_ms = (row["total_time_ms"] || row["TOTAL_TIME_MS"] || 0).to_f
79
+
80
+ current[digest_text] = { calls: calls, total_time_ms: total_time_ms }
81
+
82
+ next unless @previous.key?(digest_text)
83
+
84
+ record_delta(digest_text, calls, total_time_ms)
85
+ end
86
+
87
+ @previous = current
88
+ end
89
+
90
+ def record_delta(digest_text, calls, total_time_ms)
91
+ prev = @previous[digest_text]
92
+ delta_calls = [calls - prev[:calls], 0].max
93
+ delta_total_ms = [(total_time_ms - prev[:total_time_ms]).round(1), 0.0].max
94
+
95
+ @history.record(digest_text, {
96
+ timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
97
+ calls: delta_calls,
98
+ total_time_ms: delta_total_ms,
99
+ avg_time_ms: delta_calls.positive? ? (delta_total_ms / delta_calls).round(1) : 0.0,
100
+ })
101
+ end
102
+
103
+ def build_sql(connection)
104
+ <<~SQL
105
+ SELECT
106
+ DIGEST_TEXT,
107
+ COUNT_STAR,
108
+ ROUND(SUM_TIMER_WAIT / 1000000000, 1) AS total_time_ms
109
+ FROM performance_schema.events_statements_summary_by_digest
110
+ WHERE SCHEMA_NAME = #{connection.quote(connection.current_database)}
111
+ AND DIGEST_TEXT IS NOT NULL
112
+ AND DIGEST_TEXT NOT LIKE 'EXPLAIN%'
113
+ AND DIGEST_TEXT NOT LIKE '%`information_schema`%'
114
+ AND DIGEST_TEXT NOT LIKE '%`performance_schema`%'
115
+ AND DIGEST_TEXT NOT LIKE '%information_schema.%'
116
+ AND DIGEST_TEXT NOT LIKE '%performance_schema.%'
117
+ AND DIGEST_TEXT NOT LIKE 'SHOW %'
118
+ AND DIGEST_TEXT NOT LIKE 'SET STATEMENT %'
119
+ AND DIGEST_TEXT NOT LIKE 'SELECT VERSION ( )%'
120
+ AND DIGEST_TEXT NOT LIKE 'SELECT @@%'
121
+ ORDER BY SUM_TIMER_WAIT DESC
122
+ LIMIT #{TOP_N}
123
+ SQL
124
+ end
125
+
126
+ def wait_or_stop(seconds)
127
+ @mutex.synchronize do
128
+ return true if @stop_signal
129
+
130
+ @cv.wait(@mutex, seconds)
131
+ @stop_signal
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Analysis
6
+ # Thread-safe in-memory ring buffer that stores per-digest query stats
7
+ # snapshots. Each digest key maps to an array of snapshots capped at
8
+ # +max_samples+. Oldest entries are dropped when the cap is reached.
9
+ class StatsHistory
10
+ DEFAULT_MAX_SAMPLES = 1440
11
+
12
+ def initialize(max_samples: DEFAULT_MAX_SAMPLES)
13
+ @max_samples = max_samples
14
+ @mutex = Mutex.new
15
+ @data = {}
16
+ end
17
+
18
+ def record(digest_text, snapshot)
19
+ @mutex.synchronize do
20
+ buf = (@data[digest_text] ||= [])
21
+ buf << snapshot
22
+ buf.shift if buf.length > @max_samples
23
+ end
24
+ end
25
+
26
+ def series_for(digest_text)
27
+ @mutex.synchronize do
28
+ (@data[digest_text] || []).dup
29
+ end
30
+ end
31
+
32
+ def digests
33
+ @mutex.synchronize { @data.keys.dup }
34
+ end
35
+
36
+ def clear
37
+ @mutex.synchronize { @data.clear }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MysqlGenius
4
4
  module Core
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
@@ -47,6 +47,8 @@ require "mysql_genius/core/analysis/query_stats"
47
47
  require "mysql_genius/core/analysis/unused_indexes"
48
48
  require "mysql_genius/core/analysis/server_overview"
49
49
  require "mysql_genius/core/analysis/columns"
50
+ require "mysql_genius/core/analysis/stats_history"
51
+ require "mysql_genius/core/analysis/stats_collector"
50
52
  require "mysql_genius/core/execution_result"
51
53
  require "mysql_genius/core/query_runner/config"
52
54
  require "mysql_genius/core/query_runner"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
@@ -36,6 +36,8 @@ files:
36
36
  - lib/mysql_genius/core/analysis/duplicate_indexes.rb
37
37
  - lib/mysql_genius/core/analysis/query_stats.rb
38
38
  - lib/mysql_genius/core/analysis/server_overview.rb
39
+ - lib/mysql_genius/core/analysis/stats_collector.rb
40
+ - lib/mysql_genius/core/analysis/stats_history.rb
39
41
  - lib/mysql_genius/core/analysis/table_sizes.rb
40
42
  - lib/mysql_genius/core/analysis/unused_indexes.rb
41
43
  - lib/mysql_genius/core/column_definition.rb