tobias 0.1.0 → 0.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/lib/tobias/cli.rb +20 -43
- data/lib/tobias/container.rb +20 -0
- data/lib/tobias/evaluations/base.rb +32 -0
- data/lib/tobias/evaluations/work_mem.rb +63 -0
- data/lib/tobias/evaluations.rb +11 -0
- data/lib/tobias/version.rb +3 -1
- data/lib/tobias/work_mem.rb +72 -2
- data/lib/tobias.rb +7 -2
- metadata +18 -9
- data/lib/tobias/evaluation.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b50326ed51ec5388f5f63528cbfe0cda2f27db4e505b0cdf1349f1cc9313311
|
4
|
+
data.tar.gz: e76ddcd08800005f89ddb772953df045ad623fcfc13e114090bdfcdd49c0dabb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 456a7a52310fcac42a5755ad02aeb464bb429742888a195569fb5d5df7f33416d583cef442eca92b035af9807e7ec5d17c626b0eda20b9d43998fc4c99ccb6f3
|
7
|
+
data.tar.gz: e8c5d112db7cd2e87f37455d90b25a6dfe53eba27e60a492c4fbc4798a941bdc9b69532b813eb39f4c69b68c34812e09112ec19a80485f90e526869e2d0c1794
|
data/lib/tobias/cli.rb
CHANGED
@@ -10,10 +10,11 @@ module Tobias
|
|
10
10
|
|
11
11
|
desc "profile SCRIPT", "profile"
|
12
12
|
option :database_url, type: :string, required: true
|
13
|
-
option :iterations, type: :numeric, default:
|
13
|
+
option :iterations, type: :numeric, default: 10
|
14
|
+
option :debug, type: :boolean, default: false
|
14
15
|
def profile(script)
|
15
16
|
database = Sequel.connect(options[:database_url])
|
16
|
-
database.loggers << Logger.new(
|
17
|
+
database.loggers << Logger.new(STDERR) if options[:debug]
|
17
18
|
database.extension :pg_json
|
18
19
|
|
19
20
|
if File.exist?(script)
|
@@ -22,54 +23,30 @@ module Tobias
|
|
22
23
|
raise "Script not found at: #{script}"
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
database.run("SET LOCAL work_mem = '#{value.to_sql}'")
|
26
|
+
container = Container.new(code)
|
27
|
+
results = {}
|
28
28
|
|
29
|
-
|
29
|
+
parsed = TTY::Markdown.parse(<<~MARKDOWN)
|
30
|
+
# @tobias is thinking...
|
31
|
+
MARKDOWN
|
32
|
+
puts parsed
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
query = instance_eval(&block)
|
35
|
-
times = []
|
36
|
-
|
37
|
-
options[:iterations].to_i.times do
|
38
|
-
time = Benchmark.realtime do
|
39
|
-
database.run(query.sql)
|
40
|
-
end
|
41
|
-
times << time
|
42
|
-
end
|
34
|
+
thinking_time = Benchmark.realtime do
|
35
|
+
results = Evaluations.run(database, container, options)
|
36
|
+
end
|
43
37
|
|
44
|
-
|
38
|
+
parsed = TTY::Markdown.parse(<<~MARKDOWN)
|
39
|
+
# @tobias has sent you a new message
|
45
40
|
|
46
|
-
|
47
|
-
puts "query: #{name}"
|
48
|
-
puts "work_mem: #{value.to_sql}"
|
49
|
-
puts "clock time (mean): #{times.mean.round(2)}"
|
50
|
-
puts "clock time (95%): #{times.percentile(95).round(2)}"
|
41
|
+
I thought about your queries for precisely #{thinking_time.round(2)} seconds and here is what I recommend:
|
51
42
|
|
52
|
-
|
53
|
-
puts "Not enough work_mem"
|
54
|
-
puts "temp_files: #{stats[:temp_files]}"
|
55
|
-
puts "temp_bytes: #{stats[:temp_bytes]}"
|
56
|
-
else
|
57
|
-
puts "No temporary files written."
|
58
|
-
puts "Current work_mem: '#{value.to_sql}' is sufficient."
|
59
|
-
return
|
60
|
-
end
|
61
|
-
puts "--------------------------------"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
43
|
+
#{results.join("\n")}
|
66
44
|
|
67
|
-
|
45
|
+
Regards,
|
46
|
+
~ Tobias
|
47
|
+
MARKDOWN
|
68
48
|
|
69
|
-
|
70
|
-
@queries ||= {}
|
71
|
-
@queries[name] = block
|
49
|
+
puts parsed
|
72
50
|
end
|
73
|
-
|
74
51
|
end
|
75
52
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobias
|
4
|
+
class Container
|
5
|
+
def initialize(code)
|
6
|
+
@code = code
|
7
|
+
@queries = {}
|
8
|
+
|
9
|
+
eval(code, binding, __FILE__, __LINE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
def queries
|
13
|
+
@queries
|
14
|
+
end
|
15
|
+
|
16
|
+
def query(name, &block)
|
17
|
+
@queries[name] = block
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobias
|
4
|
+
module Evaluations
|
5
|
+
class Base
|
6
|
+
attr_reader :database, :container
|
7
|
+
|
8
|
+
def initialize(database, container)
|
9
|
+
@database = database
|
10
|
+
@container = container
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(options, &block)
|
14
|
+
results = {}
|
15
|
+
|
16
|
+
container.queries.each do |name, query|
|
17
|
+
results.merge!(run_each(name, query, options))
|
18
|
+
end
|
19
|
+
|
20
|
+
to_markdown(results)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_each(query, options)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_markdown(results)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobias
|
4
|
+
module Evaluations
|
5
|
+
class WorkMem < Base
|
6
|
+
|
7
|
+
def work_mems
|
8
|
+
Tobias::WorkMem.valid_for(database)
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
"Optional work_mem settings"
|
13
|
+
end
|
14
|
+
|
15
|
+
def run_each(name, query, options)
|
16
|
+
work_mems.each do |value|
|
17
|
+
database.transaction do
|
18
|
+
database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
19
|
+
database.run("SET LOCAL work_mem = '#{value.to_sql}'")
|
20
|
+
database.select(Sequel.function(:pg_stat_reset)).first
|
21
|
+
|
22
|
+
query_result = database.instance_eval(&query)
|
23
|
+
options[:iterations].to_i.times do
|
24
|
+
database.run(query_result.sql)
|
25
|
+
end
|
26
|
+
|
27
|
+
stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
|
28
|
+
|
29
|
+
if stats[:temp_files] == 0 && stats[:temp_bytes] == 0
|
30
|
+
return { name => value }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: Add a warning message or something.
|
36
|
+
return { name => nil }
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_markdown(results)
|
40
|
+
current_work_mem = Tobias::WorkMem.from_sql(database.fetch("SHOW work_mem").first[:work_mem])
|
41
|
+
|
42
|
+
<<~MARKDOWN
|
43
|
+
## #{description}
|
44
|
+
|
45
|
+
| Query | Required work_mem |
|
46
|
+
|-------|-------------------|
|
47
|
+
#{results.map { |name, work_mem| "| #{name} | #{work_mem.to_sql} |" }.join("\n")}
|
48
|
+
|
49
|
+
Your application will need to run with at least #{results.values.max.to_sql} of work_mem.
|
50
|
+
|
51
|
+
I see that your current work_mem setting is #{current_work_mem.to_sql}.
|
52
|
+
|
53
|
+
To apply my recommendations, run the following SQL:
|
54
|
+
|
55
|
+
```sql
|
56
|
+
ALTER SYSTEM SET work_mem = '#{results.values.max.to_sql}';
|
57
|
+
SELECT pg_reload_conf();
|
58
|
+
```
|
59
|
+
MARKDOWN
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/tobias/version.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Tobias
|
4
|
-
VERSION = "0.
|
4
|
+
VERSION = "0.3.0"
|
5
|
+
SUMMARY = "Tobias is a tool to help you find the optimal work_mem for your queries."
|
6
|
+
DESCRIPTION = "Tobias is a tool to help you find the optimal work_mem for your queries."
|
5
7
|
end
|
data/lib/tobias/work_mem.rb
CHANGED
@@ -2,10 +2,47 @@
|
|
2
2
|
|
3
3
|
module Tobias
|
4
4
|
class WorkMem
|
5
|
+
attr_reader :amount
|
6
|
+
|
7
|
+
def self.from_sql(sql)
|
8
|
+
case sql
|
9
|
+
when /^\d+B$/
|
10
|
+
new(sql.to_i)
|
11
|
+
when /^\d+kB$/
|
12
|
+
new(sql.to_i * 1024)
|
13
|
+
when /^\d+MB$/
|
14
|
+
new(sql.to_i * 1024 * 1024)
|
15
|
+
when /^\d+GB$/
|
16
|
+
new(sql.to_i * 1024 * 1024 * 1024)
|
17
|
+
else
|
18
|
+
raise "Invalid work_mem setting: #{sql}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
5
22
|
def initialize(amount)
|
6
23
|
@amount = amount
|
7
24
|
end
|
8
25
|
|
26
|
+
def >(other)
|
27
|
+
@amount > other.amount
|
28
|
+
end
|
29
|
+
|
30
|
+
def <(other)
|
31
|
+
@amount < other.amount
|
32
|
+
end
|
33
|
+
|
34
|
+
def >=(other)
|
35
|
+
@amount >= other.amount
|
36
|
+
end
|
37
|
+
|
38
|
+
def <=(other)
|
39
|
+
@amount <= other.amount
|
40
|
+
end
|
41
|
+
|
42
|
+
def <=>(other)
|
43
|
+
@amount <=> other.amount
|
44
|
+
end
|
45
|
+
|
9
46
|
def to_sql
|
10
47
|
case @amount
|
11
48
|
when 0...1024
|
@@ -30,6 +67,7 @@ module Tobias
|
|
30
67
|
[
|
31
68
|
new(64.kilobytes),
|
32
69
|
new(128.kilobytes),
|
70
|
+
new(256.kilobytes),
|
33
71
|
new(512.kilobytes),
|
34
72
|
new(1.megabyte),
|
35
73
|
new(4.megabytes),
|
@@ -44,7 +82,39 @@ module Tobias
|
|
44
82
|
new(2.gigabytes),
|
45
83
|
new(4.gigabytes),
|
46
84
|
new(8.gigabytes),
|
47
|
-
]
|
85
|
+
].sort_by(&:amount)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Inspects the database to determine the valid work_mem settings for the current user.
|
89
|
+
# We'll need at least connection_limit / effective_cache_size for an optimal
|
90
|
+
# work_mem setting, otherwise a user could run out of memory at max connections.
|
91
|
+
def self.valid_for(database)
|
92
|
+
role_conn_limit = database.select(:rolconnlimit).
|
93
|
+
from(:pg_roles).
|
94
|
+
where(rolname: Sequel.lit("current_user")).
|
95
|
+
first
|
96
|
+
|
97
|
+
max_connections = database.select(:setting).
|
98
|
+
from(:pg_settings).
|
99
|
+
where(name: "max_connections").
|
100
|
+
first
|
101
|
+
|
102
|
+
effective_cache_size = database.select(:setting, :unit).
|
103
|
+
from(:pg_settings).
|
104
|
+
where(name: "effective_cache_size").
|
105
|
+
first
|
106
|
+
|
107
|
+
effective_cache_size_bytes = effective_cache_size[:setting].to_i * 8 * 1024
|
108
|
+
|
109
|
+
connection_limit = if role_conn_limit[:rolconnlimit] > 0
|
110
|
+
role_conn_limit[:rolconnlimit]
|
111
|
+
else
|
112
|
+
max_connections[:setting].to_i
|
113
|
+
end
|
114
|
+
|
115
|
+
bytes_per_connection = effective_cache_size_bytes / connection_limit
|
116
|
+
|
117
|
+
self.all.select { |work_mem| work_mem.amount < bytes_per_connection.to_i }
|
48
118
|
end
|
49
119
|
end
|
50
|
-
end
|
120
|
+
end
|
data/lib/tobias.rb
CHANGED
@@ -19,11 +19,16 @@ require "active_support/all"
|
|
19
19
|
require "sequel"
|
20
20
|
require "enumerable-stats"
|
21
21
|
require "benchmark"
|
22
|
+
require "tty-markdown"
|
22
23
|
|
23
24
|
$LOAD_PATH.unshift File.dirname(__FILE__)
|
24
25
|
|
26
|
+
require "tobias/evaluations"
|
27
|
+
require "tobias/evaluations/base"
|
28
|
+
require "tobias/evaluations/work_mem"
|
29
|
+
|
25
30
|
module Tobias
|
26
31
|
autoload :CLI, "tobias/cli"
|
27
|
-
autoload :
|
32
|
+
autoload :Container, "tobias/container"
|
28
33
|
autoload :WorkMem, "tobias/work_mem"
|
29
|
-
end
|
34
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tobias
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Daniel
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
10
|
+
date: 2025-08-04 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|
@@ -210,20 +210,26 @@ dependencies:
|
|
210
210
|
- !ruby/object:Gem::Version
|
211
211
|
version: 1.3.0
|
212
212
|
- !ruby/object:Gem::Dependency
|
213
|
-
name:
|
213
|
+
name: tty-markdown
|
214
214
|
requirement: !ruby/object:Gem::Requirement
|
215
215
|
requirements:
|
216
216
|
- - "~>"
|
217
217
|
- !ruby/object:Gem::Version
|
218
|
-
version: '
|
219
|
-
|
218
|
+
version: '0.7'
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: 0.7.0
|
222
|
+
type: :runtime
|
220
223
|
prerelease: false
|
221
224
|
version_requirements: !ruby/object:Gem::Requirement
|
222
225
|
requirements:
|
223
226
|
- - "~>"
|
224
227
|
- !ruby/object:Gem::Version
|
225
|
-
version: '
|
226
|
-
|
228
|
+
version: '0.7'
|
229
|
+
- - ">="
|
230
|
+
- !ruby/object:Gem::Version
|
231
|
+
version: 0.7.0
|
232
|
+
description: Tobias is a tool to help you find the optimal work_mem for your queries.
|
227
233
|
email: binarycleric@gmail.com
|
228
234
|
executables:
|
229
235
|
- tobias
|
@@ -233,7 +239,10 @@ files:
|
|
233
239
|
- bin/tobias
|
234
240
|
- lib/tobias.rb
|
235
241
|
- lib/tobias/cli.rb
|
236
|
-
- lib/tobias/
|
242
|
+
- lib/tobias/container.rb
|
243
|
+
- lib/tobias/evaluations.rb
|
244
|
+
- lib/tobias/evaluations/base.rb
|
245
|
+
- lib/tobias/evaluations/work_mem.rb
|
237
246
|
- lib/tobias/version.rb
|
238
247
|
- lib/tobias/work_mem.rb
|
239
248
|
homepage: https://github.com/binarycleric/tobias
|
@@ -258,5 +267,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
258
267
|
requirements: []
|
259
268
|
rubygems_version: 3.6.2
|
260
269
|
specification_version: 4
|
261
|
-
summary: Tobias
|
270
|
+
summary: Tobias is a tool to help you find the optimal work_mem for your queries.
|
262
271
|
test_files: []
|