source

YARV provides a comprehensive event system that allows external code to observe and react to execution events as the VM runs. This event system is the foundation for debugging, profiling, code coverage, and other introspection tools in Ruby.

What Are YARV Events?

YARV events are notifications dispatched by the VM when specific execution milestones occur. Think of them as hooks into the VM’s execution lifecycle:

# When this code executes, YARV dispatches multiple events:
def greet(name)
  puts "Hello, #{name}"
end
 
greet("World")
 
# Events dispatched:
# 1. RUBY_EVENT_CALL    (entering greet method)
# 2. RUBY_EVENT_LINE    (executing line inside greet)
# 3. RUBY_EVENT_C_CALL  (calling 'puts' - C method)
# 4. RUBY_EVENT_C_RETURN (returning from 'puts')
# 5. RUBY_EVENT_RETURN  (returning from greet)

Event Categories

YARV events fall into several categories based on what they track:

Line Events

RUBY_EVENT_LINE - Dispatched when execution moves to a new line of code:

def example
  x = 1     # LINE event
  y = 2     # LINE event
  x + y     # LINE event
end

Uses:

  • Debuggers: Set breakpoints, step through code
  • Coverage tools: Track which lines executed
  • Profilers: Measure time spent per line

Method Call Events

RUBY_EVENT_CALL - Dispatched when entering a Ruby method (method frame created):

def helper(x)
  # CALL event dispatched here
  x * 2
end
 
helper(5)  # Triggers CALL event

RUBY_EVENT_RETURN - Dispatched when exiting a Ruby method:

def helper(x)
  x * 2
  # RETURN event dispatched when method returns
end

Method Event Flow:

graph LR
    A[Call Site] -->|CALL event| B[Method Frame Created]
    B --> C[Execute Method Body]
    C -->|RETURN event| D[Frame Destroyed]
    D --> E[Return to Caller]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#e8f5e9
    style D fill:#fce4ec

C Method Call Events

RUBY_EVENT_C_CALL - Dispatched when calling a C-implemented method:

"hello".upcase    # C_CALL event (upcase is implemented in C)
[1, 2].map { }    # C_CALL event (map is implemented in C)

RUBY_EVENT_C_RETURN - Dispatched when returning from C method:

result = "hello".upcase
# C_RETURN event happens here (after upcase completes)

Why separate events?

  • C methods don’t create Ruby method frames
  • Performance characteristics differ
  • Different debugging/profiling strategies needed

Block Events

RUBY_EVENT_B_CALL - Dispatched when entering a block (block frame created):

[1, 2, 3].each do |n|
  # B_CALL event dispatched here (each iteration)
  puts n
end

RUBY_EVENT_B_RETURN - Dispatched when exiting a block:

[1, 2, 3].each do |n|
  puts n
  # B_RETURN event dispatched here (each iteration)
end

Block Event Pattern:

Iteration 1: B_CALL → execute block → B_RETURN
Iteration 2: B_CALL → execute block → B_RETURN
Iteration 3: B_CALL → execute block → B_RETURN

Class Definition Events

RUBY_EVENT_CLASS - Dispatched when entering a class/module definition (class frame created):

class MyClass
  # CLASS event dispatched here
  attr_reader :name
 
  def initialize
    # Nested CLASS event for method definition context
  end
end

RUBY_EVENT_END - Dispatched when exiting a class/module definition:

class MyClass
  # ... class body ...
  # END event dispatched here
end

Uses:

  • Tracking class definition order
  • Metaprogramming tools
  • Code analysis and documentation generation

Exception Events

RUBY_EVENT_RAISE - Dispatched when an exception is raised:

def risky
  raise StandardError, "Something went wrong"
  # RAISE event dispatched here
end
 
begin
  risky
rescue => e
  # Exception caught in rescue frame
end

Characteristics:

  • Fires before rescue frame is created
  • Captures exception object and location
  • May fire multiple times if exception re-raised

Exception Event Flow:

graph TD
    A[Code raises exception] -->|RAISE event| B[Search for rescue]
    B -->|Found| C[Create rescue frame]
    B -->|Not found| D[Propagate to caller]
    C --> E[Handle exception]
    D --> F{Caller has rescue?}
    F -->|Yes| C
    F -->|No| G[Terminate program]

    style A fill:#ffebee
    style B fill:#fff4e1
    style C fill:#e8f5e9
    style D fill:#fce4ec

Thread and Fiber Events

RUBY_EVENT_THREAD_BEGIN - Dispatched when a thread starts:

Thread.new do
  # THREAD_BEGIN event dispatched here
  # ... thread work ...
end

RUBY_EVENT_THREAD_END - Dispatched when a thread terminates:

Thread.new do
  # ... thread work ...
  # THREAD_END event dispatched when block completes
end

RUBY_EVENT_FIBER_SWITCH - Dispatched when switching between fibers:

fiber = Fiber.new do
  # FIBER_SWITCH when fiber.resume called
  Fiber.yield
  # FIBER_SWITCH when fiber.resume called again
end
 
fiber.resume  # Triggers FIBER_SWITCH

Uses:

  • Concurrency debugging
  • Thread pool monitoring
  • Fiber-based async debugging

Compilation Events

RUBY_EVENT_SCRIPT_COMPILED - Dispatched when an instruction sequence is compiled:

# First time this runs:
eval("1 + 2")  # SCRIPT_COMPILED event
 
# Subsequent times may use cached iseq

Uses:

  • Monitoring code generation
  • JIT compilation tracking
  • Performance analysis

Complete Event Table

EventWhen DispatchedFrame TypeFrequency
RUBY_EVENT_LINENew line executedAnyVery High
RUBY_EVENT_CALLRuby method entrymethodHigh
RUBY_EVENT_RETURNRuby method exitmethodHigh
RUBY_EVENT_C_CALLC method entry(none)High
RUBY_EVENT_C_RETURNC method exit(none)High
RUBY_EVENT_B_CALLBlock entryblockHigh
RUBY_EVENT_B_RETURNBlock exitblockHigh
RUBY_EVENT_CLASSClass definition entryclassMedium
RUBY_EVENT_ENDClass definition exitclassMedium
RUBY_EVENT_RAISEException raisedrescueLow
RUBY_EVENT_THREAD_BEGINThread startsAnyLow
RUBY_EVENT_THREAD_ENDThread endsAnyLow
RUBY_EVENT_FIBER_SWITCHFiber context switchAnyVariable
RUBY_EVENT_SCRIPT_COMPILEDCode compiledN/ALow

The TracePoint API

Ruby exposes YARV events through the TracePoint API, which provides a clean interface for subscribing to events:

Basic TracePoint Usage

# Create a trace that watches method calls
trace = TracePoint.new(:call, :return) do |tp|
  puts "#{tp.event}: #{tp.method_id} at #{tp.path}:#{tp.lineno}"
end
 
trace.enable  # Start tracing
 
def example
  "hello"
end
 
example
# Output:
# call: example at script.rb:8
# return: example at script.rb:8
 
trace.disable  # Stop tracing

TracePoint Events

TracePoint supports all YARV events with symbolic names:

# Watch everything:
TracePoint.new(
  :line,           # RUBY_EVENT_LINE
  :call,           # RUBY_EVENT_CALL
  :return,         # RUBY_EVENT_RETURN
  :c_call,         # RUBY_EVENT_C_CALL
  :c_return,       # RUBY_EVENT_C_RETURN
  :b_call,         # RUBY_EVENT_B_CALL
  :b_return,       # RUBY_EVENT_B_RETURN
  :class,          # RUBY_EVENT_CLASS
  :end,            # RUBY_EVENT_END
  :raise,          # RUBY_EVENT_RAISE
  :thread_begin,   # RUBY_EVENT_THREAD_BEGIN
  :thread_end,     # RUBY_EVENT_THREAD_END
  :fiber_switch,   # RUBY_EVENT_FIBER_SWITCH
  :script_compiled # RUBY_EVENT_SCRIPT_COMPILED
) do |tp|
  # Handle event
end

TracePoint Information

Inside a TracePoint block, you have access to rich event information:

trace = TracePoint.new(:call) do |tp|
  tp.event        # => :call (event type)
  tp.method_id    # => :example (method name)
  tp.path         # => "script.rb" (file path)
  tp.lineno       # => 10 (line number)
  tp.defined_class # => MyClass (class where method defined)
  tp.binding      # => #<Binding> (current frame's binding)
  tp.self         # => #<MyClass> (current self/receiver)
  tp.return_value # => "result" (only for :return events)
  tp.raised_exception # => #<StandardError> (only for :raise events)
end

Selective Tracing

You can target specific methods or classes:

# Trace only specific method
trace = TracePoint.new(:call) do |tp|
  next unless tp.method_id == :target_method
  puts "Called target_method!"
end
 
# Trace only specific file
trace = TracePoint.new(:line) do |tp|
  next unless tp.path.include?('myapp')
  puts "Executing: #{tp.path}:#{tp.lineno}"
end
 
# Trace only specific class
trace = TracePoint.new(:call) do |tp|
  next unless tp.defined_class == MyClass
  puts "MyClass method called: #{tp.method_id}"
end

Scoped Tracing

Enable tracing only for specific code blocks:

trace = TracePoint.new(:line) do |tp|
  puts "Line: #{tp.lineno}"
end
 
# Enable only for this block
trace.enable do
  def example
    x = 1
    y = 2
    x + y
  end
  example
end
# Tracing automatically disabled after block

Event Implementation in YARV

Events are triggered by instructions and VM operations:

// Simplified YARV internals
void vm_trace(rb_event_flag_t event, ...) {
    if (has_tracepoint_enabled(event)) {
        dispatch_to_tracepoints(event, ...);
    }
}
 
// Called by instructions:
void insn_method_call(...) {
    vm_trace(RUBY_EVENT_CALL, ...);
    // ... execute method ...
    vm_trace(RUBY_EVENT_RETURN, ...);
}

Performance Consideration: Event checking happens at critical points, so enabling tracing has overhead. YARV optimizes by:

  • Checking if events are enabled before expensive operations
  • Using bitmap flags for fast event type checking
  • Allowing selective event enabling (only what you need)

Practical Applications

Building a Debugger

class SimpleDebugger
  def initialize
    @breakpoints = {}
    @trace = TracePoint.new(:line) do |tp|
      if @breakpoints[tp.path]&.include?(tp.lineno)
        puts "Breakpoint hit: #{tp.path}:#{tp.lineno}"
        puts "Local variables: #{tp.binding.local_variables}"
        binding.irb  # Drop into REPL
      end
    end
  end
 
  def add_breakpoint(file, line)
    @breakpoints[file] ||= []
    @breakpoints[file] << line
  end
 
  def start
    @trace.enable
  end
end

Building a Profiler

class SimpleProfiler
  def initialize
    @method_times = Hash.new { |h, k| h[k] = { count: 0, total: 0 } }
    @call_stack = []
 
    @trace = TracePoint.new(:call, :return) do |tp|
      case tp.event
      when :call
        @call_stack.push({ method: tp.method_id, start: Time.now })
      when :return
        return if @call_stack.empty?
        frame = @call_stack.pop
        elapsed = Time.now - frame[:start]
        @method_times[frame[:method]][:count] += 1
        @method_times[frame[:method]][:total] += elapsed
      end
    end
  end
 
  def start
    @trace.enable
    yield
    @trace.disable
    report
  end
 
  def report
    @method_times.each do |method, stats|
      avg = stats[:total] / stats[:count]
      puts "#{method}: #{stats[:count]} calls, avg #{avg}s"
    end
  end
end

Building a Coverage Tool

class SimpleCoverage
  def initialize
    @lines_executed = Hash.new { |h, k| h[k] = Set.new }
 
    @trace = TracePoint.new(:line) do |tp|
      @lines_executed[tp.path] << tp.lineno
    end
  end
 
  def start
    @trace.enable
    yield
    @trace.disable
    report
  end
 
  def report
    @lines_executed.each do |file, lines|
      puts "#{file}: #{lines.size} lines executed"
    end
  end
end

Event Ordering and Guarantees

YARV guarantees specific event orderings:

Method Call Order

1. RUBY_EVENT_CALL
2. RUBY_EVENT_LINE (first line of method)
3. ... (additional LINE events)
4. RUBY_EVENT_RETURN

Block Call Order

1. RUBY_EVENT_B_CALL
2. RUBY_EVENT_LINE (first line of block)
3. ... (additional LINE events)
4. RUBY_EVENT_B_RETURN

Exception Order

1. RUBY_EVENT_LINE (line that raises)
2. RUBY_EVENT_RAISE
3. ... (stack unwinding)
4. RUBY_EVENT_LINE (rescue clause, if caught)

Class Definition Order

1. RUBY_EVENT_CLASS
2. RUBY_EVENT_LINE (lines in class body)
3. RUBY_EVENT_CALL (for each method defined)
4. RUBY_EVENT_END

Performance Considerations

Overhead

Enabling events has performance costs:

# Minimal overhead (no tracing)
def fast
  x = 1
  y = 2
  x + y
end
 
# Significant overhead (line tracing enabled)
trace = TracePoint.new(:line) { }
trace.enable
def slow
  x = 1  # LINE event
  y = 2  # LINE event
  x + y  # LINE event
end

Typical overhead:

  • :line events: 10-100x slower (very frequent)
  • :call/:return: 2-5x slower (frequent)
  • :raise: Minimal (infrequent)

Optimization Strategies

  1. Selective Events: Only enable events you need

    # Bad: Watches everything
    TracePoint.new(:line, :call, :return, :c_call, :c_return)
     
    # Good: Only what's needed
    TracePoint.new(:call, :return)
  2. Conditional Logic: Filter early in callback

    TracePoint.new(:line) do |tp|
      # Fast path: check file first
      next unless tp.path.include?('myapp')
      # Expensive work only for relevant files
    end
  3. Scoped Enabling: Only trace specific blocks

    trace.enable { suspicious_code }  # Not enabled globally
  4. Sampling: Don’t trace every event

    counter = 0
    TracePoint.new(:line) do |tp|
      counter += 1
      next unless counter % 100 == 0  # Sample 1% of events
      # Do expensive work
    end

Event System Architecture

graph TD
    A[YARV Execution] --> B{Event Checkpoint}
    B -->|Event enabled?| C{TracePoint registered?}
    C -->|Yes| D[Collect Event Data]
    C -->|No| E[Continue Execution]
    B -->|No event| E

    D --> F[Call TracePoint Callbacks]
    F --> G[User Code in Callback]
    G --> H[Resume Execution]

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style D fill:#e8f5e9
    style F fill:#fce4ec
    style E fill:#f3e5f5
    style H fill:#f3e5f5