summaryrefslogtreecommitdiff
path: root/lib/gitlab/sherlock/line_profiler.rb
blob: b5f9d04004725e3e75c5f309a9d0d1ed3b0e4e43 (plain)
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
module Gitlab
  module Sherlock
    # Class for profiling code on a per line basis.
    #
    # The LineProfiler class can be used to profile code on per line basis
    # without littering your code with Ruby implementation specific profiling
    # methods.
    #
    # This profiler only includes samples taking longer than a given threshold
    # and those that occur in the actual application (e.g. files from Gems are
    # ignored).
    class LineProfiler
      # The minimum amount of time that has to be spent in a file for it to be
      # included in a list of samples.
      MINIMUM_DURATION = 10.0

      # Profiles the given block.
      #
      # Example:
      #
      #     profiler = LineProfiler.new
      #
      #     retval, samples = profiler.profile do
      #       "cats are amazing"
      #     end
      #
      #     retval  # => "cats are amazing"
      #     samples # => [#<Gitlab::Sherlock::FileSample ...>, ...]
      #
      # Returns an Array containing the block's return value and an Array of
      # FileSample objects.
      def profile(&block)
        if mri?
          profile_mri(&block)
        else
          raise NotImplementedError,
            'Line profiling is not supported on this platform'
        end
      end

      # Profiles the given block using rblineprof (MRI only).
      def profile_mri
        require 'rblineprof'

        retval  = nil
        samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield }

        file_samples = aggregate_rblineprof(samples)

        [retval, file_samples]
      end

      # Returns an Array of file samples based on the output of rblineprof.
      #
      # lineprof_stats - A Hash containing rblineprof statistics on a per file
      #                  basis.
      #
      # Returns an Array of FileSample objects.
      def aggregate_rblineprof(lineprof_stats)
        samples = []

        lineprof_stats.each do |(file, stats)|
          source_lines = File.read(file).each_line.to_a
          line_samples = []

          total_duration = microsec_to_millisec(stats[0][0])
          total_events   = stats[0][2]

          next if total_duration <= MINIMUM_DURATION

          stats[1..-1].each_with_index do |data, index|
            next unless source_lines[index]

            duration = microsec_to_millisec(data[0])
            events   = data[2]

            line_samples << LineSample.new(duration, events)
          end

          samples << FileSample
            .new(file, line_samples, total_duration, total_events)
        end

        samples
      end

      private

      def microsec_to_millisec(microsec)
        microsec / 1000.0
      end

      def mri?
        RUBY_ENGINE == 'ruby'
      end
    end
  end
end