Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
112: Memory Profiling with memory_profiler
I've been staring at a production log for a report generator I wrote a few months ago. Every time it runs, the memory usage of the worker process spikes aggressively. It doesn't actually "leak" in the sense that the memory never comes back, but the sheer volume of allocations is triggering the Garbage Collector (GC) so often that the whole process slows to a crawl. I can tell it's happening, but I can't tell where. I don't want to guess and check; I want to see the actual numbers.
Why is this report eating my RAM?
Here is the snippet of code I'm worried about. It takes a list of transactions and generates a formatted summary string. It looks innocent enough, but when the input size hits 10,000 records, it starts acting up.
def generate_report(transactions)
summary = ""
transactions.each do |t|
summary += "Transaction #{t[:id]}: #{t[:amount]} USD on #{t[:date]}\n"
end
summary
end
# Simulating a larger dataset
data = Array.new(10_000) do |i|
{ id: i, amount: rand(1..1000), date: "2023-01-01" }
end
generate_report(data)
If you run this, it finishes quickly. But in a real app, doing this inside a loop or with larger datasets creates a mountain of temporary objects. I suspect the += on the string is the culprit, but let's actually prove it using the memory_profiler gem. I'm not going to trust my gut here.
Bringing in the Profiler
First, I'll grab the gem: gem install memory_profiler. Now, instead of just calling the method, I'll wrap it in a MemoryProfiler.report block. This is the magic part—it tracks every single object allocation that happens inside that block.
require 'memory_profiler'
report = MemoryProfiler.report do
generate_report(data)
end
report.pretty_print
When I run this, the output is massive. It's a wall of text, but there are two specific sections I always look at first: allocated memory and retained memory. Allocated is everything created during the block; retained is what stayed in memory after the block finished. In this case, the "allocated" section is screaming.
Reading the Map
I scroll down to the "allocated memory by location" section. I see something like this:
# Location: report_gen.rb:3
# 10001 objects, 450.2 KB allocated
# String (10001)
There it is. Line 3 is where I'm doing the string interpolation and concatenation. I'm creating over 10,000 string objects. Wait, why 10,000? I'm only returning one final summary string.
Ah, right. In Ruby, summary += "..." doesn't just append to the existing string. It creates a brand new string object by combining the old summary and the new fragment, then throws the old summary object away. As the report grows, I'm creating increasingly larger strings and abandoning them for the GC to clean up. It's an O(n²) disaster in terms of memory allocation.
Cutting the Waste
Now that I have the evidence, I'll switch to a more efficient way of building strings. I'll use an array to collect the fragments and then join them at the end. This is a classic Ruby pattern, but seeing the profiler confirm the waste makes me feel much better about the change.
def generate_report(transactions)
fragments = []
transactions.each do |t|
fragments < < "Transaction #{t[:id]}: #{t[:amount]} USD on #{t[:date]}"
end
fragments.join("\n")
end
I'll run the profiler again. The number of allocated strings drops significantly because I'm no longer re-creating the entire report body on every single iteration. I'm just creating the small fragments and one final joined string. The "allocated memory" section now shows a much leaner profile, and the execution time is snappier because the GC isn't panicking.
My takeaway here? Don't optimize based on a "feeling" that a loop is slow. Wrap the suspect code in MemoryProfiler.report, look at the "allocated memory by location" list, and target the line that's churning through the most objects.
📋 Practical Task
Exercise: Optimizing the CSV Data Parser
You have been given a method that parses a mock CSV string into a list of hashes. Currently, it's creating a massive amount of duplicate string objects for the keys in every single row, which is bloating memory usage during large imports.
require 'memory_profiler'
def parse_csv(csv_text)
lines = csv_text.split("\n")
headers = lines.shift.split(",")
lines.map do |line|
values = line.split(",")
row = {}
headers.each_with_index do |header, index|
row[header] = values[index]
end
row
end
end
# Mock CSV data: 5000 rows with the same 3 headers
csv_data = "name,email,city\n" + (["John Doe,john@example.com,New York\n"] * 5000).join
report = MemoryProfiler.report do
parse_csv(csv_data)
end
report.pretty_print
Your Goal:
- Run the code and observe the "allocated memory" section. Notice how many
Stringobjects are being created for the keys (the headers). - Modify the
parse_csvmethod to ensure that the header strings are reused across all row hashes rather than being re-allocated or processed in a way that creates duplicates. (Hint: Look into how you handle theheadersarray or consider usingString#-@to freeze strings). - Run the profiler again and verify that the total number of allocated objects has decreased.
There are no comments for now.