summaryrefslogtreecommitdiff
path: root/lib/gitlab/sherlock/transaction.rb
blob: 400a552bf998ce5ed2519ac80ed540d006826cfd (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
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
138
module Gitlab
  module Sherlock
    class Transaction
      attr_reader :id, :type, :path, :queries, :file_samples, :started_at,
        :finished_at, :view_counts

      # type - The type of transaction (e.g. "GET", "POST", etc)
      # path - The path of the transaction (e.g. the HTTP request path)
      def initialize(type, path)
        @id = SecureRandom.uuid
        @type = type
        @path = path
        @queries = []
        @file_samples = []
        @started_at = nil
        @finished_at = nil
        @thread = Thread.current
        @view_counts = Hash.new(0)
      end

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

        retval = with_subscriptions do
          profile_lines { yield }
        end

        @finished_at = Time.now

        retval
      end

      # Returns the duration in seconds.
      def duration
        @duration ||= started_at && finished_at ? finished_at - started_at : 0
      end

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

      def to_param
        @id
      end

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

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

      # 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.
      def find_query(id)
        @queries.find { |query| query.id == id }
      end

      # 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.
      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

      def subscribe_to_active_record
        ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
          next unless same_thread?

          unless data.fetch(:cached, data[:name] == 'CACHE')
            track_query(data[:sql].strip, data[:binds], start, finish)
          end
        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

      private

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

      def track_view(path)
        @view_counts[path] += 1
      end

      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
      end
    end
  end
end