Showing posts with label profiling. Show all posts
Showing posts with label profiling. Show all posts

Friday, January 11, 2008

Real World Rubinius Performance

Well, I have good and bad news from the Rubinius front. This morning, I built the latest Rubinius from the git repository, and gave LogWatchR another try … and it worked! This is a huge step forward from my perspective, since I’ve had all kinds of wierd failures in the past.

The bad news (well, bad is an overstatement, let’s say ‘not so good news’) is that the performance is pretty bad at this point. To be fair, the Rubinius team is still focused on completeness. I’d expect performance to improve once they turn their eye to it.

Here’s my 30 second recap:

My ‘real world performance’ timings run a small Ruby app called LogWatchR, which scans syslog data for cataloged good and bad patterns. I feed it about 73,000 syslog entries (about 20 minutes worth) that I’ve got archived. I measure the execution time against repeated runs, and find the average run time and standard deviation.

Rubinius’ first successful run clocked in at an average of 143.03 seconds, with a standard deviation of 0.56 seconds.

Given that things took so long, I thought I’d try a quick profiling run too (I only ran against 1000 log entries though, to save time). Here’s the abbreviated version of what the profiler told me:

../shotgun/rubinius -r profile logwatcher.rb < short_log
Total slices: 160, 2680000 clocks
 % time   slices   name
   8.75       14   String#[]
   8.12       13   Hash#[]
   6.88       11   Array#<<
   6.88       11   Object#kind_of?
   5.00        8   #.single_load
   4.38        7   String#split
   4.38        7   Hash#keys
   4.38        7   Regexp#match_from
   3.75        6   Hash#[]=
   2.50        4   String#substring
   2.50        4   Array#replace

Now for the amazing part, the sampling debugger took almost no extra time, clocking in at 3.009 seconds of real time while a regular run took 2.805 seconds. Wow! I’m looking forward to seeing what the Legion of Rubinius Heroes can do with performance over the next couple of months.

If you’re interested in seeing more performance information, you might want to look at:

I’ve also written about profiling if you’d like to read more about that.

Wednesday, December 26, 2007

Real World Performance Profiling

It looks like my post on Ruby 1.9.0 performance is drawing some criticism over on on reddit.

I already updated the original post to deal with a comment by ‘gravity’. In a later comment, ‘Ganryu’ wrote “I don’t get it… isn’t this mainly IO dependent?” Since he doesn’t have access to the code for LogWatchR he can’t do the profiling to find this out, but it’s the same kind of assumption that a lot of people make when they decide to ‘optimize’ code.

LogWatchR runs fast enough for my purposes (it takes less than 15 seconds to analyze 20 minutes worth of log entries), so I’ve never bothered profiling it before. Since it’s come up, I decided to give it a whirl. I cranked up the latest version of ruby-prof and saw the following:

/usr/bin/ruby-prof logwatcher.rb < 20_minute_log
Thread ID: 1075677140
Total: 32.65
 
 %self     total     self     wait    child    calls  name
 39.75     24.43    12.98     0.00    11.45    73607  Array#include?
 35.07     11.45    11.45     0.00     0.00 25329928  String#==
 10.75      3.51     3.51     0.00     0.00    73607  Hash#keys
  2.91      0.95     0.95     0.00     0.00   211698  String#=~
 .
 .
 .

Given that, I feel confident in saying “No, Ganryu, this isn’t mostly IO dependent.” On the other hand, I’m a bit puzzled by the massive number of calls to String#==. There are only two places that I call == directly, they’re both in a single method and it’s only called once per log entry so I would think that I’d only see it about 146,000 times. I guess that it’s being called implicitly by something I’m doing, but I’m not sure what. Sounds like a good investigation for another day.

The moral of this little post? If your code’s not running fast enough for you, profile it before you decide where to ‘fix’ it. You’ll save yourself a lot of time and grief.

Update: Doh! I just realized where that huge number of == is coming from, it's in the Array#include? which is a part of the code I've never been convinced is needed. It looks like I could go a lot faster by getting rid of it, something to remember if I ever need to worry about speed.

If you found this post helpful, you might want to look at my ruby-prof post collection.

Tuesday, December 04, 2007

flog: Profiling Complexity

One tool that I should have included in my survey, but forgot, is flog, yet another great tool from Ryan Davis and Eric Hodel. flog is like a profiler for your code’s complexity instead of it’s performance1.

Why worry about complexity? Well, there are a three good reasons I can think of:

  1. If you’re dealing with legacy code, knowing where the real complexity is will help you prioritize your code reading as you try to figure out the code base
  2. In my experience, the complex little knots of code are where bugs are most likely to lie, so flog can tell you where to focus your testing
  3. Finally, those complex sections of code also become great candidates for refactoring—it’s always easier to debug, optimize, or add features to code that’s easier to understand.

flog is a gem so it’s easy to install, and once installed it’s easy to run. To run it against my LogWatchR tool, I just need to drop into the logwatchr/lib directory and do:

flog logwatchr.rb > flog.report

(Since this generates a pretty length report, I redirected it out to a file.) Here’s the trimmed output from running this:


  Total score = 211.720690020501
   
  WatchR#analyze_entry: (34.2)
     9.8: assignment
     7.0: branch
     4.5: mark_host_last_seen
     3.2: pattern
     2.8: []
     2.8: is_event?
     2.0: alert_type
     2.0: alert_target
     1.8: alert_msg
     1.8: notify
     1.6: event_notify?
     1.3: notify_log
     1.3: join
     1.3: split
     1.3: each
     1.3: now
     1.3: each_value
     1.3: record_host_if_unknown
     0.4: lit_fixnum
  WatchR#event_threshold_reached?: (31.6)
    21.3: []
     2.6: branch
     1.8: tv_sec
     1.6: -
     1.5: length
     1.4: >
     1.4: assignment
     1.3: >=
     1.3: mark_alert_last_seen
     1.3: delete_if
.
.
.

I’m skipping the report on WatchR#analyze_entry because, while its total score is higher than WatchR#event_threshold_reached?, it accumulates points a lot less evenly. The code for WatchR#event_threshold_reached? looks like this:


  def event_threshold_reached?(host, event_type, time)
    @hosts[host][event_type][:alert_last_seen].delete_if { |event_time|
      time.tv_sec - event_time > 
      @hosts[host][event_type][:alert_last_seen_secs]
    } 
    mark_alert_last_seen(event_type, host, time)

    if @hosts[host][event_type][:alert_last_seen].length >=
        @hosts[host][event_type][:alert_last_seen_num]
      true
    else
      false
    end
  end

The report shows a lot of complexity surrounding the hash key lookups. This corresponds to a change I keep meaning to make, but haven’t gotten around to. I think the whole nested hash structure is ugle and hard to maintain, so I’ve been planning on replacing it with a better object structure. It looks like flog agrees with me.

WatchR#event_dependencies_met? (not shown above) also reports a higher level of complexity based on hash traversal, so finally sitting down to make the change from a nested hash would be a win here too.

1 If you were looking for an article on profiling, you might also want to look at these:

Friday, December 01, 2006

Multipart Profiling (and a Rails Profiling Hint)

In a comment on my Profiling and ruby-prof: Getting Specific post, Thorsten asked:

If you profile just a part of your code, is it possible to accumulate over multiple "parts"? For example, I want to profile a Rails action invocation, but I really want to profile 100 invocations. So I want to start/stop the profiling 100 times just around the action invocation and at the end I want to print out a single profile (not 100). Can this be done?

Instead of answering him there, I thought it would be worth devoting a post to the answer, not because it's hard to answer but so that the answer doesn't get lost. (And maybe to help people see how to find things like this for themselves.

Let's start with a little code sample:


require 'profiler'

Profiler__::start_profile
puts "profiling"
ary = (1..10).to_a
puts ary.join(" ")
Profiler__::stop_profile
Profiler__::print_profile($stderr)
Running this generates output like so:

profiling
1 2 3 4 5 6 7 8 9 10
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
100.00     0.01      0.01        1    10.00    10.00  Array#join
  0.00     0.01      0.00        1     0.00     0.00  Enumerable.to_a
  0.00     0.01      0.00        4     0.00     0.00  IO#write
  0.00     0.01      0.00        1     0.00     0.00  Range#each
  0.00     0.01      0.00       10     0.00     0.00  Fixnum#to_s
  0.00     0.01      0.00        2     0.00     0.00  Kernel.puts
  0.00     0.01      0.00        1     0.00    10.00  #toplevel

Now, let's change the code so that we're profiling two different things:


require 'profiler'

Profiler__::start_profile
puts "profiling"
ary = (1..10).to_a
puts ary.join(" ")
Profiler__::stop_profile

puts "not profiling"

Profiler__::start_profile
puts "profiling again"
1.upto(100) { |i| i += 1 }
Profiler__::stop_profile

puts "back out of profiling"
Profiler__::print_profile($stderr)
Which generates:

profiling
1 2 3 4 5 6 7 8 9 10
not profiling
profiling again
back out of profiling
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
100.00     0.01      0.01        1    10.00    10.00  Integer#upto
  0.00     0.01      0.00        1     0.00     0.00  Kernel.puts
  0.00     0.01      0.00        2     0.00     0.00  IO#write
  0.00     0.01      0.00      100     0.00     0.00  Fixnum#+
  0.00     0.01      0.00        1     0.00    10.00  #toplevel

Okay, so the first bunch of profiling data got stomped on. That answers Thorstens question. But maybe not the question he meant to ask. If he's trying to benchmark his code to see how fast it is, the Benchmark library is a much better choice than Profile, Profiler__, and ruby-prof (but not a perfect answer either ... hmm, that seems like another blog post I need to write). If he wants to analyze Rails stuff and not worry about the Ruby code itself, he might want to look at Rails Analyzer by Eric Hodel (and especially at Action Profiler).

If you found this post helpful, you might want to look at my ruby-prof post collection.

Tuesday, October 10, 2006

Improving Ruby Performance, One Library at a Time

I've been looking at performance lately, and several threads are starting to come together for me:

Zed: The whole process is really just the scientific method. Since I have limited information from Ruby about performance I have to just test, evaluate, adjust, and repeat until the measurements improve. What really helps is using statistical tests to confirm that each change made a difference, or at least didn't hurt things. Without these tests I could make changes that seemed to improve things but actually made no difference.
Zed Shaw

Dave: I spend a lot more time thinking about the algorithms than anything else. I use gprof to find the bottlenecks in my code and try to rework the algorithm so that that part of the code gets called less. Then I may try and optimize the code but only in extreme cases. The other tools I really like for C development are gdb and valgrind. For those who don't know, valgrind is a debugging and profiling tool which is particularly good for finding memory related errors in C programs. I usually use it for debugging rather than profiling and I don't know how I lived without it. Unfortunately it doesn't play nice with Ruby as Ruby's garbage collector throws up a lot of red flags so I've had to overcome this by building a pretty large suppression file to get valgrind to ignore all of the Ruby errors. I still worry that I'm also suppressing errors that could be raised by Ferret but it seems to be doing a good job. Another tool I'm really starting to like is gcov which is great for checking test coverage as well as profiling.
Dave Balmain

zenspider: people get so myopically focused on using C to make things faster that they don't bother looking at their algorithms or data-structures. It is sad. Ruby may be slow for method dispatch, but bad code can be slow in ANY language. . . . C doesn't make ruby fast. Avoiding method dispatch makes ruby fast. You can do that using pure ruby quite a bit of the time by applying your noodle.
zenspider

John: What this really tells me is simple ... algorithms matter...
John Duimovich

Ruby isn't the fastest language on the block, but it's fast enough for me. Does that mean it's fast enough? Probably not. There are three main places that Ruby could be improved: in my code, in the libraries that I use, and in the Ruby core. John, zenspider, Dave, and Zed all have some good advice, but it all boils down to John's — algorithms matter, and where they're used matters too. I'm most able to change my own code, but the greatest effect comes from making changes at the most core code we can.

Have you looked at the performance of the libraries you rely on? Maybe you should. If you find ways they could be improved, contribute a patch, or (at least) talk to the implementor. Consider it a call to action. If every Ruby user just made one small improvement, think of the effect it would have on the language as a whole — sure, it costs a bit more, but it's worth it!

If you found this post helpful, you might want to look at my ruby-prof post collection.

Tuesday, August 15, 2006

Profile and ruby-prof: Getting Specific

I'll close up my profiling trilogy with a little bit about profiling parts of your Ruby code. While ruby -r profile my_program or ruby-prof my_program are great for 90% of what you want to do, there's always the odd time that you really only care about one specific portion of your code — for example, if you're doing IO or setup tasks that you don't want to muddle your profile.

The Profiler__ library is the stock Ruby way of doing this, so let's look at this option first. Profiler__ has three methods that will be of interest to us:

  • start_profile — which starts collecting data
  • stop_profile — which stops data collection
  • print_profile — which prints the collected data (it takes a parameter naming the filehandle to which it should print the data)

Say you had an rwb test script that looked like this (yes, I know I've depracated rwb, that doesn't mean I can't use it in a contrived little example):

require 'rwb'
urls = RWB::Builder.new()
urls.add_url(10, "http://localhost")
tests = RWB::Runner.new(urls, 1000, 200)
tests.run
tests.report_header
tests.report_overall

Profiling this would be a nightmare, it relies on net/http and makes several gazillion calls to the Ruby Builtin and Standard Libraries — all these calls would dominate the run and resulting profile, the stuff you really care about (and can fix) would be buried. If I just wanted to profile the reporting functions, I could modify the test script to look like this:


require 'rwb'
require 'profiler'
urls = RWB::Builder.new()
urls.add_url(10, "http://localhost")
tests = RWB::Runner.new(urls, 1000, 200)
tests.run
Profiler__::start_profile
tests.report_header
tests.report_overall
Profiler__::stop_profile
Profiler__::print_profile($stderr)

This generates output like that below (edited to show the % time, cumulative seconds and name fields of the top twenty lines):


  %   cumulative
 time   seconds  name
 40.00     0.22  Array#each
 20.00     0.33  Array#sort
 18.18     0.43  Float#<=>
 10.91     0.49  Array#push
  3.64     0.51  Float#+
  3.64     0.53  Float#**
  1.82     0.54  Float#-
  1.82     0.55  RWB::Runner#report_heade
  0.00     0.55  Float#to_f
  0.00     0.55  RWB::Runner#results_mean
  0.00     0.55  Math.sqrt
  0.00     0.55  Class#new
  0.00     0.55  Fixnum#/
  0.00     0.55  Array#shift
  0.00     0.55  Array#initialize
  0.00     0.55  Array#[]
  0.00     0.55  Kernel.puts
  0.00     0.55  Float#to_s
  0.00     0.55  RWB::Runner#report_overa
  0.00     0.55  Fixnum#+
Which differs considerably from a profile of the entire script created with the -r profile command line option. The first twenty lines are shown here for comparison (edited as above):

  %   cumulative
 time   seconds  name
113.58    18.73  TCPSocket#initialize
 92.06    33.91  Fixnum#==
  7.52    35.15  Kernel.catch
  7.16    36.33  RWB::Runner#run_test
  7.03    37.49  RWB::Builder#get_url
  6.61    38.58  URI.split
  5.82    39.54  Net::HTTP#Proxy
  5.34    40.42  Hash#include?
  5.15    41.27  Kernel.block_given?
  4.79    42.06  URI::HTTP#initialize
  4.49    42.80  URI::Generic#find_proxy
  4.37    43.52  Net::HTTP#new
  4.12    44.20  Kernel.class
  4.06    44.87  Kernel.open
  3.88    45.51  URI::Generic#split_userinfo
  3.82    46.14  OpenURI.open_loop
  3.82    46.77  Net::HTTP#do_start
  3.76    47.39  RWB::Result#initialize
  3.40    47.95  Net::HTTP#initialize
  3.09    48.46  RWB::Runner#run_thread
  3.03    48.96  ENV.include?
(And yes, there is a rounding/floating point/addition error in the second example ... I know it's there, but haven't bothered to dig into the exact cause.)

One thing to watch out for is that Profiler__ will happily overwrite any existing profile data if Profiler__::start_profile gets called multiple times.

ruby-prof provides similar control over what gets profiled — actually, it provides more options. It uses the methods:

  • start — starts a new profiling run
  • stop — stops the current profiling run and returns the data
  • profile — allows you to profile a block passed in to it

Printing is handled a bit differently though, Report objects are created, and then printed through a print method. Each kind of report has it's own Class:

  • RubyProf::FlatPrinter — a traditional profiling report
  • RubyProf::GraphPrinter — a call graph profiling report
  • RubyProf::GraphHtmlPrinter — an html call graph profiling report
Each of these classes has a print method, which takes two (optional) parameters — the output file handle and the minimum %self that a method call must take up to be printed.

We can recreate the above example like this:


require 'rwb'
require 'ruby-prof'
urls = RWB::Builder.new()
urls.add_url(10, "http://localhost")
tests = RWB::Runner.new(urls, 1000, 200)
tests.run
RubyProf.start
tests.report_header
tests.report_overall
result = RubyProf.stop
printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT, 0)

Changing it to print a call graph just means changing the second to last line to this:
printer = RubyProf::GraphPrinter.new(result)

While these may not be the kinds of profiling tasks you do every day, it's nice to know that they're there.

Happy Hacking!

If you found this post helpful, you might want to look at my ruby-prof post collection.

Monday, August 14, 2006

ruby-prof and call graphs

Sorry it's been so long since my last post. I've been on vacation for the last week, and Internet access wasn't a big part of it. I did get a bit of writing done for my book though (one of the perks of insomnia). I promised some more info about ruby-prof, so here it is.

The biggest difference between Profile and ruby-prof is the ability to generate call graphs. A call graph shows not only individual methods and their profiling data, but also the parent and child method calls associated with them. Call graphs can be generated in plain text or html. Unless you need the plain text, the html report is a better choice as it is easier to read and has some cross referencing features built into it.

ruby-prof has an optional -p flag that takes one of three options: flat,which generates a normal profiling report; graph, which generates a plain test call graph report; and graph_html, whichgenerates an html call graph report. Since call graphs are a bit less common than normal profiling data, here's a sample that we can walk through to get a better feel for what can do for you. I generated it like this: ruby-prof -p graph call_rotator test_file. (I'm also trimming the output to show just the top several methods, this report was 146 lines long.)


Thread ID: 537836452
  %total   %self total  self  children        calls Name
---------------------------------------------------------------
                  0.00  0.00    0.00            1/1   <Class::Time>#now
   0.00%   0.00%  0.00  0.00    0.00              1   Time#initialize
---------------------------------------------------------------
                  0.00  0.00    0.00    10011/10011   Comparable#>
   0.00%   0.00%  0.00  0.00    0.00          10011   Time#<=>
---------------------------------------------------------------
                  0.09  0.09    0.00    10011/10011   CallRot#read_line
  28.12%  28.12%  0.09  0.09    0.00          10011   String#split
---------------------------------------------------------------
                  0.00  0.00    0.00          12/12   Array#each
   0.00%   0.00%  0.00  0.00    0.00             12   String#gsub!
---------------------------------------------------------------
                  0.01  0.01    0.00    10011/10011   Class#new
   3.12%   3.12%  0.01  0.01    0.00          10011   OnCall#initialize
---------------------------------------------------------------
                  0.00  0.00    0.00            1/1   Class#new
   0.00%   0.00%  0.00  0.00    0.00              1   Object#initialize
---------------------------------------------------------------
                  0.00  0.00    0.00            3/6   Kernel#require__
                  0.00  0.00    0.00            3/6   Module#attr_reader
   0.00%   0.00%  0.00  0.00    0.00              6   Module#method_added
---------------------------------------------------------------
                  0.00  0.00    0.00            1/1   Kernel#require__
   0.00%   0.00%  0.00  0.00    0.00              1   Module#attr_reader
                  0.00  0.00    0.00            3/6   Module#method_added
---------------------------------------------------------------
                  0.00  0.00    0.00            1/1   Kernel#require
   0.00%   0.00%  0.00  0.00    0.00              1   Kernel#require__
                  0.00  0.00    0.00            3/6   Module#method_added
                  0.00  0.00    0.00            1/1   Module#attr_reader
                  0.00  0.00    0.00            3/3   Class#inherited
---------------------------------------------------------------
                  0.00  0.00    0.00            1/1   Kernel#load
   0.00%   0.00%  0.00  0.00    0.00              1   Kernel#require
                  0.00  0.00    0.00            1/1   Kernel#require__
---------------------------------------------------------------

The first interesting chunk of the report is the third block down. It has two lines, the top line is the calling method, the second line is the method being called. In the call graph reports, the line showing %total and %self values is the method being reported, everything above it is a calling method and everything below it is a method being called. This report shows that String#split takes up a bit over 28% of the programs running time. It is called 10011 times, all of them by CallRot#ReadLine.

A bit further down, is the chunk about Module#method_added. In this case there are two calling methods. Each calls Module#method_added three times, for a total of six calls. This is shown in the calls field, 3/6 in both cases.

Two blocks further down, you can see the report for Kernel#require__, this has one calling and three called methods. Each of the called methods also shows the number of calls made by Kernel#require__ and the total number of calls made.

Every method (calling, called, or reported) is shown with some profiling data: total, self, and children. These fields show the amount of time spent there. Because my profiling run was fairly short, there's not a lot of data to work with. If this were a program that took hours to run, we'd be able to use this information (along with the number of calls made) to identify where we really need to focus our optimization efforts.

There's one more feature of ruby-prof (and Profile for that matter) that I'd still like to cover — controlling what parts of your program are profiled. I'll look at that in another post though. Hopefully this is enough to chew on for one day.

If you found this post helpful, you might want to look at my ruby-prof post collection.

Friday, August 04, 2006

Profile and ruby-prof

I've been playing with profile (from the Standard Library) and ruby-prof (the profile replacement written by Shugo Maeda and Charlie Savage. I really like ruby-prof — it's faster (from 5x on some short runs to 100x on longer runs), it provides more detail, and it will generate call graphs. I've noticed some differences in the way they run. I'll show you what I mean.

ruby-prof doesn't sort it's output in terms of total time used, so you'll need to sort it. After a bit of munging, the (cropped) output of running ruby-prof call_rotator test_file looks something like this:


%self  cumulative total  self  children  calls self/call total/call  name
 23.64     0.13   0.13   0.13  0.00     10002   0.00     0.00     String#split
 21.82     0.30   0.39   0.12  0.27     10002   0.00     0.00     CallRot#read_line
 18.18     0.55   0.54   0.10  0.44         1   0.10     0.54     #foreach
 12.73     0.42   0.07   0.07  0.00     10002   0.00     0.00     #local
  5.45     0.33   0.05   0.03  0.02     10002   0.00     0.00     CallRot#line_is_future?
  3.64     0.44   0.02   0.02  0.00     10003   0.00     0.00     #allocate
  3.64     0.35   0.02   0.02  0.00     20004   0.00     0.00     Array#pop
  3.64     0.17   0.02   0.02  0.00     10002   0.00     0.00     Comparable#>
  3.64     0.15   0.02   0.02  0.00     10002   0.00     0.00     OnCall#initialize
  1.82     0.45   0.01   0.01  0.00         1   0.01     0.01     #open

Since ruby-prof sends it's output to stdout, it's easy to munge with standard tools (at least on Unix/Linus). It does mean that you'll need to be careful about comingling any output from your original program though. I used ruby-prof call-rotator test_file | tail +3 | sort -rn to print the methods in order of time spent in them. Since the header doesn't change, you can add that back in to make reading the table easier if you like.

Interestingly, ruby-prof runs so quickly that system activity can interfere quite a bit with the recorded timings (even to the point of changing the order of the sorted methods). Over the course of a longer running program, this should even out. I've not seen it change the order of methods actually defined in your program, but that doesn't mean it won't.

Here's the output of ruby -r profile call_rotator test_file:


  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 34.79     2.31      2.31    10002     0.23     0.42  CallRot#read_line
 13.40     3.20      0.89        1   890.00  6630.00  IO#foreach
 12.95     4.06      0.86    10002     0.09     0.15  CallRot#line_is_future?
 11.14     4.80      0.74    10003     0.07     0.11  Class#new
  7.98     5.33      0.53    10002     0.05     0.07  Comparable.>
  5.42     5.69      0.36    10002     0.04     0.04  OnCall#initialize
  4.67     6.00      0.31    20004     0.02     0.02  Array#pop
  3.61     6.24      0.24    10002     0.02     0.02  String#split
  3.46     6.47      0.23    10002     0.02     0.02  Time#local
  2.41     6.63      0.16    10002     0.02     0.02  Time#<=>

You can see that the ruby-prof output has two columns that are missing from the stock profile output. self and children break the total time into more discrete measures. self shows how much time is spent in the method itself, exclusive calls out to child methods. children shows how much time is spent in calls to those child methods. (total is the same as self in the stock output.

It's also interesting that the two methods of profiling don't pick up the same set of method calls in the program being profiled, nor is one a superset of the other. Here's a side by side comparison of the methods found by each in profiling runs of another program:


  ruby-prof                  profile
Array#<<                   Array#<<     
Array#[]     Array#[]     
Array#each     Array#each     
<Class::IO>#allocate    
<Class::IO>#open    
<Class::Time>#allocate    
<Class::Time>#now    
Enumerable#each_with_index Enumerable.each_with_index   
File#initialize     File#initialize     
Fixnum#*     Fixnum#*        
Fixnum#%     Fixnum#%        
Fixnum#+     Fixnum#+        
Fixnum#to_s     Fixnum#to_s     
Integer#downto     Integer#downto     
Integer#upto     Integer#upto     
                           IO#open   
IO#puts      IO#puts       
IO#write     IO#write       
Kernel#load     Time#-         
Time#-      Time#+       
Time#+      Time#day       
Time#day     Time#hour        
Time#hour     Time#initialize      
Time#initialize     Time#min       
Time#min     Time#mon         
Time#mon     Time#now         
Time#year     Time#year        
#toplevel     #toplevel         

The four methods not caught by profile take up significant amounts of time, but won't change your basic profiling approach. Likewise the one method missed by ruby-prof isn't a game breaker, but it's good to be aware of things like this.

In both sets of output, you can see that the read_line is the more expensive of the methods, and the one where you can focus if you need to speed things up. The ruby-prof output also shows you that more time is spent in its children than locally, so you gains will be minimized. You'll get more information from a call graph, but that's whole 'nother blog entry — I'll get around to it next time.

If you found this post helpful, you might want to look at my ruby-prof post collection.