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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f62ad896d11551538f57fe41808b243b3484b99e6271ce3aa33610794a71c96
4
- data.tar.gz: f7bd71569888157686b8a3f605ea7be8286d3967197789a55b5dc45f361f9170
3
+ metadata.gz: 4b50326ed51ec5388f5f63528cbfe0cda2f27db4e505b0cdf1349f1cc9313311
4
+ data.tar.gz: e76ddcd08800005f89ddb772953df045ad623fcfc13e114090bdfcdd49c0dabb
5
5
  SHA512:
6
- metadata.gz: f678080160f435018f4a2c4e867e0363ef4e386fed008b94542ca3a369a19f25e4ce718bf49e1e8cc24c12b5a7b12a0071395ab94128d8573c56af52abcdbae7
7
- data.tar.gz: 7cc8e7b73904fcf7dce92dcae85cc5f338c96ae0a7e52ae80b519c5d1cf1875450e3fdaf062e09c905dd52073efb889751e63ebba62343ce30a5fac64964d16d
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: 100
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(nil)
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
- WorkMem.all.each do |value|
26
- database.transaction do
27
- database.run("SET LOCAL work_mem = '#{value.to_sql}'")
26
+ container = Container.new(code)
27
+ results = {}
28
28
 
29
- eval(code, binding, script)
29
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
30
+ # @tobias is thinking...
31
+ MARKDOWN
32
+ puts parsed
30
33
 
31
- @queries.each do |name, block|
32
- database.select(Sequel.function(:pg_stat_reset)).first
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
- stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
38
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
39
+ # @tobias has sent you a new message
45
40
 
46
- puts "--------------------------------"
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
- if stats[:temp_files] > 0 || stats[:temp_bytes] > 0
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
- private
45
+ Regards,
46
+ ~ Tobias
47
+ MARKDOWN
68
48
 
69
- def query(name, &block)
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tobias
4
+ module Evaluations
5
+ def self.run(database, container, options)
6
+ results = []
7
+ results << WorkMem.new(database, container).run(options)
8
+ results
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tobias
4
- VERSION = "0.1.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
@@ -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 :Evaluation, "tobias/evaluation"
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.1.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-03 00:00:00.000000000 Z
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: rspec
213
+ name: tty-markdown
214
214
  requirement: !ruby/object:Gem::Requirement
215
215
  requirements:
216
216
  - - "~>"
217
217
  - !ruby/object:Gem::Version
218
- version: '3.12'
219
- type: :development
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: '3.12'
226
- description: Tobias
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/evaluation.rb
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: []
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tobias
4
- class Evaluation
5
- def initialize(database)
6
- @database = database
7
- end
8
- end
9
- end