tunemygc 1.0.1
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 +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
|