A Guide: Debugging and Fixing a Memory Leak in Ruby on Rails

Introduction

Memory leaks can be a nightmare to debug, especially in large-scale applications. They can cause the application to become slower over time and eventually crash. As a Ruby on Rails developer, I recently encountered a memory leak issue in one of my applications, and in this post, I will share my experience on how I found and fixed it.

Debugging with Memory Profiler Gem

When I first noticed the issue, I suspected that it might be caused by an old gem called Virtus, which had been deprecated for a couple of years. To confirm my suspicion, I used the Memory Profiler gem, which is a great tool for tracking memory usage in Ruby applications.

Originally we thought the memory leak might have originated in the HTTP client that communicates with an external API. So, we wrapped the code in a memory profiler block and ran a test multiple times. It appeared that Virtus was holding on to the most memory, but the amount was actually insignificant. Despite removing the gem, the memory usage continued to increase over time.

require 'memory_profiler'

report = MemoryProfiler.report do
	def request(http_method: :get, ...)
		...
	end
end

report.pretty_print

At one point during our investigation, we also tried wrapping various parts of our code in AppSignal instruments to help identify the memory leak. However, this approach didn't prove completely fruitful because we were looking in the wrong places.

Finding the Real Culprit

Stefan and I were actually pair programming on a completely different issue when he stumbled upon a possible solution to the memory leak problem. While we were brainstorming different ideas Stefan was searching through old GitHub issues when he stumbled upon one from March 2014 that sounded similar to our problem. After digging deeper, we found out that the issue had been fixed just 10 hours before he found the thread. It turned out to be the prepend_view_path that was causing the leak.

The reason why it’s hard to find these is because it’s an application running as a CMS with each business having its own themes. That’s why we’ve used the prepend view path.

Fixing the Issue

With Stefan's programmatic approach to problem solving and his experience with Rails, we were able to find a solution to the memory leak issue in our application. Stefan discovered an article that showed how to patch the prepend_view_path method, which was causing the memory leak. After applying the patch, the issue was resolved.

RESOLVER_CACHE = Concurrent::Map.new

def prepend_view_path(path)
  resolver = RESOLVER_CACHE.fetch_or_store(path) do
    ActionView::FileSystemResolver.new(path)
  end

  # <https://github.com/rails/rails/issues/14301#issuecomment-771651933>
  resolver.clear_cache unless ActionView::Resolver.caching?
  super(resolver)
end

Conclusion

Finding and fixing a memory leak in a Ruby on Rails application can be a challenging task, but with the right tools and knowledge, it can be achieved. However, tools like the memory profiling gem are helpful, but are often limited to looking at specific pieces of code or endpoints, which can make it difficult to track down elusive memory leaks. If you’d hit a different endpoint than the troublemaker, it is almost impossible to find this problem. Ultimately, it was Stefan's thorough research that led to the discovery of the real culprit and a solution to the problem. It is important for developers to stay up-to-date with the latest tools and best practices to ensure that their applications are performant and scalable.