Favorite

Do you find yourself writing puts statements all throughout your code - only to check which chunks are burning the most time (or memory)?

puts '*' * 300
start_time = Time.now
# your actual code
time_elapsed = Time.now - start_time
puts "Time elapsed is: #{time_elapsed}"
puts '*' * 300

Do you also find yourself manually recording these values from your terminal to later include in a PR explaining how you’ve improved code performance?

Further, how often do you reach for one of half-a-dozen different performance profiling tools to get simple measurements?

If one or more of these is you - then I think you’re going to like my nifty little gem I’ve just open-sourced: SchwadPerformanceLogger I’ve used it myself for years and reckoned it’s time to share it with my Ruby Friends.

So what does it do?

This gem allows you to log memory and time usage throughout your code quickly- and on your terms. It also gives you very quick access to basic features of a handful of other (fantastic) performance tracking tools.

Logging time and memory

In your code:

pl = SPL.new({full_memo: 'Check extract method refactoring'})

In your terminal:

**********************************************************************
Starting initialization. Current memory: 12(Mb), difference of 0
(mb) since beginning and difference of 0 since last log. time
passed: 0.004678 seconds, time since last run: 0.004678
**********************************************************************

The object is now ‘live’. Anywhere in your file call the object again with a memo of your choice:

pl.log_performance("Test memo")
*********************************************************************
Starting Test memo. Current memory: 12(Mb), difference of 0 (mb)
since beginning and difference of 0 since last log. time passed:
22.493993 seconds, time since last run: 9.616874
*********************************************************************

As you can see you can track the time since the object was last called, same with memory. You also get a running track of the total time elapsed and memory grown.

(NOTE: Remember Ruby is garbage collected, so hopefully memory doesn’t go up forever - if it does this gem will help you debug it!)

These logs can be written to the terminal, their own logfile, and even a CSV file in real time so you don’t have to write them down.

Options:

full_memo option adds an extra header in the log outputs as well as a header to each new set of csv outputs. This is not to be confused with the ‘per-run’ message passed to #log_performance which is only passed to that check.

To disable any of the outputs:

SPL.new({puts: false, log: false, csv: false})

To have the logger ‘pause’ a number of seconds during the puts logging so that you can actually see the log as it goes by. This does not affect the ‘time’ measurement:

SPL.new({pause: 8})

Further Profiling Tools

As well as logging memory and time throughout your code, SPL gives you easy access to frequently used popular profiling tools to inspect your code blocks. Note - if you want to a deep dive on a particular area of profiling I recommend visiting the gems listed below and giving them a whirl.

IPS

Handy access to Benchmark-ips measurements, just pass a block to ips:

SPL.ips do
   ary = []
   35.times do
     ary << (1..99).to_a.sample
   end
end

#=> #<Benchmark::IPS::Report:0x00007fbc7f91df50 @entries=[#<Benchmark::IPS::Report::Entry:0x00007fbc7e0c3bd0 @label="PerformanceLogMethod", @microseconds=5002798.0, @iterations=34020, @stats=#<Benchmark::IPS::Stats::SD:0x00007fbc7e0c3c48 @mean=6805.780564500376, @error=195>, @measurement_cycle=630, @show_total_time=true>], @data=nil>

Time

Same flow as above. Tired of writing out start_time and Time.now - start_time and also needing to ‘puts’ it out? Pass a block to #time. Runs ten times and spits out an average as well.

SPL.time do
   ary = []
   35.times do
     ary << (1..99).to_a.sample
   end
end

#=> Average runtime 0.0002649 seconds. Max time 0.000508.seconds

Allocate Count

Before, you would have to enable the GC before your code, use ObjectSpace to count objects before your code, then use it again after your code to compare allocated objects during your block of code. You’d also have to re-enable the GC! Gosh, that sure is a lot of work if you want to do this frequently. We make it simple.

SPL.allocate_count do
   ary = []
   35.times do
     ary << (1..99).to_a.sample
   end
end

#=> {:FREE=>-121, :T_STRING=>50, :T_ARRAY=>36, :T_IMEMO=>35}

Profile Memory

Gives you quick access to the amazing memory_profiler gem.

SPL.profile_memory do
   ary = []
   35.times do
     ary << (1..99).to_a.sample
   end
end

# Total allocated: 37576 bytes (36 objects)
# Total retained:  0 bytes (0 objects)
#
# allocated memory by gem
# -----------------------------------
#      37576  other
#
# allocated memory by file
# -----------------------------------
#      37576  (irb)
#
# allocated memory by location
# -----------------------------------
#      37240  (irb):37
#        336  (irb):35
#
# allocated memory by class
# -----------------------------------
#      37576  Array
#
# allocated objects by gem
# -----------------------------------
#         36  other
#
# allocated objects by file
# -----------------------------------
#         36  (irb)
#
# allocated objects by location
# -----------------------------------
#         35  (irb):37
#          1  (irb):35
#
# allocated objects by class
# -----------------------------------
#         36  Array

All Objects

Similarly, it’s nice to get a rundown of all objects, in hash format, instead of goofing around with ObjectSpace manually we offer that up for you as well.

SPL.all_objects do
  ary = []
  35.times do
    ary << (1..99).to_a.sample
  end
end

#=> [[Benchmark::IPS::Job, 1], [Rational, 1], [Benchmark::IPS::Report::Entry, 1], [Benchmark::IPS::Stats::SD, 1], [FFI::DynamicLibrary, 1], [DidYouMean::ClassNameChecker, 1], [Thread::Backtrace, 1], [NameError::message, 1], [NameError, 1], [#<Class:0x00007fbc7e816478>, 1], [Gem::Platform, 1], [IRB::Notifier::CompositeNotifier, 1], [IRB::Notifier::NoMsgNotifier, 1], [Enumerator, 1], [RubyToken::TkSPACE, 1], [FFI::Type::Mapped, 1], [IRB::ReadlineInputMethod, 1].... etc

Objects by Size

You can break down your objects by size as well, useful for debugging.

SPL.objects_by_size do
  ary = []
  35.times do
    ary << (1..99).to_a.sample
  end
end

#=> {:T_OBJECT=>101848, :T_CLASS=>730344, :T_MODULE=>76808, :T_FLOAT=>240, :T_STRING=>882168, :T_REGEXP=>200350, :T_ARRAY=>714384, :T_HASH=>150408, :T_STRUCT=>800, :T_BIGNUM=>80, :T_FILE=>1160, :T_DATA=>1074338, :T_MATCH=>28280, :T_COMPLEX=>40, :T_RATIONAL=>40, :T_SYMBOL=>5080, :T_IMEMO=>325040, :T_ICLASS=>3280, :TOTAL=>4294688}

That’s it!

If there’s more tooling you think I should add, or you want to help out and chuck in a PR, you are more than welcome to join the discussion on the repo thanks for reading!

Stay in Touch

I like to write about Ruby and building things, typically once every month or so. Get an email when I have written something new.