diff --git a/README.md b/README.md index d24c4fc..b0fbd0c 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ After installing, load the tracer: require 'codetracer_ruby_recorder' # native implementation # require 'codetracer_pure_ruby_recorder' # pure Ruby implementation -recorder = RubyRecorder.new +recorder = RubyRecorder.new(Dir.pwd) recorder.enable_tracing # ... your code ... -recorder.flush_trace(Dir.pwd) +recorder.flush_trace ``` ### Usage diff --git a/flake.lock b/flake.lock index b0a51d6..15a9d58 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,42 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1750969886, @@ -16,9 +53,46 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1730768919, + "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1750779888, + "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" } } }, diff --git a/gems/codetracer-pure-ruby-recorder/lib/codetracer_pure_ruby_recorder.rb b/gems/codetracer-pure-ruby-recorder/lib/codetracer_pure_ruby_recorder.rb index e2276f3..19b1783 100644 --- a/gems/codetracer-pure-ruby-recorder/lib/codetracer_pure_ruby_recorder.rb +++ b/gems/codetracer-pure-ruby-recorder/lib/codetracer_pure_ruby_recorder.rb @@ -42,7 +42,7 @@ def self.parse_argv_and_trace_ruby_file(argv) end def self.trace_ruby_file(program, out_dir, program_args = []) - tracer = PureRubyRecorder.new(debug: ENV['CODETRACER_RUBY_RECORDER_DEBUG'] == '1') + tracer = PureRubyRecorder.new(out_dir, debug: ENV['CODETRACER_RUBY_RECORDER_DEBUG'] == '1') tracer.record.register_call('', 1, '', []) tracer.ignore('lib/ruby') @@ -81,10 +81,11 @@ def self.trace_ruby_file(program, out_dir, program_args = []) 0 end - def initialize(debug: false) + def initialize(out_dir, debug: false) @tracing = false @record = TraceRecord.new @ignore_list = [] + @out_dir = out_dir @debug = debug @record.debug = debug if @record.respond_to?(:debug=) setup_tracepoints @@ -243,8 +244,8 @@ def trace_block(&block) end # Flush trace to output directory - compatible with native recorder API - def flush_trace(out_dir) - @record.serialize('', out_dir) + def flush_trace + @record.serialize('', @out_dir) end private diff --git a/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.lock b/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.lock index b894ea8..3c72bb3 100644 --- a/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.lock +++ b/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.lock @@ -67,6 +67,26 @@ dependencies = [ "capnp", ] +[[package]] +name = "cbor4ii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189a2a2e5eec2f203b2bb8bc4c2db55c7253770d2c6bf3ae5f79ace5a15c305f" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -114,6 +134,27 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "fscommon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531" +dependencies = [ + "log", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "glob" version = "0.3.2" @@ -135,6 +176,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -163,6 +214,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.7.5" @@ -205,6 +262,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -223,6 +286,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rb-sys" version = "0.9.116" @@ -284,18 +353,21 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "runtime_tracing" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e209b012ae288d447ee17341d57c798c21b7eeaee2613a361bc0d71515eb38" +checksum = "bb39bbb7e2fe3f83c9020a2f871e7affd293e1ef5cc2f1c137012d9931611db6" dependencies = [ "base64", "capnp", "capnpc", + "cbor4ii", + "fscommon", "num-derive", "num-traits", "serde", "serde_json", "serde_repr", + "zeekstd", ] [[package]] @@ -382,6 +454,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "windows-targets" version = "0.53.2" @@ -445,3 +526,40 @@ name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zeekstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ee50810d0fa308a167f17f08243378b36f0d9fd5e43e95c4943a1e51b7b8a5" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml b/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml index 81cfb97..551098c 100644 --- a/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml +++ b/gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] rb-sys = "0.9" -runtime_tracing = "0.12.1" +runtime_tracing = "0.14.0" [build-dependencies] rb-sys-env = "0.2" diff --git a/gems/codetracer-ruby-recorder/ext/native_tracer/src/lib.rs b/gems/codetracer-ruby-recorder/ext/native_tracer/src/lib.rs index 8343a4a..65f3b98 100644 --- a/gems/codetracer-ruby-recorder/ext/native_tracer/src/lib.rs +++ b/gems/codetracer-ruby-recorder/ext/native_tracer/src/lib.rs @@ -21,8 +21,9 @@ use rb_sys::{ }; use rb_sys::{Qfalse, Qnil, Qtrue}; use runtime_tracing::{ - CallRecord, EventLogKind, FieldTypeRecord, FullValueRecord, Line, RecordEvent, ReturnRecord, - TraceLowLevelEvent, Tracer, TypeKind, TypeRecord, TypeSpecificInfo, ValueRecord, + create_trace_writer, CallRecord, EventLogKind, FieldTypeRecord, FullValueRecord, Line, + RecordEvent, ReturnRecord, TraceEventsFileFormat, TraceLowLevelEvent, TraceWriter, TypeId, + TypeKind, TypeRecord, TypeSpecificInfo, ValueRecord, }; // Event hook function type from Ruby debug.h @@ -42,7 +43,7 @@ use rb_sys::{ }; struct Recorder { - tracer: Tracer, + tracer: Box, active: bool, to_s_id: ID, locals_id: ID, @@ -142,7 +143,7 @@ unsafe fn struct_value( fields: field_types, }, }; - let type_id = recorder.tracer.ensure_raw_type_id(typ); + let type_id = TraceWriter::ensure_raw_type_id(&mut *recorder.tracer, typ); ValueRecord::Struct { field_values: vals, @@ -183,20 +184,6 @@ unsafe fn get_recorder(obj: VALUE) -> *mut Recorder { } unsafe extern "C" fn ruby_recorder_alloc(klass: VALUE) -> VALUE { - let mut tracer = Tracer::new("ruby", &vec![]); - // pre-register common types to match the pure Ruby tracer - let int_type_id = tracer.ensure_type_id(TypeKind::Int, "Integer"); - let string_type_id = tracer.ensure_type_id(TypeKind::String, "String"); - let bool_type_id = tracer.ensure_type_id(TypeKind::Bool, "Bool"); - let float_type_id = runtime_tracing::NONE_TYPE_ID; - let symbol_type_id = tracer.ensure_type_id(TypeKind::String, "Symbol"); - let error_type_id = tracer.ensure_type_id(TypeKind::Error, "No type"); - let path = Path::new(""); - let func_id = tracer.ensure_function_id("", path, Line(1)); - tracer.events.push(TraceLowLevelEvent::Call(CallRecord { - function_id: func_id, - args: vec![], - })); let to_s_id = rb_intern(b"to_s\0".as_ptr() as *const c_char); let locals_id = rb_intern(b"local_variables\0".as_ptr() as *const c_char); let local_get_id = rb_intern(b"local_variable_get\0".as_ptr() as *const c_char); @@ -218,7 +205,7 @@ unsafe extern "C" fn ruby_recorder_alloc(klass: VALUE) -> VALUE { let set_const_id = rb_intern(b"Set\0".as_ptr() as *const c_char); let open_struct_const_id = rb_intern(b"OpenStruct\0".as_ptr() as *const c_char); let recorder = Box::new(Recorder { - tracer, + tracer: create_trace_writer("ruby", &vec![], TraceEventsFileFormat::Binary), active: false, to_s_id, locals_id, @@ -243,12 +230,12 @@ unsafe extern "C" fn ruby_recorder_alloc(klass: VALUE) -> VALUE { set_class: Qnil.into(), open_struct_class: Qnil.into(), struct_type_versions: HashMap::new(), - int_type_id, - float_type_id, - bool_type_id, - string_type_id, - symbol_type_id, - error_type_id, + int_type_id: runtime_tracing::TypeId::default(), + float_type_id: runtime_tracing::TypeId::default(), + bool_type_id: runtime_tracing::TypeId::default(), + string_type_id: runtime_tracing::TypeId::default(), + symbol_type_id: runtime_tracing::TypeId::default(), + error_type_id: runtime_tracing::TypeId::default(), }); let ty = std::ptr::addr_of!(RECORDER_TYPE) as *const rb_data_type_t; rb_data_typed_object_wrap(klass, Box::into_raw(recorder) as *mut c_void, ty) @@ -281,21 +268,31 @@ unsafe extern "C" fn disable_tracing(self_val: VALUE) -> VALUE { Qnil.into() } -fn flush_to_dir( - tracer: &Tracer, +fn begin_trace( dir: &Path, format: runtime_tracing::TraceEventsFileFormat, -) -> Result<(), Box> { +) -> Result, Box> { + let mut tracer = create_trace_writer("ruby", &vec![], format); std::fs::create_dir_all(dir)?; let events = match format { runtime_tracing::TraceEventsFileFormat::Json => dir.join("trace.json"), - runtime_tracing::TraceEventsFileFormat::Binary => dir.join("trace.bin"), + runtime_tracing::TraceEventsFileFormat::BinaryV0 + | runtime_tracing::TraceEventsFileFormat::Binary => dir.join("trace.bin"), }; let metadata = dir.join("trace_metadata.json"); let paths = dir.join("trace_paths.json"); - tracer.store_trace_events(&events, format)?; - tracer.store_trace_metadata(&metadata)?; - tracer.store_trace_paths(&paths)?; + + TraceWriter::begin_writing_trace_events(&mut *tracer, &events)?; + TraceWriter::begin_writing_trace_metadata(&mut *tracer, &metadata)?; + TraceWriter::begin_writing_trace_paths(&mut *tracer, &paths)?; + + Ok(tracer) +} + +fn flush_to_dir(tracer: &mut dyn TraceWriter) -> Result<(), Box> { + TraceWriter::finish_writing_trace_events(tracer)?; + TraceWriter::finish_writing_trace_metadata(tracer)?; + TraceWriter::finish_writing_trace_paths(tracer)?; Ok(()) } @@ -371,7 +368,7 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I if RB_FLOAT_TYPE_P(val) { let f = rb_num2dbl(val); let type_id = if recorder.float_type_id == runtime_tracing::NONE_TYPE_ID { - let id = recorder.tracer.ensure_type_id(TypeKind::Float, "Float"); + let id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Float, "Float"); recorder.float_type_id = id; id } else { @@ -404,7 +401,7 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I let elem = *ptr.add(i); elements.push(to_value(recorder, elem, depth - 1, to_s_id)); } - let type_id = recorder.tracer.ensure_type_id(TypeKind::Seq, "Array"); + let type_id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Seq, "Array"); return ValueRecord::Sequence { elements, is_slice: false, @@ -433,7 +430,7 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I to_s_id, )); } - let type_id = recorder.tracer.ensure_type_id(TypeKind::Seq, "Hash"); + let type_id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Seq, "Hash"); return ValueRecord::Sequence { elements, is_slice: false, @@ -467,7 +464,7 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I let elem = *ptr.add(i); elements.push(to_value(recorder, elem, depth - 1, to_s_id)); } - let type_id = recorder.tracer.ensure_type_id(TypeKind::Seq, "Set"); + let type_id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Seq, "Set"); return ValueRecord::Sequence { elements, is_slice: false, @@ -508,7 +505,8 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I || !RB_TYPE_P(values, rb_sys::ruby_value_type::RUBY_T_ARRAY) { let text = value_to_string(val, to_s_id).unwrap_or_default(); - let type_id = recorder.tracer.ensure_type_id(TypeKind::Raw, &class_name); + let type_id = + TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Raw, &class_name); return ValueRecord::Raw { r: text, type_id }; } let len = RARRAY_LEN(values) as usize; @@ -531,7 +529,8 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I recorder.open_struct_class = rb_const_get(rb_cObject, recorder.open_struct_const_id); } } - if !NIL_P(recorder.open_struct_class) && rb_obj_is_kind_of(val, recorder.open_struct_class) != 0 { + if !NIL_P(recorder.open_struct_class) && rb_obj_is_kind_of(val, recorder.open_struct_class) != 0 + { let h = rb_funcall(val, recorder.to_h_id, 0); return to_value(recorder, h, depth - 1, to_s_id); } @@ -540,7 +539,8 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I let ivars = rb_funcall(val, recorder.instance_variables_id, 0); if !RB_TYPE_P(ivars, rb_sys::ruby_value_type::RUBY_T_ARRAY) { let text = value_to_string(val, to_s_id).unwrap_or_default(); - let type_id = recorder.tracer.ensure_type_id(TypeKind::Raw, &class_name); + let type_id = + TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Raw, &class_name); return ValueRecord::Raw { r: text, type_id }; } let len = RARRAY_LEN(ivars) as usize; @@ -560,7 +560,7 @@ unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize, to_s_id: I return struct_value(recorder, &class_name, &names, &vals, depth, to_s_id); } let text = value_to_string(val, to_s_id).unwrap_or_default(); - let type_id = recorder.tracer.ensure_type_id(TypeKind::Raw, &class_name); + let type_id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Raw, &class_name); ValueRecord::Raw { r: text, type_id } } @@ -578,10 +578,12 @@ unsafe fn record_variables(recorder: &mut Recorder, binding: VALUE) -> Vec Vec Vec { let mut result = Vec::with_capacity(params.len()); for (name, val_rec) in params { - recorder - .tracer - .register_variable_with_full_value(&name, val_rec.clone()); - let var_id = recorder.tracer.ensure_variable_id(&name); + TraceWriter::register_variable_with_full_value( + &mut *recorder.tracer, + &name, + val_rec.clone(), + ); + let var_id = TraceWriter::ensure_variable_id(&mut *recorder.tracer, &name); result.push(FullValueRecord { variable_id: var_id, value: val_rec, @@ -650,33 +653,25 @@ unsafe fn register_parameter_values( result } -unsafe fn record_event(tracer: &mut Tracer, path: &str, line: i64, content: String) { - tracer.register_step(Path::new(path), Line(line)); - tracer.events.push(TraceLowLevelEvent::Event(RecordEvent { - kind: EventLogKind::Write, - metadata: String::new(), - content, - })); +unsafe fn record_event(tracer: &mut dyn TraceWriter, path: &str, line: i64, content: String) { + TraceWriter::register_step(tracer, Path::new(path), Line(line)); + TraceWriter::register_special_event(tracer, EventLogKind::Write, &content) } -unsafe extern "C" fn flush_trace(self_val: VALUE, out_dir: VALUE, format: VALUE) -> VALUE { +unsafe extern "C" fn initialize(self_val: VALUE, out_dir: VALUE, format: VALUE) -> VALUE { let recorder_ptr = get_recorder(self_val); let recorder = &mut *recorder_ptr; let ptr = RSTRING_PTR(out_dir) as *const u8; let len = RSTRING_LEN(out_dir) as usize; let slice = std::slice::from_raw_parts(ptr, len); - let fmt = if NIL_P(format) { - runtime_tracing::TraceEventsFileFormat::Json - } else if RB_SYMBOL_P(format) { + let fmt = if !NIL_P(format) && RB_SYMBOL_P(format) { let id = rb_sym2id(format); match CStr::from_ptr(rb_id2name(id)).to_str().unwrap_or("") { + "binaryv0" => runtime_tracing::TraceEventsFileFormat::BinaryV0, "binary" | "bin" => runtime_tracing::TraceEventsFileFormat::Binary, "json" => runtime_tracing::TraceEventsFileFormat::Json, - _ => { - rb_raise(rb_eIOError, b"Unknown format\0".as_ptr() as *const c_char); - runtime_tracing::TraceEventsFileFormat::Json - } + _ => rb_raise(rb_eIOError, b"Unknown format\0".as_ptr() as *const c_char), } } else { runtime_tracing::TraceEventsFileFormat::Json @@ -684,14 +679,57 @@ unsafe extern "C" fn flush_trace(self_val: VALUE, out_dir: VALUE, format: VALUE) match std::str::from_utf8(slice) { Ok(path_str) => { - if let Err(e) = flush_to_dir(&recorder.tracer, Path::new(path_str), fmt) { - let msg = std::ffi::CString::new(e.to_string()) - .unwrap_or_else(|_| std::ffi::CString::new("unknown error").unwrap()); - rb_raise( - rb_eIOError, - b"Failed to flush trace: %s\0".as_ptr() as *const c_char, - msg.as_ptr(), - ); + match begin_trace(Path::new(path_str), fmt) { + Ok(t) => { + recorder.tracer = t; + // pre-register common types to match the pure Ruby tracer + recorder.int_type_id = TraceWriter::ensure_type_id( + &mut *recorder.tracer, + TypeKind::Int, + "Integer", + ); + recorder.string_type_id = TraceWriter::ensure_type_id( + &mut *recorder.tracer, + TypeKind::String, + "String", + ); + recorder.bool_type_id = + TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Bool, "Bool"); + recorder.float_type_id = runtime_tracing::NONE_TYPE_ID; + recorder.symbol_type_id = TraceWriter::ensure_type_id( + &mut *recorder.tracer, + TypeKind::String, + "Symbol", + ); + recorder.error_type_id = TraceWriter::ensure_type_id( + &mut *recorder.tracer, + TypeKind::Error, + "No type", + ); + let path = Path::new(""); + let func_id = TraceWriter::ensure_function_id( + &mut *recorder.tracer, + "", + path, + Line(1), + ); + TraceWriter::add_event( + &mut *recorder.tracer, + TraceLowLevelEvent::Call(CallRecord { + function_id: func_id, + args: vec![], + }), + ); + } + Err(e) => { + let msg = std::ffi::CString::new(e.to_string()) + .unwrap_or_else(|_| std::ffi::CString::new("unknown error").unwrap()); + rb_raise( + rb_eIOError, + b"Failed to flush trace: %s\0".as_ptr() as *const c_char, + msg.as_ptr(), + ); + } } } Err(e) => { @@ -702,7 +740,24 @@ unsafe extern "C" fn flush_trace(self_val: VALUE, out_dir: VALUE, format: VALUE) b"Invalid UTF-8 in path: %s\0".as_ptr() as *const c_char, msg.as_ptr(), ) - }, + } + } + + Qnil.into() +} + +unsafe extern "C" fn flush_trace(self_val: VALUE) -> VALUE { + let recorder_ptr = get_recorder(self_val); + let recorder = &mut *recorder_ptr; + + if let Err(e) = flush_to_dir(&mut *recorder.tracer) { + let msg = std::ffi::CString::new(e.to_string()) + .unwrap_or_else(|_| std::ffi::CString::new("unknown error").unwrap()); + rb_raise( + rb_eIOError, + b"Failed to flush trace: %s\0".as_ptr() as *const c_char, + msg.as_ptr(), + ); } Qnil.into() @@ -724,7 +779,7 @@ unsafe extern "C" fn record_event_api( }; let line_num = rb_num2long(line) as i64; let content_str = value_to_string(content, recorder.to_s_id).unwrap_or_default(); - record_event(&mut recorder.tracer, path_slice, line_num, content_str); + record_event(&mut *recorder.tracer, path_slice, line_num, content_str); Qnil.into() } @@ -764,7 +819,7 @@ unsafe extern "C" fn event_hook_raw(data: VALUE, arg: *mut rb_trace_arg_t) { if (ev & RUBY_EVENT_LINE) != 0 { let binding = rb_tracearg_binding(arg); - recorder.tracer.register_step(Path::new(&path), Line(line)); + TraceWriter::register_step(&mut *recorder.tracer, Path::new(&path), Line(line)); if !NIL_P(binding) { record_variables(recorder, binding); } @@ -785,20 +840,23 @@ unsafe extern "C" fn event_hook_raw(data: VALUE, arg: *mut rb_trace_arg_t) { let class_name = cstr_to_string(rb_obj_classname(self_val)).unwrap_or_else(|| "Object".to_string()); let text = value_to_string_safe(self_val, recorder.to_s_id).unwrap_or_default(); - let self_type = recorder.tracer.ensure_type_id(TypeKind::Raw, &class_name); + let self_type = + TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Raw, &class_name); let self_rec = ValueRecord::Raw { r: text, type_id: self_type, }; - recorder - .tracer - .register_variable_with_full_value("self", self_rec.clone()); + TraceWriter::register_variable_with_full_value( + &mut *recorder.tracer, + "self", + self_rec.clone(), + ); - let mut args = vec![recorder.tracer.arg("self", self_rec)]; + let mut args = vec![TraceWriter::arg(&mut *recorder.tracer, "self", self_rec)]; if !param_vals.is_empty() { args.extend(register_parameter_values(recorder, param_vals)); } - recorder.tracer.register_step(Path::new(&path), Line(line)); + TraceWriter::register_step(&mut *recorder.tracer, Path::new(&path), Line(line)); let name_c = rb_id2name(mid); let mut name = if !name_c.is_null() { CStr::from_ptr(name_c).to_str().unwrap_or("").to_string() @@ -808,40 +866,33 @@ unsafe extern "C" fn event_hook_raw(data: VALUE, arg: *mut rb_trace_arg_t) { if class_name != "Object" { name = format!("{}#{}", class_name, name); } - let fid = recorder - .tracer - .ensure_function_id(&name, Path::new(&path), Line(line)); - recorder - .tracer - .events - .push(TraceLowLevelEvent::Call(CallRecord { + let fid = TraceWriter::ensure_function_id( + &mut *recorder.tracer, + &name, + Path::new(&path), + Line(line), + ); + TraceWriter::add_event( + &mut *recorder.tracer, + TraceLowLevelEvent::Call(CallRecord { function_id: fid, args, - })); + }), + ); } else if (ev & RUBY_EVENT_RETURN) != 0 { - recorder.tracer.register_step(Path::new(&path), Line(line)); + TraceWriter::register_step(&mut *recorder.tracer, Path::new(&path), Line(line)); let ret = rb_tracearg_return_value(arg); let val_rec = to_value(recorder, ret, 10, recorder.to_s_id); - recorder - .tracer - .register_variable_with_full_value("", val_rec.clone()); - recorder - .tracer - .events - .push(TraceLowLevelEvent::Return(ReturnRecord { - return_value: val_rec, - })); + TraceWriter::register_variable_with_full_value( + &mut *recorder.tracer, + "", + val_rec.clone(), + ); + TraceWriter::register_return(&mut *recorder.tracer, val_rec); } else if (ev & RUBY_EVENT_RAISE) != 0 { let exc = rb_tracearg_raised_exception(arg); if let Some(msg) = value_to_string(exc, recorder.to_s_id) { - recorder - .tracer - .events - .push(TraceLowLevelEvent::Event(RecordEvent { - kind: EventLogKind::Error, - metadata: String::new(), - content: msg, - })); + TraceWriter::register_special_event(&mut *recorder.tracer, EventLogKind::Error, &msg); } } } @@ -855,6 +906,12 @@ pub extern "C" fn Init_codetracer_ruby_recorder() { ); rb_define_alloc_func(class, Some(ruby_recorder_alloc)); + rb_define_method( + class, + b"initialize\0".as_ptr() as *const c_char, + Some(std::mem::transmute(initialize as *const ())), + 2, + ); rb_define_method( class, b"enable_tracing\0".as_ptr() as *const c_char, @@ -871,7 +928,7 @@ pub extern "C" fn Init_codetracer_ruby_recorder() { class, b"flush_trace\0".as_ptr() as *const c_char, Some(std::mem::transmute(flush_trace as *const ())), - 2, + 0, ); rb_define_method( class, diff --git a/gems/codetracer-ruby-recorder/lib/codetracer_ruby_recorder.rb b/gems/codetracer-ruby-recorder/lib/codetracer_ruby_recorder.rb index 4dc728d..f444838 100644 --- a/gems/codetracer-ruby-recorder/lib/codetracer_ruby_recorder.rb +++ b/gems/codetracer-ruby-recorder/lib/codetracer_ruby_recorder.rb @@ -41,7 +41,7 @@ def self.parse_argv_and_trace_ruby_file(argv) end def self.trace_ruby_file(program, out_dir, program_args = [], format = :json) - recorder = RubyRecorder.new + recorder = RubyRecorder.new(out_dir, format) return 1 unless recorder.available? ENV['CODETRACER_RUBY_RECORDER_OUT_DIR'] = out_dir @@ -60,7 +60,7 @@ def self.trace_ruby_file(program, out_dir, program_args = [], format = :json) ARGV.concat(original_argv) recorder.stop - recorder.flush_trace(out_dir, format) + recorder.flush_trace end 0 end @@ -70,10 +70,10 @@ def self.execute(argv) parse_argv_and_trace_ruby_file(argv) end - def initialize + def initialize(out_dir, format = :json) @recorder = nil @active = false - load_native_recorder + load_native_recorder(out_dir, format) end # Start the recorder and install kernel patches @@ -100,8 +100,8 @@ def record_event(path, line, content) end # Flush trace to output directory - def flush_trace(out_dir, format = :json) - @recorder.flush_trace(out_dir, format) if @recorder + def flush_trace + @recorder.flush_trace if @recorder end # Check if recorder is available @@ -111,7 +111,7 @@ def available? private - def load_native_recorder + def load_native_recorder(out_dir, format = :json) begin # Load native extension at module level ext_dir = File.expand_path('../ext/native_tracer/target/release', __dir__) @@ -132,7 +132,7 @@ def load_native_recorder end require target_path - @recorder = CodeTracerNativeRecorder.new + @recorder = CodeTracerNativeRecorder.new(out_dir, format) rescue Exception => e warn "native tracer unavailable: #{e}" @recorder = nil diff --git a/test/test_tracer.rb b/test/test_tracer.rb index 2c12135..2d18f41 100644 --- a/test/test_tracer.rb +++ b/test/test_tracer.rb @@ -132,7 +132,7 @@ def run_gem_installation_test(gem_bin, gem_module) script = <<~RUBY require '#{gem_module}' - recorder = #{recorder_class}.new + recorder = #{recorder_class}.new('#{out_dir_lib}') puts 'start trace' recorder.stop puts 'this will not be traced' @@ -140,7 +140,7 @@ def run_gem_installation_test(gem_bin, gem_module) puts 'this will be traced' recorder.stop puts 'tracing disabled' - recorder.flush_trace('#{out_dir_lib}') + recorder.flush_trace RUBY script_path = File.join('test', 'tmp', "use_#{gem_bin.tr('-', '_')}.rb") File.write(script_path, script)