We need to write a test to detect possible memory leaks in some Rails code. The test itself is written in Ruby.
Testing GETs
I simply put the URLs in an array:
uris = %w(/admin /users)
Then I make GET calls using the net API that comes with Ruby:
uris.each do |uri| Net::HTTP.get(uri) end
To determine the virtual memory used by Rails, execute:
memory_check = `ps -eo vsz,args | grep script/server` m = memory_check.match(/(\d+)/) virtual = m[0].to_i
If you don’t manually start the server, replace the string after grep with
the name of your process.
Testing POSTs
Operations that modify data are usually more involved to set up because the POST
data is sent in the body of the request and also because there may be constraints
in the database. We, therefore, modify the datastructure to include the POST data
separately from the URI:
uris = ["/admin",
["/admin/users", {"client_id" => "387", "email"=>"test@mycompany.com", "permissions_select"=>"standard" },
Proc.new { |hash| hash["email"] = "test#{Time.now.to_i.to_s}@mycompany.com"} ]]
We have an Array now. The first element will be still the URI. The second element contains
the submit data – Rails style. Finally, we specify a Proc object to modify the data
in a way that lets us send the request multiple times without getting constraint or
validation errors. The example Proc above ensures that each email is unique by using
the current time to build it.
Then in the code we inspect the first element of each URI to detect whether it’s a GET
or a POST:
uris.each do |uri|
post = false
case uri
when String
full_uri = uri
when Array
# POST with a hash of values
full_uri = uri[0]
post = true
end
# test code here
end
Before submitting, we check the post flag and act accordingly. We invoke the supplied
Proc object passing in the Hash containing the POST data for modification:
url = URI.parse full_uri
Net::HTTP.get(url) if !post
if post
# let's customize
uri[2].call(uri[1])
Net::HTTP.post_form(url, uri[1])
end
end
Hitting each URL multiple times
The memory test is only realistic with many hits. For this, we introduce a variable
called @how_many into our mini-framework that the caller can set.
uris = [ "/admin" ]
module Net
class HTTP
class << self
attr_accessor :prefix
attr_accessor :how_many
def fetch(uris)
data = []
uris.each do |uri|
post = false
case uri
when String
full_uri = prefix + uri
when Array
# POST with a hash of values
full_uri = prefix + uri[0]
post = true
end
url = URI.parse full_uri
@how_many.times do
Net::HTTP.get(url) if !post
if post
# let's customize
uri[2].call(uri[1])
Net::HTTP.post_form(url, uri[1])
end
memory_check = `ps -eo vsz,args | grep script/server`
m = memory_check.match(/(\d+)/)
virtual = m[0].to_i
end
end
end
end
end
end
Net::HTTP.how_many = 50
Net::HTTP.prefix = 'http://localhost:3000'
Net::HTTP.fetch(uris)
We’ve also added a prefix to the URI so that we can test against any host/port
combination.
Data collection and graph
We use an array to store the memory used after each hit.
def fetch(uris) data = [] # processing goes here memory_check = `ps -eo vsz,args | grep script/server` m = memory_check.match(/(\d+)/) virtual = m[0].to_i data << virtual # more processing end
Once we looped through all URIs and hit them as many times as specified, we have an array that we
can use the draw a graph. gruff is well-suited for this purpose. First we need to
install ImageMagick, then we implement a module to encapsulate graphing code:
require 'gruff'
module Net
module Grapher
def draw_graph(title, url_count, times, data)
puts 'drawing graph!'
g = Gruff::Line.new
g.title = title
g.data("#{url_count.to_s}/#{times.to_s}", data)
g.labels = {0 => '0', 40 => '40', data.size => "#{data.size}"}
file_name = "#{title.gsub(/\//, '')}_#{url_count.to_s}_#{times.to_s}.png"
File.delete(file_name) if File.exists? file_name
g.write(file_name)
end
end
class HTTP
extend Grapher
# more code here
end
end
The draw_grap method is available now. It takes a title, and some statistical information.
The last argument is the Array itself containing the data. Since the number of hits
and the number of URIs is important to evaluate the graph, we use that information
to construct the name of the file name:
file_name = "#{title.gsub(/\//, '')}_#{url_count.to_s}_#{times.to_s}.png"
Running the test
Let’s repeat the code in its entirety:
#!/usr/local/bin/ruby
require 'gruff'
uris = ["/admin",
["/admin/projects", {"admin_project[change_id]"=>"0",
"admin_project[new_project_support_contact_attributes][][work_phone]"=>"",
"admin_project[new_project_support_contact_attributes][][last_name]"=>"",
"admin_project[new_project_support_contact_attributes][][first_name]"=>"",
"admin_project[new_project_support_contact_attributes][][mobile_phone]"=>"209-346-4294",
"admin_project[client_id]"=>"387",
"admin_project[description]"=>"",
"admin_project[project_name]"=>"This is test4448",
"commit"=>"Save",
"client_id"=>"387"}, Proc.new { |hash| hash["admin_project[project_name]"] = "This is test#{Time.now.to_i.to_s}" } ]
]
module Net
module Grapher
def draw_graph(title, url_count, times, data)
puts 'drawing graph!'
g = Gruff::Line.new
g.title = title
g.data("#{url_count.to_s}/#{times.to_s}", data)
g.labels = {0 => '0', 40 => '40', data.size => "#{data.size}"}
file_name = "#{title.gsub(/\//, '')}_#{url_count.to_s}_#{times.to_s}.png"
File.delete(file_name) if File.exists? file_name
g.write(file_name)
end
end
class HTTP
extend Grapher
class << self
attr_accessor :prefix
attr_accessor :how_many
def fetch(uris)
data = []
uris.each do |uri|
post = false
case uri
when String
full_uri = prefix + uri
when Array
# POST with a hash of values
full_uri = prefix + uri[0]
post = true
end
url = URI.parse full_uri
@how_many.times do
Net::HTTP.get(url) if !post
if post
# let's customize
uri[2].call(uri[1])
Net::HTTP.post_form(url, uri[1])
end
memory_check = `ps -eo vsz,args | grep script/server`
m = memory_check.match(/(\d+)/)
virtual = m[0].to_i
data << virtual
end
end
draw_graph(uris[0], uris.size, @how_many, data)
end
end
end
end
Net::HTTP.how_many = 50
Net::HTTP.prefix = 'http://localhost:3000'
Net::HTTP.fetch(uris)
Make sure the file has the x flag set so it’s executable. If we save the code to
a file called test_me.rb and run ./test_me.rb, we get a nice graph like this:
br>

Evaluation
This test was run on a Linux machine with 4G of memory. We hit 5 URLs, each a 100 times. The difference between the highest
and lowest memory usage is less than 4K so we see a nice garbage collection pattern.
No memory leak so far!

