autotuner 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +55 -15
- data/lib/autotuner/configuration.rb +35 -0
- data/lib/{gc_tuner → autotuner}/data_structure/data_points.rb +9 -3
- data/lib/autotuner/gc_context.rb +31 -0
- data/lib/{gc_tuner → autotuner}/heuristic/base.rb +11 -3
- data/lib/autotuner/heuristic/gc_compact.rb +60 -0
- data/lib/autotuner/heuristic/heap_size_warmup.rb +130 -0
- data/lib/autotuner/heuristic/malloc.rb +121 -0
- data/lib/autotuner/heuristic/oldmalloc.rb +89 -0
- data/lib/autotuner/heuristic/remembered_wb_unprotected_objects.rb +103 -0
- data/lib/autotuner/heuristics.rb +12 -0
- data/lib/{gc_tuner → autotuner}/rack_plugin.rb +2 -2
- data/lib/autotuner/report/base.rb +37 -0
- data/lib/autotuner/report/multiple_environment_variables.rb +35 -0
- data/lib/autotuner/report/single_environment_variable.rb +29 -0
- data/lib/autotuner/report/string.rb +13 -0
- data/lib/autotuner/request_collector.rb +99 -0
- data/lib/{gc_tuner/request_collector.rb → autotuner/request_context.rb} +8 -17
- data/lib/autotuner/system_context.rb +21 -0
- data/lib/autotuner/version.rb +5 -0
- data/lib/autotuner.rb +29 -0
- metadata +25 -15
- data/lib/gc_tuner/configuration.rb +0 -20
- data/lib/gc_tuner/gc_context.rb +0 -17
- data/lib/gc_tuner/heuristic/size_pool_warmup.rb +0 -131
- data/lib/gc_tuner/heuristics.rb +0 -27
- data/lib/gc_tuner/version.rb +0 -5
- data/lib/gc_tuner.rb +0 -18
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
module Heuristic
|
5
|
+
class RememberedWBUnprotectedObjects < Base
|
6
|
+
class << self
|
7
|
+
private
|
8
|
+
|
9
|
+
def supported?
|
10
|
+
# Ruby 3.3.0 and later have support RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO
|
11
|
+
RUBY_VERSION >= "3.3.0"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
WB_UNPROTECTED_GC_RATIO_THRESHOLD = 0.1
|
16
|
+
MIN_WB_UNPROTECTED_GC = 10
|
17
|
+
|
18
|
+
# From the GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO macro
|
19
|
+
# https://github.com/ruby/ruby/blob/df4c77608e76068deed58b2781674b0eb247c325/gc.c#L295
|
20
|
+
DEFAULT_LIMIT_RATIO = 0.01
|
21
|
+
|
22
|
+
LIMIT_RATIO_ENV = "RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO"
|
23
|
+
|
24
|
+
attr_reader :major_gc_count
|
25
|
+
attr_reader :remembered_wb_unprotected_gc_count
|
26
|
+
|
27
|
+
def initialize(_system_context)
|
28
|
+
super
|
29
|
+
|
30
|
+
@major_gc_count = 0
|
31
|
+
@remembered_wb_unprotected_gc_count = 0
|
32
|
+
|
33
|
+
@given_suggestion = false
|
34
|
+
end
|
35
|
+
|
36
|
+
def name
|
37
|
+
"WBUnprotectedObjects"
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(request_context)
|
41
|
+
# major_by is only useful if we ran at least one major GC during the request
|
42
|
+
if request_context.after_gc_context.stat[:major_gc_count] ==
|
43
|
+
request_context.before_gc_context.stat[:major_gc_count]
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
# Technically, we could run more than one major GC in the request, but
|
48
|
+
# since we don't have information about the other major GC, we'll treat
|
49
|
+
# it as if there was only one major GC.
|
50
|
+
@major_gc_count += 1
|
51
|
+
@remembered_wb_unprotected_gc_count += 1 if request_context.after_gc_context.latest_gc_info[:major_by] == :shady
|
52
|
+
end
|
53
|
+
|
54
|
+
def tuning_report
|
55
|
+
# Don't give suggestions twice
|
56
|
+
return if @given_suggestion
|
57
|
+
# Don't report if there's very few data points
|
58
|
+
return if @remembered_wb_unprotected_gc_count < MIN_WB_UNPROTECTED_GC
|
59
|
+
|
60
|
+
wb_unprotected_gc_ratio = @remembered_wb_unprotected_gc_count.to_f / @major_gc_count
|
61
|
+
# Don't report if there's very few WB unprotected GC
|
62
|
+
return if wb_unprotected_gc_ratio <= WB_UNPROTECTED_GC_RATIO_THRESHOLD
|
63
|
+
|
64
|
+
@given_suggestion = true
|
65
|
+
|
66
|
+
Report::SingleEnvironmentVariable.new(
|
67
|
+
<<~MSG,
|
68
|
+
The following suggestions reduces the number of major garbage collection cycles, specifically a cycle called "remembered write barrier unprotected" (also know as "shady" due to historical reasons). Your app runs remembered write barrier unprotected cycles in approximately #{format("%.2f", wb_unprotected_gc_ratio * 100)}% of all major garbage collection cycles.
|
69
|
+
|
70
|
+
Reducing major garbage collection cycles can help reduce response times, especially for the extremes (e.g. p95 or p99 response times). The following tuning values aims to disable oldmalloc garbage collection cycles by setting it to an extremely high value. This may cause a slight increase in memory usage. You should monitor memory usage carefully to ensure your app is not running out of memory.
|
71
|
+
MSG
|
72
|
+
LIMIT_RATIO_ENV,
|
73
|
+
suggested_limit_ratio,
|
74
|
+
configured_limit_ratio,
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def debug_state
|
79
|
+
{
|
80
|
+
given_suggestion: @given_suggestion,
|
81
|
+
major_gc_count: @major_gc_count,
|
82
|
+
remembered_wb_unprotected_gc_count: @remembered_wb_unprotected_gc_count,
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def configured_limit_ratio
|
89
|
+
ENV[LIMIT_RATIO_ENV]&.to_f
|
90
|
+
end
|
91
|
+
|
92
|
+
def suggested_limit_ratio
|
93
|
+
if !configured_limit_ratio
|
94
|
+
DEFAULT_LIMIT_RATIO * 2
|
95
|
+
elsif configured_limit_ratio < DEFAULT_LIMIT_RATIO
|
96
|
+
DEFAULT_LIMIT_RATIO
|
97
|
+
else
|
98
|
+
configured_limit_ratio * 2
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
3
|
+
module Autotuner
|
4
4
|
class RackPlugin
|
5
5
|
def initialize(app)
|
6
6
|
@app = app
|
@@ -8,7 +8,7 @@ module GCTuner
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def call(env)
|
11
|
-
if
|
11
|
+
if Autotuner.enabled?
|
12
12
|
@request_collector.request do
|
13
13
|
@app.call(env)
|
14
14
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
module Report
|
5
|
+
class Base
|
6
|
+
DISCLAIMER_MESSAGE = <<~MSG
|
7
|
+
It is always recommended to experiment with these suggestions as some suggestions may not always yield positive performance improvements. The recommended method is to perform A/B testing where a portion of traffic does not have the these suggested values and a portion of traffic with these suggested values.
|
8
|
+
MSG
|
9
|
+
|
10
|
+
attr_reader :assist_message
|
11
|
+
|
12
|
+
def initialize(assist_message)
|
13
|
+
@assist_message = assist_message
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
msg = +assist_message
|
18
|
+
msg << "\n"
|
19
|
+
|
20
|
+
m = message
|
21
|
+
if m
|
22
|
+
msg << m
|
23
|
+
msg << "\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
msg << DISCLAIMER_MESSAGE
|
27
|
+
msg.freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def message
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
module Report
|
5
|
+
class MultipleEnvironmentVariables < Base
|
6
|
+
attr_reader :env_name
|
7
|
+
attr_reader :suggested_value
|
8
|
+
attr_reader :configured_value
|
9
|
+
|
10
|
+
def initialize(assist_message, env_name, suggested_value, configured_value)
|
11
|
+
super(assist_message)
|
12
|
+
@env_name = env_name
|
13
|
+
@suggested_value = suggested_value
|
14
|
+
@configured_value = configured_value
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def message
|
20
|
+
msg = +"Suggested tuning values:\n"
|
21
|
+
env_name.each_with_index do |env, i|
|
22
|
+
msg << suggested_tuning_str(env, suggested_value[i], configured_value[i])
|
23
|
+
end
|
24
|
+
msg
|
25
|
+
end
|
26
|
+
|
27
|
+
def suggested_tuning_str(env, suggested, configured)
|
28
|
+
str = +" #{env}=#{suggested}"
|
29
|
+
str << " (configured value: #{configured})" if configured
|
30
|
+
str << "\n"
|
31
|
+
str
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
module Report
|
5
|
+
class SingleEnvironmentVariable < Base
|
6
|
+
attr_reader :env_name
|
7
|
+
attr_reader :suggested_value
|
8
|
+
attr_reader :configured_value
|
9
|
+
|
10
|
+
def initialize(assist_message, env_name, suggested_value, configured_value)
|
11
|
+
super(assist_message)
|
12
|
+
|
13
|
+
@env_name = env_name
|
14
|
+
@suggested_value = suggested_value
|
15
|
+
@configured_value = configured_value
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def message
|
21
|
+
msg = +"Suggested tuning value:\n"
|
22
|
+
msg << " #{env_name}=#{suggested_value}"
|
23
|
+
msg << " (configured value: #{configured_value})" if configured_value
|
24
|
+
msg << "\n"
|
25
|
+
msg
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
class RequestCollector
|
5
|
+
HEURISTICS_POLLING_FREQUENCY = 100
|
6
|
+
DEBUG_EMIT_FREQUENCY = 1000
|
7
|
+
|
8
|
+
attr_reader :heuristics
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@request_count = 0
|
12
|
+
|
13
|
+
@request_context = RequestContext.new
|
14
|
+
|
15
|
+
@system_context = SystemContext.new
|
16
|
+
|
17
|
+
@heuristics = Autotuner.enabled_heuristics.map { |h| h.new(@system_context) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def request
|
21
|
+
before_request
|
22
|
+
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
after_request
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def before_request
|
31
|
+
@request_context.before_request
|
32
|
+
|
33
|
+
@request_count += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def after_request
|
37
|
+
@request_context.after_request
|
38
|
+
|
39
|
+
@system_context.update(@request_context)
|
40
|
+
|
41
|
+
heuristics.each do |heuristic|
|
42
|
+
heuristic.call(@request_context)
|
43
|
+
end
|
44
|
+
|
45
|
+
emit_heuristic_reports if @request_count % HEURISTICS_POLLING_FREQUENCY == 0
|
46
|
+
emit_debugging_states if @request_count % DEBUG_EMIT_FREQUENCY == 0
|
47
|
+
emit_metrics
|
48
|
+
end
|
49
|
+
|
50
|
+
def emit_heuristic_reports
|
51
|
+
heuristics.each do |heuristic|
|
52
|
+
report = heuristic.tuning_report
|
53
|
+
|
54
|
+
next unless report
|
55
|
+
|
56
|
+
if Autotuner.reporter
|
57
|
+
Autotuner.reporter.call(report)
|
58
|
+
else
|
59
|
+
warn("Autotuner has been enabled but Autotuner.reporter has not been configured")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def emit_debugging_states
|
65
|
+
return unless Autotuner.debug_reporter
|
66
|
+
|
67
|
+
debug_states = {
|
68
|
+
system_context: @system_context.debug_state,
|
69
|
+
}
|
70
|
+
|
71
|
+
heuristics.each do |h|
|
72
|
+
debug_states[h.name] = h.debug_state
|
73
|
+
end
|
74
|
+
|
75
|
+
Autotuner.debug_reporter.call(debug_states)
|
76
|
+
end
|
77
|
+
|
78
|
+
def emit_metrics
|
79
|
+
return unless Autotuner.metrics_reporter
|
80
|
+
|
81
|
+
metrics = {
|
82
|
+
# Diff metrics
|
83
|
+
"diff.time" => gc_stat_diff(:time),
|
84
|
+
"diff.minor_gc_count" => gc_stat_diff(:minor_gc_count),
|
85
|
+
"diff.major_gc_count" => gc_stat_diff(:major_gc_count),
|
86
|
+
"request_time" => @request_context.request_time,
|
87
|
+
|
88
|
+
# Metrics
|
89
|
+
"heap_pages" => @request_context.after_gc_context.stat[:heap_eden_pages],
|
90
|
+
}
|
91
|
+
|
92
|
+
Autotuner.metrics_reporter.call(metrics)
|
93
|
+
end
|
94
|
+
|
95
|
+
def gc_stat_diff(stat)
|
96
|
+
@request_context.after_gc_context.stat[stat] - @request_context.before_gc_context.stat[stat]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -1,36 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
class
|
3
|
+
module Autotuner
|
4
|
+
class RequestContext
|
5
|
+
attr_reader :before_gc_context
|
6
|
+
attr_reader :after_gc_context
|
7
|
+
attr_reader :request_time
|
8
|
+
|
5
9
|
def initialize
|
6
10
|
@before_gc_context = GCContext.new
|
7
11
|
@after_gc_context = GCContext.new
|
12
|
+
@request_time = 0.0
|
8
13
|
|
9
14
|
@start_time_ms = 0.0
|
10
15
|
end
|
11
16
|
|
12
|
-
def request
|
13
|
-
before_request
|
14
|
-
|
15
|
-
yield
|
16
|
-
ensure
|
17
|
-
after_request
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
17
|
def before_request
|
23
18
|
@before_gc_context.update
|
24
19
|
@start_time_ms = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
25
20
|
end
|
26
21
|
|
27
22
|
def after_request
|
28
|
-
request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @start_time_ms
|
23
|
+
@request_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @start_time_ms
|
29
24
|
@after_gc_context.update
|
30
|
-
|
31
|
-
GCTuner.heuristics.each do |heuristic|
|
32
|
-
heuristic.call(request_time, @before_gc_context, @after_gc_context)
|
33
|
-
end
|
34
25
|
end
|
35
26
|
end
|
36
27
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Autotuner
|
4
|
+
class SystemContext
|
5
|
+
attr_reader :request_time_data
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@request_time_data = DataStructure::DataPoints.new(Configuration::DATA_POINTS_COUNT)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(request_context)
|
12
|
+
@request_time_data.insert(request_context.request_time)
|
13
|
+
end
|
14
|
+
|
15
|
+
def debug_state
|
16
|
+
{
|
17
|
+
request_time_data: @request_time_data.debug_state,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/autotuner.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "autotuner/data_structure/data_points"
|
4
|
+
|
5
|
+
require_relative "autotuner/heuristic/base"
|
6
|
+
require_relative "autotuner/heuristic/gc_compact"
|
7
|
+
require_relative "autotuner/heuristic/heap_size_warmup"
|
8
|
+
require_relative "autotuner/heuristic/malloc"
|
9
|
+
require_relative "autotuner/heuristic/oldmalloc"
|
10
|
+
require_relative "autotuner/heuristic/remembered_wb_unprotected_objects"
|
11
|
+
|
12
|
+
require_relative "autotuner/report/base"
|
13
|
+
require_relative "autotuner/report/multiple_environment_variables"
|
14
|
+
require_relative "autotuner/report/single_environment_variable"
|
15
|
+
require_relative "autotuner/report/string"
|
16
|
+
|
17
|
+
require_relative "autotuner/configuration"
|
18
|
+
require_relative "autotuner/gc_context"
|
19
|
+
require_relative "autotuner/heuristics"
|
20
|
+
require_relative "autotuner/rack_plugin"
|
21
|
+
require_relative "autotuner/request_collector"
|
22
|
+
require_relative "autotuner/request_context"
|
23
|
+
require_relative "autotuner/system_context"
|
24
|
+
require_relative "autotuner/version"
|
25
|
+
|
26
|
+
module Autotuner
|
27
|
+
extend Configuration
|
28
|
+
extend Heuristics
|
29
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: autotuner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Zhu
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mocha
|
@@ -64,22 +64,32 @@ files:
|
|
64
64
|
- LICENSE.txt
|
65
65
|
- README.md
|
66
66
|
- Rakefile
|
67
|
-
- lib/
|
68
|
-
- lib/
|
69
|
-
- lib/
|
70
|
-
- lib/
|
71
|
-
- lib/
|
72
|
-
- lib/
|
73
|
-
- lib/
|
74
|
-
- lib/
|
75
|
-
- lib/
|
76
|
-
- lib/
|
77
|
-
|
67
|
+
- lib/autotuner.rb
|
68
|
+
- lib/autotuner/configuration.rb
|
69
|
+
- lib/autotuner/data_structure/data_points.rb
|
70
|
+
- lib/autotuner/gc_context.rb
|
71
|
+
- lib/autotuner/heuristic/base.rb
|
72
|
+
- lib/autotuner/heuristic/gc_compact.rb
|
73
|
+
- lib/autotuner/heuristic/heap_size_warmup.rb
|
74
|
+
- lib/autotuner/heuristic/malloc.rb
|
75
|
+
- lib/autotuner/heuristic/oldmalloc.rb
|
76
|
+
- lib/autotuner/heuristic/remembered_wb_unprotected_objects.rb
|
77
|
+
- lib/autotuner/heuristics.rb
|
78
|
+
- lib/autotuner/rack_plugin.rb
|
79
|
+
- lib/autotuner/report/base.rb
|
80
|
+
- lib/autotuner/report/multiple_environment_variables.rb
|
81
|
+
- lib/autotuner/report/single_environment_variable.rb
|
82
|
+
- lib/autotuner/report/string.rb
|
83
|
+
- lib/autotuner/request_collector.rb
|
84
|
+
- lib/autotuner/request_context.rb
|
85
|
+
- lib/autotuner/system_context.rb
|
86
|
+
- lib/autotuner/version.rb
|
87
|
+
homepage: https://github.com/Shopify/autotuner
|
78
88
|
licenses:
|
79
89
|
- MIT
|
80
90
|
metadata:
|
81
|
-
homepage_uri: https://github.com/Shopify/
|
82
|
-
source_code_uri: https://github.com/Shopify/
|
91
|
+
homepage_uri: https://github.com/Shopify/autotuner
|
92
|
+
source_code_uri: https://github.com/Shopify/autotuner
|
83
93
|
post_install_message:
|
84
94
|
rdoc_options: []
|
85
95
|
require_paths:
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GCTuner
|
4
|
-
module Configuration
|
5
|
-
attr_reader :sample_ratio
|
6
|
-
attr_writer :enabled
|
7
|
-
|
8
|
-
def enabled?
|
9
|
-
@enabled
|
10
|
-
end
|
11
|
-
|
12
|
-
def sample_ratio=(ratio)
|
13
|
-
raise ArgumentError, "ratio must be between 0 and 1.0" unless (0..1.0).include?(ratio)
|
14
|
-
|
15
|
-
@sample_ratio = ratio
|
16
|
-
|
17
|
-
self.enabled = rand < ratio
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
data/lib/gc_tuner/gc_context.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GCTuner
|
4
|
-
class GCContext
|
5
|
-
attr_reader :stat, :stat_heap
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@stat = GC.stat
|
9
|
-
@stat_heap = GC.stat_heap
|
10
|
-
end
|
11
|
-
|
12
|
-
def update
|
13
|
-
GC.stat(@stat)
|
14
|
-
GC.stat_heap(nil, @stat_heap)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,131 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GCTuner
|
4
|
-
module Heuristic
|
5
|
-
class SizePoolWarmup < Base
|
6
|
-
DATA_POINTS_COUNT = 1_000
|
7
|
-
SIZE_POOL_CONFIGURATION_DELTA_RATIO = 0.01
|
8
|
-
SIZE_POOL_CONFIGURATION_DELTA = 1
|
9
|
-
|
10
|
-
class << self
|
11
|
-
private
|
12
|
-
|
13
|
-
def supported?
|
14
|
-
# Ruby 3.3.0 and later have support RUBY_GC_HEAP_INIT_SIZE_%d_SLOTS
|
15
|
-
# RUBY_VERSION >= "3.3.0"
|
16
|
-
# TODO: use the check above
|
17
|
-
true
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def initialize
|
22
|
-
super
|
23
|
-
|
24
|
-
@request_time_data = DataStructure::DataPoints.new(DATA_POINTS_COUNT)
|
25
|
-
|
26
|
-
@size_pool_count = GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT]
|
27
|
-
@size_pools_data = Array.new(@size_pool_count)
|
28
|
-
@size_pools_tuning_configuration = Array.new(@size_pool_count)
|
29
|
-
@size_pool_count.times do |i|
|
30
|
-
@size_pools_data[i] = DataStructure::DataPoints.new(DATA_POINTS_COUNT)
|
31
|
-
@size_pools_tuning_configuration[i] = ENV[env_name_for_size_pool(i)].to_i
|
32
|
-
end
|
33
|
-
|
34
|
-
@plateaued = false
|
35
|
-
end
|
36
|
-
|
37
|
-
def call(request_time, _before_gc_context, after_gc_context)
|
38
|
-
# We only want to collect data at boot until the request time plateaus
|
39
|
-
return if @plateaued
|
40
|
-
|
41
|
-
insert_data(request_time, after_gc_context)
|
42
|
-
|
43
|
-
return unless @request_time_data.plateaued?
|
44
|
-
|
45
|
-
@plateaued = true
|
46
|
-
end
|
47
|
-
|
48
|
-
def tuning_message
|
49
|
-
msg = nil
|
50
|
-
|
51
|
-
if @plateaued
|
52
|
-
size_pool_messages = @size_pool_count.times.map do |i|
|
53
|
-
tuning_message_for_size_pool(i)
|
54
|
-
end.compact
|
55
|
-
|
56
|
-
unless size_pool_messages.empty?
|
57
|
-
msg = <<~MSG
|
58
|
-
Here are the recommended tuning values for size pools and the confidence scores.
|
59
|
-
Confidence scores are between 0 and 1.0 and represent the correlation between
|
60
|
-
the tuning value and the response time.
|
61
|
-
|
62
|
-
MSG
|
63
|
-
|
64
|
-
msg += size_pool_messages.join
|
65
|
-
end
|
66
|
-
else
|
67
|
-
msg = <<~MSG.chomp
|
68
|
-
There is not enough data and/or response times have not plateaued.
|
69
|
-
MSG
|
70
|
-
end
|
71
|
-
|
72
|
-
msg
|
73
|
-
end
|
74
|
-
|
75
|
-
def debug_message
|
76
|
-
msg = <<~MSG
|
77
|
-
plateaued: #{@plateaued}
|
78
|
-
request_time_data: #{@request_time_data}
|
79
|
-
MSG
|
80
|
-
|
81
|
-
@size_pools_data.each_with_index do |data, i|
|
82
|
-
msg += "size_pools_data[#{i}]: #{data}\n"
|
83
|
-
end
|
84
|
-
|
85
|
-
if @plateaued
|
86
|
-
msg += @size_pool_count.times.map do |i|
|
87
|
-
tuning_message_for_size_pool(i, debug: true)
|
88
|
-
end.join
|
89
|
-
end
|
90
|
-
|
91
|
-
msg
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def insert_data(request_time, after_gc_context)
|
97
|
-
@request_time_data.insert(request_time)
|
98
|
-
|
99
|
-
@size_pools_data.each_with_index do |data, i|
|
100
|
-
data.insert(after_gc_context.stat_heap[i][:heap_eden_pages])
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def env_name_for_size_pool(size_pool)
|
105
|
-
slot_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * (2**size_pool)
|
106
|
-
|
107
|
-
"RUBY_GC_HEAP_INIT_SIZE_#{slot_size}_SLOTS"
|
108
|
-
end
|
109
|
-
|
110
|
-
def tuning_message_for_size_pool(size_pool, debug: false)
|
111
|
-
configured_value = @size_pools_tuning_configuration[size_pool]
|
112
|
-
|
113
|
-
data = @size_pools_data[size_pool]
|
114
|
-
suggested_value = data.samples[data.length - 1].to_i
|
115
|
-
|
116
|
-
diff = (configured_value - suggested_value).abs
|
117
|
-
if debug ||
|
118
|
-
(diff > configured_value * SIZE_POOL_CONFIGURATION_DELTA_RATIO && diff > SIZE_POOL_CONFIGURATION_DELTA)
|
119
|
-
confidence = @request_time_data.correlation(data).abs
|
120
|
-
|
121
|
-
msg = ""
|
122
|
-
msg += "#{env_name_for_size_pool(size_pool)}=#{suggested_value} (confidence: #{format("%.2f", confidence)}"
|
123
|
-
msg += ", tuned value: #{configured_value}" if configured_value > 0
|
124
|
-
msg += ")\n"
|
125
|
-
|
126
|
-
msg
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|