Speed up development by only reloading classes if dependencies files changed.

This can be turned off by setting `config.reload_classes_only_on_change` to false.

Extensions like Active Record should add their respective files like db/schema.rb and db/structure.sql to `config.watchable_files` if they want their changes to affect classes reloading.

Thanks to https://github.com/paneq/active_reload and Pastorino for the inspiration. <3
This commit is contained in:
José Valim 2011-12-12 22:51:33 +01:00
parent 62cda03fa8
commit fa1d9a884c
15 changed files with 161 additions and 62 deletions

View File

@ -129,6 +129,15 @@ class ReloaderTest < Test::Unit::TestCase
assert cleaned
end
def test_prepend_prepare_callback
i = 10
Reloader.to_prepare { i += 1 }
Reloader.to_prepare(:prepend => true) { i = 0 }
Reloader.prepare!
assert_equal 1, i
end
def test_cleanup_callbacks_are_called_on_exceptions
cleaned = false
Reloader.to_cleanup { cleaned = true }

View File

@ -94,6 +94,11 @@ module ActiveRecord
end
end
initializer "active_record.add_watchable_files" do |app|
files = ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
config.watchable_files.concat files.select { |f| File.exist?(f) }
end
config.after_initialize do
ActiveSupport.on_load(:active_record) do
instantiate_observers

View File

@ -39,30 +39,53 @@ module ActiveSupport
@paths = paths
@glob = compile_glob(@paths.extract_options!)
@block = block
@updated_at = nil
@last_update_at = calculate ? updated_at : nil
end
def updated_at
all = []
all.concat @paths
all.concat Dir[@glob] if @glob
all.map { |path| File.mtime(path) }.max
end
def execute_if_updated
current_update_at = self.updated_at
if @last_update_at != current_update_at
@last_update_at = current_update_at
@block.call
# Check if any of the entries were updated. If so, the updated_at
# value is cached until flush! is called.
def updated?
current_updated_at = updated_at
if @last_update_at != current_updated_at
@updated_at = updated_at
true
else
false
end
end
# Flush the cache so updated? is calculated again
def flush!
@updated_at = nil
end
# Execute the block given if updated. This call
# always flush the cache.
def execute_if_updated
if updated?
@last_update_at = updated_at
@block.call
true
else
false
end
ensure
flush!
end
private
def compile_glob(hash)
def updated_at #:nodoc:
@updated_at || begin
all = []
all.concat @paths
all.concat Dir[@glob] if @glob
all.map { |path| File.mtime(path) }.max
end
end
def compile_glob(hash) #:nodoc:
return if hash.empty?
globs = []
hash.each do |key, value|
@ -71,7 +94,7 @@ module ActiveSupport
"{#{globs.join(",")}}"
end
def compile_ext(array)
def compile_ext(array) #:nodoc:
array = Array.wrap(array)
return if array.empty?
".{#{array.join(",")}}"

View File

@ -17,7 +17,8 @@ module I18n
# point, no path was added to the reloader, I18n.reload! is not triggered
# on to_prepare callbacks. This will only happen on the config.after_initialize
# callback below.
initializer "i18n.callbacks" do
initializer "i18n.callbacks" do |app|
app.reloaders << I18n::Railtie.reloader
ActionDispatch::Reloader.to_prepare do
I18n::Railtie.reloader.execute_if_updated
end

View File

@ -54,6 +54,19 @@ class FileUpdateCheckerWithEnumerableTest < Test::Unit::TestCase
assert_equal 1, i
end
def test_should_cache_updated_result_until_flushed
i = 0
checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 }
assert !checker.updated?
sleep(1)
FileUtils.touch(FILES)
assert checker.updated?
assert checker.execute_if_updated
assert !checker.updated?
end
def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
i = 0
checker = ActiveSupport::FileUpdateChecker.new([{"tmp_watcher" => [:txt]}], true){ i += 1 }

View File

@ -1,9 +1,8 @@
## Rails 3.2.0 (unreleased) ##
* New applications get a flag
`config.active_record.auto_explain_threshold_in_seconds` in the environments
configuration files. With a value of 0.5 in development.rb, and commented
out in production.rb. No mention in test.rb. *fxn*
* Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting `config.reload_classes_only_on_change` to false. *José Valim*
* New applications get a flag `config.active_record.auto_explain_threshold_in_seconds` in the environments configuration files. With a value of 0.5 in development.rb, and commented out in production.rb. No mention in test.rb. *fxn*
* Add DebugExceptions middleware which contains features extracted from ShowExceptions middleware *José Valim*

View File

@ -98,6 +98,8 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is
* +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled.
* +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true.
* +config.reload_plugins+ enables or disables plugin reloading. Defaults to false.
* +config.secret_token+ used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get +config.secret_token+ initialized to a random key in +config/initializers/secret_token.rb+.

View File

@ -71,12 +71,14 @@ module Rails
attr_accessor :assets, :sandbox
alias_method :sandbox?, :sandbox
attr_reader :reloaders
delegate :default_url_options, :default_url_options=, :to => :routes
def initialize
super
@initialized = false
@reloaders = []
end
# This method is called just after an application inherits from Rails::Application,
@ -119,6 +121,7 @@ module Rails
reloader = routes_reloader
hook = lambda { reloader.execute_if_updated }
hook.call
self.reloaders << reloader
ActionDispatch::Reloader.to_prepare(&hook)
end
@ -126,10 +129,35 @@ module Rails
# A plugin may override this if they desire to provide a more exquisite app reloading.
# :api: plugin
def set_dependencies_hook
ActionDispatch::Reloader.to_cleanup do
callback = lambda do
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
end
if config.reload_classes_only_on_change
reloader = ActiveSupport::FileUpdateChecker.new(watchable_args, true, &callback)
self.reloaders << reloader
# We need to set a to_prepare callback regardless of the reloader result, i.e.
# models should be reloaded if any of the reloaders (i18n, routes) were updated.
ActionDispatch::Reloader.to_prepare(:prepend => true, &callback)
else
ActionDispatch::Reloader.to_cleanup(&callback)
end
end
# Returns an array of file paths appended with a hash of directories-extensions
# suitable for ActiveSupport::FileUpdateChecker API.
def watchable_args
files = []
files.concat config.watchable_files
dirs = {}
dirs.merge! config.watchable_dirs
ActiveSupport::Dependencies.autoload_paths.each do |path|
dirs[path.to_s] = [:rb]
end
files << dirs
end
# Initialize the application passing the given group. By default, the
@ -223,6 +251,10 @@ module Rails
alias :build_middleware_stack :app
def reload_dependencies?
config.reload_classes_only_on_change != true || reloaders.map(&:updated?).any?
end
def default_middleware_stack
ActionDispatch::MiddlewareStack.new.tap do |middleware|
if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
@ -252,7 +284,11 @@ module Rails
middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
end
middleware.use ::ActionDispatch::Reloader unless config.cache_classes
unless config.cache_classes
app = self
middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
end
middleware.use ::ActionDispatch::Callbacks
middleware.use ::ActionDispatch::Cookies

View File

@ -7,11 +7,11 @@ module Rails
class Configuration < ::Rails::Engine::Configuration
attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets,
:cache_classes, :cache_store, :consider_all_requests_local,
:dependency_loading, :filter_parameters,
:force_ssl, :helpers_paths, :logger, :log_tags, :preload_frameworks,
:relative_url_root, :reload_plugins, :secret_token, :serve_static_assets,
:ssl_options, :static_cache_control, :session_options,
:time_zone, :whiny_nils, :railties_order, :all_initializers
:dependency_loading, :filter_parameters, :force_ssl, :helpers_paths,
:initializers_paths, :logger, :log_tags, :preload_frameworks,
:railties_order, :relative_url_root, :reload_plugins, :secret_token,
:serve_static_assets, :ssl_options, :static_cache_control, :session_options,
:time_zone, :reload_classes_only_on_change, :whiny_nils
attr_writer :log_level
attr_reader :encoding
@ -19,25 +19,26 @@ module Rails
def initialize(*)
super
self.encoding = "utf-8"
@allow_concurrency = false
@consider_all_requests_local = false
@filter_parameters = []
@helpers_paths = []
@dependency_loading = true
@serve_static_assets = true
@static_cache_control = nil
@force_ssl = false
@ssl_options = {}
@session_store = :cookie_store
@session_options = {}
@time_zone = "UTC"
@log_level = nil
@middleware = app_middleware
@generators = app_generators
@cache_store = [ :file_store, "#{root}/tmp/cache/" ]
@railties_order = [:all]
@all_initializers = []
@relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
@allow_concurrency = false
@consider_all_requests_local = false
@filter_parameters = []
@helpers_paths = []
@dependency_loading = true
@serve_static_assets = true
@static_cache_control = nil
@force_ssl = false
@ssl_options = {}
@session_store = :cookie_store
@session_options = {}
@time_zone = "UTC"
@log_level = nil
@middleware = app_middleware
@generators = app_generators
@cache_store = [ :file_store, "#{root}/tmp/cache/" ]
@railties_order = [:all]
@initializers_paths = []
@relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
@reload_classes_only_on_change = true
@assets = ActiveSupport::OrderedOptions.new
@assets.enabled = false

View File

@ -5,7 +5,7 @@ module Rails
$rails_rake_task = nil
initializer :load_config_initializers do
config.all_initializers.each { |init| load(init) }
config.initializers_paths.each { |init| load(init) }
end
initializer :add_generator_templates do
@ -64,18 +64,18 @@ module Rails
ActiveSupport.run_load_hooks(:after_initialize, self)
end
# Set app reload just after the finisher hook to ensure
# paths added in the hook are still loaded.
initializer :set_dependencies_hook, :group => :all do |app|
app.set_dependencies_hook
end
# Set app reload just after the finisher hook to ensure
# routes added in the hook are still loaded.
initializer :set_routes_reloader_hook do |app|
app.set_routes_reloader_hook
end
# Set app reload just after the finisher hook to ensure
# paths added in the hook are still loaded.
initializer :set_dependencies_hook, :group => :all do |app|
app.set_dependencies_hook
end
# Disable dependency loading during request cycle
initializer :disable_dependency_loading do
if config.cache_classes && !config.dependency_loading

View File

@ -1,21 +1,17 @@
require "active_support/core_ext/module/delegation"
module Rails
class Application
class RoutesReloader
attr_reader :route_sets
delegate :paths, :execute_if_updated, :updated?, :to => :@updater
def initialize(updater=ActiveSupport::FileUpdateChecker)
@updater = updater.new([]) { reload! }
@route_sets = []
end
def paths
@updater.paths
end
def execute_if_updated
@updater.execute_if_updated
end
def reload!
clear!
load_paths

View File

@ -584,7 +584,7 @@ module Rails
end
initializer :append_config_initializers do |app|
app.config.all_initializers.concat config.paths["config/initializers"].existent.sort
app.config.initializers_paths.concat config.paths["config/initializers"].existent.sort
end
initializer :engines_blank_point do

View File

@ -7,6 +7,18 @@ module Rails
@@options ||= {}
end
# Add files that should be watched for change.
def watchable_files
@@watchable_files ||= []
end
# Add directories that should be watched for change.
# The key of the hashes should be directories and the values should
# be an array of extensions to match in each directory.
def watchable_dirs
@@watchable_dirs ||= {}
end
# This allows you to modify the application's middlewares from Engines.
#
# All operations you run on the app_middleware will be replayed on the

View File

@ -61,7 +61,8 @@ class ConsoleTest < Test::Unit::TestCase
load_environment
assert User.new.respond_to?(:name)
assert !User.new.respond_to?(:age)
sleep(1)
app_file "app/models/user.rb", <<-MODEL
class User

View File

@ -66,6 +66,7 @@ class LoadingTest < Test::Unit::TestCase
def test_descendants_are_cleaned_on_each_request_without_cache_classes
add_to_config <<-RUBY
config.cache_classes = false
config.reload_classes_only_on_change = false
RUBY
app_file "app/models/post.rb", <<-MODEL