Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture native stack in Pf2c #47

Merged
merged 2 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ext/pf2c/backtrace_state.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <stdio.h>
#include "backtrace_state.h"

struct backtrace_state *global_backtrace_state = NULL;

void
pf2_backtrace_print_error(void *data, const char *msg, int errnum)
{
printf("libbacktrace error callback: %s (errnum %d)\n", msg, errnum);
}
8 changes: 2 additions & 6 deletions ext/pf2c/backtrace_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@

#include <backtrace.h>

struct backtrace_state *global_backtrace_state = NULL;
extern struct backtrace_state *global_backtrace_state;

void
pf2_backtrace_print_error(void *data, const char *msg, int errnum)
{
printf("libbacktrace error callback: %s (errnum %d)\n", msg, errnum);
}
void pf2_backtrace_print_error(void *data, const char *msg, int errnum);

#endif // PF2_BACKTRACE_H
48 changes: 48 additions & 0 deletions ext/pf2c/sample.c
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
#include <stdbool.h>
#include <time.h>

#include <backtrace.h>
#include <ruby.h>
#include <ruby/debug.h>

#include "backtrace_state.h"
#include "sample.h"

const int PF2_SAMPLE_MAX_NATIVE_DEPTH = 300;

static int capture_native_backtrace(struct pf2_sample *sample);
static int backtrace_on_ok(void *data, uintptr_t pc);

// Capture a sample from the current thread.
bool
pf2_sample_capture(struct pf2_sample *sample)
Expand All @@ -18,5 +25,46 @@ pf2_sample_capture(struct pf2_sample *sample)
// Obtain the current stack from Ruby
sample->depth = rb_profile_frames(0, 200, sample->cmes, sample->linenos);

// Capture C-level backtrace
sample->native_stack_depth = capture_native_backtrace(sample);

return true;
}

// Struct to be passed to backtrace_on_ok
struct bt_data {
struct pf2_sample *pf2_sample;
int index;
};

static int
capture_native_backtrace(struct pf2_sample *sample)
{
struct backtrace_state *state = global_backtrace_state;
assert(state != NULL);

struct bt_data data;
data.pf2_sample = sample;
data.index = 0;

// Capture the current PC
// Skip the first 2 frames (capture_native_backtrace, sigprof_handler)
backtrace_simple(state, 2, backtrace_on_ok, pf2_backtrace_print_error, &data);

return data.index;
}

static int
backtrace_on_ok(void *data, uintptr_t pc)
{
struct bt_data *bt_data = (struct bt_data *)data;
struct pf2_sample *sample = bt_data->pf2_sample;

// Store the PC value
if (bt_data->index < PF2_SAMPLE_MAX_NATIVE_DEPTH) {
sample->native_stack[bt_data->index] = pc;
bt_data->index++;
}

return 0; // Continue backtrace
}
6 changes: 6 additions & 0 deletions ext/pf2c/sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@

#include <ruby.h>

extern const int PF2_SAMPLE_MAX_NATIVE_DEPTH;

struct pf2_sample {
int depth;
VALUE cmes[200];
int linenos[200];

size_t native_stack_depth;
uintptr_t native_stack[200];

uint64_t consumed_time_ns;
uint64_t timestamp_ns;
};
Expand Down
109 changes: 94 additions & 15 deletions ext/pf2c/serializer.c
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
#include <ruby.h>
#include <ruby/debug.h>
#include <time.h>
#include <stdint.h>
#include <string.h>

#include <ruby.h>
#include <ruby/debug.h>

#include <backtrace.h>

#include "backtrace_state.h"
#include "serializer.h"
#include "session.h"
#include "sample.h"

static struct pf2_ser_function extract_function_from_ruby_frame(VALUE frame);
static size_t function_index_for(struct pf2_ser *serializer, struct pf2_ser_function *function);
static size_t location_index_for(struct pf2_ser *serializer, size_t function_index, int32_t lineno);
static struct pf2_ser_function extract_function_from_native_pc(uintptr_t pc);
// static int backtrace_pcinfo_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function);
static void pf2_backtrace_syminfo_callback(void *data, uintptr_t pc, const char *symname, uintptr_t symval, uintptr_t symsize);
static int function_index_for(struct pf2_ser *serializer, struct pf2_ser_function *function);
static int location_index_for(struct pf2_ser *serializer, int function_index, int32_t lineno);
static void ensure_samples_capacity(struct pf2_ser *serializer);
static void ensure_locations_capacity(struct pf2_ser *serializer);
static void ensure_functions_capacity(struct pf2_ser *serializer);
Expand Down Expand Up @@ -76,14 +83,12 @@ pf2_ser_prepare(struct pf2_ser *serializer, struct pf2_session *session) {
ensure_samples_capacity(serializer);

struct pf2_ser_sample *ser_sample = &serializer->samples[serializer->samples_count++];
ser_sample->stack = malloc(sizeof(size_t) * sample->depth);
ser_sample->stack_count = sample->depth;
ser_sample->native_stack = NULL; // Not handling native stack in this version
ser_sample->native_stack_count = 0;
ser_sample->ruby_thread_id = 0; // TODO: Add thread ID support
ser_sample->elapsed_ns = sample->timestamp_ns - serializer->start_timestamp_ns;

// Process Ruby stack frames
// Copy and process Ruby stack frames
ser_sample->stack = malloc(sizeof(size_t) * sample->depth);
ser_sample->stack_count = sample->depth;
for (int j = 0; j < sample->depth; j++) {
VALUE frame = sample->cmes[j];
int32_t lineno = sample->linenos[j];
Expand All @@ -94,6 +99,24 @@ pf2_ser_prepare(struct pf2_ser *serializer, struct pf2_session *session) {

ser_sample->stack[j] = location_index;
}

// Copy and process native stack frames, if any
if (sample->native_stack_depth > 0) {
ser_sample->native_stack = malloc(sizeof(size_t) * sample->native_stack_depth);
ser_sample->native_stack_count = sample->native_stack_depth;

for (size_t j = 0; j < sample->native_stack_depth; j++) {
struct pf2_ser_function func = extract_function_from_native_pc(sample->native_stack[j]);
size_t function_index = function_index_for(serializer, &func);
size_t location_index = location_index_for(serializer, function_index, 0);

ser_sample->native_stack[j] = location_index;
}
} else {
ser_sample->native_stack = NULL;
ser_sample->native_stack_count = 0;
}

}
}

Expand All @@ -118,8 +141,14 @@ pf2_ser_to_ruby_hash(struct pf2_ser *serializer) {
}
rb_hash_aset(sample_hash, ID2SYM(rb_intern("stack")), stack);

// Add empty native stack (not handling in this version)
rb_hash_aset(sample_hash, ID2SYM(rb_intern("native_stack")), rb_ary_new());
// Add native stack frames
VALUE native_stack = rb_ary_new_capa(sample->native_stack_count);
if (sample->native_stack != NULL) {
for (size_t j = 0; j < sample->native_stack_count; j++) {
rb_ary_push(native_stack, ULL2NUM(sample->native_stack[j]));
}
}
rb_hash_aset(sample_hash, ID2SYM(rb_intern("native_stack")), native_stack);

// Add thread ID and elapsed time
rb_hash_aset(
Expand Down Expand Up @@ -164,7 +193,7 @@ pf2_ser_to_ruby_hash(struct pf2_ser *serializer) {
rb_hash_aset(
function_hash,
ID2SYM(rb_intern("implementation")),
ID2SYM(rb_intern("ruby"))
function->implementation == IMPLEMENTATION_RUBY ? ID2SYM(rb_intern("ruby")) : ID2SYM(rb_intern("native"))
); // TODO: C functions
rb_hash_aset(
function_hash,
Expand Down Expand Up @@ -223,9 +252,59 @@ extract_function_from_ruby_frame(VALUE frame) {
return func;
}

static struct pf2_ser_function
extract_function_from_native_pc(uintptr_t pc) {
struct pf2_ser_function func;
func.implementation = IMPLEMENTATION_NATIVE;

func.start_address = 0;
func.name = NULL;
func.filename = NULL;
func.start_lineno = 0;

// Use libbacktrace to get function details
struct backtrace_state *state = global_backtrace_state;
assert(state != NULL);
backtrace_syminfo(state, pc, pf2_backtrace_syminfo_callback, pf2_backtrace_print_error, &func);

// TODO: backtrace_pcinfo could give us more information, such as filenames and linenos.
// Maybe try pcinfo first, then use syminfo as a fallback?

return func;
}

// Full callback for syminfo
static void
pf2_backtrace_syminfo_callback(void *data, uintptr_t pc, const char *symname, uintptr_t symval, uintptr_t symsize) {
struct pf2_ser_function *func = (struct pf2_ser_function *)data;

if (symname != NULL) {
func->name = strdup(symname);
}
func->start_address = symval;

return;
}

// static int
// backtrace_pcinfo_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) {
// struct pf2_ser_function *func = (struct pf2_ser_function *)data;

// if (function) {
// func->name = strdup(function);
// }
// func->start_lineno = lineno;
// if (filename) {
// func->filename = strdup(filename);
// }

// // Return non-zero to stop after first match
// return 1;
// }

// Returns the index of the function in `functions`.
// Calling this method will modify `serializer->profile` in place.
static size_t
static int
function_index_for(struct pf2_ser *serializer, struct pf2_ser_function *function) {
for (size_t i = 0; i < serializer->functions_count; i++) {
struct pf2_ser_function *existing = &serializer->functions[i];
Expand Down Expand Up @@ -253,8 +332,8 @@ function_index_for(struct pf2_ser *serializer, struct pf2_ser_function *function

// Returns the index of the location in `locations`.
// Calling this method will modify `self.profile` in place.
static size_t
location_index_for(struct pf2_ser *serializer, size_t function_index, int32_t lineno) {
static int
location_index_for(struct pf2_ser *serializer, int function_index, int32_t lineno) {
for (size_t i = 0; i < serializer->locations_count; i++) {
struct pf2_ser_location *existing = &serializer->locations[i];
if (existing->function_index == function_index && existing->lineno == lineno) {
Expand Down
8 changes: 4 additions & 4 deletions ext/pf2c/serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
#include "session.h"

struct pf2_ser_sample {
size_t *stack;
int *stack;
size_t stack_count;
size_t *native_stack;
int *native_stack;
size_t native_stack_count;
size_t ruby_thread_id;
uint64_t elapsed_ns;
};

struct pf2_ser_location {
size_t function_index;
int function_index;
int32_t lineno;
size_t address;
};

enum function_implementation {
IMPLEMENTATION_RUBY,
// IMPLEMENTATION_NATIVE
IMPLEMENTATION_NATIVE
};

struct pf2_ser_function {
Expand Down
Loading