aboutsummaryrefslogtreecommitdiffstats
path: root/tools/profile
blob: d38ba1dd8482fdc125974cee67e1e079384aaf8b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env ruby
# frozen_string_literal: true

# Profile require calls giving information about the time and the files that are called
# when loading the provided file.
#
# Example:
#   tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer]
ENV["NO_RELOAD"] ||= "1"
ENV["RAILS_ENV"] ||= "development"

module CodeTools
  class Profiler
    Error = Class.new(StandardError)

    attr_reader :path, :mode
    def initialize(path, mode = nil)
      assert_ruby_file_exists(path)
      @path, @mode = path, mode
      require "benchmark"
    end

    def profile_requires
      GC.start
      before_rss = `ps -o rss= -p #{Process.pid}`.to_i

      if mode
        require "ruby-prof"
        RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
        RubyProf.start
      else
        Object.instance_eval { include RequireProfiler }
      end

      elapsed = Benchmark.realtime { require path }
      results = RubyProf.stop if mode

      GC.start
      after_rss = `ps -o rss= -p #{Process.pid}`.to_i

      if mode
        if printer = ARGV.shift
          puts "RubyProf outputting to stderr with printer #{printer}"
          RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
        elsif RubyProf.const_defined?(:CallStackPrinter)
          filename = "#{File.basename(path, '.rb')}.#{mode}.html"
          puts "RubyProf outputting to #{filename}"
          File.open(filename, "w") do |out|
            RubyProf::CallStackPrinter.new(results).print(out)
          end
        else
          filename = "#{File.basename(path, '.rb')}.#{mode}.callgrind"
          puts "RubyProf outputting to #{filename}"
          File.open(filename, "w") do |out|
            RubyProf::CallTreePrinter.new(results).print(out)
          end
        end
      end

      RequireProfiler.stats.each do |file, depth, sec|
        if sec
          puts "%8.1f ms  %s%s" % [sec * 1000, " " * depth, file]
        else
          puts "#{' ' * (13 + depth)}#{file}"
        end
      end
      puts "%8.1f ms  %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
    end

    private
      def assert_ruby_file_exists(path)
        fail Error.new("No such file") unless File.exist?(path)
        fail Error.new("#{path} is a directory") if File.directory?(path)
        ruby_extension = File.extname(path) == ".rb"
        ruby_executable = File.open(path, "rb") { |f| f.readline } =~ [/\A#!.*ruby/]
        fail Error.new("Not a ruby file") unless ruby_extension || ruby_executable
      end

      module RequireProfiler
        private
          def require(file, *args) RequireProfiler.profile(file) { super } end
          def load(file, *args) RequireProfiler.profile(file) { super } end

          @depth, @stats = 0, []
          class << self
            attr_accessor :depth
            attr_accessor :stats

            def profile(file)
              stats << [file, depth]
              self.depth += 1
              result = nil
              elapsed = Benchmark.realtime { result = yield }
              self.depth -= 1
              stats.pop if stats.last.first == file
              stats << [file, depth, elapsed] if result
              result
            end
          end
      end
  end
end
# ruby-prof printer name causes the third arg to be sent :classify
#   which is probably overkill if you already know the name of the ruby-prof
#   printer you want to use, e.g. Graph
begin
  require "active_support/inflector"
  require "active_support/core_ext/string/inflections"
rescue LoadError
  STDERR.puts $!.message
  class String
    # File activesupport/lib/active_support/inflector/methods.rb, line 150
    def classify
      # strip out any leading schema name
      camelize(sub(/.*\./, ""))
    end
    # File activesupport/lib/active_support/inflector/methods.rb, line 68
    def camelize(uppercase_first_letter = true)
      string = self
      if uppercase_first_letter
        string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
      else
        string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
      end
      string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
    end
  end
end
if $0 == __FILE__
  if (filename = ARGV.shift)
    path = File.expand_path(filename)
    mode = ARGV.shift
    CodeTools::Profiler.new(path, mode).profile_requires
  else
    STDERR.puts "No file path entered. Usage is tools/profile path/to/file.rb [ruby-prof mode] [ruby-prof printer]"
  end
end