stackprof 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1e9663289cf7257e1ce593f22f1bc635b0d4406d
4
- data.tar.gz: 56484ba1a6c48b2c9c41bb056500b7aa432db974
3
+ metadata.gz: c4d9be2e4ad16929b326e3a1e86c841ad0e13dc6
4
+ data.tar.gz: 826eff5e56b4ae6c03c613dd9937a5ae396efa98
5
5
  SHA512:
6
- metadata.gz: 4a0062a181c777976147420395729d65eff6fa4ef10a6624153629bcaed7a5ed8c6b071e2e30176d6a28b3d6c0dcd4ed1ab5b4cc8373fbdbc814ed57dff20a93
7
- data.tar.gz: 1e004d7757b016d00cda696be9fe6a4769aa11b97651228069d63df4193219c16bbc2f9c44800234d9d98855db04dde6f1cbcc68a7c1913949c43f18c68a419e
6
+ metadata.gz: 6153df14d977ffa0b1445950de2e779a8e8dd6606e665ad22bad348619b16659862bce078d1c12eb0d48f6bb90a5c0bd8ae73bc2c4d861466d6f16cda285d4c1
7
+ data.tar.gz: 06ccf3dc44113e69b544aab7d9c4820daec59ec4b0d3b5e9784960d52b6ead75907f320d66cf4084319567f890862e4ab6954211e1b5c09d114f8553942e51ad
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  /tmp
2
2
  /lib/stackprof.bundle
3
+ /lib/stackprof.so
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stackprof (0.1.0)
4
+ stackprof (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -7,16 +7,17 @@ and written as a replacement for [perftools.rb](https://github.com/tmm1/perftool
7
7
 
8
8
  ### sampling
9
9
 
10
- three sampling modes are supported:
10
+ four sampling modes are supported:
11
11
 
12
- - cpu time (using `ITIMER_PROF` and `SIGPROF`)
13
- - wall time (using `ITIMER_REAL` and `SIGALRM`)
14
- - object allocation (using `RUBY_INTERNAL_EVENT_NEWOBJ`)
12
+ - :wall (using `ITIMER_REAL` and `SIGALRM`) [default mode]
13
+ - :cpu (using `ITIMER_PROF` and `SIGPROF`)
14
+ - :object (using `RUBY_INTERNAL_EVENT_NEWOBJ`)
15
+ - :custom (user-defined via `StackProf.sample`)
15
16
 
16
17
  samplers have a tuneable interval which can be used to reduce overhead or increase granularity:
17
18
 
18
- - cpu time: sample every _interval_ microseconds of cpu activity (default: 10000 = 10 milliseconds)
19
- - wall time: sample every _interval_ microseconds of wallclock time (default: 10000)
19
+ - wall time: sample every _interval_ microseconds of wallclock time (default: 1000)
20
+ - cpu time: sample every _interval_ microseconds of cpu activity (default: 1000 = 1 millisecond)
20
21
  - object allocation: sample every _interval_ allocations (default: 1)
21
22
 
22
23
  samples are taken using a combination of three new C-APIs in ruby 2.1:
@@ -137,11 +138,12 @@ block in A#math (/Users/tmm1/code/stackprof/sample.rb:21)
137
138
 
138
139
  ### usage
139
140
 
140
- the profiler is compiled as a C-extension and exposes a simple api: `StackProf.run(mode, interval)`.
141
+ the profiler is compiled as a C-extension and exposes a simple api: `StackProf.run(mode: [:cpu|:wall|:object])`.
141
142
  the `run` method takes a block of code and returns a profile as a simple hash.
142
143
 
143
144
  ``` ruby
144
- profile = StackProf.run(sampling_mode, sampling_interval) do
145
+ # sample after every 1ms of cpu activity
146
+ profile = StackProf.run(mode: :cpu, interval: 1000) do
145
147
  MyCode.execute
146
148
  end
147
149
  ```
@@ -154,10 +156,12 @@ the format itself is very simple. it contains a header and a list of frames. eac
154
156
  identifying information such as its name, file and line. the frame also contains sampling data, including per-line
155
157
  samples, and a list of relationships to other frames represented as weighted edges.
156
158
 
157
- ```
159
+ ``` ruby
158
160
  {:version=>1.0,
159
- :mode=>"cpu(1000)",
161
+ :mode=>:cpu,
162
+ :inteval=>1000,
160
163
  :samples=>188,
164
+ :missed_samples=>0,
161
165
  :frames=>
162
166
  {70346498324780=>
163
167
  {:name=>"A#pow",
@@ -184,14 +188,17 @@ divided up between its callee edges. all 91 calls to `A#pow` came from `A#initia
184
188
 
185
189
  ### advanced usage
186
190
 
187
- the profiler can be started, paused, resumed and stopped manually for greater control.
191
+ the profiler can be started and stopped manually. results are accumulated until retrieval, across
192
+ multiple start/stop invocations.
188
193
 
189
- ```
194
+ ``` ruby
190
195
  StackProf.running?
191
196
  StackProf.start
192
- StackProf.pause
193
- StackProf.paused?
194
- StackProf.resume
195
197
  StackProf.stop
196
198
  StackProf.results
197
199
  ```
200
+
201
+ ### todo
202
+
203
+ * file/iseq blacklist
204
+ * restore signal handlers on stop
data/bin/stackprof CHANGED
@@ -1,3 +1,46 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'optparse'
3
3
  require 'stackprof'
4
+
5
+ options = {
6
+ :format => :text,
7
+ :sort => false,
8
+ :limit => 30
9
+ }
10
+
11
+ OptionParser.new(ARGV) do |o|
12
+ o.banner = 'stackprof-report [file.dump]+'
13
+
14
+ o.on('--text', 'Text output (default)'){ options[:format] = :text }
15
+ o.on('--callgrind'){ options[:format] = :callgrind }
16
+ o.on('--graphviz'){ options[:format] = :graphviz }
17
+ o.on('--file [grep]'){ |f| options[:format] = :file; options[:filter] = f }
18
+ o.on('--files'){ |f| options[:format] = :files }
19
+ o.on('--method [grep]'){ |f| options[:format] = :method; options[:filter] = f }
20
+ o.on('--debug'){ options[:format] = :debug }
21
+ o.on('--sort-total'){ options[:sort] = true }
22
+ o.on('--limit [num]', Integer){ |n| options[:limit] = n }
23
+ end.parse!
24
+
25
+ reports = []
26
+ while ARGV.size > 0
27
+ reports << StackProf::Report.new(Marshal.load(IO.binread(ARGV.pop)))
28
+ end
29
+ report = reports.inject(:+)
30
+
31
+ case options[:format]
32
+ when :text
33
+ report.print_text(options[:sort], options[:limit])
34
+ when :callgrind
35
+ report.print_callgrind
36
+ when :graphviz
37
+ report.print_graphviz
38
+ when :method
39
+ report.print_method(options[:filter])
40
+ when :file
41
+ report.print_file(options[:filter])
42
+ when :files
43
+ report.print_files(options[:sort], options[:limit])
44
+ else
45
+ raise ArgumentError, "unknown format: #{options[:format]}"
46
+ end
data/ext/extconf.rb CHANGED
@@ -1,2 +1,9 @@
1
1
  require 'mkmf'
2
- create_makefile('stackprof')
2
+ if have_func('rb_postponed_job_register_one') &&
3
+ have_func('rb_profile_frames') &&
4
+ have_func('rb_tracepoint_new') &&
5
+ have_const('RUBY_INTERNAL_EVENT_NEWOBJ')
6
+ create_makefile('stackprof')
7
+ else
8
+ fail 'missing API: are you using ruby 2.1+?'
9
+ end
data/ext/stackprof.c CHANGED
@@ -15,7 +15,9 @@
15
15
  #include <ruby/ruby.h>
16
16
  #include <ruby/debug.h>
17
17
  #include <ruby/st.h>
18
+ #include <signal.h>
18
19
  #include <sys/time.h>
20
+ #include <pthread.h>
19
21
 
20
22
  #define BUF_SIZE 2048
21
23
 
@@ -27,76 +29,112 @@ typedef struct {
27
29
  } frame_data_t;
28
30
 
29
31
  static struct {
30
- enum {
31
- PROF_NONE = 0,
32
- PROF_CPU,
33
- PROF_WALL,
34
- PROF_OBJECT
35
- } type;
32
+ int running;
33
+ VALUE mode;
34
+ VALUE interval;
36
35
 
36
+ size_t overall_signals;
37
37
  size_t overall_samples;
38
+ size_t during_gc;
38
39
  st_table *frames;
39
40
 
40
41
  VALUE frames_buffer[BUF_SIZE];
41
42
  int lines_buffer[BUF_SIZE];
42
- } _results;
43
+ } _stackprof;
43
44
 
44
- static VALUE sym_object, sym_wall, sym_name, sym_file, sym_line;
45
- static VALUE sym_samples, sym_total_samples, sym_edges, sym_lines;
46
- static VALUE sym_version, sym_mode, sym_frames;
47
- static VALUE objtracer;
45
+ static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
46
+ static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
47
+ static VALUE sym_version, sym_mode, sym_interval, sym_frames;
48
+ static VALUE sym_gc_samples, objtracer;
48
49
  static VALUE gc_hook;
50
+ static VALUE rb_mStackProf;
49
51
 
50
52
  static void stackprof_newobj_handler(VALUE, void*);
51
53
  static void stackprof_signal_handler(int sig, siginfo_t* sinfo, void* ucontext);
52
54
 
53
55
  static VALUE
54
- stackprof_start(VALUE self, VALUE type, VALUE usec)
56
+ stackprof_start(int argc, VALUE *argv, VALUE self)
55
57
  {
56
- if (type == sym_object) {
57
- _results.type = PROF_OBJECT;
58
+ struct sigaction sa;
59
+ struct itimerval timer;
60
+ VALUE opts = Qnil, mode = Qnil, interval = Qnil;
61
+
62
+ if (_stackprof.running)
63
+ return Qfalse;
64
+
65
+ rb_scan_args(argc, argv, "0:", &opts);
66
+
67
+ if (RTEST(opts)) {
68
+ mode = rb_hash_aref(opts, sym_mode);
69
+ interval = rb_hash_aref(opts, sym_interval);
70
+ }
71
+ if (!RTEST(mode)) mode = sym_wall;
72
+
73
+ if (!_stackprof.frames) {
74
+ _stackprof.frames = st_init_numtable();
75
+ _stackprof.overall_signals = 0;
76
+ _stackprof.overall_samples = 0;
77
+ _stackprof.during_gc = 0;
78
+ }
79
+
80
+ if (mode == sym_object) {
81
+ if (!RTEST(interval)) interval = INT2FIX(1);
82
+
58
83
  objtracer = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, stackprof_newobj_handler, 0);
59
84
  rb_tracepoint_enable(objtracer);
60
- } else {
61
- if (type == sym_wall)
62
- _results.type = PROF_WALL;
63
- else
64
- _results.type = PROF_CPU;
85
+ } else if (mode == sym_wall || mode == sym_cpu) {
86
+ if (!RTEST(interval)) interval = INT2FIX(1000);
65
87
 
66
- struct sigaction sa;
67
88
  sa.sa_sigaction = stackprof_signal_handler;
68
89
  sa.sa_flags = SA_RESTART | SA_SIGINFO;
69
90
  sigemptyset(&sa.sa_mask);
70
- sigaction(_results.type == PROF_WALL ? SIGALRM : SIGPROF, &sa, NULL);
91
+ sigaction(mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL);
71
92
 
72
- struct itimerval timer;
73
93
  timer.it_interval.tv_sec = 0;
74
- timer.it_interval.tv_usec = NUM2LONG(usec);
94
+ timer.it_interval.tv_usec = NUM2LONG(interval);
75
95
  timer.it_value = timer.it_interval;
76
- setitimer(_results.type == PROF_WALL ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
96
+ setitimer(mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
97
+ } else if (mode == sym_custom) {
98
+ /* sampled manually */
99
+ interval = Qnil;
100
+ } else {
101
+ rb_raise(rb_eArgError, "unknown profiler mode");
77
102
  }
78
103
 
79
- return Qnil;
104
+ _stackprof.running = 1;
105
+ _stackprof.mode = mode;
106
+ _stackprof.interval = interval;
107
+
108
+ return Qtrue;
80
109
  }
81
110
 
82
111
  static VALUE
83
112
  stackprof_stop(VALUE self)
84
113
  {
85
- if (_results.type == PROF_OBJECT) {
114
+ struct sigaction sa;
115
+ struct itimerval timer;
116
+
117
+ if (!_stackprof.running)
118
+ return Qfalse;
119
+ _stackprof.running = 0;
120
+
121
+ if (_stackprof.mode == sym_object) {
86
122
  rb_tracepoint_disable(objtracer);
87
- } else {
88
- struct itimerval timer;
123
+ } else if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
89
124
  memset(&timer, 0, sizeof(timer));
90
- setitimer(_results.type == PROF_WALL ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
125
+ setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
91
126
 
92
- struct sigaction sa;
93
127
  sa.sa_handler = SIG_IGN;
94
128
  sa.sa_flags = SA_RESTART;
95
129
  sigemptyset(&sa.sa_mask);
96
- sigaction(_results.type == PROF_WALL ? SIGALRM : SIGPROF, &sa, NULL);
130
+ sigaction(_stackprof.mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL);
131
+ } else if (_stackprof.mode == sym_custom) {
132
+ /* sampled manually */
133
+ } else {
134
+ rb_raise(rb_eArgError, "unknown profiler mode");
97
135
  }
98
136
 
99
- return Qnil;
137
+ return Qtrue;
100
138
  }
101
139
 
102
140
  static int
@@ -114,8 +152,11 @@ frame_lines_i(st_data_t key, st_data_t val, st_data_t arg)
114
152
  {
115
153
  VALUE lines = (VALUE)arg;
116
154
 
117
- intptr_t weight = (intptr_t)val;
118
- rb_hash_aset(lines, INT2FIX(key), INT2FIX(weight));
155
+ size_t weight = (size_t)val;
156
+ size_t total = weight & (~(size_t)0 << (8*SIZEOF_SIZE_T/2));
157
+ weight -= total;
158
+ total = total >> (8*SIZEOF_SIZE_T/2);
159
+ rb_hash_aset(lines, INT2FIX(key), rb_ary_new3(2, ULONG2NUM(total), ULONG2NUM(weight)));
119
160
  return ST_CONTINUE;
120
161
  }
121
162
 
@@ -127,7 +168,6 @@ frame_i(st_data_t key, st_data_t val, st_data_t arg)
127
168
  VALUE results = (VALUE)arg;
128
169
  VALUE details = rb_hash_new();
129
170
  VALUE name, file, edges, lines;
130
- VALUE label, method_name;
131
171
  VALUE line;
132
172
 
133
173
  rb_hash_aset(results, rb_obj_id(frame), details);
@@ -167,85 +207,114 @@ frame_i(st_data_t key, st_data_t val, st_data_t arg)
167
207
  }
168
208
 
169
209
  static VALUE
170
- stackprof_run(VALUE self, VALUE type, VALUE usec)
210
+ stackprof_results(VALUE self)
171
211
  {
172
212
  VALUE results, frames;
173
- rb_need_block();
174
- if (!_results.frames)
175
- _results.frames = st_init_numtable();
176
- _results.overall_samples = 0;
177
213
 
178
- stackprof_start(self, type, usec);
179
- rb_yield(Qundef);
180
- stackprof_stop(self);
214
+ if (!_stackprof.frames || _stackprof.running)
215
+ return Qnil;
181
216
 
182
217
  results = rb_hash_new();
183
- rb_hash_aset(results, sym_version, DBL2NUM(1.0));
184
- rb_hash_aset(results, sym_mode, rb_sprintf("%"PRIsVALUE"(%"PRIsVALUE")", type, usec));
185
- rb_hash_aset(results, sym_samples, SIZET2NUM(_results.overall_samples));
218
+ rb_hash_aset(results, sym_version, DBL2NUM(1.1));
219
+ rb_hash_aset(results, sym_mode, _stackprof.mode);
220
+ rb_hash_aset(results, sym_interval, _stackprof.interval);
221
+ rb_hash_aset(results, sym_samples, SIZET2NUM(_stackprof.overall_samples));
222
+ rb_hash_aset(results, sym_gc_samples, SIZET2NUM(_stackprof.during_gc));
223
+ rb_hash_aset(results, sym_missed_samples, SIZET2NUM(_stackprof.overall_signals - _stackprof.overall_samples));
186
224
 
187
225
  frames = rb_hash_new();
188
226
  rb_hash_aset(results, sym_frames, frames);
189
- st_foreach(_results.frames, frame_i, (st_data_t)frames);
227
+ st_foreach(_stackprof.frames, frame_i, (st_data_t)frames);
228
+
229
+ st_free_table(_stackprof.frames);
230
+ _stackprof.frames = NULL;
190
231
 
191
232
  return results;
192
233
  }
193
234
 
235
+ static VALUE
236
+ stackprof_run(int argc, VALUE *argv, VALUE self)
237
+ {
238
+ rb_need_block();
239
+ stackprof_start(argc, argv, self);
240
+ rb_ensure(rb_yield, Qundef, stackprof_stop, self);
241
+ return stackprof_results(self);
242
+ }
243
+
244
+ static VALUE
245
+ stackprof_running_p(VALUE self)
246
+ {
247
+ return _stackprof.running ? Qtrue : Qfalse;
248
+ }
249
+
194
250
  static inline frame_data_t *
195
251
  sample_for(VALUE frame)
196
252
  {
197
253
  st_data_t key = (st_data_t)frame, val = 0;
198
254
  frame_data_t *frame_data;
199
255
 
200
- if (st_lookup(_results.frames, key, &val)) {
256
+ if (st_lookup(_stackprof.frames, key, &val)) {
201
257
  frame_data = (frame_data_t *)val;
202
258
  } else {
203
259
  frame_data = ALLOC_N(frame_data_t, 1);
204
260
  MEMZERO(frame_data, frame_data_t, 1);
205
261
  val = (st_data_t)frame_data;
206
- st_insert(_results.frames, key, val);
262
+ st_insert(_stackprof.frames, key, val);
207
263
  }
208
264
 
209
265
  return frame_data;
210
266
  }
211
267
 
268
+ static int
269
+ numtable_increment_callback(st_data_t *key, st_data_t *value, st_data_t arg, int existing)
270
+ {
271
+ size_t *weight = (size_t *)value;
272
+ size_t increment = (size_t)arg;
273
+
274
+ if (existing)
275
+ (*weight) += increment;
276
+ else
277
+ *weight = increment;
278
+
279
+ return ST_CONTINUE;
280
+ }
281
+
212
282
  void
213
- st_numtable_increment(st_table *table, st_data_t key)
283
+ st_numtable_increment(st_table *table, st_data_t key, size_t increment)
214
284
  {
215
- intptr_t weight = 0;
216
- st_lookup(table, key, (st_data_t *)&weight);
217
- weight++;
218
- st_insert(table, key, weight);
285
+ st_update(table, key, numtable_increment_callback, (st_data_t)increment);
219
286
  }
220
287
 
221
- static void
222
- stackprof_sample()
288
+ void
289
+ stackprof_record_sample()
223
290
  {
224
291
  int num, i;
225
- VALUE prev_frame;
226
- st_data_t key;
292
+ VALUE prev_frame = Qnil;
227
293
 
228
- _results.overall_samples++;
229
- num = rb_profile_frames(0, sizeof(_results.frames_buffer), _results.frames_buffer, _results.lines_buffer);
294
+ _stackprof.overall_samples++;
295
+ num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer), _stackprof.frames_buffer, _stackprof.lines_buffer);
230
296
 
231
297
  for (i = 0; i < num; i++) {
232
- int line = _results.lines_buffer[i];
233
- VALUE frame = _results.frames_buffer[i];
298
+ int line = _stackprof.lines_buffer[i];
299
+ VALUE frame = _stackprof.frames_buffer[i];
234
300
  frame_data_t *frame_data = sample_for(frame);
235
301
 
236
302
  frame_data->total_samples++;
237
303
 
238
304
  if (i == 0) {
239
305
  frame_data->caller_samples++;
240
- if (line > 0) {
241
- if (!frame_data->lines)
242
- frame_data->lines = st_init_numtable();
243
- st_numtable_increment(frame_data->lines, (st_data_t)line);
244
- }
245
306
  } else {
246
307
  if (!frame_data->edges)
247
308
  frame_data->edges = st_init_numtable();
248
- st_numtable_increment(frame_data->edges, (st_data_t)prev_frame);
309
+ st_numtable_increment(frame_data->edges, (st_data_t)prev_frame, 1);
310
+ }
311
+
312
+ if (line > 0) {
313
+ if (!frame_data->lines)
314
+ frame_data->lines = st_init_numtable();
315
+ size_t half = (size_t)1<<(8*SIZEOF_SIZE_T/2);
316
+ size_t increment = i == 0 ? half + 1 : half;
317
+ st_numtable_increment(frame_data->lines, (st_data_t)line, increment);
249
318
  }
250
319
 
251
320
  prev_frame = frame;
@@ -257,59 +326,122 @@ stackprof_job_handler(void *data)
257
326
  {
258
327
  static int in_signal_handler = 0;
259
328
  if (in_signal_handler) return;
329
+ if (!_stackprof.running) return;
260
330
 
261
331
  in_signal_handler++;
262
- stackprof_sample();
332
+ stackprof_record_sample();
263
333
  in_signal_handler--;
264
334
  }
265
335
 
266
336
  static void
267
337
  stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
268
338
  {
269
- rb_postponed_job_register_one(0, stackprof_job_handler, 0);
339
+ _stackprof.overall_signals++;
340
+ if (rb_during_gc())
341
+ _stackprof.during_gc++, _stackprof.overall_samples++;
342
+ else
343
+ rb_postponed_job_register_one(0, stackprof_job_handler, 0);
270
344
  }
271
345
 
272
346
  static void
273
347
  stackprof_newobj_handler(VALUE tpval, void *data)
274
348
  {
349
+ _stackprof.overall_signals++;
350
+ stackprof_job_handler(0);
351
+ }
352
+
353
+ static VALUE
354
+ stackprof_sample(VALUE self)
355
+ {
356
+ if (!_stackprof.running)
357
+ return Qfalse;
358
+
359
+ _stackprof.overall_signals++;
275
360
  stackprof_job_handler(0);
361
+ return Qtrue;
276
362
  }
277
363
 
278
364
  static int
279
365
  frame_mark_i(st_data_t key, st_data_t val, st_data_t arg)
280
366
  {
281
367
  VALUE frame = (VALUE)key;
282
- rb_gc_mark_maybe(frame);
368
+ rb_gc_mark(frame);
283
369
  return ST_CONTINUE;
284
370
  }
285
371
 
286
372
  static void
287
- stackprof_gc_mark()
373
+ stackprof_gc_mark(void *data)
374
+ {
375
+ if (_stackprof.frames)
376
+ st_foreach(_stackprof.frames, frame_mark_i, 0);
377
+ }
378
+
379
+ static void
380
+ stackprof_atfork_prepare(void)
288
381
  {
289
- if (_results.frames)
290
- st_foreach(_results.frames, frame_mark_i, 0);
382
+ struct itimerval timer;
383
+ if (_stackprof.running) {
384
+ if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
385
+ memset(&timer, 0, sizeof(timer));
386
+ setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
387
+ }
388
+ }
389
+ }
390
+
391
+ static void
392
+ stackprof_atfork_parent(void)
393
+ {
394
+ struct itimerval timer;
395
+ if (_stackprof.running) {
396
+ if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
397
+ timer.it_interval.tv_sec = 0;
398
+ timer.it_interval.tv_usec = NUM2LONG(_stackprof.interval);
399
+ timer.it_value = timer.it_interval;
400
+ setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
401
+ }
402
+ }
403
+ }
404
+
405
+ static void
406
+ stackprof_atfork_child(void)
407
+ {
408
+ stackprof_stop(rb_mStackProf);
291
409
  }
292
410
 
293
411
  void
294
412
  Init_stackprof(void)
295
413
  {
296
414
  sym_object = ID2SYM(rb_intern("object"));
297
- sym_name = ID2SYM(rb_intern("name"));
415
+ sym_custom = ID2SYM(rb_intern("custom"));
298
416
  sym_wall = ID2SYM(rb_intern("wall"));
417
+ sym_cpu = ID2SYM(rb_intern("cpu"));
418
+ sym_name = ID2SYM(rb_intern("name"));
299
419
  sym_file = ID2SYM(rb_intern("file"));
300
420
  sym_line = ID2SYM(rb_intern("line"));
301
421
  sym_total_samples = ID2SYM(rb_intern("total_samples"));
422
+ sym_gc_samples = ID2SYM(rb_intern("gc_samples"));
423
+ sym_missed_samples = ID2SYM(rb_intern("missed_samples"));
302
424
  sym_samples = ID2SYM(rb_intern("samples"));
303
425
  sym_edges = ID2SYM(rb_intern("edges"));
304
426
  sym_lines = ID2SYM(rb_intern("lines"));
305
427
  sym_version = ID2SYM(rb_intern("version"));
306
428
  sym_mode = ID2SYM(rb_intern("mode"));
429
+ sym_interval = ID2SYM(rb_intern("interval"));
307
430
  sym_frames = ID2SYM(rb_intern("frames"));
308
431
 
309
432
  gc_hook = Data_Wrap_Struct(rb_cObject, stackprof_gc_mark, NULL, NULL);
310
433
  rb_global_variable(&gc_hook);
311
434
 
312
- VALUE rb_mStackProf = rb_define_module("StackProf");
313
- rb_define_singleton_method(rb_mStackProf, "run", stackprof_run, 2);
435
+ rb_mStackProf = rb_define_module("StackProf");
436
+ rb_define_singleton_method(rb_mStackProf, "running?", stackprof_running_p, 0);
437
+ rb_define_singleton_method(rb_mStackProf, "run", stackprof_run, -1);
438
+ rb_define_singleton_method(rb_mStackProf, "start", stackprof_start, -1);
439
+ rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0);
440
+ rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, 0);
441
+ rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0);
442
+
314
443
  rb_autoload(rb_mStackProf, rb_intern_const("Report"), "stackprof/report.rb");
444
+ rb_autoload(rb_mStackProf, rb_intern_const("Middleware"), "stackprof/middleware.rb");
445
+
446
+ pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child);
315
447
  }
@@ -0,0 +1,31 @@
1
+ require 'fileutils'
2
+
3
+ module StackProf
4
+ class Middleware
5
+ def initialize(app)
6
+ @app = app
7
+ at_exit{ Middleware.save if Middleware.enabled? }
8
+ end
9
+
10
+ def call(env)
11
+ StackProf.start(mode: :cpu, interval: 1000) if self.class.enabled?
12
+ @app.call(env)
13
+ ensure
14
+ StackProf.stop if self.class.enabled?
15
+ end
16
+
17
+ class << self
18
+ attr_accessor :enabled
19
+ alias enabled? enabled
20
+
21
+ def save
22
+ if results = StackProf.results
23
+ FileUtils.mkdir_p('tmp')
24
+ File.open("tmp/stackprof-#{results[:mode]}-#{Process.pid}-#{Time.now.to_i}.dump", 'w') do |f|
25
+ f.write Marshal.dump(results)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,71 +1,279 @@
1
1
  require 'pp'
2
+ require 'digest/md5'
2
3
 
3
4
  module StackProf
4
5
  class Report
5
6
  def initialize(data)
6
7
  @data = data
8
+
9
+ frames = {}
10
+ @data[:frames].each{ |k,v| frames[k.to_s] = v }
11
+ @data[:frames] = frames
12
+ end
13
+ attr_reader :data
14
+
15
+ def frames(sort_by_total=false)
16
+ Hash[ *@data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.flatten(1) ]
17
+ end
18
+
19
+ def normalized_frames
20
+ id2hash = {}
21
+ @data[:frames].each do |frame, info|
22
+ id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}")
23
+ end
24
+ @data[:frames].inject(Hash.new) do |hash, (frame, info)|
25
+ info = hash[id2hash[frame.to_s]] = info.dup
26
+ info[:edges] = info[:edges].inject(Hash.new){ |edges, (edge, weight)| edges[id2hash[edge.to_s]] = weight; edges } if info[:edges]
27
+ hash
28
+ end
7
29
  end
8
30
 
9
- def frames
10
- @data[:frames].sort_by{ |iseq, stats| -stats[:samples] }
31
+ def version
32
+ @data[:version]
33
+ end
34
+
35
+ def modeline
36
+ "#{@data[:mode]}(#{@data[:interval]})"
11
37
  end
12
38
 
13
39
  def overall_samples
14
40
  @data[:samples]
15
41
  end
16
42
 
43
+ def max_samples
44
+ @data[:max_samples] ||= frames.max_by{ |addr, frame| frame[:samples] }.last[:samples]
45
+ end
46
+
47
+ def files
48
+ @data[:files] ||= @data[:frames].inject(Hash.new) do |hash, (addr, frame)|
49
+ if file = frame[:file] and lines = frame[:lines]
50
+ hash[file] ||= Hash.new
51
+ lines.each do |line, weight|
52
+ hash[file][line] = add_lines(hash[file][line], weight)
53
+ end
54
+ end
55
+ hash
56
+ end
57
+ end
58
+
59
+ def add_lines(a, b)
60
+ return b if a.nil?
61
+ return a+b if a.is_a? Fixnum
62
+ return [ a[0], a[1]+b ] if b.is_a? Fixnum
63
+ [ a[0]+b[0], a[1]+b[1] ]
64
+ end
65
+
17
66
  def print_debug
18
67
  pp @data
19
68
  end
20
69
 
21
- def print_graphviz
22
- f = STDOUT
70
+ def print_graphviz(filter = nil, f = STDOUT)
71
+ if filter
72
+ mark_stack = []
73
+ list = frames
74
+ list.each{ |addr, frame| mark_stack << addr if frame[:name] =~ filter }
75
+ while addr = mark_stack.pop
76
+ frame = list[addr]
77
+ unless frame[:marked]
78
+ $stderr.puts frame[:edges].inspect
79
+ mark_stack += frame[:edges].map{ |addr, weight| addr.to_s if list[addr.to_s][:total_samples] <= weight*1.2 }.compact if frame[:edges]
80
+ frame[:marked] = true
81
+ end
82
+ end
83
+ list = list.select{ |addr, frame| frame[:marked] }
84
+ list.each{ |addr, frame| frame[:edges] && frame[:edges].delete_if{ |k,v| list[k.to_s].nil? } }
85
+ list
86
+ else
87
+ list = frames
88
+ end
89
+
23
90
  f.puts "digraph profile {"
24
- frames.each do |frame, info|
91
+ list.each do |frame, info|
25
92
  call, total = info.values_at(:samples, :total_samples)
26
93
  sample = ''
27
94
  sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
28
95
  sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
29
- size = (1.0 * call / overall_samples) * 28 + 10
96
+ fontsize = (1.0 * call / max_samples) * 28 + 10
97
+ size = (1.0 * total / overall_samples) * 2.0 + 0.5
30
98
 
31
- f.puts " #{frame} [size=#{size}] [fontsize=#{size}] [shape=box] [label=\"#{info[:name]}\\n#{sample}\"];"
99
+ f.puts " #{frame} [size=#{size}] [fontsize=#{fontsize}] [penwidth=\"#{size}\"] [shape=box] [label=\"#{info[:name]}\\n#{sample}\"];"
32
100
  if edges = info[:edges]
33
101
  edges.each do |edge, weight|
34
- size = (1.0 * weight / overall_samples) * 28
35
- f.puts " #{frame} -> #{edge} [label=\"#{weight}\"];"
102
+ size = (1.0 * weight / overall_samples) * 2.0 + 0.5
103
+ f.puts " #{frame} -> #{edge} [label=\"#{weight}\"] [weight=\"#{weight}\"] [penwidth=\"#{size}\"];"
36
104
  end
37
105
  end
38
106
  end
39
107
  f.puts "}"
40
108
  end
41
109
 
42
- def print_text
43
- printf "% 10s (pct) % 10s (pct) FRAME\n" % ["TOTAL", "SAMPLES"]
44
- frames.each do |frame, info|
110
+ def print_text(sort_by_total=false, limit=nil, f = STDOUT)
111
+ f.puts "=================================="
112
+ f.printf " Mode: #{modeline}\n"
113
+ f.printf " Samples: #{@data[:samples]} (%.2f%% miss rate)\n", 100.0*@data[:missed_samples]/(@data[:missed_samples]+@data[:samples])
114
+ f.printf " GC: #{@data[:gc_samples]} (%.2f%%)\n", 100.0*@data[:gc_samples]/@data[:samples]
115
+ f.puts "=================================="
116
+ f.printf "% 10s (pct) % 10s (pct) FRAME\n" % ["TOTAL", "SAMPLES"]
117
+ list = frames(sort_by_total)
118
+ list = list.first(limit) if limit
119
+ list.each do |frame, info|
45
120
  call, total = info.values_at(:samples, :total_samples)
46
- printf "% 10d % 8s % 10d % 8s %s\n", total, "(%2.1f%%)" % (total*100.0/overall_samples), call, "(%2.1f%%)" % (call*100.0/overall_samples), info[:name]
121
+ f.printf "% 10d % 8s % 10d % 8s %s\n", total, "(%2.1f%%)" % (total*100.0/overall_samples), call, "(%2.1f%%)" % (call*100.0/overall_samples), info[:name]
47
122
  end
48
123
  end
49
124
 
50
- def print_source(name)
125
+ def print_callgrind(f = STDOUT)
126
+ f.puts "version: 1"
127
+ f.puts "creator: stackprof"
128
+ f.puts "pid: 0"
129
+ f.puts "cmd: ruby"
130
+ f.puts "part: 1"
131
+ f.puts "desc: mode: #{modeline}"
132
+ f.puts "desc: missed: #{@data[:missed_samples]})"
133
+ f.puts "positions: line"
134
+ f.puts "events: Instructions"
135
+ f.puts "summary: #{@data[:samples]}"
136
+
137
+ list = frames
138
+ list.each do |addr, frame|
139
+ f.puts "fl=#{frame[:file]}"
140
+ f.puts "fn=#{frame[:name]}"
141
+ frame[:lines].each do |line, weight|
142
+ f.puts "#{line} #{weight.is_a?(Array) ? weight[1] : weight}"
143
+ end if frame[:lines]
144
+ frame[:edges].each do |edge, weight|
145
+ oframe = list[edge.to_s]
146
+ f.puts "cfl=#{oframe[:file]}" unless oframe[:file] == frame[:file]
147
+ f.puts "cfn=#{oframe[:name]}"
148
+ f.puts "calls=#{weight} #{frame[:line] || 0}\n#{oframe[:line] || 0} #{weight}"
149
+ end if frame[:edges]
150
+ f.puts
151
+ end
152
+
153
+ f.puts "totals: #{@data[:samples]}"
154
+ end
155
+
156
+ def print_method(name, f = STDOUT)
51
157
  name = /#{Regexp.escape name}/ unless Regexp === name
52
158
  frames.each do |frame, info|
53
159
  next unless info[:name] =~ name
54
160
  file, line = info.values_at(:file, :line)
55
-
56
- maxline = info[:lines] ? info[:lines].keys.max : line + 5
57
- printf "%s (%s:%d)\n", info[:name], file, line
161
+ line ||= 1
58
162
 
59
163
  lines = info[:lines]
60
- source = File.readlines(file).each_with_index do |code, i|
61
- next unless (line-1..maxline).include?(i)
62
- if lines and samples = lines[i+1]
63
- printf "% 5d % 7s / % 7s | % 5d | %s", samples, "(%2.1f%%" % (100.0*samples/overall_samples), "%2.1f%%)" % (100.0*samples/info[:samples]), i+1, code
164
+ maxline = lines ? lines.keys.max : line + 5
165
+ f.printf "%s (%s:%d)\n", info[:name], file, line
166
+ f.printf " samples: % 5d self (%2.1f%%) / % 5d total (%2.1f%%)\n", info[:samples], 100.0*info[:samples]/overall_samples, info[:total_samples], 100.0*info[:total_samples]/overall_samples
167
+
168
+ if (callers = data[:frames].map{ |id, other| [other[:name], other[:edges][frame.to_i]] if other[:edges] && other[:edges].include?(frame.to_i) }.compact).any?
169
+ f.puts " callers:"
170
+ callers = callers.sort_by(&:last).reverse
171
+ callers.each do |name, weight|
172
+ f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/info[:total_samples]), name
173
+ end
174
+ end
175
+
176
+ if callees = info[:edges]
177
+ f.printf " callees (%d total):\n", info[:total_samples]-info[:samples]
178
+ callees = callees.map{ |k, weight| [data[:frames][k.to_s][:name], weight] }
179
+ callees.each do |name, weight|
180
+ f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/(info[:total_samples]-info[:samples])), name
181
+ end
182
+ end
183
+
184
+ f.puts " code:"
185
+ source_display(f, file, lines, line-1..maxline)
186
+ end
187
+ end
188
+
189
+ def print_files(sort_by_total=false, limit=nil, f = STDOUT)
190
+ list = files.map{ |file, vals| [file, vals.values.inject([0,0]){ |sum, n| add_lines(sum, n) }] }
191
+ list = list.sort_by{ |file, samples| -samples[1] }
192
+ list = list.first(limit) if limit
193
+ list.each do |file, vals|
194
+ total_samples, samples = *vals
195
+ f.printf "% 5d (%2.1f%%) / % 5d (%2.1f%%) %s\n", total_samples, (100.0*total_samples/overall_samples), samples, (100.0*samples/overall_samples), file
196
+ end
197
+ end
198
+
199
+ def print_file(filter, f = STDOUT)
200
+ filter = /#{Regexp.escape filter}/ unless Regexp === filter
201
+ list = files
202
+ list.select!{ |name, lines| name =~ filter }
203
+ list.sort_by{ |file, vals| -vals.values.inject(0){ |sum, n| sum + (n.is_a?(Array) ? n[1] : n) } }.each do |file, lines|
204
+ source_display(f, file, lines)
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ def source_display(f, file, lines, range=nil)
211
+ File.readlines(file).each_with_index do |code, i|
212
+ next unless range.nil? || range.include?(i)
213
+ if lines and lineinfo = lines[i+1]
214
+ total_samples, samples = lineinfo
215
+ if version == 1.0
216
+ samples = total_samples
217
+ f.printf "% 5d % 7s | % 5d | %s", samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code
218
+ elsif samples > 0
219
+ f.printf "% 5d % 8s / % 5d % 7s | % 5d | %s", total_samples, "(%2.1f%%)" % (100.0*total_samples/overall_samples), samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code
64
220
  else
65
- printf " | % 5d | %s", i+1, code
221
+ f.printf "% 5d % 8s | % 5d | %s", total_samples, "(%3.1f%%)" % (100.0*total_samples/overall_samples), i+1, code
222
+ end
223
+ else
224
+ if version == 1.0
225
+ f.printf " | % 5d | %s", i+1, code
226
+ else
227
+ f.printf " | % 5d | %s", i+1, code
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def +(other)
234
+ raise ArgumentError, "cannot combine #{other.class}" unless self.class == other.class
235
+ raise ArgumentError, "cannot combine #{modeline} with #{other.modeline}" unless modeline == other.modeline
236
+ raise ArgumentError, "cannot combine v#{version} with v#{other.version}" unless version == other.version
237
+
238
+ f1, f2 = normalized_frames, other.normalized_frames
239
+ frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id|
240
+ if f1[id].nil?
241
+ hash[id] = f2[id]
242
+ elsif f2[id]
243
+ hash[id] = f1[id]
244
+ hash[id][:total_samples] += f2[id][:total_samples]
245
+ hash[id][:samples] += f2[id][:samples]
246
+ if f2[id][:edges]
247
+ edges = hash[id][:edges] ||= {}
248
+ f2[id][:edges].each do |edge, weight|
249
+ edges[edge] ||= 0
250
+ edges[edge] += weight
251
+ end
66
252
  end
253
+ if f2[id][:lines]
254
+ lines = hash[id][:lines] ||= {}
255
+ f2[id][:lines].each do |line, weight|
256
+ lines[line] = add_lines(lines[line], weight)
257
+ end
258
+ end
259
+ else
260
+ hash[id] = f1[id]
67
261
  end
262
+ hash
68
263
  end
264
+
265
+ d1, d2 = data, other.data
266
+ data = {
267
+ version: version,
268
+ mode: d1[:mode],
269
+ interval: d1[:interval],
270
+ samples: d1[:samples] + d2[:samples],
271
+ gc_samples: d1[:gc_samples] + d2[:gc_samples],
272
+ missed_samples: d1[:missed_samples] + d2[:missed_samples],
273
+ frames: frames
274
+ }
275
+
276
+ self.class.new(data)
69
277
  end
70
278
  end
71
279
  end
data/stackprof.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'stackprof'
3
- s.version = '0.1.0'
3
+ s.version = '0.2.0'
4
4
  s.homepage = 'http://github.com/tmm1/stackprof'
5
5
 
6
6
  s.authors = 'Aman Gupta'
@@ -17,5 +17,6 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.license = 'MIT'
19
19
 
20
+ # s.add_dependency 'yajl-ruby'
20
21
  s.add_development_dependency 'rake-compiler'
21
22
  end
@@ -4,39 +4,65 @@ require 'test/unit'
4
4
 
5
5
  class StackProfTest < Test::Unit::TestCase
6
6
  def test_info
7
- profile = StackProf.run(:wall, 1000){}
8
- assert_equal 1.0, profile[:version]
9
- assert_equal "wall(1000)", profile[:mode]
7
+ profile = StackProf.run{}
8
+ assert_equal 1.1, profile[:version]
9
+ assert_equal :wall, profile[:mode]
10
+ assert_equal 1000, profile[:interval]
10
11
  assert_equal 0, profile[:samples]
11
12
  end
12
13
 
14
+ def test_running
15
+ assert_equal false, StackProf.running?
16
+ StackProf.run{ assert_equal true, StackProf.running? }
17
+ end
18
+
19
+ def test_start_stop_results
20
+ assert_equal nil, StackProf.results
21
+ assert_equal true, StackProf.start
22
+ assert_equal false, StackProf.start
23
+ assert_equal true, StackProf.running?
24
+ assert_equal nil, StackProf.results
25
+ assert_equal true, StackProf.stop
26
+ assert_equal false, StackProf.stop
27
+ assert_equal false, StackProf.running?
28
+ assert_kind_of Hash, StackProf.results
29
+ assert_equal nil, StackProf.results
30
+ end
31
+
13
32
  def test_object_allocation
14
- profile = StackProf.run(:object, 1) do
33
+ profile = StackProf.run(mode: :object) do
15
34
  Object.new
16
35
  Object.new
17
36
  end
18
- assert_equal "object(1)", profile[:mode]
37
+ assert_equal :object, profile[:mode]
38
+ assert_equal 1, profile[:interval]
19
39
  assert_equal 2, profile[:samples]
20
40
 
21
41
  frame = profile[:frames].values.first
22
42
  assert_equal "block in StackProfTest#test_object_allocation", frame[:name]
23
43
  assert_equal 2, frame[:samples]
24
- assert_equal 14, frame[:line]
25
- assert_equal 1, frame[:lines][15]
26
- assert_equal 1, frame[:lines][16]
44
+ line = __LINE__
45
+ assert_equal line-11, frame[:line]
46
+ assert_equal [1, 1], frame[:lines][line-10]
47
+ assert_equal [1, 1], frame[:lines][line-9]
48
+
49
+ frame = profile[:frames].values[1]
50
+ assert_equal [2, 0], frame[:lines][line-11]
27
51
  end
28
52
 
29
53
  def test_cputime
30
- profile = StackProf.run(:cpu, 1000) do
54
+ profile = StackProf.run(mode: :cpu, interval: 500) do
31
55
  math
32
56
  end
33
57
 
58
+ assert_operator profile[:samples], :>, 1
34
59
  frame = profile[:frames].values.first
35
60
  assert_equal "block in StackProfTest#math", frame[:name]
61
+ File.open('/tmp/cputime.dump','w'){|f| f.write Marshal.dump(profile) }
36
62
  end
37
63
 
38
64
  def test_walltime
39
- profile = StackProf.run(:wall, 1000) do
65
+ profile = StackProf.run(mode: :wall) do
40
66
  idle
41
67
  end
42
68
 
@@ -45,6 +71,43 @@ class StackProfTest < Test::Unit::TestCase
45
71
  assert_in_delta 200, frame[:samples], 5
46
72
  end
47
73
 
74
+ def test_custom
75
+ profile = StackProf.run(mode: :custom) do
76
+ 10.times do
77
+ StackProf.sample
78
+ end
79
+ end
80
+
81
+ assert_equal :custom, profile[:mode]
82
+ assert_equal 10, profile[:samples]
83
+
84
+ frame = profile[:frames].values.first
85
+ assert_equal "block (2 levels) in StackProfTest#test_custom", frame[:name]
86
+ assert_equal __LINE__-10, frame[:line]
87
+ assert_equal [10, 10], frame[:lines][__LINE__-10]
88
+ end
89
+
90
+ def test_fork
91
+ StackProf.run do
92
+ pid = fork do
93
+ exit! StackProf.running?? 1 : 0
94
+ end
95
+ Process.wait(pid)
96
+ assert_equal 0, $?.exitstatus
97
+ assert_equal true, StackProf.running?
98
+ end
99
+ end
100
+
101
+ def test_gc
102
+ profile = StackProf.run(mode: :cpu, interval: 100) do
103
+ GC.start
104
+ end
105
+
106
+ assert_empty profile[:frames]
107
+ assert_operator profile[:gc_samples], :>, 0
108
+ assert_equal 0, profile[:missed_samples]
109
+ end
110
+
48
111
  def math
49
112
  250_000.times do
50
113
  2 ** 10
@@ -52,6 +115,10 @@ class StackProfTest < Test::Unit::TestCase
52
115
  end
53
116
 
54
117
  def idle
55
- sleep 0.2
118
+ r, w = IO.pipe
119
+ IO.select([r], nil, nil, 0.2)
120
+ ensure
121
+ r.close
122
+ w.close
56
123
  end
57
124
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stackprof
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aman Gupta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-14 00:00:00.000000000 Z
11
+ date: 2013-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -41,6 +41,7 @@ files:
41
41
  - bin/stackprof
42
42
  - ext/extconf.rb
43
43
  - ext/stackprof.c
44
+ - lib/stackprof/middleware.rb
44
45
  - lib/stackprof/report.rb
45
46
  - sample.rb
46
47
  - stackprof.gemspec
@@ -65,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
66
  version: '0'
66
67
  requirements: []
67
68
  rubyforge_project:
68
- rubygems_version: 2.2.0.preview.1
69
+ rubygems_version: 2.2.0
69
70
  signing_key:
70
71
  specification_version: 4
71
72
  summary: sampling callstack-profiler for ruby 2.1+