stackprof 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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+
|