stackprof 0.2.12 → 0.2.13
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 +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
|

|
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+
|