-
-
Notifications
You must be signed in to change notification settings - Fork 119
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
113 additions
and
222 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,109 @@ | ||
# 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 | ||
# 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 | ||
|
||
# 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 | ||
|
||
# @sig Mutex | ||
attr_reader :mutex | ||
private :mutex | ||
|
||
# @sig TracePoint | ||
attr_reader :tracer | ||
private :tracer | ||
# @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) | ||
# @cpaths.empty? is cheap and, depending on the code base, often true. | ||
# | ||
# 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 | ||
# Note that due to the way Zeitwerk works, namespaces are registered | ||
# necessarily before their constant is defined, so the race codintion | ||
# due to the gap from here to the delete call down below would not | ||
# introduce a logic flaw. | ||
# | ||
# On one hand, if @cpaths is empty and a new entry is created after this | ||
# check, it won't be for mod::cname anyway. | ||
# | ||
# On the other hand, if @cpaths is not empty, what happens with @cpaths | ||
# during the gap is fine, because delete has the last word anyway. | ||
return if @cpaths.empty? | ||
|
||
# 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) | ||
|
||
# I benchmarked this against using pairs [mod, cname] as keys, and | ||
# strings won. | ||
cpath = mod.equal?(Object) ? cname.name : "#{real_mod_name(mod)}::#{cname}" | ||
@cpaths.delete(cpath) | ||
end | ||
end | ||
|
||
@cpaths = {} | ||
@mutex = Mutex.new | ||
module Synchronized | ||
extend Internal | ||
|
||
MUTEX = Mutex.new | ||
|
||
internal def register(...) | ||
MUTEX.synchronize { super } | ||
end | ||
|
||
internal def unregister_loader(...) | ||
MUTEX.synchronize { super } | ||
end | ||
|
||
# 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)) | ||
private def loader_for(...) | ||
MUTEX.synchronize { super } | ||
end | ||
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.