Skip to content

Commit

Permalink
Add raw line numbers for raw mode
Browse files Browse the repository at this point in the history
This commit records line numbers in raw mode along with the frame
information.

Before this commit, stackprof would lose information about callee frames
at a particular line.  For example, you could not answer "given a frame
and line, what function do we call in to at that line?"  This commit
encodes the line information along with the raw frame information so
that we can answer that question.

This is probably TMI, but when we ask the Ruby frame profiler, it
returns a list of memory addresses (CMEs or possibly iseqs?).  AArch64
systems guarantee the top 16 bits won't be used, x86 doesn't use them
either (but possibly could in the future, but probably not). Armed with
this information, we just put the line number in those top 16 bits and
we don't need to allocate any extra memory when doing a profile.

For backwards compatibility, this patch splits apart the information in
the `.result` method so existing tools should just work.
  • Loading branch information
tenderlove committed Jul 8, 2023
1 parent ef8b134 commit af60e41
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 6 deletions.
33 changes: 27 additions & 6 deletions ext/stackprof/stackprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ static struct {
VALUE metadata;
int ignore_gc;

VALUE *raw_samples;
uint64_t *raw_samples;
size_t raw_samples_len;
size_t raw_samples_capa;
size_t raw_sample_index;
Expand Down Expand Up @@ -133,7 +133,7 @@ static struct {

static VALUE sym_object, sym_wall, sym_cpu, sym_custom, sym_name, sym_file, sym_line;
static VALUE sym_samples, sym_total_samples, sym_missed_samples, sym_edges, sym_lines;
static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_metadata, sym_frames, sym_ignore_gc, sym_out;
static VALUE sym_version, sym_mode, sym_interval, sym_raw, sym_raw_lines, sym_metadata, sym_frames, sym_ignore_gc, sym_out;
static VALUE sym_aggregate, sym_raw_sample_timestamps, sym_raw_timestamp_deltas, sym_state, sym_marking, sym_sweeping;
static VALUE sym_gc_samples, objtracer;
static VALUE gc_hook;
Expand Down Expand Up @@ -374,14 +374,23 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
size_t len, n, o;
VALUE raw_sample_timestamps, raw_timestamp_deltas;
VALUE raw_samples = rb_ary_new_capa(_stackprof.raw_samples_len);
VALUE raw_lines = rb_ary_new_capa(_stackprof.raw_samples_len);

for (n = 0; n < _stackprof.raw_samples_len; n++) {
len = (size_t)_stackprof.raw_samples[n];
rb_ary_push(raw_samples, SIZET2NUM(len));
rb_ary_push(raw_lines, SIZET2NUM(len));

for (o = 0, n++; o < len; n++, o++) {
// Line is in the upper 16 bits
rb_ary_push(raw_lines, INT2NUM(_stackprof.raw_samples[n] >> 48));

VALUE frame = _stackprof.raw_samples[n] & ~((uint64_t)0xFFFF << 48);
rb_ary_push(raw_samples, PTR2NUM(frame));
}

for (o = 0, n++; o < len; n++, o++)
rb_ary_push(raw_samples, PTR2NUM(_stackprof.raw_samples[n]));
rb_ary_push(raw_samples, SIZET2NUM((size_t)_stackprof.raw_samples[n]));
rb_ary_push(raw_lines, SIZET2NUM((size_t)_stackprof.raw_samples[n]));
}

free(_stackprof.raw_samples);
Expand All @@ -391,6 +400,7 @@ stackprof_results(int argc, VALUE *argv, VALUE self)
_stackprof.raw_sample_index = 0;

rb_hash_aset(results, sym_raw, raw_samples);
rb_hash_aset(results, sym_raw_lines, raw_lines);

raw_sample_timestamps = rb_ary_new_capa(_stackprof.raw_sample_times_len);
raw_timestamp_deltas = rb_ary_new_capa(_stackprof.raw_sample_times_len);
Expand Down Expand Up @@ -520,7 +530,12 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti
* in the frames buffer that came from Ruby. */
for (i = num-1, n = 0; i >= 0; i--, n++) {
VALUE frame = _stackprof.frames_buffer[i];
if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != frame)
int line = _stackprof.lines_buffer[i];

// Encode the line in to the upper 16 bits.
uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame;

if (_stackprof.raw_samples[_stackprof.raw_sample_index + 1 + n] != key)
break;
}
if (i == -1) {
Expand All @@ -538,7 +553,12 @@ stackprof_record_sample_for_stack(int num, uint64_t sample_timestamp, int64_t ti
_stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)num;
for (i = num-1; i >= 0; i--) {
VALUE frame = _stackprof.frames_buffer[i];
_stackprof.raw_samples[_stackprof.raw_samples_len++] = frame;
int line = _stackprof.lines_buffer[i];

// Encode the line in to the upper 16 bits.
uint64_t key = ((uint64_t)line << 48) | (uint64_t)frame;

_stackprof.raw_samples[_stackprof.raw_samples_len++] = key;
}
_stackprof.raw_samples[_stackprof.raw_samples_len++] = (VALUE)1;
}
Expand Down Expand Up @@ -894,6 +914,7 @@ Init_stackprof(void)
S(mode);
S(interval);
S(raw);
S(raw_lines);
S(raw_sample_timestamps);
S(raw_timestamp_deltas);
S(out);
Expand Down
4 changes: 4 additions & 0 deletions test/test_stackprof.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,12 @@ def test_raw
after_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)

raw = profile[:raw]
raw_lines = profile[:raw_lines]
assert_equal 10, raw[-1]
assert_equal raw[0] + 2, raw.size
assert_equal 10, raw_lines[-1] # seen 10 times
assert_equal 0, raw_lines[-2] # StackProf.sample is a cfunc so the line number is 0
assert_equal 140, raw_lines[-3] # sample caller is on 140

offset = RUBY_VERSION >= '3' ? -3 : -2
assert_includes profile[:frames][raw[offset]][:name], 'StackProfTest#test_raw'
Expand Down

0 comments on commit af60e41

Please sign in to comment.