#! /usr/bin/env ruby # First require ruby-prof require 'ruby-prof' # Now setup option parser require 'ostruct' require 'optparse' module RubyProf # == Synopsis # # Profiles a Ruby program. # # == Usage # ruby-prof [options] [--] [profiled-script-command-line-options] # # Options: # -p, --printer=printer Select a printer: # flat - Prints a flat profile as text (default). # graph - Prints a graph profile as text. # graph_html - Prints a graph profile as html. # call_tree - format for KCacheGrind # call_stack - prints a HTML visualization of the call tree # dot - Prints a graph profile as a dot file # multi - Creates several reports in output directory # -m, --min_percent=min_percent The minimum percent a method must take before # being included in output reports. # This option is not supported for call tree. # -f, --file=path Output results to a file instead of standard out. # --mode=measure_mode Select what ruby-prof should measure: # wall - Wall time (default). # process - Process time. # allocations - Object allocations (requires patched Ruby interpreter). # memory - Allocated memory in KB (requires patched Ruby interpreter). # -s, --sort=sort_mode Select how ruby-prof results should be sorted: # total - Total time # self - Self time # wait - Wait time # child - Child time # --allow_exceptions Raise exceptions encountered during profiling (true) or suppress them (false) # -R, --require-noprof=lib require a specific library (not profiled) # -E, --eval-noprof=code execute the ruby statements (not profiled) # --exclude=methods A comma separated list of methods to exclude. # Specify instance methods via # (Integer#times) # Specify class methods via . (Integer.superclass) # --exclude-common Remove common methods from the profile # -h, --help Show help message # -v, --version version Show version (1.1.0) class Cmd # :enddoc: attr_accessor :options attr_reader :profile def initialize setup_options parse_args load_pre_libs load_pre_execs end def setup_options @options = OpenStruct.new options.printer = RubyProf::FlatPrinter options.measure_mode = RubyProf::WALL_TIME options.min_percent = 0 options.file = nil options.allow_exceptions = false options.exclude_common = false options.exclude = Array.new options.pre_libs = Array.new options.pre_execs = Array.new end # This is copied from ActiveSupport: def constantize(camel_cased_word) if !camel_cased_word.include?("::") Object.const_get(camel_cased_word) else names = camel_cased_word.split("::") # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? # Remove the first blank element in case of '::ClassName' notation. names.shift if names.size > 1 && names.first.empty? names.inject(Object) do |constant, name| if constant == Object constant.const_get(name) else candidate = constant.const_get(name) next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) # Go down the ancestors to check if it is owned directly. The check # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject(constant) do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) const end # owner is in Object, so raise constant.const_get(name, false) end end end end def option_parser OptionParser.new do |opts| opts.banner = "ruby_prof #{RubyProf::VERSION}\n" + "Usage: ruby-prof [options] [--] [profiled-script-command-line-options]" opts.separator "" opts.separator "Options:" opts.on('-p printer', '--printer=printer', [:flat, :flat_with_line_numbers, :graph, :graph_html, :call_tree, :call_stack, :dot, :multi], 'Select a printer:', ' flat - Prints a flat profile as text (default).', ' graph - Prints a graph profile as text.', ' graph_html - Prints a graph profile as html.', ' call_tree - format for KCacheGrind', ' call_stack - prints a HTML visualization of the call tree', ' dot - Prints a graph profile as a dot file', ' multi - Creates several reports in output directory' ) do |printer| case printer when :flat options.printer = RubyProf::FlatPrinter when :graph options.printer = RubyProf::GraphPrinter when :graph_html options.printer = RubyProf::GraphHtmlPrinter when :call_tree options.printer = RubyProf::CallTreePrinter when :call_stack options.printer = RubyProf::CallStackPrinter when :dot options.printer = RubyProf::DotPrinter when :multi options.printer = RubyProf::MultiPrinter end end opts.on('-m min_percent', '--min_percent=min_percent', Float, 'The minimum percent a method must take before ', ' being included in output reports.', ' This option is not supported for call tree.') do |min_percent| options.min_percent = min_percent end opts.on('-f path', '--file=path', 'Output results to a file instead of standard out.') do |file| options.file = file options.old_wd = Dir.pwd end opts.on('--mode=measure_mode', [:process, :wall, :allocations, :memory], 'Select what ruby-prof should measure:', ' wall - Wall time (default).', ' process - Process time.', ' allocations - Object allocations (requires patched Ruby interpreter).', ' memory - Allocated memory in KB (requires patched Ruby interpreter).') do |measure_mode| case measure_mode when :wall options.measure_mode = RubyProf::WALL_TIME when :process options.measure_mode = RubyProf::PROCESS_TIME when :allocations options.measure_mode = RubyProf::ALLOCATIONS when :memory options.measure_mode = RubyProf::MEMORY end end opts.on('-s sort_mode', '--sort=sort_mode', [:total, :self, :wait, :child], 'Select how ruby-prof results should be sorted:', ' total - Total time', ' self - Self time', ' wait - Wait time', ' child - Child time') do |sort_mode| options.sort_method = case sort_mode when :total :total_time when :self :self_time when :wait :wait_time when :child :children_time end end opts.on_tail("-h", "--help", "Show help message") do puts opts exit end opts.on_tail("-v version", "--version", "Show version (#{RubyProf::VERSION})") do puts "ruby_prof " + RubyProf::VERSION exit end opts.on('--allow_exceptions', 'Raise exceptions encountered during profiling (true) or suppress them (false)') do options.allow_exceptions = true end opts.on('-R lib', '--require-noprof=lib', 'require a specific library (not profiled)') do |lib| options.pre_libs << lib end opts.on('-E code', '--eval-noprof=code', 'execute the ruby statements (not profiled)') do |code| options.pre_execs << code end opts.on('--exclude=methods', String, 'A comma separated list of methods to exclude.', ' Specify instance methods via # (Integer#times)', ' Specify class methods via . (Integer.superclass)') do |exclude_string| exclude_string.split(',').each do |string| match = string.strip.match(/(.*)(#|\.)(.*)/) klass = constantize(match[1]) if match[2] == '.' klass = klass.singleton_class end method = match[3].to_sym options.exclude << [klass, method] end end opts.on('--exclude-common', 'Remove common methods from the profile') do options.exclude_common = true end end end def parse_args # Make sure the user specified at least one file if ARGV.length < 1 and not options.exec puts self.option_parser puts "" puts "Must specify a script to run" exit(-1) end self.option_parser.parse! ARGV if options.printer.needs_dir? options.file ||= "." options.old_wd ||= Dir.pwd if !File.directory?(options.file) puts "'#{options.file}' is not a directory" puts "#{options.printer} needs an existing directory path to put profiles under." exit(-1) end end rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, OptionParser::MissingArgument => e puts self.option_parser puts e.message exit(-1) end def load_pre_libs options.pre_libs.each do |lib| require lib end end def load_pre_execs options.pre_execs.each do |exec| eval(exec) end end def run script = ARGV.shift @profile = Profile.new(options.to_h) options.exclude.each do |klass, method| @profile.exclude_method!(klass, method) end profile.profile do load script end end end end # Parse command line options cmd = RubyProf::Cmd.new # Install at_exit handler. It is important that we do this # before loading the scripts so our at_exit handler run # *after* any other one that will be installed. at_exit { # Create a printer printer = cmd.options.printer.new(cmd.profile) printer_options = {:min_percent => cmd.options.min_percent, :sort_method => cmd.options.sort_method} # Get output if cmd.options.file # write it relative to the dir they *started* in, as it's a bit surprising to write it in the dir they end up in. Dir.chdir(cmd.options.old_wd) do if printer.class.needs_dir? printer.print(printer_options.merge(:path => cmd.options.file)) else File.open(cmd.options.file, 'w') do |file| printer.print(file, printer_options) end end end else # Print out results printer.print(STDOUT, printer_options) end } # Now profile some code cmd.run