diff options
author | José Valim <jose.valim@gmail.com> | 2011-12-12 22:51:33 +0100 |
---|---|---|
committer | José Valim <jose.valim@gmail.com> | 2011-12-12 22:54:04 +0100 |
commit | fa1d9a884c0d5b70c97442e3360ac98ca5fa4340 (patch) | |
tree | e39e017a9de2936a87f3b9ecf1d14b6febd0a8f1 | |
parent | 62cda03fa824ce1e1fc92aaee0367c29ade6a504 (diff) | |
download | rails-fa1d9a884c0d5b70c97442e3360ac98ca5fa4340.tar.gz rails-fa1d9a884c0d5b70c97442e3360ac98ca5fa4340.tar.bz2 rails-fa1d9a884c0d5b70c97442e3360ac98ca5fa4340.zip |
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
-rw-r--r-- | actionpack/test/dispatch/reloader_test.rb | 9 | ||||
-rw-r--r-- | activerecord/lib/active_record/railtie.rb | 5 | ||||
-rw-r--r-- | activesupport/lib/active_support/file_update_checker.rb | 43 | ||||
-rw-r--r-- | activesupport/lib/active_support/i18n_railtie.rb | 3 | ||||
-rw-r--r-- | activesupport/test/file_update_checker_test.rb | 13 | ||||
-rw-r--r-- | railties/CHANGELOG.md | 7 | ||||
-rw-r--r-- | railties/guides/source/configuring.textile | 2 | ||||
-rw-r--r-- | railties/lib/rails/application.rb | 40 | ||||
-rw-r--r-- | railties/lib/rails/application/configuration.rb | 49 | ||||
-rw-r--r-- | railties/lib/rails/application/finisher.rb | 14 | ||||
-rw-r--r-- | railties/lib/rails/application/routes_reloader.rb | 12 | ||||
-rw-r--r-- | railties/lib/rails/engine.rb | 2 | ||||
-rw-r--r-- | railties/lib/rails/railtie/configuration.rb | 12 | ||||
-rw-r--r-- | railties/test/application/console_test.rb | 3 | ||||
-rw-r--r-- | railties/test/application/loading_test.rb | 1 |
15 files changed, 157 insertions, 58 deletions
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index bd24256427..3411bd14ea 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -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 } diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index c2e31579a4..71772529d2 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -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 diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 77bc5388d6..4137bbf6a0 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -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 + # 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 - current_update_at = self.updated_at - if @last_update_at != current_update_at - @last_update_at = current_update_at + 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(",")}}" diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 4c59fe9ac9..a989ff8f57 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -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 diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb index a5a9b7a682..52c1f3260d 100644 --- a/activesupport/test/file_update_checker_test.rb +++ b/activesupport/test/file_update_checker_test.rb @@ -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 } diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2841996b56..a88f443517 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -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* diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 8e65dbccbb..8cf88cf71f 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -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+. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index acbfd7078b..0b8eac8a8b 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -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 diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4b2afe3a28..39d66ecc31 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -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 diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 17e7aa0f28..e000f6ef3a 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -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 @@ -65,17 +65,17 @@ module Rails 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 diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index c1f435a3ee..460b84dfd9 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -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 diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 8ebe1f48a5..86efc7332c 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -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 diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index f888684117..cf9e4ad500 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -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 diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 2073c780bf..6f9d8d57b1 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -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 diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 47c6fd5c6e..c4908915dc 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -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 |