alloc_track 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c2bcef216fe18bd1578ecc1b47f26e10cf6669ad
4
+ data.tar.gz: 2f003bcc8dc4f0f30a7123518192fb7c9a3c22da
5
+ SHA512:
6
+ metadata.gz: 50d0ecf47e7a19ac2f9b59e484ab84e8379243d25c7a31487be87924d6b4f486bdecb50613874fcdc07b98a90b1c60946d264257df4597cab590233ebbd6b142
7
+ data.tar.gz: 86ee7fc454d489abb83df54d01d6fb226da1f0d66f56e5dacc764bf02fbc173494b3ef0bfe1abf2af7f159a978899ca05c4e3c8030c15d62d3bbb743c009a7e7
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ /.bundle/
2
+ /lib/alloc_track/*.so
3
+ /lib/alloc_track/*.bundle
4
+ /tmp/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,18 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ alloc_track (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.3.1)
10
+ rake-compiler (0.9.2)
11
+ rake
12
+
13
+ PLATFORMS
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ alloc_track!
18
+ rake-compiler (~> 0.9)
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Scott Francis <scott.francis@shopify.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ ## alloc_track
2
+
3
+ Tracks the number of outstanding allocations on a Ruby thread using the internal tracepoint APIs.
4
+
5
+ ### Features
6
+
7
+ - C extension for webscale
8
+ - Allocations are counted per thread
9
+ - Can raise an exception when allocations exceed a certain threshold
10
+
11
+ ### Usage
12
+
13
+ It can be used to track the number of allocated objects over a period of time:
14
+
15
+ ```ruby
16
+ require 'alloc_track/alloc_track'
17
+
18
+ AllocTrack.start
19
+ 100.times { Object.new }
20
+ puts AllocTrack.delta # >= 100
21
+ GC.start
22
+ puts AllocTrack.alloc # >= 100
23
+ puts AllocTrack.delta # <= 100
24
+ puts AllocTrack.free # >= 100
25
+ AllocTrack.stop
26
+ ```
27
+
28
+ Perhaps more useful is the ability to raise when the number of allocations crosses a certain threshold:
29
+ ```ruby
30
+ require 'alloc_track/alloc_track'
31
+
32
+ AllocTrack.limit 100 do
33
+ 200.times { Object.new } # raises AllocTrack::LimitExceeded
34
+ end
35
+ ```
36
+
37
+ ### Performance
38
+
39
+ In a contrived benchmark that simply allocates 10,000,000 new objects, alloc_track adds ~30% overhead:
40
+
41
+ ```
42
+ [vagrant] ~/src/alloc_track (master *%) $ ruby -Ilib ./test/benchmark_alloc_track.rb
43
+ user system total real
44
+ none: 1.540000 0.000000 1.540000 ( 1.545192)
45
+ tracking: 2.020000 0.000000 2.020000 ( 2.034162)
46
+ ```
47
+
48
+ Expect real-world (operations other than just memory allocation) performance overhead to be much less severe.
49
+
50
+ ### Limitations
51
+
52
+ - Allocation tracker can only be run on one thread per process
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ task :default => :test
2
+
3
+ # ==========================================================
4
+ # Packaging
5
+ # ==========================================================
6
+
7
+ GEMSPEC = eval(File.read('alloc_track.gemspec'))
8
+
9
+ require 'rubygems/package_task'
10
+ Gem::PackageTask.new(GEMSPEC) do |pkg|
11
+ end
12
+
13
+ # ==========================================================
14
+ # Ruby Extension
15
+ # ==========================================================
16
+
17
+ require 'rake/extensiontask'
18
+ Rake::ExtensionTask.new('alloc_track', GEMSPEC) do |ext|
19
+ ext.ext_dir = 'ext/alloc_track'
20
+ ext.lib_dir = 'lib/alloc_track'
21
+ end
22
+ task :build => :compile
23
+
24
+ # ==========================================================
25
+ # Testing
26
+ # ==========================================================
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new 'test' do |t|
30
+ t.test_files = FileList['test/test_*.rb']
31
+ end
32
+ task :test => :build
33
+
34
+ task :benchmark => :build do
35
+ ruby "-Ilib ./test/benchmark_alloc_track.rb"
36
+ end
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'alloc_track'
3
+ s.version = '0.0.2'
4
+ s.summary = 'allocation tracker for ruby 2.1+'
5
+ s.description = 'tracks memory allocations with rgengc in ruby 2.1'
6
+
7
+ s.homepage = 'https://github.com/csfrancis/alloc_track'
8
+ s.authors = 'Scott Francis'
9
+ s.email = 'scott.francis@shopify.com'
10
+ s.license = 'MIT'
11
+
12
+ s.files = `git ls-files`.split("\n")
13
+ s.extensions = ['ext/alloc_track/extconf.rb']
14
+ s.add_development_dependency 'rake-compiler', '~> 0.9'
15
+ end
@@ -0,0 +1,270 @@
1
+ #include "ruby/ruby.h"
2
+ #include "ruby/intern.h"
3
+ #include "ruby/debug.h"
4
+
5
+ #define ALLOCTRACK_OBJ_BIT FL_USER18
6
+
7
+ typedef struct stat_collector {
8
+ struct stat_collector *next; /* not currently used */
9
+ VALUE thread;
10
+ int current_alloc;
11
+ int current_free;
12
+ int current_limit;
13
+ int limit_signal;
14
+ } stat_collector_t;
15
+
16
+ static VALUE mAllocTrack;
17
+ static VALUE tpval, tpval_exception;
18
+ static VALUE eAllocTrackError, eAllocTrackLimitExceeded;
19
+ static stat_collector_t *root_collector, *current_collector;
20
+
21
+ #define LOG(s) fprintf(stderr, s); fflush(stderr);
22
+
23
+ static stat_collector_t *
24
+ add_collector(VALUE thread)
25
+ {
26
+ stat_collector_t *c = (stat_collector_t *) calloc(1, sizeof(*c));
27
+ c->thread = thread;
28
+ if (root_collector) {
29
+ c->next = root_collector;
30
+ root_collector = c;
31
+ } else {
32
+ root_collector = c;
33
+ rb_tracepoint_enable(tpval);
34
+ }
35
+ return c;
36
+ }
37
+
38
+ static void
39
+ remove_collector(VALUE thread)
40
+ {
41
+ stat_collector_t *c, *prev = NULL;
42
+
43
+ for (c = root_collector; c != NULL; prev = c, c = c->next) {
44
+ if (c->thread == thread) {
45
+ if (!prev) {
46
+ root_collector = c->next;
47
+ } else {
48
+ prev->next = c->next;
49
+ }
50
+ current_collector = NULL;
51
+ free(c);
52
+ break;
53
+ }
54
+ }
55
+
56
+ if (!root_collector) {
57
+ rb_tracepoint_disable(tpval);
58
+ }
59
+ }
60
+
61
+ static stat_collector_t *
62
+ get_collector(VALUE thread)
63
+ {
64
+ stat_collector_t *c;
65
+
66
+ if (current_collector && current_collector->thread == thread) {
67
+ return current_collector;
68
+ }
69
+
70
+ for (c = root_collector; c != NULL; c = c->next) {
71
+ if (c->thread == thread) {
72
+ /* cache the collector so we don't have to scan the list every time */
73
+ current_collector = c;
74
+ return c;
75
+ }
76
+ }
77
+
78
+ return NULL;
79
+ }
80
+
81
+ static VALUE
82
+ started()
83
+ {
84
+ return get_collector(rb_thread_current()) ? Qtrue : Qfalse;
85
+ }
86
+
87
+ static void
88
+ validate_started()
89
+ {
90
+ if (!started()) {
91
+ rb_raise(eAllocTrackError, "allocation tracker has not been started");
92
+ }
93
+ }
94
+
95
+ static void
96
+ validate_stopped()
97
+ {
98
+ if (started()) {
99
+ rb_raise(eAllocTrackError, "allocation tracker already started");
100
+ }
101
+ }
102
+
103
+ static VALUE
104
+ start()
105
+ {
106
+ validate_stopped();
107
+ /* TODO: support multiple running trackers */
108
+ if (root_collector) {
109
+ rb_raise(eAllocTrackError, "allocation tracker already running on another thread");
110
+ }
111
+ add_collector(rb_thread_current());
112
+ return Qnil;
113
+ }
114
+
115
+ static VALUE
116
+ stop()
117
+ {
118
+ validate_started();
119
+ remove_collector(rb_thread_current());
120
+ return Qnil;
121
+ }
122
+
123
+ static VALUE
124
+ alloc()
125
+ {
126
+ validate_started();
127
+ return INT2FIX(get_collector(rb_thread_current())->current_alloc);
128
+ }
129
+
130
+ static VALUE
131
+ _free()
132
+ {
133
+ validate_started();
134
+ return INT2FIX(get_collector(rb_thread_current())->current_free);
135
+ }
136
+
137
+ static VALUE
138
+ delta()
139
+ {
140
+ stat_collector_t *c;
141
+ validate_started();
142
+ c = get_collector(rb_thread_current());
143
+ return INT2FIX(c->current_alloc - c->current_free);
144
+ }
145
+
146
+ static VALUE
147
+ do_limit(VALUE arg)
148
+ {
149
+ start();
150
+ get_collector(rb_thread_current())->current_limit = FIX2INT(arg);
151
+ return rb_yield(Qnil);
152
+ }
153
+
154
+ static VALUE
155
+ ensure_stopped(VALUE arg)
156
+ {
157
+ if (started()) {
158
+ stop();
159
+ }
160
+ return Qnil;
161
+ }
162
+
163
+ static VALUE
164
+ limit(VALUE self, VALUE num_allocs)
165
+ {
166
+ if (!rb_block_given_p()) {
167
+ rb_raise(rb_eArgError, "block required");
168
+ }
169
+ if (!RB_TYPE_P(num_allocs, T_FIXNUM)) {
170
+ rb_raise(rb_eArgError, "limit() must be passed a number");
171
+ }
172
+ validate_stopped();
173
+ return rb_ensure(do_limit, num_allocs, ensure_stopped, Qnil);
174
+ }
175
+
176
+ static int
177
+ is_collector_enabled(stat_collector_t *c)
178
+ {
179
+ return c->limit_signal == 0 ? 1 : 0;
180
+ }
181
+
182
+ static int
183
+ is_collector_limit_exceeded(stat_collector_t *c)
184
+ {
185
+ return c->limit_signal;
186
+ }
187
+
188
+ static void
189
+ tracepoint_hook(VALUE tpval, void *data)
190
+ {
191
+ stat_collector_t *c;
192
+ rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
193
+ rb_event_flag_t flag = rb_tracearg_event_flag(tparg);
194
+ VALUE obj = rb_tracearg_object(tparg);
195
+ switch(flag) {
196
+ case RUBY_INTERNAL_EVENT_NEWOBJ:
197
+ if ((c = get_collector(rb_thread_current())) != NULL && is_collector_enabled(c)) {
198
+ RBASIC(obj)->flags |= ALLOCTRACK_OBJ_BIT;
199
+ c->current_alloc++;
200
+
201
+ if (c->current_limit && (c->current_alloc - c->current_free) > c->current_limit) {
202
+ c->limit_signal = 1;
203
+ if (!rb_tracepoint_enabled_p(tpval_exception)) {
204
+ /*
205
+ it's not safe to raise an exception from an internal event handler.
206
+ in order to get around this, we enable a normal tracepoint on all
207
+ events and raise from there.
208
+ */
209
+ rb_tracepoint_enable(tpval_exception);
210
+ }
211
+ }
212
+ }
213
+ break;
214
+ case RUBY_INTERNAL_EVENT_FREEOBJ:
215
+ if ((c = get_collector(rb_thread_current())) != NULL && is_collector_enabled(c) &&
216
+ (RBASIC(obj)->flags & ALLOCTRACK_OBJ_BIT)) {
217
+ c->current_free++;
218
+ }
219
+ break;
220
+ }
221
+ }
222
+
223
+ static int
224
+ any_collectors_with_exceeded_limits()
225
+ {
226
+ stat_collector_t *c;
227
+ for (c = root_collector; c != NULL; c = c->next) {
228
+ if (c->limit_signal) {
229
+ return 1;
230
+ }
231
+ }
232
+ return 0;
233
+ }
234
+
235
+ static void
236
+ exception_tracepoint_hook(VALUE tpval, void *data)
237
+ {
238
+ VALUE th = rb_thread_current();
239
+ stat_collector_t *c;
240
+ if ((c = get_collector(th)) != NULL && is_collector_limit_exceeded(c)) {
241
+ remove_collector(th);
242
+ if (!any_collectors_with_exceeded_limits()) {
243
+ rb_tracepoint_disable(tpval_exception);
244
+ }
245
+ rb_raise(eAllocTrackLimitExceeded, "allocation limit exceeded");
246
+ }
247
+ }
248
+
249
+ void
250
+ Init_alloc_track()
251
+ {
252
+ mAllocTrack = rb_define_module("AllocTrack");
253
+
254
+ rb_define_singleton_method(mAllocTrack, "start", start, 0);
255
+ rb_define_singleton_method(mAllocTrack, "started?", started, 0);
256
+ rb_define_singleton_method(mAllocTrack, "stop", stop, 0);
257
+ rb_define_singleton_method(mAllocTrack, "alloc", alloc, 0);
258
+ rb_define_singleton_method(mAllocTrack, "free", _free, 0);
259
+ rb_define_singleton_method(mAllocTrack, "delta", delta, 0);
260
+ rb_define_singleton_method(mAllocTrack, "limit", limit, 1);
261
+
262
+ eAllocTrackError = rb_define_class_under(mAllocTrack, "Error", rb_eStandardError);
263
+ eAllocTrackLimitExceeded = rb_define_class_under(mAllocTrack, "LimitExceeded", rb_eStandardError);
264
+
265
+ tpval = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ|RUBY_INTERNAL_EVENT_FREEOBJ, tracepoint_hook, NULL);
266
+ tpval_exception = rb_tracepoint_new(0, RUBY_EVENT_TRACEPOINT_ALL, exception_tracepoint_hook, NULL);
267
+
268
+ rb_gc_register_mark_object(tpval);
269
+ rb_gc_register_mark_object(tpval_exception);
270
+ }
@@ -0,0 +1,15 @@
1
+ require 'mkmf'
2
+
3
+ $CFLAGS = "-O3"
4
+
5
+ have_func('rb_tracepoint_enable')
6
+
7
+ gc_event = have_const('RUBY_INTERNAL_EVENT_NEWOBJ')
8
+
9
+ if gc_event
10
+ create_makefile('alloc_track/alloc_track')
11
+ else
12
+ File.open('Makefile', 'w') do |f|
13
+ f.puts "install:\n\t\n"
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ require 'benchmark'
2
+ require 'alloc_track/alloc_track'
3
+
4
+ def alloc_obj
5
+ Object.new
6
+ end
7
+
8
+ n = 100
9
+ i = 100000
10
+
11
+ Benchmark.bm(8) do |x|
12
+ x.report("none:") do
13
+ GC.start
14
+ n.times do
15
+ i.times { alloc_obj }
16
+ GC.start
17
+ end
18
+ end
19
+ x.report("tracking:") do
20
+ GC.start
21
+ AllocTrack.start
22
+ n.times do
23
+ i.times { alloc_obj }
24
+ GC.start
25
+ end
26
+ AllocTrack.stop
27
+ end
28
+ end
@@ -0,0 +1,76 @@
1
+ require 'test/unit'
2
+ require 'alloc_track/alloc_track'
3
+
4
+ class TestAllocTrack < Test::Unit::TestCase
5
+ def test_allocate
6
+ AllocTrack.start
7
+ 100.times { Object.new }
8
+ assert_operator AllocTrack.delta, :>=, 100
9
+ AllocTrack.stop
10
+ end
11
+
12
+ def test_allocate_with_gc
13
+ AllocTrack.start
14
+ 100.times { Object.new }
15
+ GC.start
16
+ assert_operator AllocTrack.alloc, :>=, 100
17
+ assert_operator AllocTrack.delta, :<, 100
18
+ assert_operator AllocTrack.free, :>=, 100
19
+ AllocTrack.stop
20
+ end
21
+
22
+ def test_thread_not_included
23
+ AllocTrack.start
24
+ t = Thread.new do
25
+ 100.times { Object.new }
26
+ end
27
+ t.join
28
+ assert_operator AllocTrack.delta, :<, 100
29
+ AllocTrack.stop
30
+ end
31
+
32
+ def test_delta_raises_when_not_started
33
+ assert_raise AllocTrack::Error do
34
+ AllocTrack.delta
35
+ end
36
+ end
37
+
38
+ def test_limit_with_no_block
39
+ assert_raise ArgumentError do
40
+ AllocTrack.limit 100
41
+ end
42
+ end
43
+
44
+ def test_limit_with_non_number
45
+ assert_raise ArgumentError do
46
+ AllocTrack.limit "foo" do
47
+ end
48
+ end
49
+ end
50
+
51
+ def test_limit_raises
52
+ assert_raise AllocTrack::LimitExceeded do
53
+ AllocTrack.limit 10 do
54
+ 200.times { Object.new }
55
+ end
56
+ end
57
+ end
58
+
59
+ def test_within_limit
60
+ assert_nothing_raised do
61
+ AllocTrack.limit 100 do
62
+ 50.times { Object.new }
63
+ end
64
+ end
65
+ end
66
+
67
+ def test_limit_exception_stops
68
+ assert_raise RuntimeError do
69
+ AllocTrack.limit 100 do
70
+ raise RuntimeError
71
+ end
72
+ end
73
+ refute AllocTrack.started?
74
+ end
75
+
76
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alloc_track
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Scott Francis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ description: tracks memory allocations with rgengc in ruby 2.1
28
+ email: scott.francis@shopify.com
29
+ executables: []
30
+ extensions:
31
+ - ext/alloc_track/extconf.rb
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - LICENSE.md
38
+ - README.md
39
+ - Rakefile
40
+ - alloc_track.gemspec
41
+ - ext/alloc_track/alloc_track.c
42
+ - ext/alloc_track/extconf.rb
43
+ - test/benchmark_alloc_track.rb
44
+ - test/test_alloc_track.rb
45
+ homepage: https://github.com/csfrancis/alloc_track
46
+ licenses:
47
+ - MIT
48
+ metadata: {}
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 2.2.2
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: allocation tracker for ruby 2.1+
69
+ test_files: []