mysql_genius-core 0.5.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/mysql_genius/core/analysis/query_stats.rb +2 -0
- data/lib/mysql_genius/core/analysis/stats_collector.rb +137 -0
- data/lib/mysql_genius/core/analysis/stats_history.rb +42 -0
- data/lib/mysql_genius/core/version.rb +1 -1
- data/lib/mysql_genius/core.rb +2 -0
- data/mysql_genius-core.gemspec +0 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ff33401fa2eda90123c44ab9e7dc63f685d44a65df87cef083052f05abe2f93
|
|
4
|
+
data.tar.gz: 995386078c2ec80cbf95e3002e9cf23b6a7b51fda4a6519496600de07cae1cb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7421f6cdac811fbadff2c9faf72b46bd513e51a5b1156c0723a228b45ecb19e8b02a0dfc1d49758deb71515b7899affbc808003ba3a224161f2f80d2a69486d
|
|
7
|
+
data.tar.gz: 5f413a66dfda47ea3bab363ccf49d10e65a1dbf0b1f4a76fa4c0df3544c04aae8282afcf629591b6cb6acbfbe7f5c48f33c444cc43bd420da5295bb994285efa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.6.0
|
|
14
|
+
|
|
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.
|
|
16
|
+
|
|
3
17
|
## 0.5.0
|
|
4
18
|
|
|
5
19
|
### Added
|
|
@@ -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
|
data/lib/mysql_genius/core.rb
CHANGED
|
@@ -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"
|
data/mysql_genius-core.gemspec
CHANGED
|
@@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.required_ruby_version = ">= 2.7.0"
|
|
21
21
|
|
|
22
22
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
23
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
24
23
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/gems/mysql_genius-core/CHANGELOG.md"
|
|
25
24
|
|
|
26
25
|
spec.files = Dir.chdir(__dir__) do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mysql_genius-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antarr Byrd
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Shared library used by the mysql_genius Rails engine and the mysql_genius-desktop
|
|
14
14
|
standalone app. Contains the SQL validator, query runner, database analyses, and
|
|
@@ -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
|
|
@@ -56,7 +58,6 @@ licenses:
|
|
|
56
58
|
- MIT
|
|
57
59
|
metadata:
|
|
58
60
|
homepage_uri: https://github.com/antarr/mysql_genius
|
|
59
|
-
source_code_uri: https://github.com/antarr/mysql_genius
|
|
60
61
|
changelog_uri: https://github.com/antarr/mysql_genius/blob/main/gems/mysql_genius-core/CHANGELOG.md
|
|
61
62
|
post_install_message:
|
|
62
63
|
rdoc_options: []
|