# How to Profile Ruby Code

Ruby makes it easy to write code that works, but harder to write code that works fast. Performance bottlenecks can lurk anywhere in your application, and trying to guess where they are usually wastes more time than it saves.

The Ruby ecosystem includes several profiling tools that show you exactly where your program spends its time. These tools track method calls, measure execution duration, and reveal the relationships between different parts of your code.

This article walks you through Ruby profiling from basic timing to advanced performance analysis techniques.

[ad-logs]

## Prerequisites

Before continuing, make sure you have a recent version of Ruby (version 3.0 or higher) installed on your local machine. This guide assumes you're already comfortable with basic Ruby concepts and gem management.

## Step 1 — Getting started with Ruby profiling

For the best learning experience, set up a fresh Ruby project to experiment directly with the concepts introduced in this tutorial.

Begin by creating a new directory and setting up your project:

```command
mkdir ruby-profiling && cd ruby-profiling
```

```command
bundle init
```

Let's start with a common recursive algorithm you'll use throughout this article to demonstrate different profiling techniques - the Fibonacci sequence.

Create a file named `main.rb` with the following content:

```ruby
[label main.rb]
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
  result = fibonacci(30)
  puts "Fibonacci(30) = #{result}"
end
```

This method calculates the n-th Fibonacci number recursively. While simple to understand, this implementation has exponential time complexity - it recalculates the same Fibonacci numbers repeatedly, making it inefficient for larger values of n.

Let's see just how inefficient it is with some manual timing:

```ruby
[label main.rb]
[highlight]
require 'time'
[/highlight]

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
[highlight]
  start_time = Time.now
  result = fibonacci(30)
  end_time = Time.now
  
  puts "Fibonacci(30) = #{result}"
  puts "Time taken: #{(end_time - start_time).round(4)} seconds"
[/highlight]
end
```

Here, you use Ruby's built-in `Time` class to measure manually precisely how long the recursive Fibonacci calculation takes.

Run your script with the following command:

```command
ruby main.rb
```

You'll see output like:

```text
[output]
Fibonacci(30) = 832040
Time taken: 0.0785 seconds
```

While this basic timing approach tells us how long the method takes to run, it doesn't provide insights into what's happening inside it.

For complex applications with many methods, measuring total execution time doesn't help identify bottlenecks. This is where profiling comes in.

## Step 2 — Basic profiling with the built-in ruby-prof gem

While manual timing gives us a general idea of overall execution time, it has several limitations:

- It only shows total execution time, not where time is spent within the method
- No call count information. We can't see how many times each method was called
- We don't see the relationships between method calls
- You need to add timing code around every method you want to measure

This is where Ruby's profiling tools become invaluable. The `ruby-prof` gem provides detailed insights without requiring you to modify your code extensively.

Let's use `ruby-prof` to analyze our Fibonacci method. First, add it to your Gemfile:

```ruby
[label Gemfile]
source "https://rubygems.org"

[highlight]
gem "ruby-prof", "~> 1.7"
[/highlight]
```

Install the gem:

```command
bundle install
```

Now let's use `ruby-prof` to analyze our Fibonacci method in a new version of `main.rb`:

```ruby
[label main.rb]
[highlight]
require 'ruby-prof'
[/highlight]

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
[highlight]
  # Create a new profiler instance
  profiler = RubyProf::Profile.new
  
  # Start profiling
  profiler.start
  
  # Run the code we want to profile
  result = fibonacci(30)
  puts "Fibonacci(30) = #{result}"
  
  # Stop profiling and get results
  profiling_result = profiler.stop
  
  # Print a flat profile to stdout
  printer = RubyProf::FlatPrinter.new(profiling_result)
  printer.print(STDOUT, min_percent: 1)
[/highlight]
end
```

In the highlighted blocks, you first require the `ruby-prof` gem for gathering detailed performance data. Next, you create a profiler instance using `RubyProf::Profile.new` and start capturing execution metrics with `profiler.start`. With profiling active, you run the `fibonacci(30)` method to capture detailed execution data. Once the method finishes executing, you stop the profiler with `profiler.stop`.

Finally, you use `RubyProf::FlatPrinter` to process and format the profiling data, outputting methods that consume at least 1% of execution time to quickly identify the most time-consuming parts of your code.

Run this script:

```command
ruby main.rb
```

You should observe output similar to the following:

```text
[output]
Fibonacci(30) = 832040
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.707144
Sort by: self_time
 %self      total      self      wait     child     calls  name                           location
 71.76      1.707     1.225     0.000     0.482  2692537  *Object#fibonacci               main.rb:3
 11.70      0.200     0.200     0.000     0.000  2692537   Integer#<=                     
 10.46      0.179     0.179     0.000     0.000  2692536   Integer#-                      
  6.08      0.104     0.104     0.000     0.000  1346268   Integer#+                      
```

This output reveals something striking - our fibonacci method was called **2,692,537 times** to calculate the 30th Fibonacci number!

The profiling data shows more than just our custom method. Notice that the basic arithmetic operations (`Integer#<=`, `Integer#-`, and `Integer#+`) also appear in the profile. This happens because our recursive implementation triggers millions of these operations - each method call needs to check if `n <= 1`, subtract values for the recursive calls, and add the results together.

Let's understand what each column in the output means:

- `%self`: The percentage of time spent by this method relative to the total time in the entire program
- `total`: The total time spent by this method and its children
- `self`: The time spent by this method
- `wait`: The time this method spent waiting for other threads
- `child`: The time spent by this method's children
- `calls`: The number of times this method was called
- `name`: The name of the method
- `location`: The location of the method

The asterisk (`*`) next to `*Object#fibonacci` indicates this is a recursively called method. Ruby-prof tracks both the total number of calls and marks methods that call themselves.

With this information, you can immediately identify algorithm inefficiencies, prioritize your efforts, and make data-driven decisions about where to focus your performance tuning work.

The profile data clearly shows that our naive recursive implementation is extremely inefficient due to the millions of redundant method calls - not just to our fibonacci method, but also to basic Ruby operations.

However, working with the profiling data this way can still be cumbersome, especially for larger applications. Let's make profiling easier with a reusable solution.


## Step 3 — Creating a reusable profiling decorator

While directly using `ruby-prof` works well for one-off profiling, you'll often want to profile multiple methods in real projects. Instead of repeating the same profiling code, let's create a reusable decorator that can be applied to any method.

Create a new file named `profiler.rb`:

```ruby
[label profiler.rb]
require 'ruby-prof'

module Profiler
  def self.profile(description = nil)
    profiler = RubyProf::Profile.new
    profiler.start
    
    # Call the provided block
    result = yield
    
    # Stop profiling
    profiling_result = profiler.stop
    
    # Format and print results
    puts "\n" + "="*50
    puts "Profile: #{description}" if description
    puts "="*50
    
    printer = RubyProf::FlatPrinter.new(profiling_result)
    printer.print(STDOUT, min_percent: 1)
    
    result
  end
end
```

This module wraps any block of code with profiling logic, making it easy to profile any part of your application. The `profile` method accepts an optional description and a block, then measures the block's execution while providing formatted output.

For our Fibonacci example, you can now wrap the calculation in a clean profiling block:

```ruby
[label main.rb]
[highlight]
require_relative 'profiler'
[/highlight]

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

[highlight]
if __FILE__ == $0
  Profiler.profile("Fibonacci calculation") do
    result = fibonacci(30)
    puts "Fibonacci(30) = #{result}"
  end
end
[/highlight]
```

Now you can add profiling to any code block with a single method call. Run this script:

```command
ruby main.rb
```

You'll see the same detailed profiling information as before, but now formatted with a clear header:

![Screenshot of the output](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/4d475ec9-ac77-45f4-118b-55aa5e2cab00/orig =1754x1340)


```text
[output]
Fibonacci(30) = 832040

==================================================
Profile: Fibonacci calculation
==================================================
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.675167
Sort by: self_time

 %self      total      self      wait     child     calls  name                           location
 72.17      1.675     1.209     0.000     0.466  2692537  *Object#fibonacci               main.rb:3
 11.44      0.192     0.192     0.000     0.000  2692537   Integer#<=                     
 10.38      0.174     0.174     0.000     0.000  2692536   Integer#-                      
  6.01      0.101     0.101     0.000     0.000  1346268   Integer#+                      
```

You now have a reusable profiling tool that can be applied to any code block with a single line. This approach is much more maintainable for larger projects where you need to profile different parts of your codebase.


## Step 4 — Saving profile data to files

To analyze profiling data more thoroughly or share it with team members, we should enhance our profiler to save data to files. Let's modify our `profiler.rb`:

```ruby
[label profiler.rb]
require 'ruby-prof'

[highlight]
module Profiler
  def self.profile(description = nil, output_file = nil)
    profiler = RubyProf::Profile.new
    profiler.start
    
    # Call the provided block
    result = yield
    
    # Stop profiling
    profiling_result = profiler.stop
    
    # Print formatted results to console
    puts "\n" + "="*50
    puts "Profile: #{description}" if description
    puts "="*50
    
    printer = RubyProf::FlatPrinter.new(profiling_result)
    printer.print(STDOUT, min_percent: 1)
    
    # Save to file if requested
    if output_file
      File.open(output_file, 'w') do |file|
        printer = RubyProf::FlatPrinter.new(profiling_result)
        printer.print(file)
      end
      puts "\nProfile data saved to #{output_file}"
    end
    
    result
  end
end
[/highlight]
```

Here, you've updated the `profile` method to include an optional `output_file` parameter, enabling you to specify a file location for saving profiling results.

You've also introduced new file-saving functionality. The added `if output_file` block writes profiling data to the specified file using `RubyProf::FlatPrinter.print()`. Afterward, a confirmation message informs you that the file was created successfully.

This enhancement allows you to save profiling data in a text format for later analysis. Now, let's update your `main.rb` file to take advantage of this new feature:

```ruby
[label main.rb]
require_relative 'profiler'

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
[highlight]
  Profiler.profile("Fibonacci calculation", "fibonacci.prof") do
    result = fibonacci(30)
    puts "Fibonacci(30) = #{result}"
  end
[/highlight]
end
```

In the highlighted line, you've used the enhanced `profile` method with the new `output_file` argument.

This modification instructs the profiler to save profiling results directly into a text file named `fibonacci.prof`

Now, run your script to generate and save the profiling data:

```command
ruby main.rb
```

After the script runs, you'll receive a confirmation message indicating the profile data was successfully saved to this file:

```text
[output]
==================================================
Profile: Fibonacci calculation
==================================================
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.729846
Sort by: self_time

 %self      total      self      wait     child     calls  name                           location
 71.97      1.730     1.245     0.000     0.485  2692537  *Object#fibonacci               main.rb:3
 11.69      0.202     0.202     0.000     0.000  2692537   Integer#<=                     
 10.29      0.178     0.178     0.000     0.000  2692536   Integer#-                      
  6.05      0.105     0.105     0.000     0.000  1346268   Integer#+                      

* recursively called methods

...
Profile data saved to fibonacci.prof
```

You can verify the file exists:

```command
ls -l fibonacci.prof
```

```text
[output]
-rw-r--r--  1 stanley  staff  1234 Sep 24 15:30 fibonacci.prof
```

This `.prof` file contains all your profiling data in a text format. The benefit of saving to a file is that you can:

1. Analyze profiles offline without re-running the code
2. Compare profiles from different runs
3. Share profile data with team members
4. Use external tools to visualize and analyze the data

Saving profile data becomes especially valuable when profiling production systems or long-running processes where you can't quickly examine the immediate console output.



## Step 5 — Visualizing profile data with stackprof

With the profile data saved to a file, you can visualize it using specialized tools. One of the most popular options is [stackprof](https://github.com/tmm1/stackprof), which creates interactive graphical representations of Ruby profiling data.

First, let's switch to using stackprof for better visualization support. Add it to your Gemfile:

```ruby
[label Gemfile]
source "https://rubygems.org"

gem "ruby-prof", "~> 1.7"
[highlight]
gem "stackprof", "~> 0.2"
[/highlight]
```

```command
bundle install
```

Now let's create a version that uses stackprof for data collection and visualization:

```ruby
[label main.rb]
[highlight]
require 'stackprof'
[/highlight]

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
[highlight]
  # Profile using stackprof with raw data for flamegraphs
  StackProf.run(mode: :cpu, raw: true, out: 'fibonacci_stackprof.dump') do
    result = fibonacci(30)
    puts "Fibonacci(30) = #{result}"
  end
[/highlight]
end
```

This creates a binary dump file that can be analyzed with stackprof's command-line tools. The `raw: true` option is essential for generating visual flamegraphs.

Run this script:

```command
ruby main.rb
```

You'll see:

```text
[output]
Fibonacci(30) = 832040
```

Now you can analyze the generated dump file using stackprof's built-in report generator:

```command
bundle exec stackprof fibonacci_stackprof.dump --text
```

This command generates a text-based report showing your profiling data:

```text
[output]
==================================
  Mode: cpu(1000)
  Samples: 36 (0.00% miss rate)
  GC: 0 (0.00%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
        35  (97.2%)          35  (97.2%)     Object#fibonacci
         1   (2.8%)           1   (2.8%)     IO#write
        36 (100.0%)           0   (0.0%)     block in <main>
        36 (100.0%)           0   (0.0%)     StackProf.run
        36 (100.0%)           0   (0.0%)     <main>
         1   (2.8%)           0   (0.0%)     IO#puts
         1   (2.8%)           0   (0.0%)     Kernel#puts
```

For more detailed analysis, you can generate an interactive D3 flamegraph by redirecting the output to an HTML file:

```command
bundle exec stackprof fibonacci_stackprof.dump --d3-flamegraph > fibonacci_flamegraph.html
```

Now open the generated HTML file in your browser:

![Screenshot of the Fibonacci framegraph in the browser](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/fb8677dd-7c32-4bbc-8155-bd53d1891b00/lg1x =3248x1994)

The flamegraph displays an interactive visualization where you can hover over different sections to see method details, click to zoom into specific call paths, and use the search functionality to find particular methods. The width of each rectangle represents the proportion of execution time, and the vertical stacking shows the call depth.

For the recursive Fibonacci function, you'll see a deep tower of identical `Object#fibonacci` calls, clearly illustrating why this algorithm is so inefficient. Each level represents another recursive call, and you can visually see how the same calculations are repeated millions of times.

The interactive features include a reset zoom button, clear search functionality, and detailed information panels that update as you explore different parts of the call tree. This visual representation makes it immediately obvious where performance bottlenecks occur and how method calls relate to each other during execution.


## Final thoughts
Profiling removes the guesswork from Ruby performance tuning. Tools like `ruby-prof` and `stackprof` reveal where your application spends its time and highlight the methods most worth optimizing. With these insights integrated into your workflow, performance improvements become a focused, data-driven process instead of trial and error.