stackprof 0.2.16 → 0.2.19
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/.github/workflows/ci.yml +43 -0
- data/README.md +54 -55
- data/Rakefile +11 -25
- data/ext/stackprof/stackprof.c +187 -68
- data/lib/stackprof/report.rb +22 -53
- data/lib/stackprof.rb +1 -1
- data/stackprof.gemspec +1 -1
- data/test/test_stackprof.rb +61 -11
- metadata +10 -11
- data/.travis.yml +0 -21
- data/Dockerfile +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6662250e5e20ee3388bfce740a053c83e53fe9304b518ad22f0ce9ffc5d10077
|
4
|
+
data.tar.gz: 7480e1896e253bd530573fc2f66b3d7690db97bee3f892c77729b75c2dc8c94e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f92e6401b16f83d9398b94e8b0506f597a785a19e1552245f39e1c77ab0c2d3d40186b8b05f91972b6d899995718c28e1724acff97248d497a3768deed601bb
|
7
|
+
data.tar.gz: 0663a1cdac8730bcbc4dd68750fffa6885b72eca7c37915191b6ff44ff101349ba274648b6641531c1bfff34144179b76d527ca8426db91af2c0536629f6ee65
|
@@ -0,0 +1,43 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
rubies:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby: [ ruby-head, '3.0', '2.7', '2.6', '2.5', '2.4', '2.3', '2.2' ]
|
12
|
+
steps:
|
13
|
+
- name: Checkout
|
14
|
+
uses: actions/checkout@v2
|
15
|
+
- name: Set up Ruby
|
16
|
+
uses: ruby/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: ${{ matrix.ruby }}
|
19
|
+
- name: Install dependencies
|
20
|
+
run: bundle install
|
21
|
+
- name: Run test
|
22
|
+
run: rake
|
23
|
+
- name: Install gem
|
24
|
+
run: rake install
|
25
|
+
platforms:
|
26
|
+
strategy:
|
27
|
+
matrix:
|
28
|
+
os: [macos]
|
29
|
+
ruby: ['3.0']
|
30
|
+
runs-on: ${{ matrix.os }}-latest
|
31
|
+
steps:
|
32
|
+
- name: Checkout
|
33
|
+
uses: actions/checkout@v2
|
34
|
+
- name: Set up Ruby
|
35
|
+
uses: ruby/setup-ruby@v1
|
36
|
+
with:
|
37
|
+
ruby-version: ${{ matrix.ruby }}
|
38
|
+
- name: Install dependencies
|
39
|
+
run: bundle install
|
40
|
+
- name: Run test
|
41
|
+
run: rake
|
42
|
+
- name: Install gem
|
43
|
+
run: rake install
|
data/README.md
CHANGED
@@ -81,21 +81,21 @@ $ stackprof tmp/stackprof-cpu-*.dump --method 'Object#present?'
|
|
81
81
|
|
82
82
|
For an experimental version of WebUI reporting of stackprof, see [stackprof-webnav](https://github.com/alisnic/stackprof-webnav)
|
83
83
|
|
84
|
-
|
84
|
+
To generate flamegraphs with Stackprof, additional data must be collected using the `raw: true` flag. Once you've collected results with this flag enabled, generate a flamegraph with:
|
85
85
|
|
86
86
|
```
|
87
87
|
$ stackprof --flamegraph tmp/stackprof-cpu-myapp.dump > tmp/flamegraph
|
88
88
|
```
|
89
89
|
|
90
|
-
|
90
|
+
After the flamegraph has been generated, you can generate a viewer command with:
|
91
91
|
|
92
92
|
```
|
93
93
|
$ stackprof --flamegraph-viewer=tmp/flamegraph
|
94
94
|
```
|
95
95
|
|
96
|
-
The `--flamegraph-viewer` command will output the exact shell command you need to run to open the `tmp/flamegraph` you generated with the built
|
96
|
+
The `--flamegraph-viewer` command will output the exact shell command you need to run in order to open the `tmp/flamegraph` you generated with the built-in stackprof flamegraph viewer:
|
97
97
|
|
98
|
-

|
98
|
+

|
99
99
|
|
100
100
|
Alternatively, you can generate a flamegraph that uses [d3-flame-graph](https://github.com/spiermar/d3-flame-graph):
|
101
101
|
|
@@ -107,16 +107,16 @@ And just open the result by your browser.
|
|
107
107
|
|
108
108
|
## Sampling
|
109
109
|
|
110
|
-
|
110
|
+
Four sampling modes are supported:
|
111
111
|
|
112
|
-
-
|
113
|
-
-
|
114
|
-
-
|
115
|
-
-
|
112
|
+
- `:wall` (using `ITIMER_REAL` and `SIGALRM`) [default mode]
|
113
|
+
- `:cpu` (using `ITIMER_PROF` and `SIGPROF`)
|
114
|
+
- `:object` (using `RUBY_INTERNAL_EVENT_NEWOBJ`)
|
115
|
+
- `:custom` (user-defined via `StackProf.sample`)
|
116
116
|
|
117
|
-
|
117
|
+
Samplers have a tuneable interval which can be used to reduce overhead or increase granularity:
|
118
118
|
|
119
|
-
-
|
119
|
+
- Wall time: sample every _interval_ microseconds of wallclock time (default: 1000)
|
120
120
|
|
121
121
|
```ruby
|
122
122
|
StackProf.run(mode: :wall, out: 'tmp/stackprof.dump', interval: 1000) do
|
@@ -124,7 +124,7 @@ StackProf.run(mode: :wall, out: 'tmp/stackprof.dump', interval: 1000) do
|
|
124
124
|
end
|
125
125
|
```
|
126
126
|
|
127
|
-
-
|
127
|
+
- CPU time: sample every _interval_ microseconds of CPU activity (default: 1000 = 1 millisecond)
|
128
128
|
|
129
129
|
```ruby
|
130
130
|
StackProf.run(mode: :cpu, out: 'tmp/stackprof.dump', interval: 1000) do
|
@@ -132,7 +132,7 @@ StackProf.run(mode: :cpu, out: 'tmp/stackprof.dump', interval: 1000) do
|
|
132
132
|
end
|
133
133
|
```
|
134
134
|
|
135
|
-
-
|
135
|
+
- Object allocation: sample every _interval_ allocations (default: 1)
|
136
136
|
|
137
137
|
|
138
138
|
```ruby
|
@@ -141,36 +141,36 @@ StackProf.run(mode: :object, out: 'tmp/stackprof.dump', interval: 1) do
|
|
141
141
|
end
|
142
142
|
```
|
143
143
|
|
144
|
-
|
145
|
-
including both mark and sweep phases.
|
146
|
-
that are hard to follow
|
147
|
-
|
144
|
+
By default, samples taken during garbage collection will show as garbage collection frames
|
145
|
+
including both mark and sweep phases. For longer traces, these can leave gaps in a flamegraph
|
146
|
+
that are hard to follow. They can be disabled by setting the `ignore_gc` option to true.
|
147
|
+
Garbage collection time will still be present in the profile but not explicitly marked with
|
148
148
|
its own frame.
|
149
149
|
|
150
|
-
|
150
|
+
Samples are taken using a combination of three new C-APIs in ruby 2.1:
|
151
151
|
|
152
|
-
-
|
152
|
+
- Signal handlers enqueue a sampling job using `rb_postponed_job_register_one`.
|
153
153
|
this ensures callstack samples can be taken safely, in case the VM is garbage collecting
|
154
154
|
or in some other inconsistent state during the interruption.
|
155
155
|
|
156
|
-
-
|
157
|
-
to the VM's call stack.
|
156
|
+
- Stack frames are collected via `rb_profile_frames`, which provides low-overhead C-API access
|
157
|
+
to the VM's call stack. No object allocations occur in this path, allowing stackprof to collect
|
158
158
|
callstacks in allocation mode.
|
159
159
|
|
160
|
-
-
|
160
|
+
- In allocation mode, samples are taken via `rb_tracepoint_new(RUBY_INTERNAL_EVENT_NEWOBJ)`,
|
161
161
|
which provides a notification every time the VM allocates a new object.
|
162
162
|
|
163
163
|
## Aggregation
|
164
164
|
|
165
|
-
|
166
|
-
|
165
|
+
Each sample consists of N stack frames, where a frame looks something like `MyClass#method` or `block in MySingleton.method`.
|
166
|
+
For each of these frames in the sample, the profiler collects a few pieces of metadata:
|
167
167
|
|
168
|
-
- samples
|
169
|
-
- total_samples
|
170
|
-
- lines
|
171
|
-
- edges
|
168
|
+
- `samples`: Number of samples where this was the topmost frame
|
169
|
+
- `total_samples`: Samples where this frame was in the stack
|
170
|
+
- `lines`: Samples per line number in this frame
|
171
|
+
- `edges`: Samples per callee frame (methods invoked by this frame)
|
172
172
|
|
173
|
-
|
173
|
+
The aggregation algorithm is roughly equivalent to the following pseudo code:
|
174
174
|
|
175
175
|
``` ruby
|
176
176
|
trap('PROF') do
|
@@ -189,16 +189,16 @@ trap('PROF') do
|
|
189
189
|
end
|
190
190
|
```
|
191
191
|
|
192
|
-
|
192
|
+
This technique builds up an incremental call graph from the samples. On any given frame,
|
193
193
|
the sum of the outbound edge weights is equal to total samples collected on that frame
|
194
194
|
(`frame.total_samples == frame.edges.values.sum`).
|
195
195
|
|
196
196
|
## Reporting
|
197
197
|
|
198
|
-
|
199
|
-
-
|
200
|
-
-
|
201
|
-
-
|
198
|
+
Multiple reporting modes are supported:
|
199
|
+
- Text
|
200
|
+
- Dotgraph
|
201
|
+
- Source annotation
|
202
202
|
|
203
203
|
### `StackProf::Report.new(data).print_text`
|
204
204
|
|
@@ -217,8 +217,6 @@ multiple reporting modes are supported:
|
|
217
217
|
|
218
218
|
### `StackProf::Report.new(data).print_graphviz`
|
219
219
|
|
220
|
-

|
221
|
-
|
222
220
|
```
|
223
221
|
digraph profile {
|
224
222
|
70346498324780 [size=23.5531914893617] [fontsize=23.5531914893617] [shape=box] [label="A#pow\n91 (48.4%)\r"];
|
@@ -265,8 +263,8 @@ block in A#math (/Users/tmm1/code/stackprof/sample.rb:21)
|
|
265
263
|
|
266
264
|
## Usage
|
267
265
|
|
268
|
-
|
269
|
-
|
266
|
+
The profiler is compiled as a C-extension and exposes a simple api: `StackProf.run(mode: [:cpu|:wall|:object])`.
|
267
|
+
The `run` method takes a block of code and returns a profile as a simple hash.
|
270
268
|
|
271
269
|
``` ruby
|
272
270
|
# sample after every 1ms of cpu activity
|
@@ -275,12 +273,12 @@ profile = StackProf.run(mode: :cpu, interval: 1000) do
|
|
275
273
|
end
|
276
274
|
```
|
277
275
|
|
278
|
-
|
279
|
-
(as json/marshal for example) for later processing.
|
276
|
+
This profile data structure is part of the public API, and is intended to be saved
|
277
|
+
(as json/marshal for example) for later processing. The reports above can be generated
|
280
278
|
by passing this structure into `StackProf::Report.new`.
|
281
279
|
|
282
|
-
|
283
|
-
identifying information such as its name, file and line.
|
280
|
+
The format itself is very simple. It contains a header and a list of frames. Each frame has a unique ID and
|
281
|
+
identifying information such as its name, file, and line. The frame also contains sampling data, including per-line
|
284
282
|
samples, and a list of relationships to other frames represented as weighted edges.
|
285
283
|
|
286
284
|
``` ruby
|
@@ -307,20 +305,21 @@ samples, and a list of relationships to other frames represented as weighted edg
|
|
307
305
|
:lines=>{8=>1}},
|
308
306
|
```
|
309
307
|
|
310
|
-
|
308
|
+
Above, `A#pow` was involved in 91 samples, and in all cases it was at the top of the stack on line 12.
|
311
309
|
|
312
|
-
`A#initialize` was in 185 samples, but it was at the top of the stack in only 1 sample.
|
313
|
-
divided up between its callee edges.
|
310
|
+
`A#initialize` was in 185 samples, but it was at the top of the stack in only 1 sample. The rest of the samples are
|
311
|
+
divided up between its callee edges. All 91 calls to `A#pow` came from `A#initialize`, as seen by the edge numbered
|
314
312
|
`70346498324780`.
|
315
313
|
|
316
314
|
## Advanced usage
|
317
315
|
|
318
|
-
|
319
|
-
multiple start
|
316
|
+
The profiler can be started and stopped manually. Results are accumulated until retrieval, across
|
317
|
+
multiple `start`/`stop` invocations.
|
320
318
|
|
321
319
|
``` ruby
|
322
|
-
StackProf.running?
|
320
|
+
StackProf.running? # => false
|
323
321
|
StackProf.start(mode: :cpu)
|
322
|
+
StackProf.running? # => true
|
324
323
|
StackProf.stop
|
325
324
|
StackProf.results('/tmp/some.file')
|
326
325
|
```
|
@@ -331,14 +330,14 @@ StackProf.results('/tmp/some.file')
|
|
331
330
|
|
332
331
|
Option | Meaning
|
333
332
|
------- | ---------
|
334
|
-
`mode` |
|
335
|
-
`out` |
|
336
|
-
`interval` |
|
333
|
+
`mode` | Mode of sampling: `:cpu`, `:wall`, `:object`, or `:custom` [c.f.](#sampling)
|
334
|
+
`out` | The target file, which will be overwritten
|
335
|
+
`interval` | Mode-relative sample rate [c.f.](#sampling)
|
337
336
|
`ignore_gc` | Ignore garbage collection frames
|
338
|
-
`aggregate` |
|
339
|
-
`raw` |
|
340
|
-
`metadata` |
|
341
|
-
`save_every`| (
|
337
|
+
`aggregate` | Defaults: `true` - if `false` disables [aggregation](#aggregation)
|
338
|
+
`raw` | Defaults `false` - if `true` collects the extra data required by the `--flamegraph` and `--stackcollapse` report types
|
339
|
+
`metadata` | Defaults to `{}`. Must be a `Hash`. metadata associated with this profile
|
340
|
+
`save_every`| (Rack middleware only) write the target file after this many requests
|
342
341
|
|
343
342
|
## Todo
|
344
343
|
|
data/Rakefile
CHANGED
@@ -1,31 +1,17 @@
|
|
1
|
-
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
GEMSPEC = Gem::Specification::load('stackprof.gemspec')
|
8
|
-
|
9
|
-
require 'rubygems/package_task'
|
10
|
-
Gem::PackageTask.new(GEMSPEC) do |pkg|
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
11
8
|
end
|
12
9
|
|
13
|
-
|
14
|
-
# Ruby Extension
|
15
|
-
# ==========================================================
|
10
|
+
require "rake/extensiontask"
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
ext.lib_dir =
|
12
|
+
Rake::ExtensionTask.new("stackprof") do |ext|
|
13
|
+
ext.ext_dir = "ext/stackprof"
|
14
|
+
ext.lib_dir = "lib/stackprof"
|
20
15
|
end
|
21
|
-
task :build => :compile
|
22
16
|
|
23
|
-
|
24
|
-
# Testing
|
25
|
-
# ==========================================================
|
26
|
-
|
27
|
-
require 'rake/testtask'
|
28
|
-
Rake::TestTask.new 'test' do |t|
|
29
|
-
t.test_files = FileList['test/test_*.rb']
|
30
|
-
end
|
31
|
-
task :test => :build
|
17
|
+
task default: %i(compile test)
|
data/ext/stackprof/stackprof.c
CHANGED
@@ -7,21 +7,32 @@
|
|
7
7
|
**********************************************************************/
|
8
8
|
|
9
9
|
#include <ruby/ruby.h>
|
10
|
+
#include <ruby/version.h>
|
10
11
|
#include <ruby/debug.h>
|
11
12
|
#include <ruby/st.h>
|
12
13
|
#include <ruby/io.h>
|
13
14
|
#include <ruby/intern.h>
|
14
15
|
#include <signal.h>
|
15
16
|
#include <sys/time.h>
|
17
|
+
#include <time.h>
|
16
18
|
#include <pthread.h>
|
17
19
|
|
18
20
|
#define BUF_SIZE 2048
|
19
21
|
#define MICROSECONDS_IN_SECOND 1000000
|
22
|
+
#define NANOSECONDS_IN_SECOND 1000000000
|
20
23
|
|
21
24
|
#define FAKE_FRAME_GC INT2FIX(0)
|
22
25
|
#define FAKE_FRAME_MARK INT2FIX(1)
|
23
26
|
#define FAKE_FRAME_SWEEP INT2FIX(2)
|
24
27
|
|
28
|
+
/*
|
29
|
+
* As of Ruby 3.0, it should be safe to read stack frames at any time
|
30
|
+
* See https://github.com/ruby/ruby/commit/0e276dc458f94d9d79a0f7c7669bde84abe80f21
|
31
|
+
*/
|
32
|
+
#if RUBY_API_VERSION_MAJOR < 3
|
33
|
+
#define USE_POSTPONED_JOB
|
34
|
+
#endif
|
35
|
+
|
25
36
|
static const char *fake_frame_cstrs[] = {
|
26
37
|
"(garbage collection)",
|
27
38
|
"(marking)",
|
@@ -30,6 +41,47 @@ static const char *fake_frame_cstrs[] = {
|
|
30
41
|
|
31
42
|
#define TOTAL_FAKE_FRAMES (sizeof(fake_frame_cstrs) / sizeof(char *))
|
32
43
|
|
44
|
+
#ifdef _POSIX_MONOTONIC_CLOCK
|
45
|
+
#define timestamp_t timespec
|
46
|
+
typedef struct timestamp_t timestamp_t;
|
47
|
+
|
48
|
+
static void capture_timestamp(timestamp_t *ts) {
|
49
|
+
clock_gettime(CLOCK_MONOTONIC, ts);
|
50
|
+
}
|
51
|
+
|
52
|
+
static int64_t delta_usec(timestamp_t *start, timestamp_t *end) {
|
53
|
+
int64_t result = MICROSECONDS_IN_SECOND * (end->tv_sec - start->tv_sec);
|
54
|
+
if (end->tv_nsec < start->tv_nsec) {
|
55
|
+
result -= MICROSECONDS_IN_SECOND;
|
56
|
+
result += (NANOSECONDS_IN_SECOND + end->tv_nsec - start->tv_nsec) / 1000;
|
57
|
+
} else {
|
58
|
+
result += (end->tv_nsec - start->tv_nsec) / 1000;
|
59
|
+
}
|
60
|
+
return result;
|
61
|
+
}
|
62
|
+
|
63
|
+
static uint64_t timestamp_usec(timestamp_t *ts) {
|
64
|
+
return (MICROSECONDS_IN_SECOND * ts->tv_sec) + (ts->tv_nsec / 1000);
|
65
|
+
}
|
66
|
+
#else
|
67
|
+
#define timestamp_t timeval
|
68
|
+
typedef struct timestamp_t timestamp_t;
|
69
|
+
|
70
|
+
static void capture_timestamp(timestamp_t *ts) {
|
71
|
+
gettimeofday(ts, NULL);
|
72
|
+
}
|
73
|
+
|
74
|
+
static int64_t delta_usec(timestamp_t *start, timestamp_t *end) {
|
75
|
+
struct timeval diff;
|
76
|
+
timersub(end, start, &diff);
|
77
|
+
return (MICROSECONDS_IN_SECOND * diff.tv_sec) + diff.tv_usec;
|
78
|
+
}
|
79
|
+
|
80
|
+
static uint64_t timestamp_usec(timestamp_t *ts) {
|
81
|
+
return (MICROSECONDS_IN_SECOND * ts.tv_sec) + diff.tv_usec
|
82
|
+
}
|
83
|
+
#endif
|
84
|
+
|
33
85
|
typedef struct {
|
34
86
|
size_t total_samples;
|
35
87
|
size_t caller_samples;
|
@@ -38,6 +90,11 @@ typedef struct {
|
|
38
90
|
st_table *lines;
|
39
91
|
} frame_data_t;
|
40
92
|
|
93
|
+
typedef struct {
|
94
|
+
uint64_t timestamp_usec;
|
95
|
+
int64_t delta_usec;
|
96
|
+
} sample_time_t;
|
97
|
+
|
41
98
|
static struct {
|
42
99
|
int running;
|
43
100
|
int raw;
|
@@ -54,10 +111,10 @@ static struct {
|
|
54
111
|
size_t raw_samples_capa;
|
55
112
|
size_t raw_sample_index;
|
56
113
|
|
57
|
-
struct
|
58
|
-
|
59
|
-
size_t
|
60
|
-
size_t
|
114
|
+
struct timestamp_t last_sample_at;
|
115
|
+
sample_time_t *raw_sample_times;
|
116
|
+
size_t raw_sample_times_len;
|
117
|
+
size_t raw_sample_times_capa;
|
61
118
|
|
62
119
|
size_t overall_signals;
|
63
120
|
size_t overall_samples;
|
@@ -69,6 +126,9 @@ static struct {
|
|
69
126
|
|
70
127
|
VALUE fake_frame_names[TOTAL_FAKE_FRAMES];
|
71
128
|
VALUE empty_string;
|
129
|
+
|
130
|
+
int buffer_count;
|
131
|
+
sample_time_t buffer_time;
|
72
132
|
VALUE frames_buffer[BUF_SIZE];
|
73
133
|
int lines_buffer[BUF_SIZE];
|
74
134
|
} _stackprof;
|
@@ -76,7 +136,7 @@ static struct {
|
|
76
136
|
static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
|
77
137
|
static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
|
78
138
|
static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_ignore_gc, sym_out;
|
79
|
-
static VALUE sym_aggregate, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
|
139
|
+
static VALUE sym_aggregate, sym_raw_sample_timestamps, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
|
80
140
|
static VALUE sym_gc_samples, objtracer;
|
81
141
|
static VALUE gc_hook;
|
82
142
|
static VALUE rb_mStackProf;
|
@@ -166,7 +226,7 @@ stackprof_start(int argc, VALUE *argv, VALUE self)
|
|
166
226
|
_stackprof.out = out;
|
167
227
|
|
168
228
|
if (raw) {
|
169
|
-
|
229
|
+
capture_timestamp(&_stackprof.last_sample_at);
|
170
230
|
}
|
171
231
|
|
172
232
|
return Qtrue;
|
@@ -201,13 +261,19 @@ stackprof_stop(VALUE self)
|
|
201
261
|
return Qtrue;
|
202
262
|
}
|
203
263
|
|
264
|
+
#if SIZEOF_VOIDP == SIZEOF_LONG
|
265
|
+
# define PTR2NUM(x) (LONG2NUM((long)(x)))
|
266
|
+
#else
|
267
|
+
# define PTR2NUM(x) (LL2NUM((LONG_LONG)(x)))
|
268
|
+
#endif
|
269
|
+
|
204
270
|
static int
|
205
271
|
frame_edges_i(st_data_t key, st_data_t val, st_data_t arg)
|
206
272
|
{
|
207
273
|
VALUE edges = (VALUE)arg;
|
208
274
|
|
209
275
|
intptr_t weight = (intptr_t)val;
|
210
|
-
rb_hash_aset(edges,
|
276
|
+
rb_hash_aset(edges, PTR2NUM(key), INT2FIX(weight));
|
211
277
|
return ST_CONTINUE;
|
212
278
|
}
|
213
279
|
|
@@ -234,7 +300,7 @@ frame_i(st_data_t key, st_data_t val, st_data_t arg)
|
|
234
300
|
VALUE name, file, edges, lines;
|
235
301
|
VALUE line;
|
236
302
|
|
237
|
-
rb_hash_aset(results,
|
303
|
+
rb_hash_aset(results, PTR2NUM(frame), details);
|
238
304
|
|
239
305
|
if (FIXNUM_P(frame)) {
|
240
306
|
name = _stackprof.fake_frame_names[FIX2INT(frame)];
|
@@ -306,7 +372,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
306
372
|
|
307
373
|
if (_stackprof.raw && _stackprof.raw_samples_len) {
|
308
374
|
size_t len, n, o;
|
309
|
-
VALUE raw_timestamp_deltas;
|
375
|
+
VALUE raw_sample_timestamps, raw_timestamp_deltas;
|
310
376
|
VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len);
|
311
377
|
|
312
378
|
for (n = 0; n < _stackprof.raw_samples_len; n++) {
|
@@ -314,7 +380,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
314
380
|
rb_ary_push(raw_samples, SIZET2NUM(len));
|
315
381
|
|
316
382
|
for (o = 0, n++; o < len; n++, o++)
|
317
|
-
rb_ary_push(raw_samples,
|
383
|
+
rb_ary_push(raw_samples, PTR2NUM(_stackprof.raw_samples[n]));
|
318
384
|
rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n]));
|
319
385
|
}
|
320
386
|
|
@@ -326,17 +392,20 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
|
|
326
392
|
|
327
393
|
rb_hash_aset(results, sym_raw, raw_samples);
|
328
394
|
|
329
|
-
|
395
|
+
raw_sample_timestamps = rb_ary_new_capa(_stackprof.raw_sample_times_len);
|
396
|
+
raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_sample_times_len);
|
330
397
|
|
331
|
-
for (n = 0; n < _stackprof.
|
332
|
-
rb_ary_push(
|
398
|
+
for (n = 0; n < _stackprof.raw_sample_times_len; n++) {
|
399
|
+
rb_ary_push(raw_sample_timestamps, ULL2NUM(_stackprof.raw_sample_times[n].timestamp_usec));
|
400
|
+
rb_ary_push(raw_timestamp_deltas, LL2NUM(_stackprof.raw_sample_times[n].delta_usec));
|
333
401
|
}
|
334
402
|
|
335
|
-
free(_stackprof.
|
336
|
-
_stackprof.
|
337
|
-
_stackprof.
|
338
|
-
_stackprof.
|
403
|
+
free(_stackprof.raw_sample_times);
|
404
|
+
_stackprof.raw_sample_times = NULL;
|
405
|
+
_stackprof.raw_sample_times_len = 0;
|
406
|
+
_stackprof.raw_sample_times_capa = 0;
|
339
407
|
|
408
|
+
rb_hash_aset(results, sym_raw_sample_timestamps, raw_sample_timestamps);
|
340
409
|
rb_hash_aset(results, sym_raw_timestamp_deltas, raw_timestamp_deltas);
|
341
410
|
|
342
411
|
_stackprof.raw = 0;
|
@@ -416,14 +485,14 @@ st_numtable_increment(st_table *table, st_data_t key, size_t increment)
|
|
416
485
|
}
|
417
486
|
|
418
487
|
void
|
419
|
-
stackprof_record_sample_for_stack(int num,
|
488
|
+
stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t timestamp_delta)
|
420
489
|
{
|
421
490
|
int i, n;
|
422
491
|
VALUE prev_frame = Qnil;
|
423
492
|
|
424
493
|
_stackprof.overall_samples++;
|
425
494
|
|
426
|
-
if (_stackprof.raw) {
|
495
|
+
if (_stackprof.raw && num > 0) {
|
427
496
|
int found = 0;
|
428
497
|
|
429
498
|
/* If there's no sample buffer allocated, then allocate one. The buffer
|
@@ -475,20 +544,23 @@ stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
|
475
544
|
}
|
476
545
|
|
477
546
|
/* If there's no timestamp delta buffer, allocate one */
|
478
|
-
if (!_stackprof.
|
479
|
-
_stackprof.
|
480
|
-
_stackprof.
|
481
|
-
_stackprof.
|
547
|
+
if (!_stackprof.raw_sample_times) {
|
548
|
+
_stackprof.raw_sample_times_capa = 100;
|
549
|
+
_stackprof.raw_sample_times = malloc(sizeof(sample_time_t) * _stackprof.raw_sample_times_capa);
|
550
|
+
_stackprof.raw_sample_times_len = 0;
|
482
551
|
}
|
483
552
|
|
484
553
|
/* Double the buffer size if it's too small */
|
485
|
-
while (_stackprof.
|
486
|
-
_stackprof.
|
487
|
-
_stackprof.
|
554
|
+
while (_stackprof.raw_sample_times_capa <= _stackprof.raw_sample_times_len + 1) {
|
555
|
+
_stackprof.raw_sample_times_capa *= 2;
|
556
|
+
_stackprof.raw_sample_times = realloc(_stackprof.raw_sample_times, sizeof(sample_time_t) * _stackprof.raw_sample_times_capa);
|
488
557
|
}
|
489
558
|
|
490
|
-
/* Store the time delta (which is the amount of
|
491
|
-
_stackprof.
|
559
|
+
/* Store the time delta (which is the amount of microseconds between samples). */
|
560
|
+
_stackprof.raw_sample_times[_stackprof.raw_sample_times_len++] = (sample_time_t) {
|
561
|
+
.timestamp_usec = sample_timestamp,
|
562
|
+
.delta_usec = timestamp_delta,
|
563
|
+
};
|
492
564
|
}
|
493
565
|
|
494
566
|
for (i = 0; i < num; i++) {
|
@@ -521,48 +593,59 @@ stackprof_record_sample_for_stack(int num, int timestamp_delta)
|
|
521
593
|
}
|
522
594
|
|
523
595
|
if (_stackprof.raw) {
|
524
|
-
|
596
|
+
capture_timestamp(&_stackprof.last_sample_at);
|
525
597
|
}
|
526
598
|
}
|
527
599
|
|
600
|
+
// buffer the current profile frames
|
601
|
+
// This must be async-signal-safe
|
602
|
+
// Returns immediately if another set of frames are already in the buffer
|
528
603
|
void
|
529
|
-
|
604
|
+
stackprof_buffer_sample(void)
|
530
605
|
{
|
531
|
-
|
606
|
+
if (_stackprof.buffer_count > 0) {
|
607
|
+
// Another sample is already pending
|
608
|
+
return;
|
609
|
+
}
|
610
|
+
|
611
|
+
uint64_t start_timestamp = 0;
|
612
|
+
int64_t timestamp_delta = 0;
|
532
613
|
int num;
|
533
614
|
if (_stackprof.raw) {
|
534
|
-
struct
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
timestamp_delta = (1000 * diff.tv_sec) + diff.tv_usec;
|
615
|
+
struct timestamp_t t;
|
616
|
+
capture_timestamp(&t);
|
617
|
+
start_timestamp = timestamp_usec(&t);
|
618
|
+
timestamp_delta = delta_usec(&_stackprof.last_sample_at, &t);
|
539
619
|
}
|
620
|
+
|
540
621
|
num = rb_profile_frames(0, sizeof(_stackprof.frames_buffer) / sizeof(VALUE), _stackprof.frames_buffer, _stackprof.lines_buffer);
|
541
|
-
|
622
|
+
|
623
|
+
_stackprof.buffer_count = num;
|
624
|
+
_stackprof.buffer_time.timestamp_usec = start_timestamp;
|
625
|
+
_stackprof.buffer_time.delta_usec = timestamp_delta;
|
542
626
|
}
|
543
627
|
|
544
628
|
void
|
545
|
-
stackprof_record_gc_samples()
|
629
|
+
stackprof_record_gc_samples(void)
|
546
630
|
{
|
547
|
-
|
548
|
-
|
631
|
+
int64_t delta_to_first_unrecorded_gc_sample = 0;
|
632
|
+
uint64_t start_timestamp = 0;
|
633
|
+
size_t i;
|
549
634
|
if (_stackprof.raw) {
|
550
|
-
struct
|
551
|
-
|
552
|
-
|
553
|
-
timersub(&t, &_stackprof.last_sample_at, &diff);
|
635
|
+
struct timestamp_t t;
|
636
|
+
capture_timestamp(&t);
|
637
|
+
start_timestamp = timestamp_usec(&t);
|
554
638
|
|
555
639
|
// We don't know when the GC samples were actually marked, so let's
|
556
640
|
// assume that they were marked at a perfectly regular interval.
|
557
|
-
delta_to_first_unrecorded_gc_sample = (
|
641
|
+
delta_to_first_unrecorded_gc_sample = delta_usec(&_stackprof.last_sample_at, &t) - (_stackprof.unrecorded_gc_samples - 1) * NUM2LONG(_stackprof.interval);
|
558
642
|
if (delta_to_first_unrecorded_gc_sample < 0) {
|
559
643
|
delta_to_first_unrecorded_gc_sample = 0;
|
560
644
|
}
|
561
645
|
}
|
562
646
|
|
563
|
-
|
564
647
|
for (i = 0; i < _stackprof.unrecorded_gc_samples; i++) {
|
565
|
-
|
648
|
+
int64_t timestamp_delta = i == 0 ? delta_to_first_unrecorded_gc_sample : NUM2LONG(_stackprof.interval);
|
566
649
|
|
567
650
|
if (_stackprof.unrecorded_gc_marking_samples) {
|
568
651
|
_stackprof.frames_buffer[0] = FAKE_FRAME_MARK;
|
@@ -571,7 +654,7 @@ stackprof_record_gc_samples()
|
|
571
654
|
_stackprof.lines_buffer[1] = 0;
|
572
655
|
_stackprof.unrecorded_gc_marking_samples--;
|
573
656
|
|
574
|
-
stackprof_record_sample_for_stack(2, timestamp_delta);
|
657
|
+
stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta);
|
575
658
|
} else if (_stackprof.unrecorded_gc_sweeping_samples) {
|
576
659
|
_stackprof.frames_buffer[0] = FAKE_FRAME_SWEEP;
|
577
660
|
_stackprof.lines_buffer[0] = 0;
|
@@ -580,11 +663,11 @@ stackprof_record_gc_samples()
|
|
580
663
|
|
581
664
|
_stackprof.unrecorded_gc_sweeping_samples--;
|
582
665
|
|
583
|
-
stackprof_record_sample_for_stack(2, timestamp_delta);
|
666
|
+
stackprof_record_sample_for_stack(2, start_timestamp, timestamp_delta);
|
584
667
|
} else {
|
585
668
|
_stackprof.frames_buffer[0] = FAKE_FRAME_GC;
|
586
669
|
_stackprof.lines_buffer[0] = 0;
|
587
|
-
stackprof_record_sample_for_stack(1, timestamp_delta);
|
670
|
+
stackprof_record_sample_for_stack(1, start_timestamp, timestamp_delta);
|
588
671
|
}
|
589
672
|
}
|
590
673
|
_stackprof.during_gc += _stackprof.unrecorded_gc_samples;
|
@@ -593,34 +676,60 @@ stackprof_record_gc_samples()
|
|
593
676
|
_stackprof.unrecorded_gc_sweeping_samples = 0;
|
594
677
|
}
|
595
678
|
|
679
|
+
// record the sample previously buffered by stackprof_buffer_sample
|
596
680
|
static void
|
597
|
-
|
681
|
+
stackprof_record_buffer(void)
|
682
|
+
{
|
683
|
+
stackprof_record_sample_for_stack(_stackprof.buffer_count, _stackprof.buffer_time.timestamp_usec, _stackprof.buffer_time.delta_usec);
|
684
|
+
|
685
|
+
// reset the buffer
|
686
|
+
_stackprof.buffer_count = 0;
|
687
|
+
}
|
688
|
+
|
689
|
+
static void
|
690
|
+
stackprof_sample_and_record(void)
|
691
|
+
{
|
692
|
+
stackprof_buffer_sample();
|
693
|
+
stackprof_record_buffer();
|
694
|
+
}
|
695
|
+
|
696
|
+
static void
|
697
|
+
stackprof_job_record_gc(void *data)
|
598
698
|
{
|
599
|
-
static int in_signal_handler = 0;
|
600
|
-
if (in_signal_handler) return;
|
601
699
|
if (!_stackprof.running) return;
|
602
700
|
|
603
|
-
in_signal_handler++;
|
604
701
|
stackprof_record_gc_samples();
|
605
|
-
in_signal_handler--;
|
606
702
|
}
|
607
703
|
|
704
|
+
#ifdef USE_POSTPONED_JOB
|
705
|
+
static void
|
706
|
+
stackprof_job_sample_and_record(void *data)
|
707
|
+
{
|
708
|
+
if (!_stackprof.running) return;
|
709
|
+
|
710
|
+
stackprof_sample_and_record();
|
711
|
+
}
|
712
|
+
#endif
|
713
|
+
|
608
714
|
static void
|
609
|
-
|
715
|
+
stackprof_job_record_buffer(void *data)
|
610
716
|
{
|
611
|
-
static int in_signal_handler = 0;
|
612
|
-
if (in_signal_handler) return;
|
613
717
|
if (!_stackprof.running) return;
|
614
718
|
|
615
|
-
|
616
|
-
stackprof_record_sample();
|
617
|
-
in_signal_handler--;
|
719
|
+
stackprof_record_buffer();
|
618
720
|
}
|
619
721
|
|
620
722
|
static void
|
621
723
|
stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
622
724
|
{
|
725
|
+
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
726
|
+
|
623
727
|
_stackprof.overall_signals++;
|
728
|
+
|
729
|
+
if (!_stackprof.running) return;
|
730
|
+
if (!ruby_native_thread_p()) return;
|
731
|
+
if (pthread_mutex_trylock(&lock)) return;
|
732
|
+
|
624
733
|
if (!_stackprof.ignore_gc && rb_during_gc()) {
|
625
734
|
VALUE mode = rb_gc_latest_gc_info(sym_state);
|
626
735
|
if (mode == sym_marking) {
|
@@ -629,10 +738,19 @@ stackprof_signal_handler(int sig, siginfo_t *sinfo, void *ucontext)
|
|
629
738
|
_stackprof.unrecorded_gc_sweeping_samples++;
|
630
739
|
}
|
631
740
|
_stackprof.unrecorded_gc_samples++;
|
632
|
-
rb_postponed_job_register_one(0,
|
741
|
+
rb_postponed_job_register_one(0, stackprof_job_record_gc, (void*)0);
|
633
742
|
} else {
|
634
|
-
|
743
|
+
#ifdef USE_POSTPONED_JOB
|
744
|
+
rb_postponed_job_register_one(0, stackprof_job_sample_and_record, (void*)0);
|
745
|
+
#else
|
746
|
+
// Buffer a sample immediately, if an existing sample exists this will
|
747
|
+
// return immediately
|
748
|
+
stackprof_buffer_sample();
|
749
|
+
// Enqueue a job to record the sample
|
750
|
+
rb_postponed_job_register_one(0, stackprof_job_record_buffer, (void*)0);
|
751
|
+
#endif
|
635
752
|
}
|
753
|
+
pthread_mutex_unlock(&lock);
|
636
754
|
}
|
637
755
|
|
638
756
|
static void
|
@@ -641,7 +759,7 @@ stackprof_newobj_handler(VALUE tpval, void *data)
|
|
641
759
|
_stackprof.overall_signals++;
|
642
760
|
if (RTEST(_stackprof.interval) && _stackprof.overall_signals % NUM2LONG(_stackprof.interval))
|
643
761
|
return;
|
644
|
-
|
762
|
+
stackprof_sample_and_record();
|
645
763
|
}
|
646
764
|
|
647
765
|
static VALUE
|
@@ -651,7 +769,7 @@ stackprof_sample(VALUE self)
|
|
651
769
|
return Qfalse;
|
652
770
|
|
653
771
|
_stackprof.overall_signals++;
|
654
|
-
|
772
|
+
stackprof_sample_and_record();
|
655
773
|
return Qtrue;
|
656
774
|
}
|
657
775
|
|
@@ -730,6 +848,7 @@ Init_stackprof(void)
|
|
730
848
|
S(mode);
|
731
849
|
S(interval);
|
732
850
|
S(raw);
|
851
|
+
S(raw_sample_timestamps);
|
733
852
|
S(raw_timestamp_deltas);
|
734
853
|
S(out);
|
735
854
|
S(metadata);
|
@@ -752,9 +871,9 @@ Init_stackprof(void)
|
|
752
871
|
_stackprof.raw_samples_capa = 0;
|
753
872
|
_stackprof.raw_sample_index = 0;
|
754
873
|
|
755
|
-
_stackprof.
|
756
|
-
_stackprof.
|
757
|
-
_stackprof.
|
874
|
+
_stackprof.raw_sample_times = NULL;
|
875
|
+
_stackprof.raw_sample_times_len = 0;
|
876
|
+
_stackprof.raw_sample_times_capa = 0;
|
758
877
|
|
759
878
|
_stackprof.empty_string = rb_str_new_cstr("");
|
760
879
|
rb_global_variable(&_stackprof.empty_string);
|
data/lib/stackprof/report.rb
CHANGED
@@ -95,51 +95,10 @@ module StackProf
|
|
95
95
|
print_flamegraph(f, skip_common, true)
|
96
96
|
end
|
97
97
|
|
98
|
-
StackCursor = Struct.new(:raw, :idx, :length) do
|
99
|
-
def weight
|
100
|
-
@weight ||= raw[1 + idx + length]
|
101
|
-
end
|
102
|
-
|
103
|
-
def [](i)
|
104
|
-
if i >= length
|
105
|
-
nil
|
106
|
-
else
|
107
|
-
raw[1 + idx + i]
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def <=>(other)
|
112
|
-
i = 0
|
113
|
-
while i < length && i < other.length
|
114
|
-
if self[i] != other[i]
|
115
|
-
return self[i] <=> other[i]
|
116
|
-
end
|
117
|
-
i += 1
|
118
|
-
end
|
119
|
-
|
120
|
-
return length <=> other.length
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
98
|
def print_flamegraph(f, skip_common, alphabetical=false)
|
125
99
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
126
100
|
|
127
|
-
stacks =
|
128
|
-
max_x = 0
|
129
|
-
max_y = 0
|
130
|
-
|
131
|
-
idx = 0
|
132
|
-
loop do
|
133
|
-
len = raw[idx]
|
134
|
-
break unless len
|
135
|
-
max_y = len if len > max_y
|
136
|
-
|
137
|
-
stack = StackCursor.new(raw, idx, len)
|
138
|
-
stacks << stack
|
139
|
-
max_x += stack.weight
|
140
|
-
|
141
|
-
idx += len + 2
|
142
|
-
end
|
101
|
+
stacks, max_x, max_y = flamegraph_stacks(raw)
|
143
102
|
|
144
103
|
stacks.sort! if alphabetical
|
145
104
|
|
@@ -150,7 +109,7 @@ module StackProf
|
|
150
109
|
x = 0
|
151
110
|
|
152
111
|
stacks.each do |stack|
|
153
|
-
weight = stack.
|
112
|
+
weight = stack.last
|
154
113
|
cell = stack[y] unless y == stack.length-1
|
155
114
|
|
156
115
|
if cell.nil?
|
@@ -191,6 +150,24 @@ module StackProf
|
|
191
150
|
f.puts '])'
|
192
151
|
end
|
193
152
|
|
153
|
+
def flamegraph_stacks(raw)
|
154
|
+
stacks = []
|
155
|
+
max_x = 0
|
156
|
+
max_y = 0
|
157
|
+
idx = 0
|
158
|
+
|
159
|
+
while len = raw[idx]
|
160
|
+
idx += 1
|
161
|
+
max_y = len if len > max_y
|
162
|
+
stack = raw.slice(idx, len+1)
|
163
|
+
idx += len+1
|
164
|
+
stacks << stack
|
165
|
+
max_x += stack.last
|
166
|
+
end
|
167
|
+
|
168
|
+
return stacks, max_x, max_y
|
169
|
+
end
|
170
|
+
|
194
171
|
def flamegraph_row(f, x, y, weight, addr)
|
195
172
|
frame = @data[:frames][addr]
|
196
173
|
f.print ',' if @rows_started
|
@@ -231,15 +208,7 @@ module StackProf
|
|
231
208
|
def print_d3_flamegraph(f=STDOUT, skip_common=true)
|
232
209
|
raise "profile does not include raw samples (add `raw: true` to collecting StackProf.run)" unless raw = data[:raw]
|
233
210
|
|
234
|
-
stacks =
|
235
|
-
max_x = 0
|
236
|
-
max_y = 0
|
237
|
-
while len = raw.shift
|
238
|
-
max_y = len if len > max_y
|
239
|
-
stack = raw.slice!(0, len+1)
|
240
|
-
stacks << stack
|
241
|
-
max_x += stack.last
|
242
|
-
end
|
211
|
+
stacks, * = flamegraph_stacks(raw)
|
243
212
|
|
244
213
|
# d3-flame-grpah supports only alphabetical flamegraph
|
245
214
|
stacks.sort!
|
@@ -445,7 +414,7 @@ module StackProf
|
|
445
414
|
call, total = info.values_at(:samples, :total_samples)
|
446
415
|
break if total < node_minimum || (limit && index >= limit)
|
447
416
|
|
448
|
-
sample = ''
|
417
|
+
sample = ''.dup
|
449
418
|
sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total
|
450
419
|
sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples)
|
451
420
|
fontsize = (1.0 * call / max_samples) * 28 + 10
|
data/lib/stackprof.rb
CHANGED
data/stackprof.gemspec
CHANGED
data/test/test_stackprof.rb
CHANGED
@@ -39,16 +39,30 @@ class StackProfTest < MiniTest::Test
|
|
39
39
|
end
|
40
40
|
assert_equal :object, profile[:mode]
|
41
41
|
assert_equal 1, profile[:interval]
|
42
|
-
|
42
|
+
if RUBY_VERSION >= '3'
|
43
|
+
assert_equal 4, profile[:samples]
|
44
|
+
else
|
45
|
+
assert_equal 2, profile[:samples]
|
46
|
+
end
|
43
47
|
|
44
48
|
frame = profile[:frames].values.first
|
45
49
|
assert_includes frame[:name], "StackProfTest#test_object_allocation"
|
46
50
|
assert_equal 2, frame[:samples]
|
47
51
|
assert_includes [profile_base_line - 2, profile_base_line], frame[:line]
|
48
|
-
|
49
|
-
|
52
|
+
if RUBY_VERSION >= '3'
|
53
|
+
assert_equal [2, 1], frame[:lines][profile_base_line+1]
|
54
|
+
assert_equal [2, 1], frame[:lines][profile_base_line+2]
|
55
|
+
else
|
56
|
+
assert_equal [1, 1], frame[:lines][profile_base_line+1]
|
57
|
+
assert_equal [1, 1], frame[:lines][profile_base_line+2]
|
58
|
+
end
|
50
59
|
frame = profile[:frames].values[1] if RUBY_VERSION < '2.3'
|
51
|
-
|
60
|
+
|
61
|
+
if RUBY_VERSION >= '3'
|
62
|
+
assert_equal [4, 0], frame[:lines][profile_base_line]
|
63
|
+
else
|
64
|
+
assert_equal [2, 0], frame[:lines][profile_base_line]
|
65
|
+
end
|
52
66
|
end
|
53
67
|
|
54
68
|
def test_object_allocation_interval
|
@@ -64,8 +78,14 @@ class StackProfTest < MiniTest::Test
|
|
64
78
|
end
|
65
79
|
|
66
80
|
assert_operator profile[:samples], :>=, 1
|
67
|
-
|
68
|
-
|
81
|
+
if RUBY_VERSION >= '3'
|
82
|
+
assert profile[:frames].values.take(2).map { |f|
|
83
|
+
f[:name].include? "StackProfTest#math"
|
84
|
+
}.any?
|
85
|
+
else
|
86
|
+
frame = profile[:frames].values.first
|
87
|
+
assert_includes frame[:name], "StackProfTest#math"
|
88
|
+
end
|
69
89
|
end
|
70
90
|
|
71
91
|
def test_walltime
|
@@ -74,7 +94,11 @@ class StackProfTest < MiniTest::Test
|
|
74
94
|
end
|
75
95
|
|
76
96
|
frame = profile[:frames].values.first
|
77
|
-
|
97
|
+
if RUBY_VERSION >= '3'
|
98
|
+
assert_equal "IO.select", frame[:name]
|
99
|
+
else
|
100
|
+
assert_equal "StackProfTest#idle", frame[:name]
|
101
|
+
end
|
78
102
|
assert_in_delta 200, frame[:samples], 25
|
79
103
|
end
|
80
104
|
|
@@ -89,24 +113,51 @@ class StackProfTest < MiniTest::Test
|
|
89
113
|
assert_equal :custom, profile[:mode]
|
90
114
|
assert_equal 10, profile[:samples]
|
91
115
|
|
92
|
-
|
116
|
+
offset = RUBY_VERSION >= '3' ? 1 : 0
|
117
|
+
frame = profile[:frames].values[offset]
|
93
118
|
assert_includes frame[:name], "StackProfTest#test_custom"
|
94
119
|
assert_includes [profile_base_line-2, profile_base_line+1], frame[:line]
|
95
|
-
|
120
|
+
|
121
|
+
if RUBY_VERSION >= '3'
|
122
|
+
assert_equal [10, 0], frame[:lines][profile_base_line+2]
|
123
|
+
else
|
124
|
+
assert_equal [10, 10], frame[:lines][profile_base_line+2]
|
125
|
+
end
|
96
126
|
end
|
97
127
|
|
98
128
|
def test_raw
|
129
|
+
before_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
130
|
+
|
99
131
|
profile = StackProf.run(mode: :custom, raw: true) do
|
100
132
|
10.times do
|
101
133
|
StackProf.sample
|
134
|
+
sleep 0.0001
|
102
135
|
end
|
103
136
|
end
|
104
137
|
|
138
|
+
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
139
|
+
|
105
140
|
raw = profile[:raw]
|
106
141
|
assert_equal 10, raw[-1]
|
107
142
|
assert_equal raw[0] + 2, raw.size
|
108
|
-
|
143
|
+
|
144
|
+
offset = RUBY_VERSION >= '3' ? -3 : -2
|
145
|
+
assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw'
|
146
|
+
|
147
|
+
assert_equal 10, profile[:raw_sample_timestamps].size
|
148
|
+
profile[:raw_sample_timestamps].each_cons(2) do |t1, t2|
|
149
|
+
assert_operator t1, :>, before_monotonic
|
150
|
+
assert_operator t2, :>=, t1
|
151
|
+
assert_operator t2, :<, after_monotonic
|
152
|
+
end
|
153
|
+
|
109
154
|
assert_equal 10, profile[:raw_timestamp_deltas].size
|
155
|
+
total_duration = after_monotonic - before_monotonic
|
156
|
+
assert_operator profile[:raw_timestamp_deltas].inject(&:+), :<, total_duration
|
157
|
+
|
158
|
+
profile[:raw_timestamp_deltas].each do |delta|
|
159
|
+
assert_operator delta, :>, 0
|
160
|
+
end
|
110
161
|
end
|
111
162
|
|
112
163
|
def test_metadata
|
@@ -178,7 +229,6 @@ class StackProfTest < MiniTest::Test
|
|
178
229
|
end
|
179
230
|
end
|
180
231
|
|
181
|
-
raw = profile[:raw]
|
182
232
|
gc_frame = profile[:frames].values.find{ |f| f[:name] == "(garbage collection)" }
|
183
233
|
marking_frame = profile[:frames].values.find{ |f| f[:name] == "(marking)" }
|
184
234
|
sweeping_frame = profile[:frames].values.find{ |f| f[:name] == "(sweeping)" }
|
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.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aman Gupta
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -63,10 +63,9 @@ extensions:
|
|
63
63
|
- ext/stackprof/extconf.rb
|
64
64
|
extra_rdoc_files: []
|
65
65
|
files:
|
66
|
+
- ".github/workflows/ci.yml"
|
66
67
|
- ".gitignore"
|
67
|
-
- ".travis.yml"
|
68
68
|
- CHANGELOG.md
|
69
|
-
- Dockerfile
|
70
69
|
- Gemfile
|
71
70
|
- LICENSE
|
72
71
|
- README.md
|
@@ -95,10 +94,10 @@ licenses:
|
|
95
94
|
- MIT
|
96
95
|
metadata:
|
97
96
|
bug_tracker_uri: https://github.com/tmm1/stackprof/issues
|
98
|
-
changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.
|
99
|
-
documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.
|
100
|
-
source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.
|
101
|
-
post_install_message:
|
97
|
+
changelog_uri: https://github.com/tmm1/stackprof/blob/v0.2.19/CHANGELOG.md
|
98
|
+
documentation_uri: https://www.rubydoc.info/gems/stackprof/0.2.19
|
99
|
+
source_code_uri: https://github.com/tmm1/stackprof/tree/v0.2.19
|
100
|
+
post_install_message:
|
102
101
|
rdoc_options: []
|
103
102
|
require_paths:
|
104
103
|
- lib
|
@@ -113,8 +112,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
112
|
- !ruby/object:Gem::Version
|
114
113
|
version: '0'
|
115
114
|
requirements: []
|
116
|
-
rubygems_version: 3.
|
117
|
-
signing_key:
|
115
|
+
rubygems_version: 3.0.3.1
|
116
|
+
signing_key:
|
118
117
|
specification_version: 4
|
119
118
|
summary: sampling callstack-profiler for ruby 2.2+
|
120
119
|
test_files: []
|
data/.travis.yml
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
sudo: required
|
2
|
-
|
3
|
-
services:
|
4
|
-
- docker
|
5
|
-
|
6
|
-
language: general
|
7
|
-
|
8
|
-
env:
|
9
|
-
matrix:
|
10
|
-
- RVM_RUBY_VERSION=2.2
|
11
|
-
- RVM_RUBY_VERSION=2.3
|
12
|
-
- RVM_RUBY_VERSION=2.4
|
13
|
-
- RVM_RUBY_VERSION=2.5
|
14
|
-
- RVM_RUBY_VERSION=2.6
|
15
|
-
- RVM_RUBY_VERSION=ruby-head
|
16
|
-
|
17
|
-
before_install:
|
18
|
-
- sudo docker build -t stackprof-$RVM_RUBY_VERSION --build-arg=RVM_RUBY_VERSION=$RVM_RUBY_VERSION .
|
19
|
-
|
20
|
-
script:
|
21
|
-
- sudo docker run --name stackprof-$RVM_RUBY_VERSION stackprof-$RVM_RUBY_VERSION
|
data/Dockerfile
DELETED
@@ -1,21 +0,0 @@
|
|
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"
|