transaction.rb 3.6 KB
Newer Older
1 2 3 4
module Gitlab
  module Sherlock
    class Transaction
      attr_reader :id, :type, :path, :queries, :file_samples, :started_at,
5
        :finished_at, :view_counts
6

7 8
      # type - The type of transaction (e.g. "GET", "POST", etc)
      # path - The path of the transaction (e.g. the HTTP request path)
9 10 11 12 13 14 15 16 17
      def initialize(type, path)
        @id = SecureRandom.uuid
        @type = type
        @path = path
        @queries = []
        @file_samples = []
        @started_at = nil
        @finished_at = nil
        @thread = Thread.current
18
        @view_counts = Hash.new(0)
19 20
      end

21
      # Runs the transaction and returns the block's return value.
22 23 24
      def run
        @started_at = Time.now

25 26 27
        retval = with_subscriptions do
          profile_lines { yield }
        end
28 29 30 31 32 33

        @finished_at = Time.now

        retval
      end

34
      # Returns the duration in seconds.
35
      def duration
36
        @duration ||= started_at && finished_at ? finished_at - started_at : 0
37 38
      end

39 40 41 42 43
      # Returns the total query duration in seconds.
      def query_duration
        @query_duration ||= @queries.map { |q| q.duration }.inject(:+) / 1000.0
      end

44 45 46 47
      def to_param
        @id
      end

48
      # Returns the queries sorted in descending order by their durations.
49 50 51 52
      def sorted_queries
        @queries.sort { |a, b| b.duration <=> a.duration }
      end

53
      # Returns the file samples sorted in descending order by their durations.
54 55 56 57
      def sorted_file_samples
        @file_samples.sort { |a, b| b.duration <=> a.duration }
      end

58 59 60 61 62
      # Finds a query by the given ID.
      #
      # id - The query ID as a String.
      #
      # Returns a Query object if one could be found, nil otherwise.
63 64 65 66
      def find_query(id)
        @queries.find { |query| query.id == id }
      end

67 68 69 70 71
      # Finds a file sample by the given ID.
      #
      # id - The query ID as a String.
      #
      # Returns a FileSample object if one could be found, nil otherwise.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
      def find_file_sample(id)
        @file_samples.find { |sample| sample.id == id }
      end

      def profile_lines
        retval = nil

        if Sherlock.enable_line_profiler?
          retval, @file_samples = LineProfiler.new.profile { yield }
        else
          retval = yield
        end

        retval
      end

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
      def subscribe_to_active_record
        ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
          next unless same_thread?

          track_query(data[:sql].strip, data[:binds], start, finish)
        end
      end

      def subscribe_to_action_view
        regex = /render_(template|partial)\.action_view/

        ActiveSupport::Notifications.subscribe(regex) do |_, start, finish, _, data|
          next unless same_thread?

          track_view(data[:identifier])
        end
      end

106 107 108 109 110 111
      private

      def track_query(query, bindings, start, finish)
        @queries << Query.new_with_bindings(query, bindings, start, finish)
      end

112 113 114
      def track_view(path)
        @view_counts[path] += 1
      end
115

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
      def with_subscriptions
        ar_subscriber = subscribe_to_active_record
        av_subscriber = subscribe_to_action_view

        retval = yield

        ActiveSupport::Notifications.unsubscribe(ar_subscriber)
        ActiveSupport::Notifications.unsubscribe(av_subscriber)

        retval
      end

      # In case somebody uses a multi-threaded server locally (e.g. Puma) we
      # _only_ want to track notifications that originate from the transaction
      # thread.
      def same_thread?
        Thread.current == @thread
133 134 135 136
      end
    end
  end
end