stackprof 0.2.12 → 0.2.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +0 -1
- data/.travis.yml +22 -8
- data/CHANGELOG.md +14 -2
- data/Dockerfile +21 -0
- data/Gemfile.lock +2 -2
- data/README.md +8 -0
- data/bin/stackprof +14 -4
- data/ext/stackprof/extconf.rb +9 -0
- data/ext/stackprof/stackprof.c +696 -0
- data/lib/stackprof.rb +4 -0
- data/lib/stackprof/report.rb +226 -1
- data/stackprof.gemspec +1 -1
- data/test/test_stackprof.rb +27 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 81201dbe84f93b79242ce9987c763b7edaa54b4f66a7005ff83d22a9b4e25cb4
|
4
|
+
data.tar.gz: c80ad82b003cb989c83bc5882f4bcdeb6f6db697729315faf6d4507b7f3888fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4fec270902e0b6f21e9d37b8317a2c3614cf4647ad29956b52561bc6c86d7625c4ee3362388c1323c1b74fd2fbb6c99242e75cf0314e20f5be97491e9888122
|
7
|
+
data.tar.gz: 01a18137afc847cba43882fbc7e22483e350490e655e9536f0dc1cb5275a49c222c4613b9a4f39cb0d3265a6e834560fc87cac4fae77324634bc2f68cde0c03a
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,8 +1,22 @@
|
|
1
|
-
sudo:
|
2
|
-
|
3
|
-
|
4
|
-
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
sudo: required
|
2
|
+
|
3
|
+
services:
|
4
|
+
- docker
|
5
|
+
|
6
|
+
language: general
|
7
|
+
|
8
|
+
env:
|
9
|
+
matrix:
|
10
|
+
- RVM_RUBY_VERSION=2.1
|
11
|
+
- RVM_RUBY_VERSION=2.2
|
12
|
+
- RVM_RUBY_VERSION=2.3
|
13
|
+
- RVM_RUBY_VERSION=2.4
|
14
|
+
- RVM_RUBY_VERSION=2.5
|
15
|
+
- RVM_RUBY_VERSION=2.6
|
16
|
+
- RVM_RUBY_VERSION=ruby-head
|
17
|
+
|
18
|
+
before_install:
|
19
|
+
- sudo docker build -t stackprof-$RVM_RUBY_VERSION --build-arg=RVM_RUBY_VERSION=$RVM_RUBY_VERSION .
|
20
|
+
|
21
|
+
script:
|
22
|
+
- sudo docker run --name stackprof-$RVM_RUBY_VERSION stackprof-$RVM_RUBY_VERSION
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
-
# 0.2.
|
1
|
+
# 0.2.13
|
2
2
|
|
3
|
-
*
|
3
|
+
* Remove /ext from .gitignore
|
4
|
+
* update gemfile
|
5
|
+
* Add ruby 2.5 to CI targets
|
6
|
+
* comment some of the inner workings
|
7
|
+
* feature: add --json format
|
8
|
+
* Add test coverage around the string branch in result writing
|
9
|
+
* Flip conditional to use duck typing
|
10
|
+
* Allow Pathname objects for Stackprof :out
|
11
|
+
* Fix a compilation error and a compilation warning
|
12
|
+
* Add `--alphabetical-flamegraph` for population-based instead of timeline
|
13
|
+
* Add `--d3-flamegraph` to output html using d3-flame-graph
|
14
|
+
* Avoid JSON::NestingError when processing deep stacks
|
15
|
+
* Use docker for CI
|
data/Dockerfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
FROM ubuntu:16.04
|
2
|
+
ARG DEBIAN_FRONTEND=noninteractive
|
3
|
+
RUN apt-get update -q && \
|
4
|
+
apt-get install -qy \
|
5
|
+
curl ca-certificates gnupg2 dirmngr build-essential \
|
6
|
+
gawk git autoconf automake pkg-config \
|
7
|
+
bison libffi-dev libgdbm-dev libncurses5-dev libsqlite3-dev libtool \
|
8
|
+
libyaml-dev sqlite3 zlib1g-dev libgmp-dev libreadline-dev libssl-dev \
|
9
|
+
ruby --no-install-recommends && \
|
10
|
+
apt-get clean
|
11
|
+
|
12
|
+
RUN gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
13
|
+
RUN curl -sSL https://get.rvm.io | bash -s
|
14
|
+
ARG RVM_RUBY_VERSION=ruby-head
|
15
|
+
RUN /bin/bash -l -c "echo $RVM_RUBY_VERSION"
|
16
|
+
RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && rvm install $RVM_RUBY_VERSION --binary || rvm install $RVM_RUBY_VERSION"
|
17
|
+
ADD . /stackprof/
|
18
|
+
WORKDIR /stackprof/
|
19
|
+
RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && gem install bundler:1.16.0"
|
20
|
+
RUN /bin/bash -l -c ". /etc/profile.d/rvm.sh && bundle install"
|
21
|
+
CMD /bin/bash -l -c ". /etc/profile.d/rvm.sh && bundle exec rake"
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -97,6 +97,14 @@ The `--flamegraph-viewer` command will output the exact shell command you need t
|
|
97
97
|
|
98
98
|
![](http://i.imgur.com/EwndrgD.png)
|
99
99
|
|
100
|
+
Alternatively, you can generate a flamegraph that uses [d3-flame-graph](https://github.com/spiermar/d3-flame-graph):
|
101
|
+
|
102
|
+
```
|
103
|
+
$ stackprof --d3-flamegraph tmp/stackprof-cpu-myapp.dump > flamegraph.html
|
104
|
+
```
|
105
|
+
|
106
|
+
And just open the result by your browser.
|
107
|
+
|
100
108
|
## Sampling
|
101
109
|
|
102
110
|
four sampling modes are supported:
|
data/bin/stackprof
CHANGED
@@ -8,6 +8,7 @@ parser = OptionParser.new(ARGV) do |o|
|
|
8
8
|
o.banner = "Usage: stackprof [file.dump]+ [--text|--method=NAME|--callgrind|--graphviz]"
|
9
9
|
|
10
10
|
o.on('--text', 'Text summary per method (default)'){ options[:format] = :text }
|
11
|
+
o.on('--json', 'JSON output (use with web viewers)'){ options[:format] = :json }
|
11
12
|
o.on('--files', 'List of files'){ |f| options[:format] = :files }
|
12
13
|
o.on('--limit [num]', Integer, 'Limit --text, --files, or --graphviz output to N entries'){ |n| options[:limit] = n }
|
13
14
|
o.on('--sort-total', "Sort --text or --files output on total samples\n\n"){ options[:sort] = true }
|
@@ -18,11 +19,14 @@ parser = OptionParser.new(ARGV) do |o|
|
|
18
19
|
o.on('--graphviz', "Graphviz output (use with dot)"){ options[:format] = :graphviz }
|
19
20
|
o.on('--node-fraction [frac]', OptionParser::DecimalNumeric, 'Drop nodes representing less than [frac] fraction of samples'){ |n| options[:node_fraction] = n }
|
20
21
|
o.on('--stackcollapse', 'stackcollapse.pl compatible output (use with stackprof-flamegraph.pl)'){ options[:format] = :stackcollapse }
|
21
|
-
o.on('--flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :
|
22
|
-
o.on('--flamegraph
|
22
|
+
o.on('--timeline-flamegraph', "timeline-flamegraph output (js)"){ options[:format] = :timeline_flamegraph }
|
23
|
+
o.on('--alphabetical-flamegraph', "alphabetical-flamegraph output (js)"){ options[:format] = :alphabetical_flamegraph }
|
24
|
+
o.on('--flamegraph', "alias to --timeline-flamegraph"){ options[:format] = :timeline_flamegraph }
|
25
|
+
o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output"){ |file|
|
23
26
|
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}")
|
24
27
|
exit
|
25
28
|
}
|
29
|
+
o.on('--d3-flamegraph', "flamegraph output (html using d3-flame-graph)\n\n"){ options[:format] = :d3_flamegraph }
|
26
30
|
o.on('--select-files []', String, 'Show results of matching files'){ |path| (options[:select_files] ||= []) << File.expand_path(path) }
|
27
31
|
o.on('--reject-files []', String, 'Exclude results of matching files'){ |path| (options[:reject_files] ||= []) << File.expand_path(path) }
|
28
32
|
o.on('--select-names []', Regexp, 'Show results of matching method names'){ |regexp| (options[:select_names] ||= []) << regexp }
|
@@ -62,6 +66,8 @@ options.delete(:limit) if options[:limit] == 0
|
|
62
66
|
case options[:format]
|
63
67
|
when :text
|
64
68
|
report.print_text(options[:sort], options[:limit], options[:select_files], options[:reject_files], options[:select_names], options[:reject_names])
|
69
|
+
when :json
|
70
|
+
report.print_json
|
65
71
|
when :debug
|
66
72
|
report.print_debug
|
67
73
|
when :dump
|
@@ -72,8 +78,12 @@ when :graphviz
|
|
72
78
|
report.print_graphviz(options)
|
73
79
|
when :stackcollapse
|
74
80
|
report.print_stackcollapse
|
75
|
-
when :
|
76
|
-
report.
|
81
|
+
when :timeline_flamegraph
|
82
|
+
report.print_timeline_flamegraph
|
83
|
+
when :alphabetical_flamegraph
|
84
|
+
report.print_alphabetical_flamegraph
|
85
|
+
when :d3_flamegraph
|
86
|
+
report.print_d3_flamegraph
|
77
87
|
when :method
|
78
88
|
options[:walk] ? report.walk_method(options[:filter]) : report.print_method(options[:filter])
|
79
89
|
when :file
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'mkmf'
|
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/stackprof')
|
7
|
+
else
|
8
|
+
fail 'missing API: are you using ruby 2.1+?'
|
9
|
+
end
|
@@ -0,0 +1,696 @@
|
|
1
|
+
/**********************************************************************
|
2
|
+
|
3
|
+
stackprof.c - Sampling call-stack frame profiler for MRI.
|
4
|
+
|
5
|
+
vim: noexpandtab shiftwidth=4 tabstop=8 softtabstop=4
|
6
|
+
|
7
|
+
**********************************************************************/
|
8
|
+
|
9
|
+
#include <ruby/ruby.h>
|
10
|
+
#include <ruby/debug.h>
|
11
|
+
#include <ruby/st.h>
|
12
|
+
#include <ruby/io.h>
|
13
|
+
#include <ruby/intern.h>
|
14
|
+
#include <signal.h>
|
15
|
+
#include <sys/time.h>
|
16
|
+
#include <pthread.h>
|
17
|
+
|
18
|
+
#define BUF_SIZE 2048
|
19
|
+
|
20
|
+
typedef struct {
|
21
|
+
size_t total_samples;
|
22
|
+
size_t caller_samples;
|
23
|
+
size_t seen_at_sample_number;
|
24
|
+
st_table *edges;
|
25
|
+
st_table *lines;
|
26
|
+
} frame_data_t;
|
27
|
+
|
28
|
+
static struct {
|
29
|
+
int running;
|
30
|
+
int raw;
|
31
|
+
int aggregate;
|
32
|
+
|
33
|
+
VALUE mode;
|
34
|
+
VALUE interval;
|
35
|
+
VALUE out;
|
36
|
+
|
37
|
+
VALUE *raw_samples;
|
38
|
+
size_t raw_samples_len;
|
39
|
+
size_t raw_samples_capa;
|
40
|
+
size_t raw_sample_index;
|
41
|
+
|
42
|
+
struct timeval last_sample_at;
|
43
|
+
int *raw_timestamp_deltas;
|
44
|
+
size_t raw_timestamp_deltas_len;
|
45
|
+
size_t raw_timestamp_deltas_capa;
|
46
|
+
|
47
|
+
size_t overall_signals;
|
48
|
+
size_t overall_samples;
|
49
|
+
size_t during_gc;
|
50
|
+
size_t unrecorded_gc_samples;
|
51
|
+
st_table *frames;
|
52
|
+
|
53
|
+
VALUE fake_gc_frame;
|
54
|
+
VALUE fake_gc_frame_name;
|
55
|
+
VALUE empty_string;
|
56
|
+
VALUE frames_buffer[BUF_SIZE];
|
57
|
+
int lines_buffer[BUF_SIZE];
|
58
|
+
} _stackprof;
|
59
|
+
|
60
|
+
static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
|
61
|
+
static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
|
62
|
+
static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_frames, sym_out, sym_aggregate, sym_raw_timestamp_deltas;
|
63
|
+
static VALUE sym_gc_samples, objtracer;
|
64
|
+
static VALUE gc_hook;
|
65
|
+
static VALUE rb_mStackProf;
|
66
|
+
|
67
|
+
static void stackprof_newobj_handler(VALUE, void*);
|
68
|
+
static void stackprof_signal_handler(int sig, siginfo_t* sinfo, void* ucontext);
|
69
|
+
|
70
|
+
static VALUE
|
71
|
+
stackprof_start(int argc, VALUE *argv, VALUE self)
|
72
|
+
{
|
73
|
+
struct sigaction sa;
|
74
|
+
struct itimerval timer;
|
75
|
+
VALUE opts = Qnil, mode = Qnil, interval = Qnil, out = Qfalse;
|
76
|
+
int raw = 0, aggregate = 1;
|
77
|
+
|
78
|
+
if (_stackprof.running)
|
79
|
+
return Qfalse;
|
80
|
+
|
81
|
+
rb_scan_args(argc, argv, "0:", &opts);
|
82
|
+
|
83
|
+
if (RTEST(opts)) {
|
84
|
+
mode = rb_hash_aref(opts, sym_mode);
|
85
|
+
interval = rb_hash_aref(opts, sym_interval);
|
86
|
+
out = rb_hash_aref(opts, sym_out);
|
87
|
+
|
88
|
+
if (RTEST(rb_hash_aref(opts, sym_raw)))
|
89
|
+
raw = 1;
|
90
|
+
if (rb_hash_lookup2(opts, sym_aggregate, Qundef) == Qfalse)
|
91
|
+
aggregate = 0;
|
92
|
+
}
|
93
|
+
if (!RTEST(mode)) mode = sym_wall;
|
94
|
+
|
95
|
+
if (!_stackprof.frames) {
|
96
|
+
_stackprof.frames = st_init_numtable();
|
97
|
+
_stackprof.overall_signals = 0;
|
98
|
+
_stackprof.overall_samples = 0;
|
99
|
+
_stackprof.during_gc = 0;
|
100
|
+
}
|
101
|
+
|
102
|
+
if (mode == sym_object) {
|
103
|
+
if (!RTEST(interval)) interval = INT2FIX(1);
|
104
|
+
|
105
|
+
objtracer = rb_tracepoint_new(Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, stackprof_newobj_handler, 0);
|
106
|
+
rb_tracepoint_enable(objtracer);
|
107
|
+
} else if (mode == sym_wall || mode == sym_cpu) {
|
108
|
+
if (!RTEST(interval)) interval = INT2FIX(1000);
|
109
|
+
|
110
|
+
sa.sa_sigaction = stackprof_signal_handler;
|
111
|
+
sa.sa_flags = SA_RESTART | SA_SIGINFO;
|
112
|
+
sigemptyset(&sa.sa_mask);
|
113
|
+
sigaction(mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL);
|
114
|
+
|
115
|
+
timer.it_interval.tv_sec = 0;
|
116
|
+
timer.it_interval.tv_usec = NUM2LONG(interval);
|
117
|
+
timer.it_value = timer.it_interval;
|
118
|
+
setitimer(mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
|
119
|
+
} else if (mode == sym_custom) {
|
120
|
+
/* sampled manually */
|
121
|
+
interval = Qnil;
|
122
|
+
} else {
|
123
|
+
rb_raise(rb_eArgError, "unknown profiler mode");
|
124
|
+
}
|
125
|
+
|
126
|
+
_stackprof.running = 1;
|
127
|
+
_stackprof.raw = raw;
|
128
|
+
_stackprof.aggregate = aggregate;
|
129
|
+
_stackprof.mode = mode;
|
130
|
+
_stackprof.interval = interval;
|
131
|
+
_stackprof.out = out;
|
132
|
+
|
133
|
+
if (raw) {
|
134
|
+
gettimeofday(&_stackprof.last_sample_at, NULL);
|
135
|
+
}
|
136
|
+
|
137
|
+
return Qtrue;
|
138
|
+
}
|
139
|
+
|
140
|
+
static VALUE
|
141
|
+
stackprof_stop(VALUE self)
|
142
|
+
{
|
143
|
+
struct sigaction sa;
|
144
|
+
struct itimerval timer;
|
145
|
+
|
146
|
+
if (!_stackprof.running)
|
147
|
+
return Qfalse;
|
148
|
+
_stackprof.running = 0;
|
149
|
+
|
150
|
+
if (_stackprof.mode == sym_object) {
|
151
|
+
rb_tracepoint_disable(objtracer);
|
152
|
+
} else if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
|
153
|
+
memset(&timer, 0, sizeof(timer));
|
154
|
+
setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
|
155
|
+
|
156
|
+
sa.sa_handler = SIG_IGN;
|
157
|
+
sa.sa_flags = SA_RESTART;
|
158
|
+
sigemptyset(&sa.sa_mask);
|
159
|
+
sigaction(_stackprof.mode == sym_wall ? SIGALRM : SIGPROF, &sa, NULL);
|
160
|
+
} else if (_stackprof.mode == sym_custom) {
|
161
|
+
/* sampled manually */
|
162
|
+
} else {
|
163
|
+
rb_raise(rb_eArgError, "unknown profiler mode");
|
164
|
+
}
|
165
|
+
|
166
|
+
return Qtrue;
|
167
|
+
}
|
168
|
+
|
169
|
+
static int
|
170
|
+
frame_edges_i(st_data_t key, st_data_t val, st_data_t arg)
|
171
|
+
{
|
172
|
+
VALUE edges = (VALUE)arg;
|
173
|
+
|
174
|
+
intptr_t weight = (intptr_t)val;
|
175
|
+
rb_hash_aset(edges, rb_obj_id((VALUE)key), INT2FIX(weight));
|
176
|
+
return ST_CONTINUE;
|
177
|
+
}
|
178
|
+
|
179
|
+
static int
|
180
|
+
frame_lines_i(st_data_t key, st_data_t val, st_data_t arg)
|
181
|
+
{
|
182
|
+
VALUE lines = (VALUE)arg;
|
183
|
+
|
184
|
+
size_t weight = (size_t)val;
|
185
|
+
size_t total = weight & (~(size_t)0 << (8*SIZEOF_SIZE_T/2));
|
186
|
+
weight -= total;
|
187
|
+
total = total >> (8*SIZEOF_SIZE_T/2);
|
188
|
+
rb_hash_aset(lines, INT2FIX(key), rb_ary_new3(2, ULONG2NUM(total), ULONG2NUM(weight)));
|
189
|
+
return ST_CONTINUE;
|
190
|
+
}
|
191
|
+
|
192
|
+
static int
|
193
|
+
frame_i(st_data_t key, st_data_t val, st_data_t arg)
|
194
|
+
{
|
195
|
+
VALUE frame = (VALUE)key;
|
196
|
+
frame_data_t *frame_data = (frame_data_t *)val;
|
197
|
+
VALUE results = (VALUE)arg;
|
198
|
+
VALUE details = rb_hash_new();
|
199
|
+
VALUE name, file, edges, lines;
|
200
|
+
VALUE line;
|
201
|
+
|
202
|
+
rb_hash_aset(results, rb_obj_id(frame), details);
|
203
|
+
|
204
|
+
if (frame == _stackprof.fake_gc_frame) {
|
205
|
+
name = _stackprof.fake_gc_frame_name;
|
206
|
+
file = _stackprof.empty_string;
|
207
|
+
line = INT2FIX(0);
|
208
|
+
} else {
|
209
|
+
name = rb_profile_frame_full_label(frame);
|
210
|
+
|
211
|
+
file = rb_profile_frame_absolute_path(frame);
|
212
|
+
if (NIL_P(file))
|
213
|
+
file = rb_profile_frame_path(frame);
|
214
|
+
line = rb_profile_frame_first_lineno(frame);
|
215
|
+
}
|
216
|
+
|
217
|
+
rb_hash_aset(details, sym_name, name);
|
218
|
+
rb_hash_aset(details, sym_file, file);
|
219
|
+
if (line != INT2FIX(0)) {
|
220
|
+
rb_hash_aset(details, sym_line, line);
|
221
|
+
}
|
222
|
+
|
223
|
+
rb_hash_aset(details, sym_total_samples, SIZET2NUM(frame_data->total_samples));
|
224
|
+
rb_hash_aset(details, sym_samples, SIZET2NUM(frame_data->caller_samples));
|
225
|
+
|
226
|
+
if (frame_data->edges) {
|
227
|
+
edges = rb_hash_new();
|
228
|
+
rb_hash_aset(details, sym_edges, edges);
|
229
|
+
st_foreach(frame_data->edges, frame_edges_i, (st_data_t)edges);
|
230
|
+
st_free_table(frame_data->edges);
|
231
|
+
frame_data->edges = NULL;
|
232
|
+
}
|
233
|
+
|
234
|
+
if (frame_data->lines) {
|
235
|
+
lines = rb_hash_new();
|
236
|
+
rb_hash_aset(details, sym_lines, lines);
|
237
|
+
st_foreach(frame_data->lines, frame_lines_i, (st_data_t)lines);
|
238
|
+
st_free_table(frame_data->lines);
|
239
|
+
frame_data->lines = NULL;
|
240
|
+
}
|
241
|
+
|
242
|
+
xfree(frame_data);
|
243
|
+
return ST_DELETE;
|
244
|
+
}
|
245
|
+
|
246
|
+
static VALUE
|
247
|
+
stackprof_results(int argc, VALUE *argv, VALUE self)
|
248
|
+
{
|
249
|
+
VALUE results, frames;
|
250
|
+
|
251
|
+
if (!_stackprof.frames || _stackprof.running)
|
252
|
+
return Qnil;
|
253
|
+
|
254
|
+
results = rb_hash_new();
|
255
|
+
rb_hash_aset(results, sym_version, DBL2NUM(1.2));
|
256
|
+
rb_hash_aset(results, sym_mode, _stackprof.mode);
|
257
|
+
rb_hash_aset(results, sym_interval, _stackprof.interval);
|
258
|
+
rb_hash_aset(results, sym_samples, SIZET2NUM(_stackprof.overall_samples));
|
259
|
+
rb_hash_aset(results, sym_gc_samples, SIZET2NUM(_stackprof.during_gc));
|
260
|
+
rb_hash_aset(results, sym_missed_samples, SIZET2NUM(_stackprof.overall_signals - _stackprof.overall_samples));
|
261
|
+
|
262
|
+
frames = rb_hash_new();
|
263
|
+
rb_hash_aset(results, sym_frames, frames);
|
264
|
+
st_foreach(_stackprof.frames, frame_i, (st_data_t)frames);
|
265
|
+
|
266
|
+
st_free_table(_stackprof.frames);
|
267
|
+
_stackprof.frames = NULL;
|
268
|
+
|
269
|
+
if (_stackprof.raw && _stackprof.raw_samples_len) {
|
270
|
+
size_t len, n, o;
|
271
|
+
VALUE raw_timestamp_deltas;
|
272
|
+
VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len);
|
273
|
+
|
274
|
+
for (n = 0; n < _stackprof.raw_samples_len; n++) {
|
275
|
+
len = (size_t)_stackprof.raw_samples[n];
|
276
|
+
rb_ary_push(raw_samples, SIZET2NUM(len));
|
277
|
+
|
278
|
+
for (o = 0, n++; o < len; n++, o++)
|
279
|
+
rb_ary_push(raw_samples, rb_obj_id(_stackprof.raw_samples[n]));
|
280
|
+
rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n]));
|
281
|
+
}
|
282
|
+
|
283
|
+
free(_stackprof.raw_samples);
|
284
|
+
_stackprof.raw_samples = NULL;
|
285
|
+
_stackprof.raw_samples_len = 0;
|
286
|
+
_stackprof.raw_samples_capa = 0;
|
287
|
+
_stackprof.raw_sample_index = 0;
|
288
|
+
|
289
|
+
rb_hash_aset(results, sym_raw, raw_samples);
|
290
|
+
|
291
|
+
raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_timestamp_deltas_len);
|
292
|
+
|
293
|
+
for (n = 0; n < _stackprof.raw_timestamp_deltas_len; n++) {
|
294
|
+
rb_ary_push(raw_timestamp_deltas, INT2FIX(_stackprof.raw_timestamp_deltas[n]));
|
295
|
+
}
|
296
|
+
|
297
|
+
free(_stackprof.raw_timestamp_deltas);
|
298
|
+
_stackprof.raw_timestamp_deltas = NULL;
|
299
|
+
_stackprof.raw_timestamp_deltas_len = 0;
|
300
|
+
_stackprof.raw_timestamp_deltas_capa = 0;
|
301
|
+
|
302
|
+
rb_hash_aset(results, sym_raw_timestamp_deltas, raw_timestamp_deltas);
|
303
|
+
|
304
|
+
_stackprof.raw = 0;
|
305
|
+
}
|
306
|
+
|
307
|
+
if (argc == 1)
|
308
|
+
_stackprof.out = argv[0];
|
309
|
+
|
310
|
+
if (RTEST(_stackprof.out)) {
|
311
|
+
VALUE file;
|
312
|
+
if (rb_respond_to(_stackprof.out, rb_intern("to_io"))) {
|
313
|
+
file = rb_io_check_io(_stackprof.out);
|
314
|
+
} else {
|
315
|
+
file = rb_file_open_str(_stackprof.out, "w");
|
316
|
+
}
|
317
|
+
|
318
|
+
rb_marshal_dump(results, file);
|
319
|
+
rb_io_flush(file);
|
320
|
+
_stackprof.out = Qnil;
|
321
|
+
return file;
|
322
|
+
} else {
|
323
|
+
return results;
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
static VALUE
|
328
|
+
stackprof_run(int argc, VALUE *argv, VALUE self)
|
329
|
+
{
|
330
|
+
rb_need_block();
|
331
|
+
stackprof_start(argc, argv, self);
|
332
|
+
rb_ensure(rb_yield, Qundef, stackprof_stop, self);
|
333
|
+
return stackprof_results(0, 0, self);
|
334
|
+
}
|
335
|
+
|
336
|
+
static VALUE
|
337
|
+
stackprof_running_p(VALUE self)
|
338
|
+
{
|
339
|
+
return _stackprof.running ? Qtrue : Qfalse;
|
340
|
+
}
|
341
|
+
|
342
|
+
static inline frame_data_t *
|
343
|
+
sample_for(VALUE frame)
|
344
|
+
{
|
345
|
+
st_data_t key = (st_data_t)frame, val = 0;
|
346
|
+
frame_data_t *frame_data;
|
347
|
+
|
348
|
+
if (st_lookup(_stackprof.frames, key, &val)) {
|
349
|
+
frame_data = (frame_data_t *)val;
|
350
|
+
} else {
|
351
|
+
frame_data = ALLOC_N(frame_data_t, 1);
|
352
|
+
MEMZERO(frame_data, frame_data_t, 1);
|
353
|
+
val = (st_data_t)frame_data;
|
354
|
+
st_insert(_stackprof.frames, key, val);
|
355
|
+
}
|
356
|
+
|
357
|
+
return frame_data;
|
358
|
+
}
|
359
|
+
|
360
|
+
static int
|
361
|
+
numtable_increment_callback(st_data_t *key, st_data_t *value, st_data_t arg, int existing)
|
362
|
+
{
|
363
|
+
size_t *weight = (size_t *)value;
|
364
|
+
size_t increment = (size_t)arg;
|
365
|
+
|
366
|
+
if (existing)
|
367
|
+
(*weight) += increment;
|
368
|
+
else
|
369
|
+
*weight = increment;
|
370
|
+
|
371
|
+
return ST_CONTINUE;
|
372
|
+
}
|
373
|
+
|
374
|
+
void
|
375
|
+
st_numtable_increment(st_table *table, st_data_t key, size_t increment)
|
376
|
+
{
|
377
|
+
st_update(table, key, numtable_increment_callback, (st_data_t)increment);
|
378
|
+
}
|
379
|
+
|
380
|
+
void
|
381
|
+
stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
382
|
+
{
|
383
|
+
int i, n;
|
384
|
+
VALUE prev_frame = Qnil;
|
385
|
+
|
386
|
+
_stackprof.overall_samples++;
|
387
|
+
|
388
|
+
if (_stackprof.raw) {
|
389
|
+
int found = 0;
|
390
|
+
|
391
|
+
/* If there's no sample buffer allocated, then allocate one. The buffer
|
392
|
+
* format is the number of frames (num), then the list of frames (from
|
393
|
+
* `_stackprof.raw_samples`), followed by the number of times this
|
394
|
+
* particular stack has been seen in a row. Each "new" stack is added
|
395
|
+
* to the end of the buffer, but if the previous stack is the same as
|
396
|
+
* the current stack, the counter will be incremented. */
|
397
|
+
if (!_stackprof.raw_samples) {
|
398
|
+
_stackprof.raw_samples_capa = num * 100;
|
399
|
+
_stackprof.raw_samples = malloc(sizeof(VALUE) * _stackprof.raw_samples_capa);
|
400
|
+
}
|
401
|
+
|
402
|
+
/* If we can't fit all the samples in the buffer, double the buffer size. */
|
403
|
+
while (_stackprof.raw_samples_capa <= _stackprof.raw_samples_len + (num + 2)) {
|
404
|
+
_stackprof.raw_samples_capa *= 2;
|
405
|
+
_stackprof.raw_samples = realloc(_stackprof.raw_samples, sizeof(VALUE) * _stackprof.raw_samples_capa);
|
406
|
+
}
|
407
|
+
|
408
|
+
/* If we've seen this stack before in the last sample, then increment the "seen" count. */
|
409
|
+
if (_stackprof.raw_samples_len > 0 && _stackprof.raw_samples[_stackprof.raw_sample_index] == (VALUE)num) {
|
410
|
+
/* The number of samples could have been the same, but the stack
|
411
|
+
* might be different, so we need to check the stack here. Stacks
|
412
|
+
* in the raw buffer are stored in the opposite direction of stacks
|
413
|
+
* in the frames buffer that came from Ruby. */
|
414
|
+
for (i = num-1, n = 0; i >= 0; i--, n++) {
|
415
|
+
VALUE frame = _stackprof.frames_buffer[i];
|
416
|
+
if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != frame)
|
417
|
+
break;
|
418
|
+
}
|
419
|
+
if (i == -1) {
|
420
|
+
_stackprof.raw_samples[_stackprof.raw_samples_len-1] += 1;
|
421
|
+
found = 1;
|
422
|
+
}
|
423
|
+
}
|
424
|
+
|
425
|
+
/* If we haven't seen the stack, then add it to the buffer along with
|
426
|
+
* the length of the stack and a 1 for the "seen" count */
|
427
|
+
if (!found) {
|
428
|
+
/* Bump the `raw_sample_index` up so that the next iteration can
|
429
|
+
* find the previously recorded stack size. */
|
430
|
+
_stackprof.raw_sample_index = _stackprof.raw_samples_len;
|
431
|
+
_stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)num;
|
432
|
+
for (i = num-1; i >= 0; i--) {
|
433
|
+
VALUE frame = _stackprof.frames_buffer[i];
|
434
|
+
_stackprof.raw_samples[_stackprof.raw_samples_len++] = frame;
|
435
|
+
}
|
436
|
+
_stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)1;
|
437
|
+
}
|
438
|
+
|
439
|
+
/* If there's no timestamp delta buffer, allocate one */
|
440
|
+
if (!_stackprof.raw_timestamp_deltas) {
|
441
|
+
_stackprof.raw_timestamp_deltas_capa = 100;
|
442
|
+
_stackprof.raw_timestamp_deltas = malloc(sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
|
443
|
+
_stackprof.raw_timestamp_deltas_len = 0;
|
444
|
+
}
|
445
|
+
|
446
|
+
/* Double the buffer size if it's too small */
|
447
|
+
while (_stackprof.raw_timestamp_deltas_capa <= _stackprof.raw_timestamp_deltas_len + 1) {
|
448
|
+
_stackprof.raw_timestamp_deltas_capa *= 2;
|
449
|
+
_stackprof.raw_timestamp_deltas = realloc(_stackprof.raw_timestamp_deltas, sizeof(int) * _stackprof.raw_timestamp_deltas_capa);
|
450
|
+
}
|
451
|
+
|
452
|
+
/* Store the time delta (which is the amount of time between samples) */
|
453
|
+
_stackprof.raw_timestamp_deltas[_stackprof.raw_timestamp_deltas_len++] = timestamp_delta;
|
454
|
+
}
|
455
|
+
|
456
|
+
for (i = 0; i < num; i++) {
|
457
|
+
int line = _stackprof.lines_buffer[i];
|
458
|
+
VALUE frame = _stackprof.frames_buffer[i];
|
459
|
+
frame_data_t *frame_data = sample_for(frame);
|
460
|
+
|
461
|
+
if (frame_data->seen_at_sample_number != _stackprof.overall_samples) {
|
462
|
+
frame_data->total_samples++;
|
463
|
+
}
|
464
|
+
frame_data->seen_at_sample_number = _stackprof.overall_samples;
|
465
|
+
|
466
|
+
if (i == 0) {
|
467
|
+
frame_data->caller_samples++;
|
468
|
+
} else if (_stackprof.aggregate) {
|
469
|
+
if (!frame_data->edges)
|
470
|
+
frame_data->edges = st_init_numtable();
|
471
|
+
st_numtable_increment(frame_data->edges, (st_data_t)prev_frame, 1);
|
472
|
+
}
|
473
|
+
|
474
|
+
if (_stackprof.aggregate && line > 0) {
|
475
|
+
size_t half = (size_t)1<<(8*SIZEOF_SIZE_T/2);
|
476
|
+
size_t increment = i == 0 ? half + 1 : half;
|
477
|
+
if (!frame_data->lines)
|
478
|
+
frame_data->lines = st_init_numtable();
|
479
|
+
st_numtable_increment(frame_data->lines, (st_data_t)line, increment);
|
480
|
+
}
|
481
|
+
|
482
|
+
prev_frame = frame;
|
483
|
+
}
|
484
|
+
|
485
|
+
if (_stackprof.raw) {
|
486
|
+
gettimeofday(&_stackprof.last_sample_at, NULL);
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
void
|
491
|
+
stackprof_record_sample()
|
492
|
+
{
|
493
|
+
int timestamp_delta = 0;
|
494
|
+
int num;
|
495
|
+
if (_stackprof.raw) {
|
496
|
+
struct timeval t;
|
497
|
+
struct timeval diff;
|
498
|
+
gettimeofday(&t, NULL);
|
499
|
+
timersub(&t, &_stackprof.last_sample_at, &diff);
|
500
|
+
timestamp_delta = (1000 * diff.tv_sec) + diff.tv_usec;
|
501
|
+
}
|
502
|
+
num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer);
|
503
|
+
stackprof_record_sample_for_stack(num, timestamp_delta);
|
504
|
+
}
|
505
|
+
|
506
|
+
void
|
507
|
+
stackprof_record_gc_samples()
|
508
|
+
{
|
509
|
+
int delta_to_first_unrecorded_gc_sample = 0;
|
510
|
+
int i;
|
511
|
+
if (_stackprof.raw) {
|
512
|
+
struct timeval t;
|
513
|
+
struct timeval diff;
|
514
|
+
gettimeofday(&t, NULL);
|
515
|
+
timersub(&t, &_stackprof.last_sample_at, &diff);
|
516
|
+
|
517
|
+
// We don't know when the GC samples were actually marked, so let's
|
518
|
+
// assume that they were marked at a perfectly regular interval.
|
519
|
+
delta_to_first_unrecorded_gc_sample = (1000 * diff.tv_sec + diff.tv_usec) - (_stackprof.unrecorded_gc_samples - 1) * NUM2LONG(_stackprof.interval);
|
520
|
+
if (delta_to_first_unrecorded_gc_sample < 0) {
|
521
|
+
delta_to_first_unrecorded_gc_sample = 0;
|
522
|
+
}
|
523
|
+
}
|
524
|
+
|
525
|
+
_stackprof.frames_buffer[0] = _stackprof.fake_gc_frame;
|
526
|
+
_stackprof.lines_buffer[0] = 0;
|
527
|
+
|
528
|
+
for (i = 0; i < _stackprof.unrecorded_gc_samples; i++) {
|
529
|
+
int timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : NUM2LONG(_stackprof.interval);
|
530
|
+
stackprof_record_sample_for_stack(1, timestamp_delta);
|
531
|
+
}
|
532
|
+
_stackprof.during_gc += _stackprof.unrecorded_gc_samples;
|
533
|
+
_stackprof.unrecorded_gc_samples = 0;
|
534
|
+
}
|
535
|
+
|
536
|
+
static void
|
537
|
+
stackprof_gc_job_handler(void *data)
|
538
|
+
{
|
539
|
+
static int in_signal_handler = 0;
|
540
|
+
if (in_signal_handler) return;
|
541
|
+
if (!_stackprof.running) return;
|
542
|
+
|
543
|
+
in_signal_handler++;
|
544
|
+
stackprof_record_gc_samples();
|
545
|
+
in_signal_handler--;
|
546
|
+
}
|
547
|
+
|
548
|
+
static void
|
549
|
+
stackprof_job_handler(void *data)
|
550
|
+
{
|
551
|
+
static int in_signal_handler = 0;
|
552
|
+
if (in_signal_handler) return;
|
553
|
+
if (!_stackprof.running) return;
|
554
|
+
|
555
|
+
in_signal_handler++;
|
556
|
+
stackprof_record_sample();
|
557
|
+
in_signal_handler--;
|
558
|
+
}
|
559
|
+
|
560
|
+
static void
|
561
|
+
stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
562
|
+
{
|
563
|
+
_stackprof.overall_signals++;
|
564
|
+
if (rb_during_gc()) {
|
565
|
+
_stackprof.unrecorded_gc_samples++;
|
566
|
+
rb_postponed_job_register_one(0, stackprof_gc_job_handler, (void*)0);
|
567
|
+
} else {
|
568
|
+
rb_postponed_job_register_one(0, stackprof_job_handler, (void*)0);
|
569
|
+
}
|
570
|
+
}
|
571
|
+
|
572
|
+
static void
|
573
|
+
stackprof_newobj_handler(VALUE tpval, void *data)
|
574
|
+
{
|
575
|
+
_stackprof.overall_signals++;
|
576
|
+
if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval))
|
577
|
+
return;
|
578
|
+
stackprof_job_handler(0);
|
579
|
+
}
|
580
|
+
|
581
|
+
static VALUE
|
582
|
+
stackprof_sample(VALUE self)
|
583
|
+
{
|
584
|
+
if (!_stackprof.running)
|
585
|
+
return Qfalse;
|
586
|
+
|
587
|
+
_stackprof.overall_signals++;
|
588
|
+
stackprof_job_handler(0);
|
589
|
+
return Qtrue;
|
590
|
+
}
|
591
|
+
|
592
|
+
static int
|
593
|
+
frame_mark_i(st_data_t key, st_data_t val, st_data_t arg)
|
594
|
+
{
|
595
|
+
VALUE frame = (VALUE)key;
|
596
|
+
rb_gc_mark(frame);
|
597
|
+
return ST_CONTINUE;
|
598
|
+
}
|
599
|
+
|
600
|
+
static void
|
601
|
+
stackprof_gc_mark(void *data)
|
602
|
+
{
|
603
|
+
if (RTEST(_stackprof.out))
|
604
|
+
rb_gc_mark(_stackprof.out);
|
605
|
+
|
606
|
+
if (_stackprof.frames)
|
607
|
+
st_foreach(_stackprof.frames, frame_mark_i, 0);
|
608
|
+
}
|
609
|
+
|
610
|
+
static void
|
611
|
+
stackprof_atfork_prepare(void)
|
612
|
+
{
|
613
|
+
struct itimerval timer;
|
614
|
+
if (_stackprof.running) {
|
615
|
+
if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
|
616
|
+
memset(&timer, 0, sizeof(timer));
|
617
|
+
setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
|
618
|
+
}
|
619
|
+
}
|
620
|
+
}
|
621
|
+
|
622
|
+
static void
|
623
|
+
stackprof_atfork_parent(void)
|
624
|
+
{
|
625
|
+
struct itimerval timer;
|
626
|
+
if (_stackprof.running) {
|
627
|
+
if (_stackprof.mode == sym_wall || _stackprof.mode == sym_cpu) {
|
628
|
+
timer.it_interval.tv_sec = 0;
|
629
|
+
timer.it_interval.tv_usec = NUM2LONG(_stackprof.interval);
|
630
|
+
timer.it_value = timer.it_interval;
|
631
|
+
setitimer(_stackprof.mode == sym_wall ? ITIMER_REAL : ITIMER_PROF, &timer, 0);
|
632
|
+
}
|
633
|
+
}
|
634
|
+
}
|
635
|
+
|
636
|
+
static void
|
637
|
+
stackprof_atfork_child(void)
|
638
|
+
{
|
639
|
+
stackprof_stop(rb_mStackProf);
|
640
|
+
}
|
641
|
+
|
642
|
+
void
|
643
|
+
Init_stackprof(void)
|
644
|
+
{
|
645
|
+
#define S(name) sym_##name = ID2SYM(rb_intern(#name));
|
646
|
+
S(object);
|
647
|
+
S(custom);
|
648
|
+
S(wall);
|
649
|
+
S(cpu);
|
650
|
+
S(name);
|
651
|
+
S(file);
|
652
|
+
S(line);
|
653
|
+
S(total_samples);
|
654
|
+
S(gc_samples);
|
655
|
+
S(missed_samples);
|
656
|
+
S(samples);
|
657
|
+
S(edges);
|
658
|
+
S(lines);
|
659
|
+
S(version);
|
660
|
+
S(mode);
|
661
|
+
S(interval);
|
662
|
+
S(raw);
|
663
|
+
S(raw_timestamp_deltas);
|
664
|
+
S(out);
|
665
|
+
S(frames);
|
666
|
+
S(aggregate);
|
667
|
+
#undef S
|
668
|
+
|
669
|
+
gc_hook = Data_Wrap_Struct(rb_cObject, stackprof_gc_mark, NULL, &_stackprof);
|
670
|
+
rb_global_variable(&gc_hook);
|
671
|
+
|
672
|
+
_stackprof.raw_samples = NULL;
|
673
|
+
_stackprof.raw_samples_len = 0;
|
674
|
+
_stackprof.raw_samples_capa = 0;
|
675
|
+
_stackprof.raw_sample_index = 0;
|
676
|
+
|
677
|
+
_stackprof.raw_timestamp_deltas = NULL;
|
678
|
+
_stackprof.raw_timestamp_deltas_len = 0;
|
679
|
+
_stackprof.raw_timestamp_deltas_capa = 0;
|
680
|
+
|
681
|
+
_stackprof.fake_gc_frame = INT2FIX(0x9C);
|
682
|
+
_stackprof.empty_string = rb_str_new_cstr("");
|
683
|
+
_stackprof.fake_gc_frame_name = rb_str_new_cstr("(garbage collection)");
|
684
|
+
rb_global_variable(&_stackprof.fake_gc_frame_name);
|
685
|
+
rb_global_variable(&_stackprof.empty_string);
|
686
|
+
|
687
|
+
rb_mStackProf = rb_define_module("StackProf");
|
688
|
+
rb_define_singleton_method(rb_mStackProf, "running?", stackprof_running_p, 0);
|
689
|
+
rb_define_singleton_method(rb_mStackProf, "run", stackprof_run, -1);
|
690
|
+
rb_define_singleton_method(rb_mStackProf, "start", stackprof_start, -1);
|
691
|
+
rb_define_singleton_method(rb_mStackProf, "stop", stackprof_stop, 0);
|
692
|
+
rb_define_singleton_method(rb_mStackProf, "results", stackprof_results, -1);
|
693
|
+
rb_define_singleton_method(rb_mStackProf, "sample", stackprof_sample, 0);
|
694
|
+
|
695
|
+
pthread_atfork(stackprof_atfork_prepare, stackprof_atfork_parent, stackprof_atfork_child);
|
696
|
+
}
|
data/lib/stackprof.rb
CHANGED
data/lib/stackprof/report.rb
CHANGED
@@ -68,6 +68,11 @@ module StackProf
|
|
68
68
|
f.puts Marshal.dump(@data.reject{|k,v| k == :files })
|
69
69
|
end
|
70
70
|
|
71
|
+
def print_json(f=STDOUT)
|
72
|
+
require "json"
|
73
|
+
f.puts JSON.generate(@data, max_nesting: false)
|
74
|
+
end
|
75
|
+
|
71
76
|
def print_stackcollapse
|
72
77
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
73
78
|
|
@@ -80,7 +85,15 @@ module StackProf
|
|
80
85
|
end
|
81
86
|
end
|
82
87
|
|
83
|
-
def
|
88
|
+
def print_timeline_flamegraph(f=STDOUT, skip_common=true)
|
89
|
+
print_flamegraph(f, skip_common, false)
|
90
|
+
end
|
91
|
+
|
92
|
+
def print_alphabetical_flamegraph(f=STDOUT, skip_common=true)
|
93
|
+
print_flamegraph(f, skip_common, true)
|
94
|
+
end
|
95
|
+
|
96
|
+
def print_flamegraph(f, skip_common, alphabetical=false)
|
84
97
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
85
98
|
|
86
99
|
stacks = []
|
@@ -93,6 +106,8 @@ module StackProf
|
|
93
106
|
max_x += stack.last
|
94
107
|
end
|
95
108
|
|
109
|
+
stacks.sort! if alphabetical
|
110
|
+
|
96
111
|
f.puts 'flamegraph(['
|
97
112
|
max_y.times do |y|
|
98
113
|
row_prev = nil
|
@@ -148,6 +163,216 @@ module StackProf
|
|
148
163
|
f.puts %{{"x":#{x},"y":#{y},"width":#{weight},"frame_id":#{addr},"frame":#{frame[:name].dump},"file":#{frame[:file].dump}}}
|
149
164
|
end
|
150
165
|
|
166
|
+
def convert_to_d3_flame_graph_format(name, stacks, depth)
|
167
|
+
weight = 0
|
168
|
+
children = []
|
169
|
+
stacks.chunk do |stack|
|
170
|
+
if depth == stack.length - 1
|
171
|
+
:leaf
|
172
|
+
else
|
173
|
+
stack[depth]
|
174
|
+
end
|
175
|
+
end.each do |val, child_stacks|
|
176
|
+
if val == :leaf
|
177
|
+
child_stacks.each do |stack|
|
178
|
+
weight += stack.last
|
179
|
+
end
|
180
|
+
else
|
181
|
+
frame = frames[val]
|
182
|
+
child_name = "#{ frame[:name] } : #{ frame[:file] }"
|
183
|
+
child_data = convert_to_d3_flame_graph_format(child_name, child_stacks, depth + 1)
|
184
|
+
weight += child_data["value"]
|
185
|
+
children << child_data
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
{
|
190
|
+
"name" => name,
|
191
|
+
"value" => weight,
|
192
|
+
"children" => children,
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
def print_d3_flamegraph(f=STDOUT, skip_common=true)
|
197
|
+
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
198
|
+
|
199
|
+
stacks = []
|
200
|
+
max_x = 0
|
201
|
+
max_y = 0
|
202
|
+
while len = raw.shift
|
203
|
+
max_y = len if len > max_y
|
204
|
+
stack = raw.slice!(0, len+1)
|
205
|
+
stacks << stack
|
206
|
+
max_x += stack.last
|
207
|
+
end
|
208
|
+
|
209
|
+
# d3-flame-grpah supports only alphabetical flamegraph
|
210
|
+
stacks.sort!
|
211
|
+
|
212
|
+
require "json"
|
213
|
+
json = JSON.generate(convert_to_d3_flame_graph_format("<root>", stacks, 0), max_nesting: false)
|
214
|
+
|
215
|
+
# This html code is almost copied from d3-flame-graph sample code.
|
216
|
+
# (Apache License 2.0)
|
217
|
+
# https://github.com/spiermar/d3-flame-graph/blob/gh-pages/index.html
|
218
|
+
|
219
|
+
f.print <<-END
|
220
|
+
<!DOCTYPE html>
|
221
|
+
<html lang="en">
|
222
|
+
<head>
|
223
|
+
<meta charset="utf-8">
|
224
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
225
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
226
|
+
|
227
|
+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
228
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css">
|
229
|
+
|
230
|
+
<style>
|
231
|
+
|
232
|
+
/* Space out content a bit */
|
233
|
+
body {
|
234
|
+
padding-top: 20px;
|
235
|
+
padding-bottom: 20px;
|
236
|
+
}
|
237
|
+
|
238
|
+
/* Custom page header */
|
239
|
+
.header {
|
240
|
+
padding-bottom: 20px;
|
241
|
+
padding-right: 15px;
|
242
|
+
padding-left: 15px;
|
243
|
+
border-bottom: 1px solid #e5e5e5;
|
244
|
+
}
|
245
|
+
|
246
|
+
/* Make the masthead heading the same height as the navigation */
|
247
|
+
.header h3 {
|
248
|
+
margin-top: 0;
|
249
|
+
margin-bottom: 0;
|
250
|
+
line-height: 40px;
|
251
|
+
}
|
252
|
+
|
253
|
+
/* Customize container */
|
254
|
+
.container {
|
255
|
+
max-width: 990px;
|
256
|
+
}
|
257
|
+
|
258
|
+
address {
|
259
|
+
text-align: right;
|
260
|
+
}
|
261
|
+
</style>
|
262
|
+
|
263
|
+
<title>stackprof (mode: #{ data[:mode] })</title>
|
264
|
+
|
265
|
+
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
266
|
+
<!--[if lt IE 9]>
|
267
|
+
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
268
|
+
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
269
|
+
<![endif]-->
|
270
|
+
</head>
|
271
|
+
<body>
|
272
|
+
<div class="container">
|
273
|
+
<div class="header clearfix">
|
274
|
+
<nav>
|
275
|
+
<div class="pull-right">
|
276
|
+
<form class="form-inline" id="form">
|
277
|
+
<a class="btn" href="javascript: resetZoom();">Reset zoom</a>
|
278
|
+
<a class="btn" href="javascript: clear();">Clear</a>
|
279
|
+
<div class="form-group">
|
280
|
+
<input type="text" class="form-control" id="term">
|
281
|
+
</div>
|
282
|
+
<a class="btn btn-primary" href="javascript: search();">Search</a>
|
283
|
+
</form>
|
284
|
+
</div>
|
285
|
+
</nav>
|
286
|
+
<h3 class="text-muted">stackprof (mode: #{ data[:mode] })</h3>
|
287
|
+
</div>
|
288
|
+
<div id="chart">
|
289
|
+
</div>
|
290
|
+
<address>
|
291
|
+
powered by <a href="https://github.com/spiermar/d3-flame-graph">d3-flame-graph</a>
|
292
|
+
</address>
|
293
|
+
<hr>
|
294
|
+
<div id="details">
|
295
|
+
</div>
|
296
|
+
</div>
|
297
|
+
|
298
|
+
<!-- D3.js -->
|
299
|
+
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
|
300
|
+
|
301
|
+
<!-- d3-tip -->
|
302
|
+
<script type="text/javascript" src=https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js></script>
|
303
|
+
|
304
|
+
<!-- d3-flamegraph -->
|
305
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js"></script>
|
306
|
+
|
307
|
+
<script type="text/javascript">
|
308
|
+
var flameGraph = d3.flamegraph()
|
309
|
+
.width(960)
|
310
|
+
.cellHeight(18)
|
311
|
+
.transitionDuration(750)
|
312
|
+
.minFrameSize(5)
|
313
|
+
.transitionEase(d3.easeCubic)
|
314
|
+
.sort(true)
|
315
|
+
//Example to sort in reverse order
|
316
|
+
//.sort(function(a,b){ return d3.descending(a.name, b.name);})
|
317
|
+
.title("")
|
318
|
+
.onClick(onClick)
|
319
|
+
.differential(false)
|
320
|
+
.selfValue(false);
|
321
|
+
|
322
|
+
|
323
|
+
// Example on how to use custom tooltips using d3-tip.
|
324
|
+
// var tip = d3.tip()
|
325
|
+
// .direction("s")
|
326
|
+
// .offset([8, 0])
|
327
|
+
// .attr('class', 'd3-flame-graph-tip')
|
328
|
+
// .html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
|
329
|
+
|
330
|
+
// flameGraph.tooltip(tip);
|
331
|
+
|
332
|
+
var details = document.getElementById("details");
|
333
|
+
flameGraph.setDetailsElement(details);
|
334
|
+
|
335
|
+
// Example on how to use custom labels
|
336
|
+
// var label = function(d) {
|
337
|
+
// return "name: " + d.name + ", value: " + d.value;
|
338
|
+
// }
|
339
|
+
// flameGraph.label(label);
|
340
|
+
|
341
|
+
// Example of how to set fixed chart height
|
342
|
+
// flameGraph.height(540);
|
343
|
+
|
344
|
+
d3.select("#chart")
|
345
|
+
.datum(#{ json })
|
346
|
+
.call(flameGraph);
|
347
|
+
|
348
|
+
document.getElementById("form").addEventListener("submit", function(event){
|
349
|
+
event.preventDefault();
|
350
|
+
search();
|
351
|
+
});
|
352
|
+
|
353
|
+
function search() {
|
354
|
+
var term = document.getElementById("term").value;
|
355
|
+
flameGraph.search(term);
|
356
|
+
}
|
357
|
+
|
358
|
+
function clear() {
|
359
|
+
document.getElementById('term').value = '';
|
360
|
+
flameGraph.clear();
|
361
|
+
}
|
362
|
+
|
363
|
+
function resetZoom() {
|
364
|
+
flameGraph.resetZoom();
|
365
|
+
}
|
366
|
+
|
367
|
+
function onClick(d) {
|
368
|
+
console.info("Clicked on " + d.data.name);
|
369
|
+
}
|
370
|
+
</script>
|
371
|
+
</body>
|
372
|
+
</html>
|
373
|
+
END
|
374
|
+
end
|
375
|
+
|
151
376
|
def print_graphviz(options = {}, f = STDOUT)
|
152
377
|
if filter = options[:filter]
|
153
378
|
mark_stack = []
|
data/stackprof.gemspec
CHANGED
data/test/test_stackprof.rb
CHANGED
@@ -2,6 +2,7 @@ $:.unshift File.expand_path('../../lib', __FILE__)
|
|
2
2
|
require 'stackprof'
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require 'tempfile'
|
5
|
+
require 'pathname'
|
5
6
|
|
6
7
|
class StackProfTest < MiniTest::Test
|
7
8
|
def test_info
|
@@ -167,6 +168,32 @@ class StackProfTest < MiniTest::Test
|
|
167
168
|
refute_empty profile[:frames]
|
168
169
|
end
|
169
170
|
|
171
|
+
def test_out_to_path_string
|
172
|
+
tmpfile = Tempfile.new('stackprof-out')
|
173
|
+
ret = StackProf.run(mode: :custom, out: tmpfile.path) do
|
174
|
+
StackProf.sample
|
175
|
+
end
|
176
|
+
|
177
|
+
refute_equal tmpfile, ret
|
178
|
+
assert_equal tmpfile.path, ret.path
|
179
|
+
tmpfile.rewind
|
180
|
+
profile = Marshal.load(tmpfile.read)
|
181
|
+
refute_empty profile[:frames]
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_pathname_out
|
185
|
+
tmpfile = Tempfile.new('stackprof-out')
|
186
|
+
pathname = Pathname.new(tmpfile.path)
|
187
|
+
ret = StackProf.run(mode: :custom, out: pathname) do
|
188
|
+
StackProf.sample
|
189
|
+
end
|
190
|
+
|
191
|
+
assert_equal tmpfile.path, ret.path
|
192
|
+
tmpfile.rewind
|
193
|
+
profile = Marshal.load(tmpfile.read)
|
194
|
+
refute_empty profile[:frames]
|
195
|
+
end
|
196
|
+
|
170
197
|
def math
|
171
198
|
250_000.times do
|
172
199
|
2 ** 10
|
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.2.
|
4
|
+
version: 0.2.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aman Gupta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-10-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- ".gitignore"
|
67
67
|
- ".travis.yml"
|
68
68
|
- CHANGELOG.md
|
69
|
+
- Dockerfile
|
69
70
|
- Gemfile
|
70
71
|
- Gemfile.lock
|
71
72
|
- LICENSE
|
@@ -109,8 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
110
|
- !ruby/object:Gem::Version
|
110
111
|
version: '0'
|
111
112
|
requirements: []
|
112
|
-
|
113
|
-
rubygems_version: 2.4.6
|
113
|
+
rubygems_version: 3.1.0.pre1
|
114
114
|
signing_key:
|
115
115
|
specification_version: 4
|
116
116
|
summary: sampling callstack-profiler for ruby 2.1+
|