|
| 1 | +# Listing 001 |
| 2 | + |
| 3 | +This listing introduces the development environment for **codetracer-ruby-recorder**. We review documentation for installation and environment variables (`README.md`), project dependencies (`Gemfile`), build and test tasks (`Rakefile` and `Justfile`), and then walk through the primary `RubyRecorder` class that powers the native tracer. |
| 4 | + |
| 5 | +**Project heading states this gem records Ruby programs to produce CodeTracer traces.** |
| 6 | +```markdown |
| 7 | +## codetracer-ruby-recorder |
| 8 | + |
| 9 | +A recorder of Ruby programs that produces [CodeTracer](https://github.com/metacraft-labs/CodeTracer) traces. |
| 10 | +``` |
| 11 | + |
| 12 | +**Installation instructions show gem installation and fallback to pure Ruby version.** |
| 13 | +```bash |
| 14 | +gem install codetracer-ruby-recorder |
| 15 | +gem install codetracer-pure-ruby-recorder |
| 16 | +``` |
| 17 | + |
| 18 | +**Environment variables toggle debug logging and specify trace output directory.** |
| 19 | +```markdown |
| 20 | +* if you pass `CODETRACER_RUBY_RECORDER_DEBUG=1`, you enable some additional debug-related logging |
| 21 | +* `CODETRACER_RUBY_RECORDER_OUT_DIR` can be used to specify the directory for trace files |
| 22 | +``` |
| 23 | + |
| 24 | +**Development setup suggests installing debugging gems and running tests manually.** |
| 25 | +```bash |
| 26 | +gem install debug pry |
| 27 | +ruby -I lib -I test test/test_tracer.rb |
| 28 | +``` |
| 29 | + |
| 30 | +**Gemfile sets source and references both native and pure-Ruby recorder gems locally.** |
| 31 | +```ruby |
| 32 | +# frozen_string_literal: true |
| 33 | +source "https://rubygems.org" |
| 34 | + |
| 35 | +gem "codetracer-ruby-recorder", path: "gems/codetracer-ruby-recorder" |
| 36 | +gem "codetracer-pure-ruby-recorder", path: "gems/codetracer-pure-ruby-recorder" |
| 37 | +``` |
| 38 | + |
| 39 | +**Optional development gems for debugging are commented out; rubocop is included for development.** |
| 40 | +```ruby |
| 41 | +# gem "debug", "~> 1.7" # Ruby debugging with rdbg |
| 42 | +# gem "pry", "~> 0.14" # Interactive debugging and REPL |
| 43 | +gem "rubocop", "~> 1.77", :group => :development |
| 44 | +``` |
| 45 | + |
| 46 | +**Rakefile loads rb_sys extension task.** |
| 47 | +```ruby |
| 48 | +require 'rb_sys/extensiontask' |
| 49 | +``` |
| 50 | + |
| 51 | +**Extension task configuration specifies build and library directories.** |
| 52 | +```ruby |
| 53 | +RbSys::ExtensionTask.new('codetracer_ruby_recorder') do |ext| |
| 54 | + ext.ext_dir = 'gems/codetracer-ruby-recorder/ext/native_tracer' |
| 55 | + ext.lib_dir = 'gems/codetracer-ruby-recorder/lib' |
| 56 | + ext.gem_spec = Gem::Specification.load('gems/codetracer-ruby-recorder/codetracer-ruby-recorder.gemspec') |
| 57 | +end |
| 58 | +``` |
| 59 | + |
| 60 | +**Alias for running tests and the test command executes installation checks and unit tests.** |
| 61 | +```make |
| 62 | +alias t := test |
| 63 | +test: |
| 64 | + ruby -Itest test/gem_installation.rb |
| 65 | + ruby -Itest -e 'Dir["test/test_*.rb"].each { |f| require File.expand_path(f) }' |
| 66 | +``` |
| 67 | + |
| 68 | +**Benchmark task runs benchmarks with pattern and report options.** |
| 69 | +```make |
| 70 | +bench pattern="*" write_report="console": |
| 71 | + ruby test/benchmarks/run_benchmarks.rb '{{pattern}}' --write-report={{write_report}} |
| 72 | +``` |
| 73 | + |
| 74 | +**Build native extension via Cargo.** |
| 75 | +```make |
| 76 | +build-extension: |
| 77 | + cargo build --release --manifest-path gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml |
| 78 | +``` |
| 79 | + |
| 80 | +**Formatting tasks for Rust, Nix, and Ruby.** |
| 81 | +```make |
| 82 | +format-rust: |
| 83 | + cargo fmt --manifest-path gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml |
| 84 | + |
| 85 | +format-nix: |
| 86 | + if command -v nixfmt >/dev/null; then find . -name '*.nix' -print0 | xargs -0 nixfmt; fi |
| 87 | + |
| 88 | +format-ruby: |
| 89 | + if command -v bundle >/dev/null && bundle exec rubocop -v >/dev/null 2>&1; then bundle exec rubocop -A; else echo "Ruby formatter not available; skipping"; fi |
| 90 | +``` |
| 91 | + |
| 92 | +**Aggregate formatting and linting tasks with an alias.** |
| 93 | +```make |
| 94 | +format: |
| 95 | + just format-rust |
| 96 | + just format-ruby |
| 97 | + just format-nix |
| 98 | + |
| 99 | +lint-rust: |
| 100 | + cargo fmt --check --manifest-path gems/codetracer-ruby-recorder/ext/native_tracer/Cargo.toml |
| 101 | + |
| 102 | +lint-nix: |
| 103 | + if command -v nixfmt >/dev/null; then find . -name '*.nix' -print0 | xargs -0 nixfmt --check; fi |
| 104 | + |
| 105 | +lint-ruby: |
| 106 | + if command -v bundle >/dev/null && bundle exec rubocop -v >/dev/null 2>&1; then bundle exec rubocop; else echo "rubocop not available; skipping"; fi |
| 107 | + |
| 108 | +lint: |
| 109 | + just lint-rust |
| 110 | + just lint-ruby |
| 111 | + just lint-nix |
| 112 | + |
| 113 | +alias fmt := format |
| 114 | +``` |
| 115 | + |
| 116 | +**Header comments declare license and purpose.** |
| 117 | +```ruby |
| 118 | +# SPDX-License-Identifier: MIT |
| 119 | +# Library providing a helper method to execute the native tracer. |
| 120 | +``` |
| 121 | + |
| 122 | +**Load option parsing, file utilities, configuration, and kernel patches.** |
| 123 | +```ruby |
| 124 | +require 'optparse' |
| 125 | +require 'fileutils' |
| 126 | +require 'rbconfig' |
| 127 | +require_relative 'codetracer/kernel_patches' |
| 128 | +``` |
| 129 | + |
| 130 | +**Define RubyRecorder inside CodeTracer module.** |
| 131 | +```ruby |
| 132 | +module CodeTracer |
| 133 | + class RubyRecorder |
| 134 | +``` |
| 135 | + |
| 136 | +**Begin parsing CLI arguments and set up OptionParser.** |
| 137 | +```ruby |
| 138 | + def self.parse_argv_and_trace_ruby_file(argv) |
| 139 | + options = {} |
| 140 | + parser = OptionParser.new do |opts| |
| 141 | + opts.banner = 'usage: codetracer-ruby-recorder [options] <program> [args]' |
| 142 | +``` |
| 143 | + |
| 144 | +**Accept output directory and format options.** |
| 145 | +```ruby |
| 146 | + opts.on('-o DIR', '--out-dir DIR', 'Directory to write trace files') do |dir| |
| 147 | + options[:out_dir] = dir |
| 148 | + end |
| 149 | + opts.on('-f FORMAT', '--format FORMAT', 'trace format: json or binary') do |fmt| |
| 150 | + options[:format] = fmt |
| 151 | + end |
| 152 | +``` |
| 153 | + |
| 154 | +**Provide help flag and finalize option parsing.** |
| 155 | +```ruby |
| 156 | + opts.on('-h', '--help', 'Print this help') do |
| 157 | + puts opts |
| 158 | + exit |
| 159 | + end |
| 160 | + end |
| 161 | + parser.order!(argv) |
| 162 | +``` |
| 163 | + |
| 164 | +**Extract program argument and handle missing program.** |
| 165 | +```ruby |
| 166 | + program = argv.shift |
| 167 | + if program.nil? |
| 168 | + $stderr.puts parser |
| 169 | + exit 1 |
| 170 | + end |
| 171 | +``` |
| 172 | + |
| 173 | +**Capture remaining program arguments and determine output directory and format.** |
| 174 | +```ruby |
| 175 | + # Remaining arguments after the program name are passed to the traced program |
| 176 | + program_args = argv.dup |
| 177 | + |
| 178 | + out_dir = options[:out_dir] || ENV['CODETRACER_RUBY_RECORDER_OUT_DIR'] || Dir.pwd |
| 179 | + format = (options[:format] || 'json').to_sym |
| 180 | + trace_ruby_file(program, out_dir, program_args, format) |
| 181 | + 0 |
| 182 | + end |
| 183 | +``` |
| 184 | + |
| 185 | +**Trace specified Ruby file with selected options.** |
| 186 | +```ruby |
| 187 | + def self.trace_ruby_file(program, out_dir, program_args = [], format = :json) |
| 188 | + recorder = RubyRecorder.new(out_dir, format) |
| 189 | + return 1 unless recorder.available? |
| 190 | + |
| 191 | + ENV['CODETRACER_RUBY_RECORDER_OUT_DIR'] = out_dir |
| 192 | +``` |
| 193 | + |
| 194 | +**Execute program under recorder, adjusting ARGV temporarily.** |
| 195 | +```ruby |
| 196 | + recorder.start |
| 197 | + begin |
| 198 | + # Set ARGV to contain the program arguments |
| 199 | + original_argv = ARGV.dup |
| 200 | + ARGV.clear |
| 201 | + ARGV.concat(program_args) |
| 202 | + |
| 203 | + load program |
| 204 | + ensure |
| 205 | + # Restore original ARGV |
| 206 | + ARGV.clear |
| 207 | + ARGV.concat(original_argv) |
| 208 | + |
| 209 | + recorder.stop |
| 210 | + recorder.flush_trace |
| 211 | + end |
| 212 | + 0 |
| 213 | + end |
| 214 | +``` |
| 215 | + |
| 216 | +**Entry point to run CLI logic.** |
| 217 | +```ruby |
| 218 | + # Execute the native tracer CLI logic with the provided +argv+. |
| 219 | + def self.execute(argv) |
| 220 | + parse_argv_and_trace_ruby_file(argv) |
| 221 | + end |
| 222 | +``` |
| 223 | + |
| 224 | +**Initialize recorder and load native implementation.** |
| 225 | +```ruby |
| 226 | + def initialize(out_dir, format = :json) |
| 227 | + @recorder = nil |
| 228 | + @active = false |
| 229 | + load_native_recorder(out_dir, format) |
| 230 | + end |
| 231 | +``` |
| 232 | + |
| 233 | +**Start recording and apply kernel patches if not already active.** |
| 234 | +```ruby |
| 235 | + # Start the recorder and install kernel patches |
| 236 | + def start |
| 237 | + return if @active || @recorder.nil? |
| 238 | + |
| 239 | + @recorder.enable_tracing |
| 240 | + CodeTracer::KernelPatches.install(self) |
| 241 | + @active = true |
| 242 | + end |
| 243 | +``` |
| 244 | + |
| 245 | +**Stop recording and remove patches.** |
| 246 | +```ruby |
| 247 | + # Stop the recorder and remove kernel patches |
| 248 | + def stop |
| 249 | + return unless @active |
| 250 | + |
| 251 | + CodeTracer::KernelPatches.uninstall(self) |
| 252 | + @recorder.disable_tracing if @recorder |
| 253 | + @active = false |
| 254 | + end |
| 255 | +``` |
| 256 | + |
| 257 | +**Delegate recording events to native recorder.** |
| 258 | +```ruby |
| 259 | + # Record event for kernel patches integration |
| 260 | + def record_event(path, line, content) |
| 261 | + @recorder.record_event(path, line, content) if @recorder |
| 262 | + end |
| 263 | +``` |
| 264 | + |
| 265 | +**Flush trace data and report availability.** |
| 266 | +```ruby |
| 267 | + # Flush trace to output directory |
| 268 | + def flush_trace |
| 269 | + @recorder.flush_trace if @recorder |
| 270 | + end |
| 271 | + |
| 272 | + # Check if recorder is available |
| 273 | + def available? |
| 274 | + !@recorder.nil? |
| 275 | + end |
| 276 | +``` |
| 277 | + |
| 278 | +**Mark following methods as private and begin loading native recorder.** |
| 279 | +```ruby |
| 280 | + private |
| 281 | + |
| 282 | + def load_native_recorder(out_dir, format = :json) |
| 283 | + begin |
| 284 | + # Load native extension at module level |
| 285 | +``` |
| 286 | + |
| 287 | +**Resolve extension directory and target library path based on platform.** |
| 288 | +```ruby |
| 289 | + ext_dir = File.expand_path('../ext/native_tracer/target/release', __dir__) |
| 290 | + dlext = RbConfig::CONFIG['DLEXT'] |
| 291 | + target_path = File.join(ext_dir, "codetracer_ruby_recorder.#{dlext}") |
| 292 | + unless File.exist?(target_path) |
| 293 | + extensions = %w[so bundle dylib dll] |
| 294 | +``` |
| 295 | + |
| 296 | +**Search for alternative library names and create symlink or copy as needed.** |
| 297 | +```ruby |
| 298 | + alt_path = extensions |
| 299 | + .map { |ext| File.join(ext_dir, "libcodetracer_ruby_recorder.#{ext}") } |
| 300 | + .find { |path| File.exist?(path) } |
| 301 | + if alt_path |
| 302 | + begin |
| 303 | + File.symlink(alt_path, target_path) |
| 304 | + rescue StandardError |
| 305 | + FileUtils.cp(alt_path, target_path) |
| 306 | + end |
| 307 | + end |
| 308 | + end |
| 309 | +``` |
| 310 | + |
| 311 | +**Load library and build recorder instance.** |
| 312 | +```ruby |
| 313 | + require target_path |
| 314 | + @recorder = CodeTracerNativeRecorder.new(out_dir, format) |
| 315 | +``` |
| 316 | + |
| 317 | +**On errors, emit warning and fall back to nil recorder.** |
| 318 | +```ruby |
| 319 | + rescue Exception => e |
| 320 | + warn "native tracer unavailable: #{e}" |
| 321 | + @recorder = nil |
| 322 | + end |
| 323 | + end |
| 324 | +``` |
| 325 | + |
| 326 | +**Terminate the RubyRecorder class and CodeTracer module.** |
| 327 | +```ruby |
| 328 | + end |
| 329 | +end |
| 330 | +``` |
0 commit comments