Skip to content

Instantly share code, notes, and snippets.

@braindeaf
Created September 10, 2025 20:34
Show Gist options
  • Select an option

  • Save braindeaf/2f957bcad91a3c39eaa9bd957f1f5a8e to your computer and use it in GitHub Desktop.

Select an option

Save braindeaf/2f957bcad91a3c39eaa9bd957f1f5a8e to your computer and use it in GitHub Desktop.
Using MethodTrace to identify where Memoizing might be helpful.
#
# With reference to
# Just a run-down of TracePoint
#
# https://medium.com/@baweaver/exploring-tracepoint-in-ruby-part-one-example-code-2cf9b1a1b956
# Rotoscope tracks method invocations but not arguments
#
# https://github.com/Shopify/rotoscope
#
# Rack::MethodTrace - allows us to see which methods are called more than once and which are memoized
#
if ActiveModel::Type::Boolean.new.cast(ENV["METHOD_TRACE"])
module Rack
class MethodTrace
ROOTS_REGEXP = %r{^(#{Rails.root}|#{Gem::Specification.find_by_name("memery").gem_dir})}
def initialize(app)
@app = app
end
def call(env)
trace.enable do
status, headers, response = @app.call(env)
log(env["REQUEST_URI"])
[status, headers, response]
end
end
private
def trace
TracePoint.new(:call) do |tp|
next unless ROOTS_REGEXP.match?(tp.path)
# memoized = (tp.defined_class.try(:memoized_methods) || []).map(&:memoized_method).include?(tp.method_id)
store.concat(Rover::DataFrame.new([{
class: tp.defined_class.name,
object_id: tp.binding.eval("object_id"),
method: tp.method_id.to_s,
# memoized: memoized,
lineno: "#{tp.path.sub(ROOTS_REGEXP, "")}:#{tp.lineno}",
args: extract_args(tp).to_s
}]))
end
end
def log(path)
grouped = store.group(:class, :object_id, :method, :lineno, :args).count
results = grouped[grouped["count"] > 1].to_a
return if results.empty?
table = Text::Table.new
table.head = ["Class", "ObjectId", "Method", "Line No.", "Args", "Count"]
results.each do |row|
table.rows << [row[:class], row[:object_id], row[:method], row[:lineno], row[:args][0..20], row["count"]]
end
logger.info([path, table.to_s].join("\n"))
end
def store
RequestStore.store[:methods] ||= Rover::DataFrame.new([])
end
def logger
@logger ||= ::Logger.new(Rails.root.join("log", "method_trace.log"), formatter: proc { |severity, datetime, progname, msg|
"#{datetime}\n#{msg}\n"
})
end
def extract_args(tp)
tp.parameters.map do |f, v|
next if f == :rest || f == :keyrest || f == :block
[v, tp.binding.eval(v.to_s).to_s]
end.compact.to_h
end
Rails.application.config.middleware.insert_after(ActionDispatch::Static, self)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment