Bug #18452
closedDramatic performance regression in Zeitwerk with 3.1
Description
Hi!
In Ruby 3.1, Zeitwerk loads implicit namespaces way slower than in previous versions. In this benchmark:
require 'fileutils'
require 'tmpdir'
require 'zeitwerk'
Dir.mktmpdir do |dir|
Dir.chdir(dir)
for i in 'a'..'z'
for j in 'a'..'z'
FileUtils.mkdir_p("lib/#{i}/#{j}")
end
end
loader = Zeitwerk::Loader.new
loader.push_dir('lib')
loader.setup
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
loader.eager_load
puts Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
end
you go from about 0.06s to +11s. That is like a 180x slow down .
I have seen that the single cause of the slow down is one line: Zeitwerk's Kernel#require
wrapper pushing to $LOADED_FEATURES
when it intercepts a directory source code. For context, Zeitwerk sets autoloads for directories that define namespaces, so that their corresponding modules get autovivified when first referenced by the user.
I have written a self-contained script that reproduces the root of the issue without Zeitwerk, in case that helps debugging:
module Kernel
alias original_require require
def require(path)
if path.start_with?('/foo')
$LOADED_FEATURES << path
true
else
original_require(path)
end
end
end
foos = []
('a'..'z').each do |i|
('a'..'z').each do |j|
foos << "/foo#{i}#{j}"
end
end
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
foos.each { |foo| require foo }
puts Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
I have seen that this commit is responsible for the majority of the regression, But it is not the only cause, before that commit, the script is faster, but not as fast by an important factor too.