tunemygc 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +19 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +125 -0
- data/README.md +193 -0
- data/Rakefile +32 -0
- data/assets/discourse_bench.png +0 -0
- data/assets/tunemygc-graphic2x-80dac1571cacc70d9b272bb62ae9f6df.png +0 -0
- data/bin/tunemygc +6 -0
- data/ext/tunemygc/extconf.rb +92 -0
- data/ext/tunemygc/getRSS.c +122 -0
- data/ext/tunemygc/tunemygc_ext.c +156 -0
- data/ext/tunemygc/tunemygc_ext.h +21 -0
- data/lib/tunemygc/agent.rb +44 -0
- data/lib/tunemygc/cli.rb +86 -0
- data/lib/tunemygc/configurator.rb +25 -0
- data/lib/tunemygc/interposer.rb +60 -0
- data/lib/tunemygc/railtie.rb +13 -0
- data/lib/tunemygc/request_subscriber.rb +18 -0
- data/lib/tunemygc/snapshotter.rb +49 -0
- data/lib/tunemygc/spies/action_controller.rb +44 -0
- data/lib/tunemygc/subscriber.rb +14 -0
- data/lib/tunemygc/syncer.rb +91 -0
- data/lib/tunemygc/version.rb +5 -0
- data/lib/tunemygc.rb +22 -0
- data/test/fixtures.rb +9 -0
- data/test/helper.rb +30 -0
- data/test/test_agent.rb +21 -0
- data/test/test_interposer.rb +69 -0
- data/test/test_railtie.rb +21 -0
- data/test/test_snapshotter.rb +41 -0
- data/test/test_syncer.rb +146 -0
- data/tunemygc-1.0.gem +0 -0
- data/tunemygc.gemspec +50 -0
- metadata +194 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
#include "tunemygc_ext.h"
|
2
|
+
|
3
|
+
VALUE rb_mTunemygc;
|
4
|
+
static ID id_tunemygc_tracepoint;
|
5
|
+
static ID id_tunemygc_raw_snapshot;
|
6
|
+
|
7
|
+
static VALUE sym_gc_cycle_started;
|
8
|
+
static VALUE sym_gc_cycle_ended;
|
9
|
+
|
10
|
+
/* For 2.2.x incremental GC */
|
11
|
+
#ifdef RUBY_INTERNAL_EVENT_GC_ENTER
|
12
|
+
static VALUE sym_gc_cycle_entered;
|
13
|
+
static VALUE sym_gc_cycle_exited;
|
14
|
+
#endif
|
15
|
+
|
16
|
+
/* From @tmm1/gctools */
|
17
|
+
static double _tunemygc_walltime()
|
18
|
+
{
|
19
|
+
struct timespec ts;
|
20
|
+
#ifdef HAVE_CLOCK_GETTIME
|
21
|
+
if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
|
22
|
+
rb_sys_fail("clock_gettime");
|
23
|
+
}
|
24
|
+
#else
|
25
|
+
{
|
26
|
+
struct timeval tv;
|
27
|
+
if (gettimeofday(&tv, 0) < 0) {
|
28
|
+
rb_sys_fail("gettimeofday");
|
29
|
+
}
|
30
|
+
ts.tv_sec = tv.tv_sec;
|
31
|
+
ts.tv_nsec = tv.tv_usec * 1000;
|
32
|
+
}
|
33
|
+
#endif
|
34
|
+
return ts.tv_sec + ts.tv_nsec * 1e-9;
|
35
|
+
}
|
36
|
+
|
37
|
+
static VALUE tunemygc_walltime(VALUE mod)
|
38
|
+
{
|
39
|
+
return DBL2NUM(_tunemygc_walltime());
|
40
|
+
}
|
41
|
+
|
42
|
+
/* Postponed job callback that fires when the VM is in a consistent state again (sometime
|
43
|
+
* after the GC cycle, notably RUBY_INTERNAL_EVENT_GC_END_SWEEP)
|
44
|
+
*/
|
45
|
+
static void tunemygc_invoke_gc_snapshot(void *data)
|
46
|
+
{
|
47
|
+
tunemygc_stat_record *stat = (tunemygc_stat_record *)data;
|
48
|
+
VALUE snapshot = tunemygc_get_stat_record(stat);
|
49
|
+
rb_funcall(rb_mTunemygc, id_tunemygc_raw_snapshot, 1, snapshot);
|
50
|
+
free(stat);
|
51
|
+
}
|
52
|
+
|
53
|
+
/* GC tracepoint hook. Snapshots GC state using new low level helpers which are safe
|
54
|
+
* to call from within tracepoint handlers as they don't allocate and change the heap state
|
55
|
+
*/
|
56
|
+
static void tunemygc_gc_hook_i(VALUE tpval, void *data)
|
57
|
+
{
|
58
|
+
rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
|
59
|
+
rb_event_flag_t flag = rb_tracearg_event_flag(tparg);
|
60
|
+
|
61
|
+
tunemygc_stat_record *stat = ((tunemygc_stat_record*)malloc(sizeof(tunemygc_stat_record)));
|
62
|
+
stat->ts = _tunemygc_walltime();
|
63
|
+
stat->peak_rss = getPeakRSS();
|
64
|
+
stat->current_rss = getCurrentRSS();
|
65
|
+
|
66
|
+
switch (flag) {
|
67
|
+
case RUBY_INTERNAL_EVENT_GC_START:
|
68
|
+
stat->stage = sym_gc_cycle_started;
|
69
|
+
break;
|
70
|
+
case RUBY_INTERNAL_EVENT_GC_END_SWEEP:
|
71
|
+
stat->stage = sym_gc_cycle_ended;
|
72
|
+
break;
|
73
|
+
#ifdef RUBY_INTERNAL_EVENT_GC_ENTER
|
74
|
+
case RUBY_INTERNAL_EVENT_GC_ENTER:
|
75
|
+
stat->stage = sym_gc_cycle_entered;
|
76
|
+
break;
|
77
|
+
case RUBY_INTERNAL_EVENT_GC_EXIT:
|
78
|
+
stat->stage = sym_gc_cycle_exited;
|
79
|
+
break;
|
80
|
+
#endif
|
81
|
+
}
|
82
|
+
|
83
|
+
tunemygc_set_stat_record(stat);
|
84
|
+
rb_postponed_job_register(0, tunemygc_invoke_gc_snapshot, (void *)stat);
|
85
|
+
}
|
86
|
+
|
87
|
+
/* Installs the GC tracepoint and declare interest only in start of the cycle and end of sweep
|
88
|
+
* events
|
89
|
+
*/
|
90
|
+
static VALUE tunemygc_install_gc_tracepoint(VALUE mod)
|
91
|
+
{
|
92
|
+
rb_event_flag_t events;
|
93
|
+
VALUE tunemygc_tracepoint = rb_ivar_get(rb_mTunemygc, id_tunemygc_tracepoint);
|
94
|
+
if (!NIL_P(tunemygc_tracepoint)) {
|
95
|
+
rb_tracepoint_disable(tunemygc_tracepoint);
|
96
|
+
rb_ivar_set(rb_mTunemygc, id_tunemygc_tracepoint, Qnil);
|
97
|
+
}
|
98
|
+
events = RUBY_INTERNAL_EVENT_GC_START | RUBY_INTERNAL_EVENT_GC_END_SWEEP;
|
99
|
+
tunemygc_tracepoint = rb_tracepoint_new(0, events, tunemygc_gc_hook_i, (void *)0);
|
100
|
+
if (NIL_P(tunemygc_tracepoint)) rb_warn("Could not install GC tracepoint!");
|
101
|
+
rb_tracepoint_enable(tunemygc_tracepoint);
|
102
|
+
rb_ivar_set(rb_mTunemygc, id_tunemygc_tracepoint, tunemygc_tracepoint);
|
103
|
+
return Qnil;
|
104
|
+
}
|
105
|
+
|
106
|
+
/* Removes a previously enabled GC tracepoint */
|
107
|
+
static VALUE tunemygc_uninstall_gc_tracepoint(VALUE mod)
|
108
|
+
{
|
109
|
+
VALUE tunemygc_tracepoint = rb_ivar_get(rb_mTunemygc, id_tunemygc_tracepoint);
|
110
|
+
if (!NIL_P(tunemygc_tracepoint)) {
|
111
|
+
rb_tracepoint_disable(tunemygc_tracepoint);
|
112
|
+
rb_ivar_set(rb_mTunemygc, id_tunemygc_tracepoint, Qnil);
|
113
|
+
}
|
114
|
+
return Qnil;
|
115
|
+
}
|
116
|
+
|
117
|
+
static VALUE tunemygc_peak_rss(VALUE mod)
|
118
|
+
{
|
119
|
+
return SIZET2NUM(getPeakRSS());
|
120
|
+
}
|
121
|
+
|
122
|
+
static VALUE tunemygc_current_rss(VALUE mod)
|
123
|
+
{
|
124
|
+
return SIZET2NUM(getCurrentRSS());
|
125
|
+
}
|
126
|
+
|
127
|
+
void Init_tunemygc_ext()
|
128
|
+
{
|
129
|
+
/* Warm up the symbol table */
|
130
|
+
id_tunemygc_tracepoint = rb_intern("__tunemygc_tracepoint");
|
131
|
+
id_tunemygc_raw_snapshot = rb_intern("raw_snapshot");
|
132
|
+
rb_funcall(rb_mGC, rb_intern("stat"), 0);
|
133
|
+
rb_funcall(rb_mGC, rb_intern("latest_gc_info"), 0);
|
134
|
+
|
135
|
+
/* Symbol warmup */
|
136
|
+
sym_gc_cycle_started = ID2SYM(rb_intern("GC_CYCLE_STARTED"));
|
137
|
+
sym_gc_cycle_ended = ID2SYM(rb_intern("GC_CYCLE_ENDED"));
|
138
|
+
|
139
|
+
/* For 2.2.x incremental GC */
|
140
|
+
#ifdef RUBY_INTERNAL_EVENT_GC_ENTER
|
141
|
+
sym_gc_cycle_entered = ID2SYM(rb_intern("GC_CYCLE_ENTERED"));
|
142
|
+
sym_gc_cycle_exited = ID2SYM(rb_intern("GC_CYCLE_EXITED"));
|
143
|
+
#endif
|
144
|
+
|
145
|
+
tunemygc_setup_trace_symbols();
|
146
|
+
|
147
|
+
rb_mTunemygc = rb_define_module("TuneMyGc");
|
148
|
+
rb_ivar_set(rb_mTunemygc, id_tunemygc_tracepoint, Qnil);
|
149
|
+
|
150
|
+
rb_define_module_function(rb_mTunemygc, "install_gc_tracepoint", tunemygc_install_gc_tracepoint, 0);
|
151
|
+
rb_define_module_function(rb_mTunemygc, "uninstall_gc_tracepoint", tunemygc_uninstall_gc_tracepoint, 0);
|
152
|
+
|
153
|
+
rb_define_module_function(rb_mTunemygc, "walltime", tunemygc_walltime, 0);
|
154
|
+
rb_define_module_function(rb_mTunemygc, "peak_rss", tunemygc_peak_rss, 0);
|
155
|
+
rb_define_module_function(rb_mTunemygc, "current_rss", tunemygc_current_rss, 0);
|
156
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#ifndef TUNEMYGC_EXT_H
|
2
|
+
#define TUNEMYGC_EXT_H
|
3
|
+
|
4
|
+
#include "ruby/ruby.h"
|
5
|
+
#include "ruby/debug.h"
|
6
|
+
|
7
|
+
extern VALUE rb_mTunemygc;
|
8
|
+
|
9
|
+
#include <stddef.h>
|
10
|
+
/* for walltime */
|
11
|
+
#include <time.h>
|
12
|
+
#include <sys/time.h>
|
13
|
+
|
14
|
+
/* header we codegen'ed in extconf.rb from VM specific GC stats */
|
15
|
+
#include "tunemygc_env.h"
|
16
|
+
|
17
|
+
/* From getRSS.c */
|
18
|
+
size_t getPeakRSS();
|
19
|
+
size_t getCurrentRSS();
|
20
|
+
|
21
|
+
#endif
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "tunemygc/tunemygc_ext"
|
4
|
+
require "tunemygc/interposer"
|
5
|
+
require "tunemygc/snapshotter"
|
6
|
+
require "logger"
|
7
|
+
|
8
|
+
module TuneMyGc
|
9
|
+
MUTEX = Mutex.new
|
10
|
+
|
11
|
+
attr_accessor :logger, :interposer, :snapshotter
|
12
|
+
|
13
|
+
def snapshot(stage, timestamp = nil, meta = nil)
|
14
|
+
snapshotter.take(stage, timestamp, meta)
|
15
|
+
end
|
16
|
+
|
17
|
+
def raw_snapshot(snapshot)
|
18
|
+
snapshotter.take_raw(snapshot)
|
19
|
+
end
|
20
|
+
|
21
|
+
def log(message)
|
22
|
+
logger.info "[TuneMyGC] #{message}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def reccommendations
|
26
|
+
MUTEX.synchronize do
|
27
|
+
require "tunemygc/syncer"
|
28
|
+
syncer = TuneMyGc::Syncer.new
|
29
|
+
config = syncer.sync(snapshotter)
|
30
|
+
require "tunemygc/configurator"
|
31
|
+
TuneMyGc::Configurator.new(config).configure
|
32
|
+
end
|
33
|
+
rescue Exception => e
|
34
|
+
log "Config reccommendation error (#{e.message})"
|
35
|
+
end
|
36
|
+
|
37
|
+
extend self
|
38
|
+
|
39
|
+
MUTEX.synchronize do
|
40
|
+
self.logger = Logger.new($stdout)
|
41
|
+
self.interposer = TuneMyGc::Interposer.new
|
42
|
+
self.snapshotter = TuneMyGc::Snapshotter.new
|
43
|
+
end
|
44
|
+
end
|
data/lib/tunemygc/cli.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'certified'
|
5
|
+
require 'timeout'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
module TuneMyGc
|
9
|
+
class CLI
|
10
|
+
TIMEOUT = 10
|
11
|
+
|
12
|
+
attr_reader :uri, :client, :options
|
13
|
+
|
14
|
+
def self.start(args)
|
15
|
+
args = ["-h"] if args.empty?
|
16
|
+
options = {}
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: tunemygc [options]"
|
19
|
+
|
20
|
+
opts.on("-r ", "--register EMAIL", "Register this Rails app with the https://tunemygc.com service") do |email|
|
21
|
+
options[:email] = email
|
22
|
+
end
|
23
|
+
opts.on("-c ", "--config TOKEN", "Fetch the last known config for a given Rails app") do |token|
|
24
|
+
options[:config] = token
|
25
|
+
end
|
26
|
+
opts.on_tail("-h", "--help", "How to use the TuneMyGC agent CLI") do
|
27
|
+
puts opts
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
end.parse!(args)
|
31
|
+
new(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(options)
|
35
|
+
@options = options
|
36
|
+
@uri = URI("http://#{TuneMyGc::HOST}")
|
37
|
+
@client = Net::HTTP.new(@uri.host, @uri.port)
|
38
|
+
@client.use_ssl = (uri.port == 443)
|
39
|
+
@client.read_timeout = TIMEOUT
|
40
|
+
register if options[:email]
|
41
|
+
fetch_config if options[:config]
|
42
|
+
end
|
43
|
+
|
44
|
+
def register
|
45
|
+
timeout do
|
46
|
+
registration = Net::HTTP::Post.new('/accounts')
|
47
|
+
registration.set_form_data(:email => options[:email])
|
48
|
+
response = client.request(registration)
|
49
|
+
if Net::HTTPUnprocessableEntity === response
|
50
|
+
puts "Registration error: #{response.body}"
|
51
|
+
elsif Net::HTTPSuccess === response
|
52
|
+
puts "Application registered. Use RUBY_GC_TOKEN=#{response.body} in your environment."
|
53
|
+
else
|
54
|
+
puts "Registration error: #{response.body}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue Exception => e
|
58
|
+
puts "Registration error: #{e.inspect}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch_config
|
62
|
+
timeout do
|
63
|
+
config = Net::HTTP::Get.new("/apps/#{options[:config]}")
|
64
|
+
response = client.request(config)
|
65
|
+
if Net::HTTPNoContent === response
|
66
|
+
puts "There is no configuration for Rails app with token #{options[:config]} yet"
|
67
|
+
elsif Net::HTTPNotFound === response
|
68
|
+
puts "Rails app with token #{options[:config]} doesn't exist"
|
69
|
+
elsif Net::HTTPSuccess === response
|
70
|
+
puts "=== Suggested GC configuration:"
|
71
|
+
puts
|
72
|
+
puts response.body
|
73
|
+
else
|
74
|
+
puts "Config retrieval error: #{response.body}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
rescue Exception => e
|
78
|
+
puts "Config retrieval error: #{e.inspect}"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def timeout(&block)
|
83
|
+
Timeout.timeout(TIMEOUT + 1){ block.call }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TuneMyGc
|
4
|
+
class Configurator
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
if Hash === config && !config.empty?
|
13
|
+
TuneMyGc.log "==== Recommended GC configs from #{config.delete("callback")}"
|
14
|
+
write_env("Speed")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def write_env(strategy)
|
20
|
+
config.delete(strategy).each do |var,val|
|
21
|
+
TuneMyGc.log "export #{var}=#{val}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'tunemygc/spies/action_controller'
|
5
|
+
|
6
|
+
module TuneMyGc
|
7
|
+
class Interposer
|
8
|
+
attr_reader :spy
|
9
|
+
attr_accessor :installed
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_initialized
|
16
|
+
GC.start(full_mark: true, :immediate_sweep => true)
|
17
|
+
TuneMyGc.install_gc_tracepoint
|
18
|
+
TuneMyGc.log "hooked: GC tracepoints"
|
19
|
+
TuneMyGc.snapshot(:BOOTED)
|
20
|
+
|
21
|
+
TuneMyGc.interposer.spy.install
|
22
|
+
end
|
23
|
+
|
24
|
+
def install
|
25
|
+
return if @installed
|
26
|
+
TuneMyGc.log "interposing"
|
27
|
+
ActiveSupport.on_load(:after_initialize) do
|
28
|
+
TuneMyGc.interposer.on_initialized
|
29
|
+
end
|
30
|
+
TuneMyGc.log "hooked: after_initialize"
|
31
|
+
|
32
|
+
at_exit do
|
33
|
+
if @installed
|
34
|
+
TuneMyGc.log "at_exit"
|
35
|
+
@spy.uninstall
|
36
|
+
TuneMyGc.snapshot(:TERMINATED)
|
37
|
+
TuneMyGc.reccommendations
|
38
|
+
end
|
39
|
+
end
|
40
|
+
TuneMyGc.log "hooked: at_exit"
|
41
|
+
@installed = true
|
42
|
+
TuneMyGc.log "interposed"
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_uninstall
|
46
|
+
@spy.check_uninstall
|
47
|
+
end
|
48
|
+
|
49
|
+
def uninstall
|
50
|
+
@spy.uninstall
|
51
|
+
reset
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def reset
|
56
|
+
@installed = false
|
57
|
+
@spy = TuneMyGc::Spies::ActionController.new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tunemygc/subscriber'
|
4
|
+
|
5
|
+
module TuneMyGc
|
6
|
+
class StartRequestSubscriber < Subscriber
|
7
|
+
def start(name, id, payload)
|
8
|
+
TuneMyGc.snapshot(:REQUEST_PROCESSING_STARTED)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class EndRequestSubscriber < Subscriber
|
13
|
+
def finish(name, id, payload)
|
14
|
+
TuneMyGc.snapshot(:REQUEST_PROCESSING_ENDED)
|
15
|
+
TuneMyGc.interposer.check_uninstall
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module TuneMyGc
|
6
|
+
class Snapshotter
|
7
|
+
MAX_SAMPLES = 1000
|
8
|
+
|
9
|
+
attr_reader :buffer
|
10
|
+
|
11
|
+
def initialize(buf = Queue.new)
|
12
|
+
@buffer = buf
|
13
|
+
end
|
14
|
+
|
15
|
+
def take(stage, timestamp = nil, meta = nil)
|
16
|
+
_buffer([(timestamp || TuneMyGc.walltime), TuneMyGc.peak_rss, TuneMyGc.current_rss, stage, GC.stat, GC.latest_gc_info, meta])
|
17
|
+
end
|
18
|
+
|
19
|
+
# low level interface, for tests and GC callback
|
20
|
+
def take_raw(snapshot)
|
21
|
+
_buffer(snapshot)
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
@buffer.clear
|
26
|
+
end
|
27
|
+
|
28
|
+
def size
|
29
|
+
@buffer.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def deq
|
33
|
+
@buffer.deq
|
34
|
+
end
|
35
|
+
|
36
|
+
def empty?
|
37
|
+
@buffer.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def _buffer(snapshot)
|
42
|
+
if size < MAX_SAMPLES
|
43
|
+
@buffer << snapshot
|
44
|
+
else
|
45
|
+
TuneMyGc.log "Discarding snapshot #{snapshot.inspect} (max samples threshold of #{MAX_SAMPLES} reached)"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tunemygc/request_subscriber'
|
4
|
+
|
5
|
+
module TuneMyGc
|
6
|
+
module Spies
|
7
|
+
class ActionController
|
8
|
+
attr_reader :subscriptions
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@subscriptions = []
|
12
|
+
@requests_processed = 0
|
13
|
+
@requests_limit = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def install
|
17
|
+
@subscriptions << ActiveSupport::Notifications.subscribe(/^start_processing.action_controller$/, TuneMyGc::StartRequestSubscriber.new)
|
18
|
+
TuneMyGc.log "hooked: start_processing.action_controller"
|
19
|
+
|
20
|
+
@subscriptions << ActiveSupport::Notifications.subscribe(/^process_action.action_controller$/, TuneMyGc::EndRequestSubscriber.new)
|
21
|
+
TuneMyGc.log "hooked: process_action.action_controller"
|
22
|
+
end
|
23
|
+
|
24
|
+
def uninstall
|
25
|
+
TuneMyGc.uninstall_gc_tracepoint
|
26
|
+
TuneMyGc.log "uninstalled GC tracepoint"
|
27
|
+
@subscriptions.each{|s| ActiveSupport::Notifications.unsubscribe(s) }
|
28
|
+
@subscriptions.clear
|
29
|
+
TuneMyGc.log "cleared ActiveSupport subscriptions"
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_uninstall
|
33
|
+
if ENV["RUBY_GC_TUNE_REQUESTS"]
|
34
|
+
@requests_limit ||= Integer(ENV["RUBY_GC_TUNE_REQUESTS"])
|
35
|
+
@requests_processed += 1
|
36
|
+
if @requests_processed == @requests_limit
|
37
|
+
uninstall
|
38
|
+
TuneMyGc.log "kamikaze after #{@requests_processed} of #{@requests_limit} requests"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'certified'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module TuneMyGc
|
8
|
+
class Syncer
|
9
|
+
TIMEOUT = 10 #seconds
|
10
|
+
ENVIRONMENT = [ENV['RUBY_GC_TOKEN'], RUBY_VERSION, Rails.version, ENV.select {|k,v| k =~ /RUBY_GC_/ }, TuneMyGc::VERSION, GC::OPTS, GC::INTERNAL_CONSTANTS].freeze
|
11
|
+
|
12
|
+
attr_reader :uri, :client
|
13
|
+
|
14
|
+
def initialize(host = TuneMyGc::HOST)
|
15
|
+
@uri = URI("http://#{host}/ruby")
|
16
|
+
@client = Net::HTTP.new(@uri.host, @uri.port)
|
17
|
+
@client.use_ssl = (uri.port == 443)
|
18
|
+
@client.read_timeout = TIMEOUT
|
19
|
+
end
|
20
|
+
|
21
|
+
def sync(snapshotter)
|
22
|
+
response = nil
|
23
|
+
timeout do
|
24
|
+
response = sync_with_tuner(snapshotter)
|
25
|
+
end
|
26
|
+
timeout do
|
27
|
+
process_config_callback(response)
|
28
|
+
end if response
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def timeout(&block)
|
33
|
+
Timeout.timeout(TIMEOUT + 1){ block.call }
|
34
|
+
end
|
35
|
+
|
36
|
+
def sync_with_tuner(snapshotter)
|
37
|
+
snapshots = 0
|
38
|
+
# Fallback to Timeout if Net::HTTP read timeout fails
|
39
|
+
snapshots = snapshotter.size
|
40
|
+
TuneMyGc.log "Syncing #{snapshots} snapshots"
|
41
|
+
payload = [ENVIRONMENT]
|
42
|
+
debug = ENV["RUBY_GC_TUNE_DEBUG"]
|
43
|
+
TuneMyGc.log "=== Snapshots ===" if debug
|
44
|
+
while !snapshotter.empty?
|
45
|
+
snapshot = snapshotter.deq
|
46
|
+
TuneMyGc.log(snapshot) if debug
|
47
|
+
payload << snapshot
|
48
|
+
end
|
49
|
+
data = ActiveSupport::JSON.encode(payload)
|
50
|
+
response = client.post(uri.path, data, TuneMyGc::HEADERS)
|
51
|
+
if Net::HTTPNotFound === response
|
52
|
+
TuneMyGc.log "Invalid application token. Please generate one with 'bundle exec tunemygc <a_valid_email_address>' and set the RUBY_GC_TOKEN environment variable"
|
53
|
+
return false
|
54
|
+
elsif Net::HTTPNotImplemented === response
|
55
|
+
TuneMyGc.log "Ruby version #{RUBY_VERSION} or Rails version #{Rails.version} not supported. Failed to sync #{snapshots} snapshots"
|
56
|
+
return false
|
57
|
+
elsif Net::HTTPUpgradeRequired === response
|
58
|
+
TuneMyGc.log "Agent version #{response.body} required - please upgrade. Failed to sync #{snapshots} snapshots"
|
59
|
+
return false
|
60
|
+
elsif Net::HTTPPreconditionFailed === response
|
61
|
+
TuneMyGc.log "The GC is already tuned by environment variables (#{response.body}) - we respect that, doing nothing. Failed to sync #{snapshots} snapshots"
|
62
|
+
return false
|
63
|
+
elsif Net::HTTPBadRequest === response
|
64
|
+
TuneMyGc.log "Invalid payload (#{response.body}). Failed to sync #{snapshots} snapshots"
|
65
|
+
return false
|
66
|
+
elsif Net::HTTPInternalServerError === response
|
67
|
+
TuneMyGc.log "An internal error occurred (#{response.body}). Failed to sync #{snapshots} snapshots"
|
68
|
+
return false
|
69
|
+
elsif Net::HTTPSuccess === response
|
70
|
+
response
|
71
|
+
else
|
72
|
+
TuneMyGc.log "Unknown error: #{response.body}"
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
rescue Exception => e
|
76
|
+
TuneMyGc.log "Failed to sync #{snapshots} snapshots (error: #{e})"
|
77
|
+
return false
|
78
|
+
ensure
|
79
|
+
# Prefer to loose data points than accumulate buffers indefinitely on error or other conditions
|
80
|
+
snapshotter.clear
|
81
|
+
end
|
82
|
+
|
83
|
+
def process_config_callback(response)
|
84
|
+
config = client.get(URI(response.body).path)
|
85
|
+
ActiveSupport::JSON.decode(config.body).merge('callback' => response.body)
|
86
|
+
rescue Exception => e
|
87
|
+
TuneMyGc.log "Failed to process config callback url #{response.body} (error: #{e})"
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/tunemygc.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
tunemygc_min_ruby_version = "2.1.0"
|
4
|
+
|
5
|
+
if RUBY_VERSION >= tunemygc_min_ruby_version
|
6
|
+
require "tunemygc/version" unless defined? TuneMyGc::VERSION
|
7
|
+
|
8
|
+
module TuneMyGc
|
9
|
+
HOST = (ENV['RUBY_GC_TUNE_HOST'] || "tunemygc.com:443").freeze
|
10
|
+
HEADERS = { "Content-Type" => "application/json",
|
11
|
+
"Accept" => "application/json",
|
12
|
+
"User-Agent" => "TuneMyGC #{TuneMyGc::VERSION}"}.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV["RUBY_GC_TUNE"] && defined?(Rails) && Rails.version >= "4.0"
|
16
|
+
require 'tunemygc/railtie'
|
17
|
+
else
|
18
|
+
puts "[TuneMyGC] not enabled"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
puts "[TuneMyGC] requires a Ruby version #{tunemygc_min_ruby_version} or newer"
|
22
|
+
end
|
data/test/fixtures.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Fixtures
|
6
|
+
STAGE_BOOTED = [1420152606.1162581, "BOOTED", {"count"=>32, "heap_used"=>950, "heap_length"=>1519, "heap_increment"=>569, "heap_live_slot"=>385225, "heap_free_slot"=>2014, "heap_final_slot"=>0, "heap_swept_slot"=>101119, "heap_eden_page_length"=>950, "heap_tomb_page_length"=>0, "total_allocated_object"=>2184137, "total_freed_object"=>1798912, "malloc_increase"=>9665288, "malloc_limit"=>16777216, "minor_gc_count"=>26, "major_gc_count"=>6, "remembered_shady_object"=>5145, "remembered_shady_object_limit"=>6032, "old_object"=>230164, "old_object_limit"=>301030, "oldmalloc_increase"=>11715304, "oldmalloc_limit"=>24159190}, {"major_by"=>nil, "gc_by"=>"newobj", "have_finalizer"=>false, "immediate_sweep"=>false}, nil]
|
7
|
+
|
8
|
+
CONFIG = {"Memory"=>{"RUBY_GC_HEAP_INIT_SLOTS"=>307562,"RUBY_GC_HEAP_FREE_SLOTS"=>6151,"RUBY_GC_HEAP_GROWTH_FACTOR"=>0.07,"RUBY_GC_HEAP_GROWTH_MAX_SLOTS"=>6151,"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR"=>0.11,"RUBY_GC_MALLOC_LIMIT"=>2000000,"RUBY_GC_MALLOC_LIMIT_MAX"=>4000000,"RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR"=>0.11,"RUBY_GC_OLDMALLOC_LIMIT"=>2000000,"RUBY_GC_OLDMALLOC_LIMIT_MAX"=>4000000,"RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR"=>0.11},"Speed"=>{"RUBY_GC_HEAP_INIT_SLOTS"=>369074,"RUBY_GC_HEAP_FREE_SLOTS"=>184537,"RUBY_GC_HEAP_GROWTH_FACTOR"=>1.2,"RUBY_GC_HEAP_GROWTH_MAX_SLOTS"=>123024,"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR"=>2.0,"RUBY_GC_MALLOC_LIMIT"=>64000000,"RUBY_GC_MALLOC_LIMIT_MAX"=>128000000,"RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR"=>1.2,"RUBY_GC_OLDMALLOC_LIMIT"=>64000000,"RUBY_GC_OLDMALLOC_LIMIT_MAX"=>128000000,"RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR"=>1.2}, "callback"=>"https://tunemygc.com/configs/8d07e13cc5a7bba2da3510b9ca5e75f4.json"}
|
9
|
+
end
|