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
Event | When Dispatched | Frame Type | Frequency |
---|---|---|---|
RUBY_EVENT_LINE | New line executed | Any | Very High |
RUBY_EVENT_CALL | Ruby method entry | method | High |
RUBY_EVENT_RETURN | Ruby method exit | method | High |
RUBY_EVENT_C_CALL | C method entry | (none) | High |
RUBY_EVENT_C_RETURN | C method exit | (none) | High |
RUBY_EVENT_B_CALL | Block entry | block | High |
RUBY_EVENT_B_RETURN | Block exit | block | High |
RUBY_EVENT_CLASS | Class definition entry | class | Medium |
RUBY_EVENT_END | Class definition exit | class | Medium |
RUBY_EVENT_RAISE | Exception raised | rescue | Low |
RUBY_EVENT_THREAD_BEGIN | Thread starts | Any | Low |
RUBY_EVENT_THREAD_END | Thread ends | Any | Low |
RUBY_EVENT_FIBER_SWITCH | Fiber context switch | Any | Variable |
RUBY_EVENT_SCRIPT_COMPILED | Code compiled | N/A | Low |
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
-
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)
-
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
-
Scoped Enabling: Only trace specific blocks
trace.enable { suspicious_code } # Not enabled globally
-
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