alloc_track 0.0.2

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 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: []