-
-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace TracePoint with const_added for explicit namespaces
- Loading branch information
Showing
12 changed files
with
107 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# frozen_string_literal: true | ||
|
||
module Zeitwerk::ExplicitNamespacesRegistry | ||
def const_added(cname) | ||
Zeitwerk::ExplicitNamespace.__on_const_added(self, cname) | ||
super | ||
end | ||
|
||
Module.prepend(self) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,93 +1,102 @@ | ||
# frozen_string_literal: true | ||
|
||
module Zeitwerk | ||
# Centralizes the logic for the trace point used to detect the creation of | ||
# explicit namespaces, needed to descend into matching subdirectories right | ||
# after the constant has been defined. | ||
# Centralizes the logic needed to descend into matching subdirectories right | ||
# after the constant for an explicit namespace has been defined. | ||
# | ||
# The implementation assumes an explicit namespace is managed by one loader. | ||
# Loaders that reopen namespaces owned by other projects are responsible for | ||
# loading their constant before setup. This is documented. | ||
module ExplicitNamespace # :nodoc: all | ||
class << self | ||
include RealModName | ||
module Synchronized | ||
extend Internal | ||
|
||
# Maps constant paths that correspond to explicit namespaces according to | ||
# the file system, to the loader responsible for them. | ||
# | ||
# @sig Hash[String, Zeitwerk::Loader] | ||
attr_reader :cpaths | ||
private :cpaths | ||
MUTEX = Mutex.new | ||
|
||
internal def register(...) | ||
MUTEX.synchronize { super } | ||
end | ||
|
||
# @sig Mutex | ||
attr_reader :mutex | ||
private :mutex | ||
internal def unregister_loader(...) | ||
MUTEX.synchronize { super } | ||
end | ||
|
||
# @sig TracePoint | ||
attr_reader :tracer | ||
private :tracer | ||
private def loader_for(...) | ||
MUTEX.synchronize { super } | ||
end | ||
end | ||
|
||
# Maps cpaths of explicit namespaces with their corresponding loader. | ||
# Entries are added as the namespaces are found, and removed as they are | ||
# autoloaded. | ||
# | ||
# @sig Hash[String => Zeitwerk::Loader] | ||
@cpaths = {} | ||
|
||
class << self | ||
include RealModName | ||
extend Internal | ||
|
||
# @sig (Module, Symbol) -> void | ||
internal def on_const_added(mod, cname) | ||
if loader = loader_for(mod, cname) | ||
namespace = mod.const_get(cname, false) | ||
loader.on_namespace_loaded(namespace) | ||
end | ||
end | ||
|
||
# Asserts `cpath` corresponds to an explicit namespace for which `loader` | ||
# is responsible. | ||
# | ||
# @sig (String, Zeitwerk::Loader) -> void | ||
internal def register(cpath, loader) | ||
mutex.synchronize do | ||
cpaths[cpath] = loader | ||
# We check enabled? because, looking at the C source code, enabling an | ||
# enabled tracer does not seem to be a simple no-op. | ||
tracer.enable unless tracer.enabled? | ||
end | ||
@cpaths[cpath] = loader | ||
end | ||
|
||
# @sig (Zeitwerk::Loader) -> void | ||
internal def unregister_loader(loader) | ||
cpaths.delete_if { |_cpath, l| l == loader } | ||
disable_tracer_if_unneeded | ||
@cpaths.delete_if { _2.equal?(loader) } | ||
end | ||
|
||
# This is an internal method only used by the test suite. | ||
# | ||
# @sig (String) -> bool | ||
internal def registered?(cpath) | ||
cpaths.key?(cpath) | ||
@cpaths[cpath] | ||
end | ||
|
||
# This is an internal method only used by the test suite. | ||
# | ||
# @sig () -> void | ||
private def disable_tracer_if_unneeded | ||
mutex.synchronize do | ||
tracer.disable if cpaths.empty? | ||
end | ||
internal def clear | ||
@cpaths.clear | ||
end | ||
|
||
# @sig (TracePoint) -> void | ||
private def tracepoint_class_callback(event) | ||
# If the class is a singleton class, we won't do anything with it so we | ||
# can bail out immediately. This is several orders of magnitude faster | ||
# than accessing its name. | ||
return if event.self.singleton_class? | ||
|
||
# It might be tempting to return if name.nil?, to avoid the computation | ||
# of a hash code and delete call. But Ruby does not trigger the :class | ||
# event on Class.new or Module.new, so that would incur in an extra call | ||
# for nothing. | ||
# Returns the loader registerd for cpath, if any. This method deletes | ||
# cpath from @cpath if present. | ||
# | ||
# @sig (String) -> Zeitwerk::Loader? | ||
private def loader_for(mod, cname) | ||
# We check for @cpaths.empty? because it is very cheap and common. | ||
# | ||
# On the other hand, if we were called, cpaths is not empty. Otherwise | ||
# the tracer is disabled. So we do need to go ahead with the hash code | ||
# computation and delete call. | ||
if loader = cpaths.delete(real_mod_name(event.self)) | ||
loader.on_namespace_loaded(event.self) | ||
disable_tracer_if_unneeded | ||
end | ||
end | ||
end | ||
# Note that due to the way Zeitwerk works, registering an explicit | ||
# namespace happens necessarily before its constant is defined, so the | ||
# gap from here to the delete call below is not a race condition. That | ||
# is, if @cpaths is empty and a new entry was created after this check, | ||
# it would not be for mod::cname anyway, so we can safely return | ||
return if @cpaths.empty? | ||
|
||
@cpaths = {} | ||
@mutex = Mutex.new | ||
# Module#const_added is triggered when an autoload is defined too. This | ||
# callback is only for constants that are defined for real. In the case | ||
# of inceptions we get a false nil, but this is covered in the loader by | ||
# doing things in a certain order. | ||
return if mod.autoload?(cname, false) | ||
|
||
# We go through a method instead of defining a block mainly to have a better | ||
# label when profiling. | ||
@tracer = TracePoint.new(:class, &method(:tracepoint_class_callback)) | ||
cpath = mod.equal?(Object) ? cname.name : "#{real_mod_name(mod)}::#{cname}" | ||
@cpaths.delete(cpath) | ||
end | ||
|
||
# prepend Synchronized unless RUBY_ENGINE == "ruby" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.