CrackedRuby logo

CrackedRuby

Performance Monitoring

Overview

Performance monitoring in Ruby encompasses profiling application execution, measuring resource consumption, and collecting runtime metrics. Ruby provides built-in tools like Benchmark and ObjectSpace alongside third-party gems for comprehensive performance analysis.

The core performance monitoring approach involves three primary areas: execution profiling to identify bottlenecks, memory analysis to track allocation patterns, and benchmarking to measure performance changes over time. Ruby's profiling ecosystem centers around statistical sampling, call graph generation, and allocation tracking.

require 'benchmark'
require 'objspace'

# Basic execution timing
result = Benchmark.measure do
  1000.times { "string" * 100 }
end
puts result.real  # => 0.001234

Ruby-prof serves as the primary profiling library, offering multiple profiling modes including flat profiling, graph profiling, and call stack analysis. Memory profilers like memory_profiler and allocation trackers provide detailed insights into object creation and garbage collection patterns.

require 'ruby-prof'

RubyProf.start
complex_operation
result = RubyProf.stop

printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT)

Production performance monitoring typically involves APM integration with services like New Relic, Datadog, or custom metric collection systems. These tools provide continuous monitoring of application performance, error rates, and resource utilization patterns.

Basic Usage

The Benchmark module provides standard performance measurement capabilities for timing code execution. The measure method returns a Benchmark::Tms object containing real time, CPU time, and system time measurements.

require 'benchmark'

# Single operation timing
time = Benchmark.measure do
  array = (1..100000).to_a
  array.map(&:to_s).join(',')
end

puts "Real time: #{time.real}"      # Wall clock time
puts "CPU time: #{time.total}"      # CPU time used
puts "User time: #{time.utime}"     # User CPU time
puts "System time: #{time.stime}"   # System CPU time

The bm method compares multiple approaches, displaying results in tabular format. This proves valuable when evaluating different implementation strategies or optimization attempts.

Benchmark.bm(7) do |x|
  x.report("for:")   { for i in 1..100000; i.to_s; end }
  x.report("times:") { 100000.times { |i| i.to_s } }
  x.report("upto:")  { 1.upto(100000) { |i| i.to_s } }
end

Memory profiling requires the memory_profiler gem to track object allocations and memory usage patterns. The profiler captures allocation details including object count, memory consumption, and allocation locations.

require 'memory_profiler'

report = MemoryProfiler.report do
  strings = []
  1000.times do |i|
    strings << "string_#{i}"
  end
  strings.map(&:upcase)
end

puts report.pretty_print
# Shows total allocations, memory usage, and allocation locations

ObjectSpace provides built-in memory analysis capabilities, including object counting and garbage collection statistics. These tools help identify memory leaks and allocation patterns without external dependencies.

require 'objspace'

# Count objects by class
ObjectSpace.count_objects.each do |type, count|
  puts "#{type}: #{count}" if count > 1000
end

# Enable allocation tracing
ObjectSpace.trace_object_allocations_start

array = Array.new(1000) { |i| "item_#{i}" }

ObjectSpace.trace_object_allocations_stop

# Find allocation info for specific objects
array.each_with_index do |obj, i|
  next unless i < 5  # Check first 5 objects
  file = ObjectSpace.allocation_sourcefile(obj)
  line = ObjectSpace.allocation_sourceline(obj)
  puts "Object allocated at #{file}:#{line}"
end

Advanced Usage

Ruby-prof provides comprehensive profiling capabilities with multiple measurement modes and output formats. The gem supports flat profiling, graph profiling, and call tree analysis for detailed performance investigation.

require 'ruby-prof'

# Configure profiling mode
RubyProf.measure_mode = RubyProf::WALL_TIME
# Alternative modes: PROCESS_TIME, CPU_TIME, ALLOCATIONS, MEMORY

class PerformanceAnalyzer
  def self.profile_with_exclusions(method_patterns: [], &block)
    # Exclude specific methods from profiling
    method_patterns.each do |pattern|
      RubyProf.exclude_methods!(pattern)
    end
    
    RubyProf.start
    result = yield
    profile_result = RubyProf.stop
    
    {
      result: result,
      profile: profile_result,
      call_info: extract_call_info(profile_result)
    }
  end
  
  private
  
  def self.extract_call_info(profile_result)
    profile_result.threads.first.methods.map do |method_info|
      {
        method_name: method_info.full_name,
        self_time: method_info.self_time,
        total_time: method_info.total_time,
        call_count: method_info.called
      }
    end.sort_by { |info| -info[:total_time] }
  end
end

# Usage with custom analysis
analysis = PerformanceAnalyzer.profile_with_exclusions(
  method_patterns: [Object, /^Array#/]
) do
  data = (1..10000).map { |i| { id: i, name: "item_#{i}" } }
  data.select { |item| item[:id] % 2 == 0 }
       .map { |item| item[:name].upcase }
       .sort
end

# Generate multiple output formats
printer = RubyProf::CallTreePrinter.new(analysis[:profile])
File.open('callgrind.out', 'w') do |file|
  printer.print(file)
end

Custom profiling decorators enable method-level performance monitoring with minimal code changes. This approach proves valuable for selective profiling in production environments.

module ProfilingDecorator
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def profile_method(method_name, options = {})
      alias_method :"#{method_name}_without_profiling", method_name
      
      define_method(method_name) do |*args, &block|
        threshold = options[:threshold] || 0.1
        
        start_time = Time.now
        result = send(:"#{method_name}_without_profiling", *args, &block)
        execution_time = Time.now - start_time
        
        if execution_time > threshold
          logger = options[:logger] || Logger.new(STDOUT)
          logger.warn("Slow method execution: #{self.class}##{method_name} " \
                     "took #{execution_time.round(4)}s")
          
          # Optional detailed profiling for slow methods
          if options[:detailed_profiling] && execution_time > threshold * 2
            detailed_profile = MemoryProfiler.report do
              send(:"#{method_name}_without_profiling", *args, &block)
            end
            logger.warn("Memory usage: #{detailed_profile.total_allocated_memsize} bytes")
          end
        end
        
        result
      end
    end
  end
end

class DataProcessor
  include ProfilingDecorator
  
  def process_large_dataset(data)
    data.map { |item| expensive_transformation(item) }
        .select { |result| result[:valid] }
  end
  
  profile_method :process_large_dataset, 
                 threshold: 0.5, 
                 detailed_profiling: true
  
  private
  
  def expensive_transformation(item)
    # Simulate complex processing
    sleep(0.001)
    { id: item[:id], valid: item[:id] % 10 != 0, processed: Time.now }
  end
end

Advanced memory profiling involves tracking specific allocation patterns and identifying memory hotspots through detailed allocation analysis.

require 'objspace'

class MemoryLeakDetector
  def initialize
    @snapshots = []
  end
  
  def take_snapshot(label)
    GC.start  # Clean up before snapshot
    
    snapshot = {
      label: label,
      timestamp: Time.now,
      objects: ObjectSpace.count_objects,
      memsize: ObjectSpace.memsize_of_all,
      live_objects: live_object_analysis
    }
    
    @snapshots << snapshot
    snapshot
  end
  
  def analyze_growth
    return [] if @snapshots.length < 2
    
    base = @snapshots[-2]
    current = @snapshots[-1]
    
    growth_analysis = {}
    
    current[:objects].each do |type, count|
      base_count = base[:objects][type] || 0
      growth = count - base_count
      
      if growth > 100  # Significant growth threshold
        growth_analysis[type] = {
          growth: growth,
          percentage: base_count > 0 ? (growth.to_f / base_count * 100).round(2) : Float::INFINITY,
          current: count,
          previous: base_count
        }
      end
    end
    
    growth_analysis.sort_by { |_, data| -data[:growth] }
  end
  
  private
  
  def live_object_analysis
    analysis = Hash.new(0)
    
    ObjectSpace.each_object do |obj|
      analysis[obj.class.name] += 1
    end
    
    analysis
  end
end

# Usage for leak detection
detector = MemoryLeakDetector.new
detector.take_snapshot("baseline")

# Simulate potential memory leak
1000.times do |i|
  @global_cache ||= {}
  @global_cache["key_#{i}"] = "value" * 1000
end

detector.take_snapshot("after_operation")
growth = detector.analyze_growth

puts "Memory growth analysis:"
growth.first(5).each do |type, data|
  puts "#{type}: +#{data[:growth]} objects (+#{data[:percentage]}%)"
end

Error Handling & Debugging

Performance monitoring introduces specific error conditions including profiling overhead, memory exhaustion during analysis, and timing measurement inaccuracies. Handle these scenarios with appropriate error recovery strategies.

class SafeProfiler
  def self.measure_with_fallback(timeout: 30, &block)
    Timeout.timeout(timeout) do
      start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      result = yield
      end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      
      {
        result: result,
        execution_time: end_time - start_time,
        profiling_error: nil
      }
    end
  rescue Timeout::Error
    {
      result: nil,
      execution_time: timeout,
      profiling_error: "Operation timed out after #{timeout}s"
    }
  rescue StandardError => e
    {
      result: nil,
      execution_time: nil,
      profiling_error: "Profiling failed: #{e.class}: #{e.message}"
    }
  end
end

# Safe profiling with resource limits
profile_result = SafeProfiler.measure_with_fallback(timeout: 10) do
  # Potentially problematic operation
  large_array = Array.new(1_000_000) { rand(1000) }
  large_array.sort.uniq
end

if profile_result[:profiling_error]
  puts "Profiling issue: #{profile_result[:profiling_error]}"
else
  puts "Execution time: #{profile_result[:execution_time].round(4)}s"
end

Memory profiling can exhaust available memory when analyzing memory-intensive operations. Implement streaming analysis and memory limits to prevent system instability.

require 'memory_profiler'

class MemoryProfilerWrapper
  MAX_TRACKED_OBJECTS = 100_000
  MAX_REPORT_SIZE = 50 * 1024 * 1024  # 50MB
  
  def self.safe_profile(max_objects: MAX_TRACKED_OBJECTS, &block)
    initial_object_count = ObjectSpace.count_objects[:TOTAL]
    
    report = nil
    begin
      report = MemoryProfiler.report(top: 50) do  # Limit report size
        yield
      end
      
      final_object_count = ObjectSpace.count_objects[:TOTAL]
      objects_created = final_object_count - initial_object_count
      
      if objects_created > max_objects
        return {
          report: truncated_report(report),
          warning: "Large object allocation detected: #{objects_created} objects",
          truncated: true
        }
      end
      
      {
        report: report,
        warning: nil,
        truncated: false
      }
      
    rescue NoMemoryError
      GC.start
      {
        report: nil,
        warning: "Memory exhausted during profiling",
        truncated: true
      }
    rescue StandardError => e
      {
        report: nil,
        warning: "Profiling error: #{e.message}",
        truncated: false
      }
    end
  end
  
  private
  
  def self.truncated_report(full_report)
    return nil unless full_report
    
    # Create summary instead of full report if too large
    summary = {
      total_allocated: full_report.total_allocated,
      total_retained: full_report.total_retained,
      allocated_objects: full_report.allocated_objects_by_location.first(10),
      retained_objects: full_report.retained_objects_by_location.first(10)
    }
    
    summary
  end
end

Benchmark timing can produce misleading results due to garbage collection interference, system load, and measurement granularity. Address these issues with multiple runs and statistical analysis.

class ReliableBenchmark
  def self.measure_multiple(iterations: 10, warmup: 3, &block)
    # Warmup runs to stabilize JIT and memory allocation
    warmup.times { yield }
    GC.start
    
    measurements = []
    
    iterations.times do
      gc_before = GC.stat
      
      time = Benchmark.measure(&block)
      
      gc_after = GC.stat
      gc_runs = gc_after[:count] - gc_before[:count]
      
      measurements << {
        real_time: time.real,
        cpu_time: time.total,
        gc_runs: gc_runs,
        gc_affected: gc_runs > 0
      }
    end
    
    analyze_measurements(measurements)
  end
  
  private
  
  def self.analyze_measurements(measurements)
    real_times = measurements.map { |m| m[:real_time] }
    cpu_times = measurements.map { |m| m[:cpu_time] }
    gc_affected_count = measurements.count { |m| m[:gc_affected] }
    
    {
      real_time: {
        mean: mean(real_times),
        median: median(real_times),
        std_dev: standard_deviation(real_times),
        min: real_times.min,
        max: real_times.max
      },
      cpu_time: {
        mean: mean(cpu_times),
        median: median(cpu_times)
      },
      gc_interference: {
        affected_runs: gc_affected_count,
        percentage: (gc_affected_count.to_f / measurements.length * 100).round(2)
      },
      reliability_score: calculate_reliability(real_times, gc_affected_count)
    }
  end
  
  def self.calculate_reliability(times, gc_affected)
    cv = coefficient_of_variation(times)  # Lower is better
    gc_penalty = gc_affected.to_f / times.length  # Lower is better
    
    # Score from 0-100, higher is more reliable
    base_score = [100 - (cv * 100), 0].max
    gc_adjusted = base_score * (1 - gc_penalty * 0.5)
    
    gc_adjusted.round(2)
  end
  
  def self.mean(array)
    array.sum.to_f / array.length
  end
  
  def self.median(array)
    sorted = array.sort
    mid = sorted.length / 2
    sorted.length.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
  end
  
  def self.standard_deviation(array)
    avg = mean(array)
    variance = array.sum { |x| (x - avg) ** 2 } / array.length
    Math.sqrt(variance)
  end
  
  def self.coefficient_of_variation(array)
    std_dev = standard_deviation(array)
    avg = mean(array)
    avg == 0 ? 0 : std_dev / avg
  end
end

# Usage with reliability analysis
benchmark_result = ReliableBenchmark.measure_multiple(iterations: 20) do
  array = (1..10000).to_a.shuffle
  array.sort
end

puts "Mean execution time: #{benchmark_result[:real_time][:mean].round(6)}s"
puts "Standard deviation: #{benchmark_result[:real_time][:std_dev].round(6)}s"
puts "Reliability score: #{benchmark_result[:reliability_score]}/100"
puts "GC interference: #{benchmark_result[:gc_interference][:percentage]}% of runs"

Performance & Memory

Benchmark-driven optimization requires systematic measurement of performance changes across different implementation approaches. Establish baseline measurements before optimization attempts and validate improvements with statistical significance.

require 'benchmark/ips'

class PerformanceComparison
  def self.compare_implementations(name, implementations, duration: 5)
    puts "=== #{name} ==="
    
    Benchmark.ips do |x|
      x.config(time: duration, warmup: 2)
      
      implementations.each do |impl_name, impl_block|
        x.report(impl_name, &impl_block)
      end
      
      x.compare!
    end
    
    puts "\nMemory usage comparison:"
    implementations.each do |impl_name, impl_block|
      report = MemoryProfiler.report(&impl_block)
      puts "#{impl_name}: #{report.total_allocated} objects, " \
           "#{report.total_allocated_memsize} bytes"
    end
  end
end

# Compare string concatenation methods
PerformanceComparison.compare_implementations(
  "String Concatenation Methods",
  {
    "String interpolation" => proc { 
      1000.times { |i| "Item number #{i} of 1000" }
    },
    "String concatenation" => proc { 
      1000.times { |i| "Item number " + i.to_s + " of 1000" }
    },
    "Array join" => proc {
      1000.times { |i| ["Item number ", i, " of 1000"].join }
    },
    "String format" => proc {
      1000.times { |i| "Item number %d of 1000" % i }
    }
  }
)

Memory-intensive operations require allocation tracking to identify optimization opportunities. Track object creation patterns and identify areas for object pooling or allocation reduction.

class AllocationTracker
  def self.track_allocations(&block)
    ObjectSpace.trace_object_allocations_start
    
    before_stats = ObjectSpace.count_objects
    before_memsize = ObjectSpace.memsize_of_all
    
    GC.disable
    result = yield
    GC.enable
    
    after_stats = ObjectSpace.count_objects
    after_memsize = ObjectSpace.memsize_of_all
    
    ObjectSpace.trace_object_allocations_stop
    
    allocation_diff = {}
    after_stats.each do |type, count|
      before_count = before_stats[type] || 0
      diff = count - before_count
      allocation_diff[type] = diff if diff > 0
    end
    
    {
      result: result,
      allocations: allocation_diff,
      memory_delta: after_memsize - before_memsize,
      allocation_hotspots: find_allocation_hotspots
    }
  end
  
  private
  
  def self.find_allocation_hotspots
    hotspots = Hash.new(0)
    
    ObjectSpace.each_object do |obj|
      file = ObjectSpace.allocation_sourcefile(obj)
      line = ObjectSpace.allocation_sourceline(obj)
      
      if file && line
        location = "#{File.basename(file)}:#{line}"
        hotspots[location] += 1
      end
    end
    
    hotspots.sort_by { |_, count| -count }.first(10)
  end
end

# Optimize array processing
def process_data_naive(data)
  results = []
  data.each do |item|
    if item[:active]
      processed = {
        id: item[:id],
        name: item[:name].upcase,
        category: item[:category] || 'default',
        timestamp: Time.now
      }
      results << processed
    end
  end
  results
end

def process_data_optimized(data, timestamp = Time.now)
  data.filter_map do |item|
    next unless item[:active]
    
    {
      id: item[:id],
      name: item[:name].upcase,
      category: item[:category] || 'default',
      timestamp: timestamp
    }
  end
end

# Compare allocation patterns
test_data = 10000.times.map do |i|
  {
    id: i,
    name: "item_#{i}",
    category: i % 5 == 0 ? nil : "category_#{i % 3}",
    active: i % 4 != 0
  }
end

puts "Naive implementation:"
naive_result = AllocationTracker.track_allocations do
  process_data_naive(test_data)
end

puts "Optimized implementation:"
optimized_result = AllocationTracker.track_allocations do
  process_data_optimized(test_data)
end

puts "Allocation reduction:"
naive_result[:allocations].each do |type, count|
  optimized_count = optimized_result[:allocations][type] || 0
  reduction = count - optimized_count
  if reduction > 0
    puts "#{type}: #{count} -> #{optimized_count} (-#{reduction})"
  end
end

CPU profiling identifies performance bottlenecks in computational workloads. Use statistical profiling to find methods consuming the most execution time.

require 'ruby-prof'

class CPUProfiler
  def self.profile_cpu_intensive(duration: 10, &block)
    RubyProf.measure_mode = RubyProf::CPU_TIME
    
    RubyProf.start
    start_time = Time.now
    
    result = yield
    
    end_time = Time.now
    profile_result = RubyProf.stop
    
    analyze_cpu_profile(profile_result, end_time - start_time)
  end
  
  private
  
  def self.analyze_cpu_profile(profile_result, wall_time)
    methods_by_cpu_time = []
    
    profile_result.threads.each do |thread_info|
      thread_info.methods.each do |method_info|
        methods_by_cpu_time << {
          method: method_info.full_name,
          self_time: method_info.self_time,
          total_time: method_info.total_time,
          call_count: method_info.called,
          time_per_call: method_info.called > 0 ? 
                          method_info.self_time / method_info.called : 0
        }
      end
    end
    
    methods_by_cpu_time.sort_by! { |m| -m[:total_time] }
    
    total_cpu_time = methods_by_cpu_time.sum { |m| m[:self_time] }
    cpu_utilization = wall_time > 0 ? (total_cpu_time / wall_time * 100) : 0
    
    {
      cpu_utilization: cpu_utilization.round(2),
      wall_time: wall_time,
      cpu_time: total_cpu_time,
      top_methods: methods_by_cpu_time.first(15),
      bottlenecks: identify_bottlenecks(methods_by_cpu_time)
    }
  end
  
  def self.identify_bottlenecks(methods)
    total_time = methods.sum { |m| m[:total_time] }
    threshold = total_time * 0.05  # Methods using >5% of total time
    
    methods.select { |m| m[:total_time] > threshold }
           .map do |m|
      {
        method: m[:method],
        percentage: (m[:total_time] / total_time * 100).round(2),
        total_time: m[:total_time],
        optimization_potential: m[:call_count] > 100 ? 'HIGH' : 'MEDIUM'
      }
    end
  end
end

Production Patterns

Production performance monitoring requires continuous metrics collection with minimal overhead. Implement sampling-based monitoring and asynchronous metric reporting to avoid impacting application performance.

require 'concurrent-ruby'

class ProductionProfiler
  def initialize(sample_rate: 0.01, reporting_interval: 60)
    @sample_rate = sample_rate
    @reporting_interval = reporting_interval
    @metrics = Concurrent::Hash.new
    @metric_queue = Concurrent::Array.new
    @reporter_thread = start_reporter_thread
  end
  
  def monitor_method(object, method_name)
    original_method = object.method(method_name)
    
    object.define_singleton_method(method_name) do |*args, &block|
      if Random.rand <= @sample_rate
        monitor_execution(method_name, original_method, args, &block)
      else
        original_method.call(*args, &block)
      end
    end
  end
  
  def record_custom_metric(name, value, tags = {})
    @metric_queue << {
      name: name,
      value: value,
      tags: tags,
      timestamp: Time.now.to_f
    }
  end
  
  def get_metrics_summary
    @metrics.each_with_object({}) do |(key, data), summary|
      summary[key] = {
        count: data[:count],
        avg_duration: data[:total_time] / data[:count],
        min_duration: data[:min_time],
        max_duration: data[:max_time],
        error_rate: data[:errors].to_f / data[:count] * 100
      }
    end
  end
  
  private
  
  def monitor_execution(method_name, original_method, args, &block)
    start_time = Time.now
    
    begin
      result = original_method.call(*args, &block)
      record_success(method_name, Time.now - start_time)
      result
    rescue StandardError => e
      record_error(method_name, Time.now - start_time, e)
      raise
    end
  end
  
  def record_success(method_name, duration)
    update_metrics(method_name, duration, false)
  end
  
  def record_error(method_name, duration, error)
    update_metrics(method_name, duration, true)
    
    @metric_queue << {
      name: 'method_error',
      value: 1,
      tags: { method: method_name, error_class: error.class.name },
      timestamp: Time.now.to_f
    }
  end
  
  def update_metrics(method_name, duration, is_error)
    @metrics.compute(method_name) do |_, current|
      if current
        {
          count: current[:count] + 1,
          total_time: current[:total_time] + duration,
          min_time: [current[:min_time], duration].min,
          max_time: [current[:max_time], duration].max,
          errors: current[:errors] + (is_error ? 1 : 0)
        }
      else
        {
          count: 1,
          total_time: duration,
          min_time: duration,
          max_time: duration,
          errors: is_error ? 1 : 0
        }
      end
    end
  end
  
  def start_reporter_thread
    Thread.new do
      loop do
        sleep(@reporting_interval)
        report_metrics
      end
    end
  end
  
  def report_metrics
    return if @metric_queue.empty?
    
    metrics_batch = @metric_queue.slice!(0, @metric_queue.length)
    
    # Send to monitoring service (example with HTTP)
    send_to_monitoring_service(metrics_batch)
  rescue StandardError => e
    warn "Failed to report metrics: #{e.message}"
  end
  
  def send_to_monitoring_service(metrics)
    # Example integration with monitoring service
    # In production, use proper HTTP client with retries
    require 'net/http'
    require 'json'
    
    uri = URI(ENV['METRICS_ENDPOINT'] || 'http://localhost:8080/metrics')
    
    Net::HTTP.post(uri, {
      metrics: metrics,
      hostname: Socket.gethostname,
      application: ENV['APP_NAME'] || 'ruby_app'
    }.to_json, 'Content-Type' => 'application/json')
  end
end

# Integration example
class OrderProcessor
  def initialize
    @profiler = ProductionProfiler.new(sample_rate: 0.05)
    @profiler.monitor_method(self, :process_order)
  end
  
  def process_order(order_data)
    validate_order(order_data)
    charge_payment(order_data)
    fulfill_order(order_data)
    send_confirmation(order_data)
    
    @profiler.record_custom_metric('orders_processed', 1, 
                                   { customer_type: order_data[:customer_type] })
  end
  
  private
  
  def validate_order(order_data)
    # Order validation logic
    sleep(0.01)  # Simulate processing time
  end
  
  def charge_payment(order_data)
    # Payment processing
    sleep(0.05)
  end
  
  def fulfill_order(order_data)
    # Fulfillment logic
    sleep(0.02)
  end
  
  def send_confirmation(order_data)
    # Send confirmation email
    sleep(0.01)
  end
end

Application Performance Monitoring integration enables comprehensive production monitoring with distributed tracing and error tracking. Implement correlation IDs and structured logging for request flow analysis.

class APMIntegration
  def initialize(service_name:, version:, environment:)
    @service_name = service_name
    @version = version
    @environment = environment
    @active_spans = {}
  end
  
  def trace_request(request_id, operation_name, &block)
    span_context = {
      request_id: request_id,
      operation: operation_name,
      service: @service_name,
      version: @version,
      environment: @environment,
      start_time: Time.now,
      tags: {}
    }
    
    begin
      result = yield(span_context)
      span_context[:status] = 'success'
      span_context[:end_time] = Time.now
      result
    rescue StandardError => e
      span_context[:status] = 'error'
      span_context[:error] = {
        class: e.class.name,
        message: e.message,
        backtrace: e.backtrace&.first(10)
      }
      span_context[:end_time] = Time.now
      raise
    ensure
      report_span(span_context)
    end
  end
  
  def add_span_tag(request_id, key, value)
    span = @active_spans[request_id]
    span[:tags][key] = value if span
  end
  
  private
  
  def report_span(span_context)
    duration_ms = ((span_context[:end_time] - span_context[:start_time]) * 1000).round(2)
    
    log_entry = {
      timestamp: span_context[:start_time].iso8601(3),
      service: span_context[:service],
      operation: span_context[:operation],
      request_id: span_context[:request_id],
      duration_ms: duration_ms,
      status: span_context[:status],
      tags: span_context[:tags]
    }
    
    log_entry[:error] = span_context[:error] if span_context[:error]
    
    # Structured logging for APM ingestion
    puts log_entry.to_json
    
    # Optional: Send to APM service via HTTP
    send_to_apm_service(log_entry) if production_environment?
  end
  
  def production_environment?
    @environment == 'production'
  end
  
  def send_to_apm_service(span_data)
    # Example APM service integration
    Thread.new do
      begin
        require 'net/http'
        uri = URI(ENV['APM_ENDPOINT'])
        
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = uri.scheme == 'https'
        
        request = Net::HTTP::Post.new(uri)
        request['Content-Type'] = 'application/json'
        request['Authorization'] = "Bearer #{ENV['APM_TOKEN']}"
        request.body = span_data.to_json
        
        response = http.request(request)
        warn "APM report failed: #{response.code}" unless response.code == '200'
      rescue StandardError => e
        warn "APM integration error: #{e.message}"
      end
    end
  end
end

Reference

Benchmark Module

Method Parameters Returns Description
Benchmark.measure &block Benchmark::Tms Measures execution time of block
Benchmark.bm(width=0) width (Integer), &block Array Comparative benchmarking with labels
Benchmark.bmbm(width=0) width (Integer), &block Array Rehearsal and measurement benchmarking
Benchmark.realtime &block Float Returns wall clock time only

Benchmark::Tms Object

Method Returns Description
#real Float Wall clock elapsed time
#utime Float User CPU time
#stime Float System CPU time
#total Float Total CPU time (utime + stime)
#cstime Float System CPU time for child processes
#cutime Float User CPU time for child processes

ObjectSpace Module

Method Parameters Returns Description
ObjectSpace.count_objects result_hash (Hash, optional) Hash Object counts by type
ObjectSpace.memsize_of(obj) obj (Object) Integer Memory size of object in bytes
ObjectSpace.memsize_of_all klass (Class, optional) Integer Total memory usage
ObjectSpace.trace_object_allocations_start None nil Enable allocation tracing
ObjectSpace.trace_object_allocations_stop None nil Disable allocation tracing
ObjectSpace.allocation_sourcefile(obj) obj (Object) String or nil Source file where object allocated
ObjectSpace.allocation_sourceline(obj) obj (Object) Integer or nil Source line where object allocated

Ruby-prof Configuration

Measure Mode Constant Description
Wall Time RubyProf::WALL_TIME Real elapsed time
Process Time RubyProf::PROCESS_TIME CPU time for process
CPU Time RubyProf::CPU_TIME User + system CPU time
Allocations RubyProf::ALLOCATIONS Object allocation count
Memory RubyProf::MEMORY Memory allocation size

Ruby-prof Printers

Printer Class Output Format Use Case
RubyProf::FlatPrinter Flat list Method execution summary
RubyProf::GraphPrinter Call graph Method call relationships
RubyProf::CallTreePrinter Callgrind format Integration with kcachegrind
RubyProf::CallStackPrinter HTML call stack Visual call stack analysis

Memory Profiler Report Methods

Method Returns Description
#total_allocated Integer Total objects allocated
#total_allocated_memsize Integer Total memory allocated in bytes
#total_retained Integer Total objects retained
#total_retained_memsize Integer Total memory retained in bytes
#allocated_objects_by_location Array Allocation locations sorted by count
#retained_objects_by_location Array Retention locations sorted by count
#allocated_objects_by_class Array Allocations grouped by class

GC Statistics

GC.stat Key Type Description
:count Integer Number of GC runs
:heap_allocated_pages Integer Pages allocated to heap
:heap_live_slots Integer Live object slots
:heap_free_slots Integer Free object slots
:major_gc_count Integer Major GC collections
:minor_gc_count Integer Minor GC collections
:total_allocated_objects Integer Total objects allocated
:total_freed_objects Integer Total objects freed

Performance Monitoring Patterns

Pattern Implementation Trade-offs
Sampling Random selection of requests Low overhead, statistical accuracy
Always-on Monitor every request Complete coverage, higher overhead
Threshold-based Monitor slow operations only Focused on problems, may miss patterns
Periodic Regular profiling intervals Scheduled overhead, time-based insights
Event-driven Monitor on specific conditions Targeted analysis, complex triggering logic

Production Monitoring Considerations

Aspect Recommendation Rationale
Sample Rate 1-5% for high-traffic applications Balance insight with performance impact
Metric Retention 90 days for detailed metrics Historical analysis without excessive storage
Alert Thresholds 95th percentile response times Catch performance degradation early
Profiling Duration 30-60 seconds maximum Prevent profiling overhead accumulation
Memory Tracking Enable for staging environments Production memory tracking has high overhead