905f8d763a
This reduces the overhead of the method instrumentation code primarily by reducing the number of method calls. There are also some other small optimisations such as not casting timing values to Floats (there's no particular need for this), using Symbols for method call metric names, and reducing the number of Hash lookups for instrumented methods. The exact impact depends on the code being executed. For example, for a method that's only called once the difference won't be very noticeable. However, for methods that are called many times the difference can be more significant. For example, the loading time of a large commit (nrclark/dummy_project@81ebdea5df) was reduced from around 19 seconds to around 15 seconds using these changes.
172 lines
5.4 KiB
Ruby
172 lines
5.4 KiB
Ruby
module Gitlab
|
|
module Metrics
|
|
# Module for instrumenting methods.
|
|
#
|
|
# This module allows instrumenting of methods without having to actually
|
|
# alter the target code (e.g. by including modules).
|
|
#
|
|
# Example usage:
|
|
#
|
|
# Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
|
|
module Instrumentation
|
|
PROXY_IVAR = :@__gitlab_instrumentation_proxy
|
|
|
|
def self.configure
|
|
yield self
|
|
end
|
|
|
|
# Returns the name of the series to use for storing method calls.
|
|
def self.series
|
|
@series ||= "#{Metrics.series_prefix}method_calls"
|
|
end
|
|
|
|
# Instruments a class method.
|
|
#
|
|
# mod - The module to instrument as a Module/Class.
|
|
# name - The name of the method to instrument.
|
|
def self.instrument_method(mod, name)
|
|
instrument(:class, mod, name)
|
|
end
|
|
|
|
# Instruments an instance method.
|
|
#
|
|
# mod - The module to instrument as a Module/Class.
|
|
# name - The name of the method to instrument.
|
|
def self.instrument_instance_method(mod, name)
|
|
instrument(:instance, mod, name)
|
|
end
|
|
|
|
# Recursively instruments all subclasses of the given root module.
|
|
#
|
|
# This can be used to for example instrument all ActiveRecord models (as
|
|
# these all inherit from ActiveRecord::Base).
|
|
#
|
|
# This method can optionally take a block to pass to `instrument_methods`
|
|
# and `instrument_instance_methods`.
|
|
#
|
|
# root - The root module for which to instrument subclasses. The root
|
|
# module itself is not instrumented.
|
|
def self.instrument_class_hierarchy(root, &block)
|
|
visit = root.subclasses
|
|
|
|
until visit.empty?
|
|
klass = visit.pop
|
|
|
|
instrument_methods(klass, &block)
|
|
instrument_instance_methods(klass, &block)
|
|
|
|
klass.subclasses.each { |c| visit << c }
|
|
end
|
|
end
|
|
|
|
# Instruments all public and private methods of a module.
|
|
#
|
|
# This method optionally takes a block that can be used to determine if a
|
|
# method should be instrumented or not. The block is passed the receiving
|
|
# module and an UnboundMethod. If the block returns a non truthy value the
|
|
# method is not instrumented.
|
|
#
|
|
# mod - The module to instrument.
|
|
def self.instrument_methods(mod)
|
|
methods = mod.methods(false) + mod.private_methods(false)
|
|
methods.each do |name|
|
|
method = mod.method(name)
|
|
|
|
if method.owner == mod.singleton_class
|
|
if !block_given? || block_given? && yield(mod, method)
|
|
instrument_method(mod, name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Instruments all public and private instance methods of a module.
|
|
#
|
|
# See `instrument_methods` for more information.
|
|
#
|
|
# mod - The module to instrument.
|
|
def self.instrument_instance_methods(mod)
|
|
methods = mod.instance_methods(false) + mod.private_instance_methods(false)
|
|
methods.each do |name|
|
|
method = mod.instance_method(name)
|
|
|
|
if method.owner == mod
|
|
if !block_given? || block_given? && yield(mod, method)
|
|
instrument_instance_method(mod, name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Returns true if a module is instrumented.
|
|
#
|
|
# mod - The module to check
|
|
def self.instrumented?(mod)
|
|
mod.instance_variable_defined?(PROXY_IVAR)
|
|
end
|
|
|
|
# Returns the proxy module (if any) of `mod`.
|
|
def self.proxy_module(mod)
|
|
mod.instance_variable_get(PROXY_IVAR)
|
|
end
|
|
|
|
# Instruments a method.
|
|
#
|
|
# type - The type (:class or :instance) of method to instrument.
|
|
# mod - The module containing the method.
|
|
# name - The name of the method to instrument.
|
|
def self.instrument(type, mod, name)
|
|
return unless Metrics.enabled?
|
|
|
|
name = name.to_sym
|
|
target = type == :instance ? mod : mod.singleton_class
|
|
|
|
if type == :instance
|
|
target = mod
|
|
label = "#{mod.name}##{name}"
|
|
method = mod.instance_method(name)
|
|
else
|
|
target = mod.singleton_class
|
|
label = "#{mod.name}.#{name}"
|
|
method = mod.method(name)
|
|
end
|
|
|
|
unless instrumented?(target)
|
|
target.instance_variable_set(PROXY_IVAR, Module.new)
|
|
end
|
|
|
|
proxy_module = self.proxy_module(target)
|
|
|
|
# Some code out there (e.g. the "state_machine" Gem) checks the arity of
|
|
# a method to make sure it only passes arguments when the method expects
|
|
# any. If we were to always overwrite a method to take an `*args`
|
|
# signature this would break things. As a result we'll make sure the
|
|
# generated method _only_ accepts regular arguments if the underlying
|
|
# method also accepts them.
|
|
if method.arity == 0
|
|
args_signature = ''
|
|
else
|
|
args_signature = '*args'
|
|
end
|
|
|
|
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
|
|
def #{name}(#{args_signature})
|
|
if trans = Gitlab::Metrics::Instrumentation.transaction
|
|
trans.method_call_for(#{label.to_sym.inspect}).measure { super }
|
|
else
|
|
super
|
|
end
|
|
end
|
|
EOF
|
|
|
|
target.prepend(proxy_module)
|
|
end
|
|
|
|
# Small layer of indirection to make it easier to stub out the current
|
|
# transaction.
|
|
def self.transaction
|
|
Transaction.current
|
|
end
|
|
end
|
|
end
|
|
end
|