living_dead 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +108 -0
  7. data/Rakefile +17 -0
  8. data/changelog.md +5 -0
  9. data/ext/living_dead/extconf.rb +2 -0
  10. data/ext/living_dead/living_dead.c +217 -0
  11. data/lib/living_dead.rb +96 -0
  12. data/lib/living_dead/version.rb +3 -0
  13. data/living_dead.gemspec +28 -0
  14. data/scratch.rb +20 -0
  15. data/spec/fixtures/singleton_class/in_class.rb +24 -0
  16. data/spec/fixtures/singleton_class/in_proc.rb +23 -0
  17. data/spec/fixtures/singleton_class/method_in_proc.rb +21 -0
  18. data/spec/fixtures/singleton_class/retained.rb +20 -0
  19. data/spec/fixtures/singleton_class/simple.rb +21 -0
  20. data/spec/fixtures/singleton_class_instance_eval/in_class.rb +24 -0
  21. data/spec/fixtures/singleton_class_instance_eval/in_proc.rb +24 -0
  22. data/spec/fixtures/singleton_class_instance_eval/method_in_proc.rb +22 -0
  23. data/spec/fixtures/singleton_class_instance_eval/retained.rb +21 -0
  24. data/spec/fixtures/singleton_class_instance_eval/simple.rb +22 -0
  25. data/spec/fixtures/string/not_retained.rb +19 -0
  26. data/spec/fixtures/string/retained.rb +19 -0
  27. data/spec/fixtures/string/string_in_class.rb +22 -0
  28. data/spec/fixtures/string/string_in_proc.rb +21 -0
  29. data/spec/fixtures/string/string_method_in_proc.rb +20 -0
  30. data/spec/fixtures/times_map/in_class.rb +24 -0
  31. data/spec/fixtures/times_map/in_proc.rb +25 -0
  32. data/spec/fixtures/times_map/method_in_proc.rb +22 -0
  33. data/spec/fixtures/times_map/retained.rb +22 -0
  34. data/spec/fixtures/times_map/simple.rb +22 -0
  35. data/spec/living_dead/singleton_class_instance_eval_spec.rb +30 -0
  36. data/spec/living_dead/singleton_class_spec.rb +30 -0
  37. data/spec/living_dead/string_spec.rb +24 -0
  38. data/spec/living_dead/times_map_spec.rb +30 -0
  39. data/spec/living_dead_spec.rb +24 -0
  40. data/spec/spec_helper.rb +28 -0
  41. metadata +168 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6409ddbc4318a1d8753890c7f29344587e66fe9f
4
+ data.tar.gz: 26d81012fe0155e3f8fe0c359f4cbae0d9190d98
5
+ SHA512:
6
+ metadata.gz: 28f80d02748e796776325a4fc6e8922a0442480ae9ec9395ce66c040ca8ee2824e45e9e3325032683ac6e640dff5be58bd8eabe6171a265be1ad337d5c352a4f
7
+ data.tar.gz: c10210bc2c3d0ab0fb3e5175810824b4423fcfdff639c3ef871c3c66d45c0244076e1d1b086945d7c062072aa61e12369792338183d2bc16b31232072e6ec5c0
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
19
+ *.bundle
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.3
4
+ before_install:
5
+ - gem update bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in living_dead.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Richard Schneeman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # LivingDead
2
+
3
+ [![Build Status](https://travis-ci.org/schneems/living_dead.svg?branch=master)](https://travis-ci.org/schneems/living_dead)
4
+
5
+ This module allows to see if an object is retained "still alive" or if it is freed "dead".
6
+
7
+ ![Dancing Zombies](https://www.dropbox.com/s/lshgqzek77107mh/zombies.gif?dl=1)
8
+
9
+ ## Problems
10
+
11
+ There be dragons see `LivingDead.gc_start` to see some of the hacks we have to do for who knows why to get this to work.
12
+
13
+ ## Install
14
+
15
+ In your Gemfile add:
16
+
17
+ ```
18
+ gem 'living_dead'
19
+ ```
20
+
21
+ Then run
22
+
23
+ ```
24
+ $ bundle install
25
+ ```
26
+
27
+ ## How it works
28
+
29
+ Before you use this you should understand how it works. This gem is a c-extension. It hooks into the Ruby tracepoint API and registeres a hook for the `RUBY_INTERNAL_EVENT_FREEOBJ` event. This
30
+ event gets called when an object is freed (i.e. it is not retained and garbage collection has been called).
31
+
32
+ When you call `LivingDead.trace(obj)` we store the `object_id` of the thing you are "tracing" in a hash. Then inside of our c-extension hook we listen for when an object is freed. When this happens we check to see if that object's `object_id` matches one in our hash. If it does we mark it down in a seperate "freed" hash.
33
+
34
+ We don't retain the objects you are tracing but we do keep a copy of the `object_id`, we can then use this same number to check the freed hash to see if it was recorded as being freed.
35
+
36
+ > WARNING: Seriously, see `LivingDead.gc_start`. This library isn't bullet proof.
37
+
38
+ ## Quick Start
39
+
40
+ Require the library and use `LivingDead.trace` to "trace" an object. Later use `LivingDead.traced_objects` to iterate through "tracers" of each object.
41
+
42
+ Here is an example of tracing an object that is not retained:
43
+
44
+ ```
45
+ require 'living_dead'
46
+
47
+ def run
48
+ obj = Object.new
49
+ LivingDead.trace(obj)
50
+
51
+ return nil
52
+ end
53
+
54
+ run
55
+
56
+ puts LivingDead.traced_objects.select {|tracer| tracer.retained? }.count
57
+ # => 0
58
+ ```
59
+
60
+ > Note: Calling `LivingDead.traced_objects` auto calls `GC.start`, you don't need to do it manually. However you should look at the implementation of `LivingDead.gc_start` to understand the depth of hacks you're playing with.
61
+
62
+ Here is an example of tracing an object that IS retained:
63
+
64
+ ```
65
+ require 'living_dead'
66
+
67
+ def run
68
+ obj = Object.new
69
+ LivingDead.trace(obj)
70
+
71
+ return obj
72
+ end
73
+
74
+ @retained_here = run
75
+
76
+ puts LivingDead.traced_objects.select {|tracer| tracer.retained? }.count
77
+ # => 1
78
+ ```
79
+
80
+ You can get more ways of interacting with a tracer by looking at `LivingDead::ObjectTrace`.
81
+
82
+ ## Development
83
+
84
+ Compile the code:
85
+
86
+ ```
87
+ $ rake compile
88
+ ```
89
+
90
+ Run the tests:
91
+
92
+ ```
93
+ $ rake spec
94
+ ```
95
+
96
+ or "why not both":
97
+
98
+ ```
99
+ $ rake compile spec
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
105
+
106
+ Copyright Richard Schneeman 2016
107
+
108
+
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/extensiontask"
3
+ require 'rspec/core/rake_task'
4
+
5
+ spec = Gem::Specification.load('living_dead.gemspec')
6
+
7
+ Rake::ExtensionTask.new("living_dead", spec){|ext|
8
+ ext.lib_dir = "lib/living_dead"
9
+ }
10
+
11
+ RSpec::Core::RakeTask.new('spec' => 'compile')
12
+
13
+ task default: :spec
14
+
15
+ task :run => 'compile' do
16
+ ruby %q{-I ./lib test.rb}
17
+ end
@@ -0,0 +1,5 @@
1
+ ## Master
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial release
@@ -0,0 +1,2 @@
1
+ require 'mkmf'
2
+ create_makefile('living_dead/living_dead')
@@ -0,0 +1,217 @@
1
+ #include "ruby/ruby.h"
2
+ #include "ruby/debug.h"
3
+ #include <assert.h>
4
+
5
+ size_t rb_obj_memsize_of(VALUE obj); /* in gc.c */
6
+
7
+ static VALUE rb_mLivingDead;
8
+
9
+ #define MAX_KEY_DATA 4
10
+
11
+ #define KEY_PATH (1<<1)
12
+ #define KEY_LINE (1<<2)
13
+ #define KEY_TYPE (1<<3)
14
+ #define KEY_CLASS (1<<4)
15
+
16
+ #define MAX_VAL_DATA 6
17
+
18
+ #define VAL_COUNT (1<<1)
19
+ #define VAL_OLDCOUNT (1<<2)
20
+ #define VAL_TOTAL_AGE (1<<3)
21
+ #define VAL_MIN_AGE (1<<4)
22
+ #define VAL_MAX_AGE (1<<5)
23
+ #define VAL_MEMSIZE (1<<6)
24
+
25
+ struct traceobj_arg {
26
+ int running;
27
+ int keys, vals;
28
+ st_table *object_table; /* obj (VALUE) -> allocation_info */
29
+ st_table *str_table; /* cstr -> refcount */
30
+
31
+ st_table *aggregate_table; /* user defined key -> [count, total_age, max_age, min_age] */
32
+ struct allocation_info *freed_allocation_info;
33
+
34
+ /* */
35
+ size_t **lifetime_table;
36
+ size_t allocated_count_table[T_MASK];
37
+ size_t freed_count_table[T_MASK];
38
+ };
39
+
40
+ struct memcmp_key_data {
41
+ int n;
42
+ st_data_t data[MAX_KEY_DATA];
43
+ };
44
+
45
+ static int
46
+ memcmp_hash_compare(st_data_t a, st_data_t b)
47
+ {
48
+ struct memcmp_key_data *k1 = (struct memcmp_key_data *)a;
49
+ struct memcmp_key_data *k2 = (struct memcmp_key_data *)b;
50
+ return memcmp(&k1->data[0], &k2->data[0], k1->n * sizeof(st_data_t));
51
+ }
52
+
53
+ static st_index_t
54
+ memcmp_hash_hash(st_data_t a)
55
+ {
56
+ struct memcmp_key_data *k = (struct memcmp_key_data *)a;
57
+ return rb_memhash(k->data, sizeof(st_data_t) * k->n);
58
+ }
59
+
60
+ static const struct st_hash_type memcmp_hash_type = {
61
+ memcmp_hash_compare, memcmp_hash_hash
62
+ };
63
+
64
+ static struct traceobj_arg *tmp_trace_arg;
65
+
66
+ /*
67
+ * I honestly coppied this from https://github.com/ko1/allocation_tracer
68
+ * We don't need all this per-say. However it's not hurting anything by being here
69
+ * at the moment. Same goes for the majority of code above this point.
70
+ */
71
+ static struct traceobj_arg *
72
+ get_traceobj_arg(void)
73
+ {
74
+ if (tmp_trace_arg == 0) {
75
+ tmp_trace_arg = ALLOC_N(struct traceobj_arg, 1);
76
+ MEMZERO(tmp_trace_arg, struct traceobj_arg, 1);
77
+ tmp_trace_arg->running = 0;
78
+ tmp_trace_arg->keys = 0;
79
+ tmp_trace_arg->vals = VAL_COUNT | VAL_OLDCOUNT | VAL_TOTAL_AGE | VAL_MAX_AGE | VAL_MIN_AGE | VAL_MEMSIZE;
80
+ tmp_trace_arg->aggregate_table = st_init_table(&memcmp_hash_type);
81
+ tmp_trace_arg->object_table = st_init_numtable();
82
+ tmp_trace_arg->str_table = st_init_strtable();
83
+ tmp_trace_arg->freed_allocation_info = NULL;
84
+ tmp_trace_arg->lifetime_table = NULL;
85
+ }
86
+ return tmp_trace_arg;
87
+ }
88
+
89
+ /*
90
+ *
91
+ * [Internal] Callback for the RUBY_INTERNAL_EVENT_FREEOBJ event
92
+ *
93
+ * When an object is freed we check if we are tracing it.
94
+ * If we are, then we register in the `freed_hash` that it has
95
+ * been freed by setting the `object_id` key to a value of `true`
96
+ *
97
+ */
98
+ static void
99
+ freeobj_i(VALUE tpval, void *data)
100
+ {
101
+ // struct traceobj_arg *arg = (struct traceobj_arg *)data;
102
+ VALUE freed_object_id_hash = rb_ivar_get(rb_mLivingDead, rb_intern("freed_object_id_hash"));
103
+ VALUE object_id_tracing_hash = rb_ivar_get(rb_mLivingDead, rb_intern("object_id_tracing_hash"));
104
+
105
+ rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
106
+ VALUE obj = rb_tracearg_object(tparg);
107
+ VALUE object_id = rb_obj_id(obj);
108
+
109
+
110
+ // Useful for debugging, we cannot use `rb_p` here since it allocates new memory
111
+ // Using `rb_p` here will cause a SEGV.
112
+ //
113
+ // long oid = NUM2LONG(rb_obj_id(obj));
114
+ // printf("Freed: %lu\n", oid);
115
+
116
+ if (rb_hash_aref(object_id_tracing_hash, object_id) != Qnil ) {
117
+ rb_hash_aset(freed_object_id_hash, rb_obj_id(obj), Qtrue);
118
+ }
119
+ }
120
+
121
+ // Used for debugging only
122
+ //
123
+ // static void
124
+ // newobj_i(VALUE tpval, void *data)
125
+ // {
126
+ // // struct traceobj_arg *arg = (struct traceobj_arg *)data;
127
+
128
+ // rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval);
129
+ // VALUE obj = rb_tracearg_object(tparg);
130
+ // long oid = NUM2LONG(rb_obj_id(obj));
131
+ // printf("NEW: %lu\n", oid);
132
+ // }
133
+
134
+
135
+ /*
136
+ *
137
+ * call-seq:
138
+ * LivingDead.start -> nil
139
+ *
140
+ * Begins tracing object free events
141
+ *
142
+ */
143
+ static VALUE
144
+ living_dead_start(VALUE self)
145
+ {
146
+ VALUE freeobj_hook;
147
+ // VALUE newobj_hook;
148
+ struct traceobj_arg *arg = get_traceobj_arg();
149
+
150
+ if ((freeobj_hook = rb_ivar_get(rb_mLivingDead, rb_intern("freeobj_hook"))) == Qnil) {
151
+ rb_ivar_set(rb_mLivingDead, rb_intern("freeobj_hook"), freeobj_hook = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, arg));
152
+ rb_ivar_set(rb_mLivingDead, rb_intern("object_id_tracing_hash"), rb_hash_new());
153
+ rb_ivar_set(rb_mLivingDead, rb_intern("freed_object_id_hash"), rb_hash_new());
154
+
155
+ // new hook for debugging only
156
+ // rb_ivar_set(rb_mLivingDead, rb_intern("newobj_hook"), newobj_hook = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, arg));
157
+
158
+ // rb_tracepoint_enable(newobj_hook);
159
+ rb_tracepoint_enable(freeobj_hook);
160
+ }
161
+
162
+ return Qnil;
163
+ }
164
+
165
+
166
+ /*
167
+ *
168
+ * call-seq:
169
+ * LivingDead.freed_hash -> {}
170
+ *
171
+ * Returns a hash of freed object ids
172
+ *
173
+ * The keys are the object ID, values are `true` if they have been freed
174
+ * otherwise `false`. Note you must invoke GC before calling this method
175
+ * see LivingDead.gc_start
176
+ *
177
+ */
178
+ static VALUE
179
+ living_dead_freed_hash(VALUE self)
180
+ {
181
+ VALUE freed_object_id_hash = rb_ivar_get(rb_mLivingDead, rb_intern("freed_object_id_hash"));
182
+
183
+ return freed_object_id_hash;
184
+ }
185
+
186
+ /*
187
+ *
188
+ * call-seq:
189
+ * LivingDead.tracing_hash -> {}
190
+ *
191
+ * Returns a hash of traced object ids
192
+ *
193
+ * The keys are the object ID of the object being traced, values are all truthy.
194
+ * In `LivingDead.trace` we set the value to be an instance of `LivingDead::ObjectTrace`.
195
+ *
196
+ */
197
+ static VALUE
198
+ living_dead_tracing_hash(VALUE self)
199
+ {
200
+ VALUE freed_object_id_hash = rb_ivar_get(rb_mLivingDead, rb_intern("object_id_tracing_hash"));
201
+
202
+ return freed_object_id_hash;
203
+ }
204
+
205
+
206
+ void
207
+ Init_living_dead(void)
208
+ {
209
+ VALUE mod = rb_mLivingDead = rb_const_get(rb_cObject, rb_intern("LivingDead"));
210
+
211
+ // LivingDead.trace is a ruby level method
212
+ // rb_define_module_function(mod, "trace", living_dead_trace, 1);
213
+ rb_define_module_function(mod, "start", living_dead_start, 0);
214
+ rb_define_module_function(mod, "freed_hash", living_dead_freed_hash, 0);
215
+ rb_define_module_function(mod, "tracing_hash", living_dead_tracing_hash, 0);
216
+
217
+ }
@@ -0,0 +1,96 @@
1
+ require "living_dead/version"
2
+ require "living_dead/living_dead"
3
+
4
+ require 'stringio'
5
+
6
+ module LivingDead
7
+
8
+ @string_io = StringIO.new
9
+
10
+ def self.trace(*args)
11
+ self.start
12
+ trace = ObjectTrace.new(*args)
13
+
14
+ self.tracing_hash[trace.key] = trace
15
+ self.freed_hash[trace.key] = false
16
+ end
17
+
18
+ def self.traced_objects
19
+ gc_start
20
+ tracing_hash.map do |_, trace|
21
+ trace
22
+ end
23
+ end
24
+
25
+ private
26
+ # GIANT BALL OF HACKS || THERE BE DRAGONS
27
+ #
28
+ # There is so much I don't understand on why I need to do the things
29
+ # I'm doing in this method.
30
+ def self.gc_start
31
+ # During debugging I found calling "puts" made some things
32
+ # mysteriously work, I have no idea why. If you remove this line
33
+ # then (more) tests fail. Maybe it has something to do with the way
34
+ # GC interacts with IO? I seriously have no idea.
35
+ #
36
+ @string_io.puts "=="
37
+
38
+ # Calling flush so we don't create a memory leak.
39
+ # Funny enough maybe calling flush without `puts` also works?
40
+ # IDK
41
+ #
42
+ @string_io.flush
43
+
44
+ # Why do we need this? Well I'll tell you...
45
+ # LivingDead calling `singleton_class.instance_eval` does not retain in the simple case
46
+ # fails without this.
47
+ #
48
+ LivingDead.freed_hash
49
+
50
+ # Calling GC multiple times fixes a different class of things
51
+ # Specifically the singleton_class.instance_eval tests.
52
+ # It might also be related to calling GC in a block, but changing
53
+ # to 1.times brings back failures.
54
+ #
55
+ # Calling 2 times results in eventual failure https://twitter.com/schneems/status/804369346910896128
56
+ # Calling 5 times results in eventual failure https://twitter.com/schneems/status/804382968307445760
57
+ # Trying 10 times
58
+ #
59
+ 10.times { GC.start }
60
+ end
61
+ public
62
+
63
+ def self.freed_objects
64
+ gc_start
65
+ freed_hash.map do |key, _|
66
+ tracing_hash[key]
67
+ end
68
+ end
69
+
70
+ class ObjectTrace
71
+ def initialize(obj = nil, object_id: nil, to_s: nil)
72
+ @object_id = object_id || obj.object_id
73
+ @to_s = to_s&.dup || obj.to_s.dup
74
+ end
75
+
76
+ def to_s
77
+ "#<LivingDead::ObjectTrace:#{ "0x#{ (object_id << 1).to_s(16) }" } @object_id=#{@object_id} @to_s=#{ @to_s.inspect }, @freed=#{ freed? }>"
78
+ end
79
+
80
+ def inspect
81
+ to_s
82
+ end
83
+
84
+ def retained?
85
+ !freed?
86
+ end
87
+
88
+ def freed?
89
+ !!LivingDead.freed_hash[@object_id]
90
+ end
91
+
92
+ def key
93
+ @object_id
94
+ end
95
+ end
96
+ end