diff options
Diffstat (limited to 'lib/gitlab/sherlock')
-rw-r--r-- | lib/gitlab/sherlock/collection.rb | 51 | ||||
-rw-r--r-- | lib/gitlab/sherlock/file_sample.rb | 33 | ||||
-rw-r--r-- | lib/gitlab/sherlock/line_profiler.rb | 100 | ||||
-rw-r--r-- | lib/gitlab/sherlock/line_sample.rb | 38 | ||||
-rw-r--r-- | lib/gitlab/sherlock/location.rb | 28 | ||||
-rw-r--r-- | lib/gitlab/sherlock/middleware.rb | 43 | ||||
-rw-r--r-- | lib/gitlab/sherlock/query.rb | 112 | ||||
-rw-r--r-- | lib/gitlab/sherlock/transaction.rb | 140 |
8 files changed, 0 insertions, 545 deletions
diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb deleted file mode 100644 index ce3a376cf75..00000000000 --- a/lib/gitlab/sherlock/collection.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - # A collection of transactions recorded by Sherlock. - # - # Method calls for this class are synchronized using a mutex to allow - # sharing of a single Collection instance between threads (e.g. when using - # Puma as a webserver). - class Collection - include Enumerable - - def initialize - @transactions = [] - @mutex = Mutex.new - end - - def add(transaction) - synchronize { @transactions << transaction } - end - - alias_method :<<, :add - - def each(&block) - synchronize { @transactions.each(&block) } - end - - def clear - synchronize { @transactions.clear } - end - - def empty? - synchronize { @transactions.empty? } - end - - def find_transaction(id) - find { |trans| trans.id == id } - end - - def newest_first - sort { |a, b| b.finished_at <=> a.finished_at } - end - - private - - def synchronize(&block) - @mutex.synchronize(&block) - end - end - end -end diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb deleted file mode 100644 index 5d10d8c4877..00000000000 --- a/lib/gitlab/sherlock/file_sample.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - class FileSample - attr_reader :id, :file, :line_samples, :events, :duration - - # file - The full path to the file this sample belongs to. - # line_samples - An array of LineSample objects. - # duration - The total execution time in milliseconds. - # events - The total amount of events. - def initialize(file, line_samples, duration, events) - @id = SecureRandom.uuid - @file = file - @line_samples = line_samples - @duration = duration - @events = events - end - - def relative_path - @relative_path ||= @file.gsub(%r{^#{Rails.root}/?}, '') - end - - def to_param - @id - end - - def source - @source ||= File.read(@file) - end - end - end -end diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb deleted file mode 100644 index aa25eb5a571..00000000000 --- a/lib/gitlab/sherlock/line_profiler.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -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}/) { 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..].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 diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb deleted file mode 100644 index c92fa9ea1ff..00000000000 --- a/lib/gitlab/sherlock/line_sample.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - class LineSample - attr_reader :duration, :events - - # duration - The execution time in milliseconds. - # events - The amount of events. - def initialize(duration, events) - @duration = duration - @events = events - end - - # Returns the sample duration percentage relative to the given duration. - # - # Example: - # - # sample.duration # => 150 - # sample.percentage_of(1500) # => 10.0 - # - # total_duration - The total duration to compare with. - # - # Returns a float - def percentage_of(total_duration) - (duration.to_f / total_duration) * 100.0 - end - - # Returns true if the current sample takes up the majority of the given - # duration. - # - # total_duration - The total duration to compare with. - def majority_of?(total_duration) - percentage_of(total_duration) >= 30 - end - end - end -end diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb deleted file mode 100644 index 4bba60f3490..00000000000 --- a/lib/gitlab/sherlock/location.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - class Location - attr_reader :path, :line - - SHERLOCK_DIR = File.dirname(__FILE__) - - # Creates a new Location from a `Thread::Backtrace::Location`. - def self.from_ruby_location(location) - new(location.path, location.lineno) - end - - # path - The full path of the frame as a String. - # line - The line number of the frame as a Fixnum. - def initialize(path, line) - @path = path - @line = line - end - - # Returns true if the current frame originated from the application. - def application? - @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR) - end - end - end -end diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb deleted file mode 100644 index f7b08d58e49..00000000000 --- a/lib/gitlab/sherlock/middleware.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - # Rack middleware used for tracking request metrics. - class Middleware - CONTENT_TYPES = %r{text/html|application/json}i.freeze - - IGNORE_PATHS = %r{^/sherlock}.freeze - - def initialize(app) - @app = app - end - - # env - A Hash containing Rack environment details. - def call(env) - if instrument?(env) - call_with_instrumentation(env) - else - @app.call(env) - end - end - - def call_with_instrumentation(env) - trans = transaction_from_env(env) - retval = trans.run { @app.call(env) } - - Sherlock.collection.add(trans) - - retval - end - - def instrument?(env) - !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES && - env['REQUEST_URI'] !~ IGNORE_PATHS) - end - - def transaction_from_env(env) - Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI']) - end - end - end -end diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb deleted file mode 100644 index 6f1d2ad23c1..00000000000 --- a/lib/gitlab/sherlock/query.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Sherlock - class Query - attr_reader :id, :query, :started_at, :finished_at, :backtrace - - # SQL identifiers that should be prefixed with newlines. - PREFIX_NEWLINE = %r{ - \s+(FROM - |(LEFT|RIGHT)?INNER\s+JOIN - |(LEFT|RIGHT)?OUTER\s+JOIN - |WHERE - |AND - |GROUP\s+BY - |ORDER\s+BY - |LIMIT - |OFFSET)\s+}ix.freeze # Vim indent breaks when this is on a newline :< - - # Creates a new Query using a String and a separate Array of bindings. - # - # query - A String containing a SQL query, optionally with numeric - # placeholders (`$1`, `$2`, etc). - # - # bindings - An Array of ActiveRecord columns and their values. - # started_at - The start time of the query as a Time-like object. - # finished_at - The completion time of the query as a Time-like object. - # - # Returns a new Query object. - def self.new_with_bindings(query, bindings, started_at, finished_at) - bindings.each_with_index do |(_, value), index| - quoted_value = ActiveRecord::Base.connection.quote(value) - - query = query.gsub("$#{index + 1}", quoted_value) - end - - new(query, started_at, finished_at) - end - - # query - The SQL query as a String (without placeholders). - # started_at - The start time of the query as a Time-like object. - # finished_at - The completion time of the query as a Time-like object. - def initialize(query, started_at, finished_at) - @id = SecureRandom.uuid - @query = query - @started_at = started_at - @finished_at = finished_at - @backtrace = caller_locations.map do |loc| - Location.from_ruby_location(loc) - end - - unless @query.end_with?(';') - @query = "#{@query};" - end - end - - # Returns the query duration in milliseconds. - def duration - @duration ||= (@finished_at - @started_at) * 1000.0 - end - - def to_param - @id - end - - # Returns a human readable version of the query. - def formatted_query - @formatted_query ||= format_sql(@query) - end - - # Returns the last application frame of the backtrace. - def last_application_frame - @last_application_frame ||= @backtrace.find(&:application?) - end - - # Returns an Array of application frames (excluding Gems and the likes). - def application_backtrace - @application_backtrace ||= @backtrace.select(&:application?) - end - - # Returns the query plan as a String. - def explain - unless @explain - ActiveRecord::Base.connection.transaction do - @explain = raw_explain(@query).values.flatten.join("\n") - - # Roll back any queries that mutate data so we don't mess up - # anything when running explain on an INSERT, UPDATE, DELETE, etc. - raise ActiveRecord::Rollback - end - end - - @explain - end - - private - - def raw_explain(query) - explain = "EXPLAIN ANALYZE #{query};" - - ActiveRecord::Base.connection.execute(explain) - end - - def format_sql(query) - query.each_line - .map { |line| line.strip } - .join("\n") - .gsub(PREFIX_NEWLINE) { "\n#{Regexp.last_match(1)} " } - end - end - end -end diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb deleted file mode 100644 index d04624977dc..00000000000 --- a/lib/gitlab/sherlock/transaction.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -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 |