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 +4 -4
- data/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +22 -15
- data/bin/stackprof +43 -0
- data/ext/extconf.rb +8 -1
- data/ext/stackprof.c +209 -77
- data/lib/stackprof/middleware.rb +31 -0
- data/lib/stackprof/report.rb +230 -22
- data/stackprof.gemspec +2 -1
- data/test/test_stackprof.rb +78 -11
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4d9be2e4ad16929b326e3a1e86c841ad0e13dc6
|
4
|
+
data.tar.gz: 826eff5e56b4ae6c03c613dd9937a5ae396efa98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6153df14d977ffa0b1445950de2e779a8e8dd6606e665ad22bad348619b16659862bce078d1c12eb0d48f6bb90a5c0bd8ae73bc2c4d861466d6f16cda285d4c1
|
7
|
+
data.tar.gz: 06ccf3dc44113e69b544aab7d9c4820daec59ec4b0d3b5e9784960d52b6ead75907f320d66cf4084319567f890862e4ab6954211e1b5c09d114f8553942e51ad
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
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
|
-
|
10
|
+
four sampling modes are supported:
|
11
11
|
|
12
|
-
-
|
13
|
-
-
|
14
|
-
- object
|
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
|
-
-
|
19
|
-
-
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
}
|
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(
|
56
|
+
stackprof_start(int argc, VALUE *argv, VALUE self)
|
55
57
|
{
|
56
|
-
|
57
|
-
|
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 (
|
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(
|
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(
|
94
|
+
timer.it_interval.tv_usec = NUM2LONG(interval);
|
75
95
|
timer.it_value = timer.it_interval;
|
76
|
-
setitimer(
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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
|
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
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
179
|
-
|
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.
|
184
|
-
rb_hash_aset(results, sym_mode,
|
185
|
-
rb_hash_aset(results,
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
229
|
-
num = rb_profile_frames(0, sizeof(
|
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 =
|
233
|
-
VALUE frame =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
290
|
-
|
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
|
-
|
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
|
-
|
313
|
-
rb_define_singleton_method(rb_mStackProf, "
|
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
|
data/lib/stackprof/report.rb
CHANGED
@@ -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
|
10
|
-
@data[:
|
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
|
-
|
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
|
-
|
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
|
-
|
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) *
|
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
|
-
|
44
|
-
|
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
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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 "
|
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.
|
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
|
data/test/test_stackprof.rb
CHANGED
@@ -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
|
8
|
-
assert_equal 1.
|
9
|
-
assert_equal
|
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
|
33
|
+
profile = StackProf.run(mode: :object) do
|
15
34
|
Object.new
|
16
35
|
Object.new
|
17
36
|
end
|
18
|
-
assert_equal
|
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
|
-
|
25
|
-
assert_equal
|
26
|
-
assert_equal 1, frame[:lines][
|
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,
|
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
|
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
|
-
|
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.
|
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-
|
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
|
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+
|