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.
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:
mkdir ruby-profiling && cd ruby-profiling
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:
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:
require 'time'
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
if __FILE__ == $0
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"
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:
ruby main.rb
You'll see output like:
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:
source "https://rubygems.org"
gem "ruby-prof", "~> 1.7"
Install the gem:
bundle install
Now let's use ruby-prof
to analyze our Fibonacci method in a new version of main.rb
:
require 'ruby-prof'
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
if __FILE__ == $0
# 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)
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:
ruby main.rb
You should observe output similar to the following:
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 programtotal
: The total time spent by this method and its childrenself
: The time spent by this methodwait
: The time this method spent waiting for other threadschild
: The time spent by this method's childrencalls
: The number of times this method was calledname
: The name of the methodlocation
: 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
:
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:
require_relative 'profiler'
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
if __FILE__ == $0
Profiler.profile("Fibonacci calculation") do
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
end
end
Now you can add profiling to any code block with a single method call. Run this script:
ruby main.rb
You'll see the same detailed profiling information as before, but now formatted with a clear header:
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
:
require 'ruby-prof'
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
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:
require_relative 'profiler'
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
if __FILE__ == $0
Profiler.profile("Fibonacci calculation", "fibonacci.prof") do
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
end
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:
ruby main.rb
After the script runs, you'll receive a confirmation message indicating the profile data was successfully saved to this file:
==================================================
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:
ls -l fibonacci.prof
-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:
- Analyze profiles offline without re-running the code
- Compare profiles from different runs
- Share profile data with team members
- 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, 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:
source "https://rubygems.org"
gem "ruby-prof", "~> 1.7"
gem "stackprof", "~> 0.2"
bundle install
Now let's create a version that uses stackprof for data collection and visualization:
require 'stackprof'
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
if __FILE__ == $0
# 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
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:
ruby main.rb
You'll see:
Fibonacci(30) = 832040
Now you can analyze the generated dump file using stackprof's built-in report generator:
bundle exec stackprof fibonacci_stackprof.dump --text
This command generates a text-based report showing your profiling data:
==================================
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:
bundle exec stackprof fibonacci_stackprof.dump --d3-flamegraph > fibonacci_flamegraph.html
Now open the generated HTML file in your browser:
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.