#! /usr/bin/env ruby

# First require ruby-prof
require 'rubygems'
require 'ruby-prof'

# Now setup option parser
require 'ostruct'
require 'optparse'

module RubyProf
  # == Synopsis
  #
  # Profiles a Ruby program.
  #
  # == Usage
  #
  # ruby_prof [options] <script.rb> [--] [script-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 reports.
  #
  #  -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
  #                                   memory - Allocated memory
  #
  #  -s, --sort=sort_mode           Select how ruby-prof results should be sorted:
  #                                   total - Total time
  #                                   self - Self time
  #                                   wait - Wait time
  #                                   child - Child time
  #
  #  --replace-progname             Replace $0 when loading the .rb files.
  #
  #  --specialized-instruction      Turn on specified instruction.
  #
  #  -v                             Show version, set $VERBOSE to true, profile script if option given
  #
  #  -d                             Set $DEBUG to true
  #
  #  -R, --require-noprof lib       Require a specific library (not profiled)
  #
  #  -E, --eval-noprof code         Execute the ruby statements (not profiled)
  #
  #  -x, --exclude regexp           Exclude methods by regexp (see method elimination)
  #
  #  -X, --exclude-file file        Exclude methods by regexp listed in file (see method elimination)
  #
  #  --exclude-common-cycles        Make common iterators like Integer#times appear inlined
  #
  #  --exclude-common-callbacks     Make common callbacks invocations like Integer#times appear inlined so you can see call origins in graph
  #
  #  -h, --help                     Show help message
  #
  #  --version                      Show version
  #
  #
  class Cmd
    # :enddoc:
    attr_accessor :options

    def initialize
      setup_options
      parse_args

      load_pre_libs
      load_pre_execs
    end

    def setup_options
      @options = OpenStruct.new
      options.printer = RubyProf::FlatPrinter
      options.min_percent = 0
      options.file = nil
      options.replace_prog_name = false
      options.specialized_instruction = false

      options.pre_libs = Array.new
      options.pre_execs = Array.new
    end

    def option_parser
      OptionParser.new do |opts|
        opts.banner = "ruby_prof #{RubyProf::VERSION}\n" +
            "Usage: ruby-prof [options] <script.rb> [--] [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("--replace-progname", "Replace $0 when loading the .rb files.") do
          options.replace_prog_name = true
        end

        if defined?(RubyVM)
          opts.on("--specialized-instruction", "Turn on specified instruction.") do
            options.specialized_instruction = true
          end
        end

        opts.on_tail("-h", "--help", "Show help message") do
          puts opts
          exit
        end

        opts.on_tail("--version", "Show version #{RubyProf::VERSION}") do
          puts "ruby_prof " + RubyProf::VERSION
          exit
        end

        opts.on("-v","Show version, set $VERBOSE to true, profile script if option given") do
          puts "ruby version: " + [RUBY_PATCHLEVEL, RUBY_PLATFORM, RUBY_VERSION].join(' ')
          $VERBOSE = true
        end

        opts.on("-d", "Set $DEBUG to true") do
          $DEBUG = 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('-x regexp', '--exclude regexp', 'exclude methods by regexp (see method elimination)') do |meth|
          options.eliminate_methods ||= []
          options.eliminate_methods << Regexp.new(meth)
        end

        opts.on('-X file', '--exclude-file file', 'exclude methods by regexp listed in file (see method elimination)') do|file|
          options.eliminate_methods_files ||= []
          options.eliminate_methods_files << file
        end

        opts.on('--exclude-common-cycles', 'make common iterators like Integer#times appear inlined') do |meth|
          options.eliminate_methods ||= []
          options.eliminate_methods += %w{
            Integer#times
            Integer#upto
            Integer#downto
            Enumerator#each
            Enumerator#each_with_index
            Enumerator#each_with_object

            Array#each
            Array#each_index
            Array#reverse_each
            Array#map

            Hash#each
            Hash#each_pair
            Hash#each_key
            Hash#each_value

            Range#each
            Enumerable#each_cons
            Enumerable#each_entry
            Enumerable#each_slice
            Enumerable#each_with_index
            Enumerable#each_with_object
            Enumerable#reverse_each
            Enumerable#inject
            Enumerable#collect
            Enumerable#reduce
          }
          #TODO: may be the whole Enumerable module should be excluded via 'Enumerable#.*', we need feedback on use cases.
        end

        opts.on('--exclude-common-callbacks', 'make common callbacks invocations like Integer#times appear inlined so you can see call origins in graph') do|meth|
          options.eliminate_methods ||= []
          options.eliminate_methods += %w{
            Method#call
            Proc#call
            ActiveSupport::Callbacks::ClassMethods#__run_callback
          }
        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
      # Get the script we will execute
      script = ARGV.shift
      if options.replace_prog_name
        $0 = File.expand_path(script)
      end

      # Set VM compile option
      if defined?(RubyVM)
        RubyVM::InstructionSequence.compile_option = {
            :trace_instruction => true,
            :specialized_instruction => options.specialized_instruction
        }
      end

      # Set the measure mode
      RubyProf.measure_mode = options.measure_mode if options.measure_mode
      RubyProf.start_script(script)
    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 {
  # Stop profiling
  result = RubyProf.stop

  # Eliminate unwanted methods from call graph
  if cmd.options.eliminate_methods
    result.eliminate_methods!(cmd.options.eliminate_methods)
  end

  if cmd.options.eliminate_methods_files
    cmd.options.eliminate_methods_files.each {|f| result.eliminate_methods!(f)}
  end

  # Create a printer
  printer = cmd.options.printer.new(result)
  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
