Skip to content

Commit c03bba4

Browse files
committed
trace autoloads, and document hints for troubleshooting
Closes #32885.
1 parent c3e5695 commit c03bba4

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

activesupport/lib/active_support/dependencies.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ def self.unload_interlock
7979
# to allow arbitrary constants to be marked for unloading.
8080
mattr_accessor :explicitly_unloadable_constants, default: []
8181

82+
# The logger used when tracing autoloads.
83+
mattr_accessor :logger
84+
85+
# If true, trace autoloads with +logger.debug+.
86+
mattr_accessor :verbose, default: false
87+
8288
# The WatchStack keeps a stack of the modules being watched as files are
8389
# loaded. If a file in the process of being loaded (parent.rb) triggers the
8490
# load of another file (child.rb) the stack will ensure that child.rb
@@ -416,7 +422,7 @@ def loadable_constants_for_path(path, bases = autoload_paths)
416422

417423
# Search for a file in autoload_paths matching the provided suffix.
418424
def search_for_file(path_suffix)
419-
path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze)
425+
path_suffix += ".rb" unless path_suffix.ends_with?(".rb")
420426

421427
autoload_paths.each do |root|
422428
path = File.join(root, path_suffix)
@@ -450,6 +456,7 @@ def autoload_module!(into, const_name, qualified_name, path_suffix)
450456
return nil unless base_path = autoloadable_module?(path_suffix)
451457
mod = Module.new
452458
into.const_set const_name, mod
459+
log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})")
453460
autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
454461
autoloaded_constants.uniq!
455462
mod
@@ -491,7 +498,7 @@ def load_missing_constant(from_mod, const_name)
491498
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
492499
end
493500

494-
qualified_name = qualified_name_for from_mod, const_name
501+
qualified_name = qualified_name_for(from_mod, const_name)
495502
path_suffix = qualified_name.underscore
496503

497504
file_path = search_for_file(path_suffix)
@@ -504,8 +511,13 @@ def load_missing_constant(from_mod, const_name)
504511
raise "Circular dependency detected while autoloading constant #{qualified_name}"
505512
else
506513
require_or_load(expanded, qualified_name)
507-
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
508-
return from_mod.const_get(const_name)
514+
515+
if from_mod.const_defined?(const_name, false)
516+
log("constant #{qualified_name} autoloaded from #{expanded}.rb")
517+
return from_mod.const_get(const_name)
518+
else
519+
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it"
520+
end
509521
end
510522
elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix)
511523
return mod
@@ -554,6 +566,7 @@ def load_missing_constant(from_mod, const_name)
554566
# as the environment will be in an inconsistent state, e.g. other constants
555567
# may have already been unloaded and not accessible.
556568
def remove_unloadable_constants!
569+
log("removing unloadable constants")
557570
autoloaded_constants.each { |const| remove_constant const }
558571
autoloaded_constants.clear
559572
Reference.clear!
@@ -743,6 +756,10 @@ def remove_constant(const) #:nodoc:
743756
# The constant is no longer reachable, just skip it.
744757
end
745758
end
759+
760+
def log(message)
761+
logger.debug("autoloading: #{message}") if logger && verbose
762+
end
746763
end
747764
end
748765

activesupport/test/dependencies_test.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,3 +1130,52 @@ def test_load_and_require_stay_private
11301130
ActiveSupport::Dependencies.hook!
11311131
end
11321132
end
1133+
1134+
class DependenciesLogging < ActiveSupport::TestCase
1135+
MESSAGE = "message"
1136+
1137+
def with_settings(logger, verbose)
1138+
original_logger = ActiveSupport::Dependencies.logger
1139+
original_verbose = ActiveSupport::Dependencies.verbose
1140+
1141+
ActiveSupport::Dependencies.logger = logger
1142+
ActiveSupport::Dependencies.verbose = verbose
1143+
1144+
yield
1145+
ensure
1146+
ActiveSupport::Dependencies.logger = original_logger
1147+
ActiveSupport::Dependencies.verbose = original_verbose
1148+
end
1149+
1150+
def fake_logger
1151+
Class.new do
1152+
def self.debug(message)
1153+
message
1154+
end
1155+
end
1156+
end
1157+
1158+
test "does not log if the logger is nil and verbose is false" do
1159+
with_settings(nil, false) do
1160+
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
1161+
end
1162+
end
1163+
1164+
test "does not log if the logger is nil and verbose is true" do
1165+
with_settings(nil, true) do
1166+
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
1167+
end
1168+
end
1169+
1170+
test "does not log if the logger is set and verbose is false" do
1171+
with_settings(fake_logger, false) do
1172+
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
1173+
end
1174+
end
1175+
1176+
test "logs if the logger is set and verbose is true" do
1177+
with_settings(fake_logger, true) do
1178+
assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE)
1179+
end
1180+
end
1181+
end

guides/source/autoloading_and_reloading_constants.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,3 +1350,34 @@ With the [Spring](https://github.com/rails/spring) pre-loader (included with new
13501350

13511351
Occasionally you may need to explicitly eager_load by using `Rails
13521352
.application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test).
1353+
1354+
## Troubleshooting
1355+
1356+
### Tracing Autoloads
1357+
1358+
Active Support is able to report constants as they are autoloaded. To enable these traces in a Rails application, put the following two lines in some initializer:
1359+
1360+
```ruby
1361+
ActiveSupport::Dependencies.logger = Rails.logger
1362+
ActiveSupport::Dependencies.verbose = true
1363+
```
1364+
1365+
### Where is a Given Autoload Triggered?
1366+
1367+
If constant `Foo` is being autoloaded, and you'd like to know where is that autoload coming from, just throw
1368+
1369+
```ruby
1370+
puts caller
1371+
```
1372+
1373+
at the top of `foo.rb` and inspect the printed stack trace.
1374+
1375+
### Which Constants Have Been Autoloaded?
1376+
1377+
At any given time,
1378+
1379+
```ruby
1380+
ActiveSupport::Dependencies.autoloaded_constants
1381+
```
1382+
1383+
has the collection of constants that have been autoloaded so far.

0 commit comments

Comments
 (0)