97f8c6279f
This makes it easier to see if a problem is caused by slow queries or slow Ruby code (unrelated to any SQL queries that might be used).
136 lines
3.6 KiB
Ruby
136 lines
3.6 KiB
Ruby
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?
|
|
|
|
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
|
|
|
|
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
|