tobias 0.1.0 → 0.2.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: 342518e2c3730a589319c0d16967f7e9fdc80affde011c3820ef2c1034fcbc3e
4
+ data.tar.gz: adcdc3152a07d4e1203ad6ceff26901e999f5a8110e883675aa20c9e894ae932
5
5
  SHA512:
6
- metadata.gz: f678080160f435018f4a2c4e867e0363ef4e386fed008b94542ca3a369a19f25e4ce718bf49e1e8cc24c12b5a7b12a0071395ab94128d8573c56af52abcdbae7
7
- data.tar.gz: 7cc8e7b73904fcf7dce92dcae85cc5f338c96ae0a7e52ae80b519c5d1cf1875450e3fdaf062e09c905dd52073efb889751e63ebba62343ce30a5fac64964d16d
6
+ metadata.gz: af1341883626cda785ba564368baad44e0cec9e55751bb7c00d204b1e12cf32ebc3817002f2c7fd81e7b57a6ddd2a78be74b11fa0e1f3f837741552f8064f338
7
+ data.tar.gz: 58d79c4c82d5bf1cd21e42c6770dcc454c52bfab5a22a5fa1d15ed6dfe8b3abfd340d7a31b07c6205b7955787214395814176cd997307e2dad083256c29c5139
data/lib/tobias/cli.rb CHANGED
@@ -11,9 +11,10 @@ module Tobias
11
11
  desc "profile SCRIPT", "profile"
12
12
  option :database_url, type: :string, required: true
13
13
  option :iterations, type: :numeric, default: 100
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,46 @@ 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}'")
28
-
29
- eval(code, binding, script)
30
-
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
43
-
44
- stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
45
-
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)}"
51
-
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
26
+ container = Container.new(code)
27
+ work_mems = WorkMem.valid_for(database)
28
+ results = {}
29
+
30
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
31
+ # @tobias is thinking...
32
+ MARKDOWN
33
+ puts parsed
34
+
35
+ thinking_time = Benchmark.realtime do
36
+ container.queries.each do |name, block|
37
+ work_mem = Evaluation.new(database, work_mems).run(options, &block)
38
+
39
+ results[name] = work_mem
63
40
  end
64
41
  end
65
- end
66
42
 
67
- private
43
+ parsed = TTY::Markdown.parse(<<~MARKDOWN)
44
+ # @tobias has sent you a new message
68
45
 
69
- def query(name, &block)
70
- @queries ||= {}
71
- @queries[name] = block
72
- end
46
+ I thought about your queries for precisely #{thinking_time.round(2)} seconds and here is what I recommend:
47
+
48
+ | Query | Required work_mem |
49
+ |-------|-------------------|
50
+ #{results.map { |name, work_mem| "| #{name} | #{work_mem.to_sql} |" }.join("\n")}
73
51
 
52
+ Your application will need to run with at least #{results.values.max.to_sql} of work_mem.
53
+
54
+ To apply my recommendations, run the following SQL:
55
+
56
+ ```sql
57
+ ALTER SYSTEM SET work_mem = '#{results.values.max.to_sql}';
58
+ SELECT pg_reload_conf();
59
+ ```
60
+
61
+ Regards,
62
+ ~ Tobias
63
+ MARKDOWN
64
+
65
+ puts parsed
66
+ end
74
67
  end
75
68
  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
@@ -2,8 +2,35 @@
2
2
 
3
3
  module Tobias
4
4
  class Evaluation
5
- def initialize(database)
5
+ attr_reader :database
6
+
7
+ def initialize(database, work_mems)
6
8
  @database = database
9
+ @work_mems = work_mems
10
+ end
11
+
12
+ def run(options, &block)
13
+ @work_mems.each do |value|
14
+ database.transaction do
15
+ database.run("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
16
+ database.run("SET LOCAL work_mem = '#{value.to_sql}'")
17
+ database.select(Sequel.function(:pg_stat_reset)).first
18
+ database.instance_eval(&block)
19
+
20
+ query = database.instance_eval(&block)
21
+ options[:iterations].to_i.times do
22
+ database.run(query.sql)
23
+ end
24
+
25
+ stats = database[:pg_stat_database].where(datname: Sequel.function(:current_database)).first
26
+
27
+ if stats[:temp_files] == 0 && stats[:temp_bytes] == 0
28
+ return value
29
+ end
30
+ end
31
+ end
32
+
33
+ raise "No work_mem found."
7
34
  end
8
35
  end
9
36
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tobias
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
+
6
+ SUMMARY = "Tobias is a tool to help you find the optimal work_mem for your queries."
7
+ DESCRIPTION = "Tobias is a tool to help you find the optimal work_mem for your queries."
5
8
  end
@@ -2,10 +2,32 @@
2
2
 
3
3
  module Tobias
4
4
  class WorkMem
5
+ attr_reader :amount
6
+
5
7
  def initialize(amount)
6
8
  @amount = amount
7
9
  end
8
10
 
11
+ def >(other)
12
+ @amount > other.amount
13
+ end
14
+
15
+ def <(other)
16
+ @amount < other.amount
17
+ end
18
+
19
+ def >=(other)
20
+ @amount >= other.amount
21
+ end
22
+
23
+ def <=(other)
24
+ @amount <= other.amount
25
+ end
26
+
27
+ def <=>(other)
28
+ @amount <=> other.amount
29
+ end
30
+
9
31
  def to_sql
10
32
  case @amount
11
33
  when 0...1024
@@ -44,7 +66,39 @@ module Tobias
44
66
  new(2.gigabytes),
45
67
  new(4.gigabytes),
46
68
  new(8.gigabytes),
47
- ]
69
+ ].sort_by(&:amount)
70
+ end
71
+
72
+ # Inspects the database to determine the valid work_mem settings for the current user.
73
+ # We'll need at least connection_limit / effective_cache_size for an optimal
74
+ # work_mem setting, otherwise a user could run out of memory at max connections.
75
+ def self.valid_for(database)
76
+ role_conn_limit = database.select(:rolconnlimit).
77
+ from(:pg_roles).
78
+ where(rolname: Sequel.lit("current_user")).
79
+ first
80
+
81
+ max_connections = database.select(:setting).
82
+ from(:pg_settings).
83
+ where(name: "max_connections").
84
+ first
85
+
86
+ effective_cache_size = database.select(:setting, :unit).
87
+ from(:pg_settings).
88
+ where(name: "effective_cache_size").
89
+ first
90
+
91
+ effective_cache_size_bytes = effective_cache_size[:setting].to_i * 8 * 1024
92
+
93
+ connection_limit = if role_conn_limit[:rolconnlimit] > 0
94
+ role_conn_limit[:rolconnlimit]
95
+ else
96
+ max_connections[:setting].to_i
97
+ end
98
+
99
+ bytes_per_connection = effective_cache_size_bytes / connection_limit
100
+
101
+ self.all.select { |work_mem| work_mem.amount < bytes_per_connection.to_i }
48
102
  end
49
103
  end
50
104
  end
data/lib/tobias.rb CHANGED
@@ -19,11 +19,13 @@ 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
 
25
26
  module Tobias
26
27
  autoload :CLI, "tobias/cli"
28
+ autoload :Container, "tobias/container"
27
29
  autoload :Evaluation, "tobias/evaluation"
28
30
  autoload :WorkMem, "tobias/work_mem"
29
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Daniel
@@ -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,6 +239,7 @@ files:
233
239
  - bin/tobias
234
240
  - lib/tobias.rb
235
241
  - lib/tobias/cli.rb
242
+ - lib/tobias/container.rb
236
243
  - lib/tobias/evaluation.rb
237
244
  - lib/tobias/version.rb
238
245
  - lib/tobias/work_mem.rb
@@ -258,5 +265,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
258
265
  requirements: []
259
266
  rubygems_version: 3.6.2
260
267
  specification_version: 4
261
- summary: Tobias
268
+ summary: Tobias is a tool to help you find the optimal work_mem for your queries.
262
269
  test_files: []