living_dead 0.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.
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