Skip to content

Commit 1ae20c2

Browse files
committed
feat: add RubyRecorder class
1 parent e6fa804 commit 1ae20c2

File tree

3 files changed

+128
-28
lines changed

3 files changed

+128
-28
lines changed

examples/selective_tracing.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env ruby
2+
ext_base = File.expand_path('../gems/native-tracer/ext/native_tracer/target/release/libcodetracer_ruby_recorder', __dir__)
3+
require ext_base
4+
5+
recorder = RubyRecorder.new
6+
7+
puts 'start trace'
8+
recorder.disable_tracing
9+
puts 'this will not be traced'
10+
recorder.enable_tracing
11+
puts 'this will be traced'
12+
recorder.disable_tracing
13+
puts 'tracing disabled'
14+
recorder.flush_trace(ENV['CODETRACER_RUBY_RECORDER_OUT_DIR'] || Dir.pwd)
Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,97 @@
11
#![allow(clippy::missing_safety_doc)]
22

3-
use std::{ffi::CStr, mem::transmute, os::raw::c_char};
3+
use std::{
4+
ffi::{CStr, CString},
5+
mem::transmute,
6+
os::raw::{c_char, c_int, c_void},
7+
path::{Path, PathBuf},
8+
ptr,
9+
sync::{Mutex},
10+
};
411

512
use rb_sys::{
6-
// frequently used public items
7-
rb_add_event_hook2, rb_event_flag_t,
13+
rb_add_event_hook2, rb_remove_event_hook_with_data, rb_define_class,
14+
rb_define_alloc_func, rb_define_method, rb_obj_alloc,
15+
rb_ivar_set, rb_ivar_get, rb_intern, rb_ull2inum, rb_num2ull,
816
rb_event_hook_flag_t::RUBY_EVENT_HOOK_FLAG_RAW_ARG,
9-
ID, VALUE, RUBY_EVENT_LINE,
10-
11-
// the raw-trace-API symbols live in the generated `bindings` module
12-
bindings::{
13-
rb_trace_arg_t, // struct rb_trace_arg
14-
rb_tracearg_event_flag, // event kind helpers
15-
rb_tracearg_lineno,
16-
rb_tracearg_path,
17-
},
17+
rb_event_flag_t, rb_trace_arg_t,
18+
rb_tracearg_event_flag, rb_tracearg_lineno, rb_tracearg_path,
19+
rb_cObject, VALUE, ID, RUBY_EVENT_LINE,
20+
RSTRING_PTR, RSTRING_LEN,
1821
};
22+
use runtime_tracing::{Tracer, Line};
23+
24+
struct Recorder {
25+
tracer: Mutex<Tracer>,
26+
active: bool,
27+
}
28+
29+
static mut PTR_IVAR: ID = 0;
30+
31+
unsafe fn get_recorder(obj: VALUE) -> *mut Recorder {
32+
let val = rb_ivar_get(obj, PTR_IVAR);
33+
rb_num2ull(val) as *mut Recorder
34+
}
35+
36+
unsafe extern "C" fn ruby_recorder_alloc(klass: VALUE) -> VALUE {
37+
let obj = rb_obj_alloc(klass);
38+
let recorder = Box::new(Recorder { tracer: Mutex::new(Tracer::new("ruby", &vec![])), active: false });
39+
rb_ivar_set(obj, PTR_IVAR, rb_ull2inum(Box::into_raw(recorder) as u64));
40+
obj
41+
}
42+
43+
unsafe extern "C" fn ruby_recorder_initialize(_self: VALUE) -> VALUE {
44+
// nothing special for now
45+
rb_sys::Qnil.into()
46+
}
47+
48+
unsafe extern "C" fn enable_tracing(self_val: VALUE) -> VALUE {
49+
let recorder = &mut *get_recorder(self_val);
50+
if !recorder.active {
51+
let raw_cb: unsafe extern "C" fn(VALUE, *mut rb_trace_arg_t) = event_hook_raw;
52+
let cb: unsafe extern "C" fn(rb_event_flag_t, VALUE, VALUE, ID, VALUE) = transmute(raw_cb);
53+
rb_add_event_hook2(Some(cb), RUBY_EVENT_LINE, self_val, RUBY_EVENT_HOOK_FLAG_RAW_ARG);
54+
recorder.active = true;
55+
}
56+
rb_sys::Qnil.into()
57+
}
58+
59+
unsafe extern "C" fn disable_tracing(self_val: VALUE) -> VALUE {
60+
let recorder = &mut *get_recorder(self_val);
61+
if recorder.active {
62+
let raw_cb: unsafe extern "C" fn(VALUE, *mut rb_trace_arg_t) = event_hook_raw;
63+
let cb: unsafe extern "C" fn(rb_event_flag_t, VALUE, VALUE, ID, VALUE) = transmute(raw_cb);
64+
rb_remove_event_hook_with_data(Some(cb), self_val);
65+
recorder.active = false;
66+
}
67+
rb_sys::Qnil.into()
68+
}
69+
70+
fn flush_to_dir(tracer: &Tracer, dir: &Path) {
71+
let _ = std::fs::create_dir_all(dir);
72+
let events = dir.join("trace.json");
73+
let metadata = dir.join("trace_metadata.json");
74+
let paths = dir.join("trace_paths.json");
75+
let _ = tracer.store_trace_events(&events);
76+
let _ = tracer.store_trace_metadata(&metadata);
77+
let _ = tracer.store_trace_paths(&paths);
78+
}
79+
80+
unsafe extern "C" fn flush_trace(self_val: VALUE, out_dir: VALUE) -> VALUE {
81+
let recorder_ptr = get_recorder(self_val);
82+
let recorder = &mut *recorder_ptr;
83+
let ptr = RSTRING_PTR(out_dir) as *const u8;
84+
let len = RSTRING_LEN(out_dir) as usize;
85+
let slice = std::slice::from_raw_parts(ptr, len);
86+
if let Ok(path_str) = std::str::from_utf8(slice) {
87+
if let Ok(t) = recorder.tracer.lock() {
88+
flush_to_dir(&t, Path::new(path_str));
89+
}
90+
}
91+
drop(Box::from_raw(recorder_ptr));
92+
rb_ivar_set(self_val, PTR_IVAR, rb_ull2inum(0));
93+
rb_sys::Qnil.into()
94+
}
1995

2096
/// Raw-argument callback (Ruby will call it when we set
2197
/// `RUBY_EVENT_HOOK_FLAG_RAW_ARG`).
@@ -24,41 +100,46 @@ use rb_sys::{
24100
/// ```c
25101
/// void (*)(VALUE data, rb_trace_arg_t *arg);
26102
/// ```
27-
unsafe extern "C" fn event_hook_raw(_data: VALUE, arg: *mut rb_trace_arg_t) {
103+
unsafe extern "C" fn event_hook_raw(data: VALUE, arg: *mut rb_trace_arg_t) {
28104
if arg.is_null() {
29105
return;
30106
}
31107

108+
let recorder = &mut *get_recorder(data);
109+
if !recorder.active {
110+
return;
111+
}
112+
32113
let ev: rb_event_flag_t = rb_tracearg_event_flag(arg);
33114
if (ev & RUBY_EVENT_LINE) == 0 {
34115
return;
35116
}
36117

37118
let path_ptr = rb_tracearg_path(arg) as *const c_char;
38-
let line = rb_tracearg_lineno(arg) as u32;
119+
let line = rb_tracearg_lineno(arg) as i64;
39120

40121
if !path_ptr.is_null() {
41122
if let Ok(path) = CStr::from_ptr(path_ptr).to_str() {
42-
println!("Path: {path}, Line: {line}");
123+
if let Ok(mut t) = recorder.tracer.lock() {
124+
t.register_step(Path::new(path), Line(line));
125+
}
43126
}
44127
}
45128
}
46129

47130
#[no_mangle]
48131
pub extern "C" fn Init_codetracer_ruby_recorder() {
49132
unsafe {
50-
// rb_add_event_hook2’s first parameter is a function pointer with the
51-
// classic five-argument signature. We cast our raw callback to that
52-
// type via an intermediate variable so the sizes match.
53-
let raw_cb: unsafe extern "C" fn(VALUE, *mut rb_trace_arg_t) = event_hook_raw;
54-
let cb: unsafe extern "C" fn(rb_event_flag_t, VALUE, VALUE, ID, VALUE) =
55-
transmute(raw_cb);
56-
57-
rb_add_event_hook2(
58-
Some(cb), // callback (now cast)
59-
RUBY_EVENT_LINE, // which events
60-
0, // user data
61-
RUBY_EVENT_HOOK_FLAG_RAW_ARG,
62-
);
133+
PTR_IVAR = rb_intern(b"@ptr\0".as_ptr() as *const c_char);
134+
let class = rb_define_class(b"RubyRecorder\0".as_ptr() as *const c_char, rb_cObject);
135+
rb_define_alloc_func(class, Some(ruby_recorder_alloc));
136+
let init_cb: unsafe extern "C" fn(VALUE) -> VALUE = ruby_recorder_initialize;
137+
let enable_cb: unsafe extern "C" fn(VALUE) -> VALUE = enable_tracing;
138+
let disable_cb: unsafe extern "C" fn(VALUE) -> VALUE = disable_tracing;
139+
let flush_cb: unsafe extern "C" fn(VALUE, VALUE) -> VALUE = flush_trace;
140+
rb_define_method(class, b"initialize\0".as_ptr() as *const c_char, Some(transmute(init_cb)), 0);
141+
rb_define_method(class, b"enable_tracing\0".as_ptr() as *const c_char, Some(transmute(enable_cb)), 0);
142+
rb_define_method(class, b"disable_tracing\0".as_ptr() as *const c_char, Some(transmute(disable_cb)), 0);
143+
rb_define_method(class, b"flush_trace\0".as_ptr() as *const c_char, Some(transmute(flush_cb)), 1);
63144
}
64145
}

gems/native-tracer/lib/native_trace.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
ext_path = File.expand_path('../ext/native_tracer/target/release/libcodetracer_ruby_recorder', __dir__)
3030
require ext_path
3131

32+
recorder = RubyRecorder.new
33+
recorder.enable_tracing
34+
3235
program = ARGV.shift
3336
load program
37+
recorder.disable_tracing
38+
recorder.flush_trace(out_dir)
3439

0 commit comments

Comments
 (0)