tobias 0.3.0 → 0.4.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/bin/tobias +4 -1
- data/lib/tobias/cli.rb +35 -3
- data/lib/tobias/container.rb +79 -4
- data/lib/tobias/evaluations/base.rb +38 -6
- data/lib/tobias/evaluations/work_mem.rb +20 -22
- data/lib/tobias/evaluations.rb +1 -1
- data/lib/tobias/version.rb +1 -1
- data/lib/tobias.rb +7 -0
- metadata +86 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e8024f2a5efa0e4ffe41d2973817f1906fccfb32193782b86e55f822a82f5e7
|
4
|
+
data.tar.gz: 5b6b27888ed7341c76fea8503bc8e577662949903089970930fced41367583e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e289f868dad239629020799f64c05be1f5658f2facd905d917ce162bcccbefc957e979c19bdb36ec37e1c21eb89ae26d7b2f2a458f11b8d1d335eb5b9a9a37c
|
7
|
+
data.tar.gz: fece84c5f4c2d65961c091ee4bb7e2879b43646e60a8c470079d20529541757e0e61d9f4a2163e0615660c38b62c758504bdde9009a35e426f0578d598089468
|
data/bin/tobias
CHANGED
data/lib/tobias/cli.rb
CHANGED
@@ -8,14 +8,46 @@ module Tobias
|
|
8
8
|
true
|
9
9
|
end
|
10
10
|
|
11
|
+
desc "recommend", "recommend a work_mem setting for a database"
|
12
|
+
option :database_url, type: :string, required: true
|
13
|
+
option :debug, type: :boolean, default: false
|
14
|
+
def recommend
|
15
|
+
database = Sequel.connect(options[:database_url])
|
16
|
+
database.extension :pgvector
|
17
|
+
database.loggers << Logger.new(STDERR) if options[:debug]
|
18
|
+
|
19
|
+
parsed = TTY::Markdown.parse(<<~MARKDOWN)
|
20
|
+
# @tobias is thinking...
|
21
|
+
MARKDOWN
|
22
|
+
puts parsed
|
23
|
+
|
24
|
+
work_mem = Tobias::WorkMem.valid_for(database).sort_by(&:amount).reverse.first
|
25
|
+
|
26
|
+
parsed = TTY::Markdown.parse(<<~MARKDOWN)
|
27
|
+
# @tobias has sent you a new message
|
28
|
+
|
29
|
+
I've reviewed your database by analyzing your shared buffers and connection limits and
|
30
|
+
recommend setting `work_mem` to `#{work_mem.to_sql}`. To apply my recommendation, run the following SQL:
|
31
|
+
|
32
|
+
```sql
|
33
|
+
ALTER SYSTEM SET work_mem = '#{work_mem.to_sql}';
|
34
|
+
SELECT pg_reload_conf();
|
35
|
+
```
|
36
|
+
|
37
|
+
Regards,
|
38
|
+
~ Tobias
|
39
|
+
MARKDOWN
|
40
|
+
puts parsed
|
41
|
+
end
|
42
|
+
|
11
43
|
desc "profile SCRIPT", "profile"
|
12
44
|
option :database_url, type: :string, required: true
|
13
|
-
option :iterations, type: :numeric, default: 10
|
14
45
|
option :debug, type: :boolean, default: false
|
15
46
|
def profile(script)
|
16
|
-
database = Sequel.connect(options[:database_url])
|
47
|
+
database = Sequel.connect(options[:database_url], max_connections: Etc.nprocessors + 2)
|
17
48
|
database.loggers << Logger.new(STDERR) if options[:debug]
|
18
49
|
database.extension :pg_json
|
50
|
+
database.extension :pgvector
|
19
51
|
|
20
52
|
if File.exist?(script)
|
21
53
|
code = File.read(script)
|
@@ -23,7 +55,7 @@ module Tobias
|
|
23
55
|
raise "Script not found at: #{script}"
|
24
56
|
end
|
25
57
|
|
26
|
-
container = Container.new(code)
|
58
|
+
container = Container.new(code, database)
|
27
59
|
results = {}
|
28
60
|
|
29
61
|
parsed = TTY::Markdown.parse(<<~MARKDOWN)
|
data/lib/tobias/container.rb
CHANGED
@@ -1,20 +1,95 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "open3"
|
4
|
+
|
3
5
|
module Tobias
|
4
6
|
class Container
|
5
|
-
def initialize(code)
|
7
|
+
def initialize(code, database)
|
6
8
|
@code = code
|
7
|
-
@
|
9
|
+
@database = database
|
10
|
+
@queries = Concurrent::Hash.new
|
11
|
+
@sql = Concurrent::Hash.new
|
12
|
+
@options = Concurrent::Hash.new
|
13
|
+
@setup = Proc.new { }
|
14
|
+
@teardown = Proc.new { }
|
15
|
+
@load_data = Proc.new { }
|
16
|
+
@helpers = Module.new
|
8
17
|
|
9
18
|
eval(code, binding, __FILE__, __LINE__)
|
10
19
|
end
|
11
20
|
|
21
|
+
module DefaultHelpers
|
22
|
+
def db
|
23
|
+
@database
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_parallel(list = Etc.nprocessors.times, &block)
|
27
|
+
db.disconnect
|
28
|
+
|
29
|
+
Parallel.each(list, in_processes: Etc.nprocessors) do |item|
|
30
|
+
instance_exec(item, &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_setup
|
36
|
+
@database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
37
|
+
run_action(@setup)
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_query(query)
|
41
|
+
@database.run(run_action(query).sql)
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_teardown
|
45
|
+
run_action(@teardown)
|
46
|
+
end
|
47
|
+
|
48
|
+
def options
|
49
|
+
Struct.new(*@options.keys).new(*@options.values)
|
50
|
+
end
|
51
|
+
|
52
|
+
def run_action(action)
|
53
|
+
helpers = @helpers
|
54
|
+
|
55
|
+
class_eval do
|
56
|
+
include DefaultHelpers
|
57
|
+
include helpers
|
58
|
+
end
|
59
|
+
|
60
|
+
instance_eval(&action)
|
61
|
+
end
|
62
|
+
|
12
63
|
def queries
|
13
64
|
@queries
|
14
65
|
end
|
15
66
|
|
16
|
-
def
|
17
|
-
@
|
67
|
+
def option(name, default = nil, &block)
|
68
|
+
@options[name] = block || default
|
69
|
+
end
|
70
|
+
|
71
|
+
def helpers(&block)
|
72
|
+
@helpers.class_eval(&block) if block_given?
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup(&block)
|
76
|
+
@setup = block
|
77
|
+
end
|
78
|
+
|
79
|
+
def teardown(&block)
|
80
|
+
@teardown = block
|
81
|
+
end
|
82
|
+
|
83
|
+
def load_data(&block)
|
84
|
+
@load_data = block
|
85
|
+
end
|
86
|
+
|
87
|
+
def query(name, sql = nil, &block)
|
88
|
+
if sql.is_a?(String)
|
89
|
+
@queries[name] = Proc.new { sql }
|
90
|
+
else
|
91
|
+
@queries[name] = block || Proc.new { raise "No SQL provided for query '#{name}'" }
|
92
|
+
end
|
18
93
|
end
|
19
94
|
end
|
20
95
|
end
|
@@ -1,32 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
class MarkdownTableBorder < TTY::Table::Border
|
4
|
+
def_border do
|
5
|
+
left "|"
|
6
|
+
center "|"
|
7
|
+
right "|"
|
8
|
+
bottom " "
|
9
|
+
bottom_mid " "
|
10
|
+
bottom_left " "
|
11
|
+
bottom_right " "
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
module Tobias
|
4
16
|
module Evaluations
|
17
|
+
Result = Struct.new(:name, :value, keyword_init: true) do
|
18
|
+
def <=>(other)
|
19
|
+
value <=> other.value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
5
23
|
class Base
|
6
|
-
attr_reader :database, :container
|
24
|
+
attr_reader :database, :container, :options
|
7
25
|
|
8
|
-
def initialize(database, container)
|
26
|
+
def initialize(database, container, options)
|
9
27
|
@database = database
|
10
28
|
@container = container
|
29
|
+
@options = options
|
11
30
|
end
|
12
31
|
|
13
|
-
def run(
|
14
|
-
results =
|
32
|
+
def run(&block)
|
33
|
+
results = Concurrent::Array.new
|
15
34
|
|
35
|
+
container.run_setup
|
16
36
|
container.queries.each do |name, query|
|
17
|
-
|
37
|
+
result = run_each(name, query)
|
38
|
+
results << result if result
|
18
39
|
end
|
19
40
|
|
20
41
|
to_markdown(results)
|
42
|
+
ensure
|
43
|
+
container.run_teardown
|
21
44
|
end
|
22
45
|
|
23
|
-
def run_each(query
|
46
|
+
def run_each(query)
|
24
47
|
raise NotImplementedError
|
25
48
|
end
|
26
49
|
|
27
50
|
def to_markdown(results)
|
28
51
|
raise NotImplementedError
|
29
52
|
end
|
53
|
+
|
54
|
+
def render_table(headers:, body:)
|
55
|
+
table = TTY::Table.new(header: headers)
|
56
|
+
body.each do |row|
|
57
|
+
table << row
|
58
|
+
end
|
59
|
+
|
60
|
+
table.render_with(MarkdownTableBorder)
|
61
|
+
end
|
30
62
|
end
|
31
63
|
end
|
32
64
|
end
|
@@ -3,57 +3,55 @@
|
|
3
3
|
module Tobias
|
4
4
|
module Evaluations
|
5
5
|
class WorkMem < Base
|
6
|
-
|
7
6
|
def work_mems
|
8
|
-
Tobias::WorkMem.valid_for(database)
|
7
|
+
@work_mems ||= Tobias::WorkMem.valid_for(database)
|
8
|
+
end
|
9
|
+
|
10
|
+
def current_work_mem
|
11
|
+
@current_work_mem ||= Tobias::WorkMem.from_sql(database.fetch("SHOW work_mem").first[:work_mem])
|
9
12
|
end
|
10
13
|
|
11
14
|
def description
|
12
|
-
"
|
15
|
+
"Optimal work_mem settings"
|
13
16
|
end
|
14
17
|
|
15
|
-
def run_each(name, query
|
18
|
+
def run_each(name, query)
|
19
|
+
database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
20
|
+
|
16
21
|
work_mems.each do |value|
|
17
22
|
database.transaction do
|
18
|
-
database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
19
23
|
database.run("SET LOCAL work_mem = '#{value.to_sql}'")
|
20
24
|
database.select(Sequel.function(:pg_stat_reset)).first
|
25
|
+
container.run_query(query)
|
21
26
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
|
27
|
+
stats = database[:pg_stat_database].
|
28
|
+
where(datname: Sequel.function(:current_database)).
|
29
|
+
first
|
28
30
|
|
29
31
|
if stats[:temp_files] == 0 && stats[:temp_bytes] == 0
|
30
|
-
return
|
32
|
+
return Result.new(name: name, value: value)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
|
-
#
|
36
|
-
|
37
|
+
# Fallback to the highest work_mem setting if no results are found.
|
38
|
+
Result.new(name: name, value: work_mems.last)
|
37
39
|
end
|
38
40
|
|
39
41
|
def to_markdown(results)
|
40
|
-
current_work_mem = Tobias::WorkMem.from_sql(database.fetch("SHOW work_mem").first[:work_mem])
|
41
|
-
|
42
42
|
<<~MARKDOWN
|
43
43
|
## #{description}
|
44
44
|
|
45
|
-
|
46
|
-
|-------|-------------------|
|
47
|
-
#{results.map { |name, work_mem| "| #{name} | #{work_mem.to_sql} |" }.join("\n")}
|
45
|
+
#{render_table(headers: ["Query", "Required work_mem"], body: results.map { |r| [r.name, r.value.to_sql] })}
|
48
46
|
|
49
|
-
|
47
|
+
I see that your current `work_mem` setting is `#{current_work_mem.to_sql}`.
|
50
48
|
|
51
|
-
|
49
|
+
Your application will need to run with at least `#{results.max.value.to_sql}` of `work_mem`.
|
52
50
|
|
53
51
|
To apply my recommendations, run the following SQL:
|
54
52
|
|
55
53
|
```sql
|
56
|
-
ALTER SYSTEM SET work_mem = '#{results.
|
54
|
+
ALTER SYSTEM SET work_mem = '#{results.max.value.to_sql}';
|
57
55
|
SELECT pg_reload_conf();
|
58
56
|
```
|
59
57
|
MARKDOWN
|
data/lib/tobias/evaluations.rb
CHANGED
data/lib/tobias/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Tobias
|
4
|
-
VERSION = "0.
|
4
|
+
VERSION = "0.4.0"
|
5
5
|
SUMMARY = "Tobias is a tool to help you find the optimal work_mem for your queries."
|
6
6
|
DESCRIPTION = "Tobias is a tool to help you find the optimal work_mem for your queries."
|
7
7
|
end
|
data/lib/tobias.rb
CHANGED
@@ -11,15 +11,22 @@ if RUBY_ENGINE == "ruby"
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
# See: https://github.com/ged/ruby-pg/issues/538#issuecomment-1591629049
|
15
|
+
ENV["PGGSSENCMODE"] = "disable"
|
16
|
+
|
14
17
|
require "bundler/setup"
|
15
18
|
Bundler.require(:default)
|
16
19
|
|
17
20
|
require "thor"
|
18
21
|
require "active_support/all"
|
19
22
|
require "sequel"
|
23
|
+
require "pgvector"
|
20
24
|
require "enumerable-stats"
|
21
25
|
require "benchmark"
|
26
|
+
require "parquet"
|
27
|
+
require "parallel"
|
22
28
|
require "tty-markdown"
|
29
|
+
require "tty-table"
|
23
30
|
|
24
31
|
$LOAD_PATH.unshift File.dirname(__FILE__)
|
25
32
|
|
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tobias
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Daniel
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-23 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: activesupport
|
@@ -189,6 +190,66 @@ dependencies:
|
|
189
190
|
- - ">="
|
190
191
|
- !ruby/object:Gem::Version
|
191
192
|
version: 5.76.0
|
193
|
+
- !ruby/object:Gem::Dependency
|
194
|
+
name: parquet
|
195
|
+
requirement: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - "~>"
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0.7'
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: 0.7.3
|
203
|
+
type: :runtime
|
204
|
+
prerelease: false
|
205
|
+
version_requirements: !ruby/object:Gem::Requirement
|
206
|
+
requirements:
|
207
|
+
- - "~>"
|
208
|
+
- !ruby/object:Gem::Version
|
209
|
+
version: '0.7'
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: 0.7.3
|
213
|
+
- !ruby/object:Gem::Dependency
|
214
|
+
name: pgvector
|
215
|
+
requirement: !ruby/object:Gem::Requirement
|
216
|
+
requirements:
|
217
|
+
- - "~>"
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: '0.3'
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: 0.3.0
|
223
|
+
type: :runtime
|
224
|
+
prerelease: false
|
225
|
+
version_requirements: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0.3'
|
230
|
+
- - ">="
|
231
|
+
- !ruby/object:Gem::Version
|
232
|
+
version: 0.3.0
|
233
|
+
- !ruby/object:Gem::Dependency
|
234
|
+
name: parallel
|
235
|
+
requirement: !ruby/object:Gem::Requirement
|
236
|
+
requirements:
|
237
|
+
- - "~>"
|
238
|
+
- !ruby/object:Gem::Version
|
239
|
+
version: '1.20'
|
240
|
+
- - ">="
|
241
|
+
- !ruby/object:Gem::Version
|
242
|
+
version: 1.20.0
|
243
|
+
type: :runtime
|
244
|
+
prerelease: false
|
245
|
+
version_requirements: !ruby/object:Gem::Requirement
|
246
|
+
requirements:
|
247
|
+
- - "~>"
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: '1.20'
|
250
|
+
- - ">="
|
251
|
+
- !ruby/object:Gem::Version
|
252
|
+
version: 1.20.0
|
192
253
|
- !ruby/object:Gem::Dependency
|
193
254
|
name: thor
|
194
255
|
requirement: !ruby/object:Gem::Requirement
|
@@ -229,6 +290,26 @@ dependencies:
|
|
229
290
|
- - ">="
|
230
291
|
- !ruby/object:Gem::Version
|
231
292
|
version: 0.7.0
|
293
|
+
- !ruby/object:Gem::Dependency
|
294
|
+
name: tty-table
|
295
|
+
requirement: !ruby/object:Gem::Requirement
|
296
|
+
requirements:
|
297
|
+
- - "~>"
|
298
|
+
- !ruby/object:Gem::Version
|
299
|
+
version: '0.12'
|
300
|
+
- - ">="
|
301
|
+
- !ruby/object:Gem::Version
|
302
|
+
version: 0.12.0
|
303
|
+
type: :runtime
|
304
|
+
prerelease: false
|
305
|
+
version_requirements: !ruby/object:Gem::Requirement
|
306
|
+
requirements:
|
307
|
+
- - "~>"
|
308
|
+
- !ruby/object:Gem::Version
|
309
|
+
version: '0.12'
|
310
|
+
- - ">="
|
311
|
+
- !ruby/object:Gem::Version
|
312
|
+
version: 0.12.0
|
232
313
|
description: Tobias is a tool to help you find the optimal work_mem for your queries.
|
233
314
|
email: binarycleric@gmail.com
|
234
315
|
executables:
|
@@ -251,6 +332,7 @@ licenses:
|
|
251
332
|
metadata:
|
252
333
|
source_code_uri: https://github.com/binarycleric/tobias
|
253
334
|
rubygems_mfa_required: 'true'
|
335
|
+
post_install_message:
|
254
336
|
rdoc_options: []
|
255
337
|
require_paths:
|
256
338
|
- lib
|
@@ -265,7 +347,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
265
347
|
- !ruby/object:Gem::Version
|
266
348
|
version: '0'
|
267
349
|
requirements: []
|
268
|
-
rubygems_version: 3.
|
350
|
+
rubygems_version: 3.5.22
|
351
|
+
signing_key:
|
269
352
|
specification_version: 4
|
270
353
|
summary: Tobias is a tool to help you find the optimal work_mem for your queries.
|
271
354
|
test_files: []
|