module GemFootprintAnalyzer::RequireSpy

A module keeping hacks required to hijack {Kernel.require} and {Kernel.require_relative} and plug in versions of them that communicate meta data to the {Analyzer}.

Constants

ACTIVESUPPORT_REQUIRE_DEPENDENCY

Suitable for versions 3.0.stable up to 5.2.1

Public Class Methods

alias_require_methods() click to toggle source

Aliases original methods, so they are accessible from methods that shadow them

# File lib/gem_footprint_analyzer/require_spy.rb, line 63
def alias_require_methods
  kernels.each do |k|
    k.send :alias_method, :regular_require, :require
    k.send :alias_method, :regular_require_relative, :require_relative
  end
end
define_require(klass, transport) click to toggle source

@param klass [Class] Target class to have the spying require defined @param transport [Transport] Instance of transport to be used by the require proxy method Replaces require methods with proxied versions

# File lib/gem_footprint_analyzer/require_spy.rb, line 83
def define_require(klass, transport)
  klass.send :define_method, :require do |name|
    transport.ready_and_wait_for_start
    duration, result = GemFootprintAnalyzer::RequireSpy.timed_exec { regular_require(name) }
    bare_name = GemFootprintAnalyzer::RequireSpy.relative_path(name)

    transport.report_require(GemFootprintAnalyzer::RequireSpy.without_extension(bare_name),
                             GemFootprintAnalyzer::RequireSpy.first_foreign_caller(caller),
                             duration)

    result
  end
end
define_require_relatives() click to toggle source

Replaces require_relative methods with proxied versions

# File lib/gem_footprint_analyzer/require_spy.rb, line 98
def define_require_relatives
  # As of Ruby 2.5.1, both :require and :require_relative use an unexposed native method
  # rb_safe_require, however it's challenging to plug into it and using original
  # :require_relative is not really possible (it does path calculation magic) so instead
  # we're redirecting :require_relative to the regular :require
  kernels.each do |k|
    k.send :define_method, :require_relative do |name|
      return require(name) if name.start_with?('/')

      last_caller = caller(1..1).first
      relative_path = GemFootprintAnalyzer::RequireSpy.relative_path(last_caller, name)
      require(relative_path)
    end
  end
end
define_requires(transport) click to toggle source

@param transport [Transport] Instance of transport to be used by the require proxy method

# File lib/gem_footprint_analyzer/require_spy.rb, line 76
def define_requires(transport)
  kernels.each { |k| define_require(k, transport) }
end
first_foreign_caller(caller_list) click to toggle source

@param [Array<String>] List of caller stack frames @return [String|nil] First caller entry that doesn't originate from this gem

# File lib/gem_footprint_analyzer/require_spy.rb, line 38
def first_foreign_caller(caller_list)
  ffc = caller_list.find do |c|
    c !~ ACTIVESUPPORT_REQUIRE_DEPENDENCY &&
      relative_path(c) !~ /gem_footprint_analyzer/
  end
  without_extension(relative_path(ffc)) if ffc
end
kernels() click to toggle source

@return [Array<Class>] CLasses that have require* methods that we'll spy on

# File lib/gem_footprint_analyzer/require_spy.rb, line 71
def kernels
  @kernels ||= [(class << ::Kernel; self; end), Kernel]
end
load_paths() click to toggle source

@return [Array<String>] All configured load paths in the full directory form

# File lib/gem_footprint_analyzer/require_spy.rb, line 26
def load_paths
  @load_paths ||= $LOAD_PATH.map { |path| File.expand_path(path) }
end
relative_path(caller_entry, require_name = nil) click to toggle source

@param caller_entry [String] A single stack frame @param require_name [String|nil] An optional require name to calculate full_path from @return [String] path relative to the gem lib directory

# File lib/gem_footprint_analyzer/require_spy.rb, line 13
def relative_path(caller_entry, require_name = nil)
  caller_file = caller_entry.split(':')[0]
  if require_name
    caller_dir = File.dirname(caller_file)
    full_path = File.join(caller_dir, require_name)
  else
    full_path = caller_file
  end
  load_path = load_paths.find { |lp| full_path.start_with?(lp) }
  full_path.sub(%r{\A#{load_path}/}, '')
end
spy_require(transport) click to toggle source

Installs require spying on all relevant methods

# File lib/gem_footprint_analyzer/require_spy.rb, line 47
def spy_require(transport)
  alias_require_methods

  define_require_relatives
  define_requires(transport)
end
timed_exec() { || ... } click to toggle source

@return [Array] Tuple with method call duration and return value

# File lib/gem_footprint_analyzer/require_spy.rb, line 55
def timed_exec
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  result = yield
  duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(4)
  [duration, result]
end
without_extension(name) click to toggle source

@param name [String] require name @return [String] name with the .rb extension truncated

# File lib/gem_footprint_analyzer/require_spy.rb, line 32
def without_extension(name)
  name.sub(/\.rb\z/, '')
end