Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit fa1d9a884c0d5b70c97442e3360ac98ca5fa4340 1 parent 62cda03
José Valim josevalim authored
9 actionpack/test/dispatch/reloader_test.rb
View
@@ -129,6 +129,15 @@ def test_manual_reloading
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 }
5 activerecord/lib/active_record/railtie.rb
View
@@ -94,6 +94,11 @@ class Railtie < Rails::Railtie
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
43 activesupport/lib/active_support/file_update_checker.rb
View
@@ -39,30 +39,53 @@ def initialize(paths, calculate=false, &block)
@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 @@ def compile_glob(hash)
"{#{globs.join(",")}}"
end
- def compile_ext(array)
+ def compile_ext(array) #:nodoc:
array = Array.wrap(array)
return if array.empty?
".{#{array.join(",")}}"
3  activesupport/lib/active_support/i18n_railtie.rb
View
@@ -17,7 +17,8 @@ def self.reloader
# 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
13 activesupport/test/file_update_checker_test.rb
View
@@ -54,6 +54,19 @@ def test_should_invoke_the_block_if_a_file_has_changed
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 }
7 railties/CHANGELOG.md
View
@@ -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*
2  railties/guides/source/configuring.textile
View
@@ -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+.
40 railties/lib/rails/application.rb
View
@@ -71,12 +71,14 @@ def inherited(base)
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 @@ def set_routes_reloader_hook
reloader = routes_reloader
hook = lambda { reloader.execute_if_updated }
hook.call
+ self.reloaders << reloader
ActionDispatch::Reloader.to_prepare(&hook)
end
@@ -126,10 +129,35 @@ def set_routes_reloader_hook
# 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
Santiago Pastorino Owner

dirs = config.watchable_dirs.dup ?

José Valim Owner

feel free to change it bro :) the line above as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ 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 @@ def helpers_paths #:nodoc:
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 @@ def default_middleware_stack
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
49 railties/lib/rails/application/configuration.rb
View
@@ -7,11 +7,11 @@ class Application
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 @@ class Configuration < ::Rails::Engine::Configuration
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
14 railties/lib/rails/application/finisher.rb
View
@@ -5,7 +5,7 @@ module Finisher
$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 Finisher
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
12 railties/lib/rails/application/routes_reloader.rb
View
@@ -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
2  railties/lib/rails/engine.rb
View
@@ -584,7 +584,7 @@ def load_seed
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
12 railties/lib/rails/railtie/configuration.rb
View
@@ -7,6 +7,18 @@ def initialize
@@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
3  railties/test/application/console_test.rb
View
@@ -61,7 +61,8 @@ class User
load_environment
assert User.new.respond_to?(:name)
- assert !User.new.respond_to?(:age)
+
+ sleep(1)

Sleep Driven Development :P

José Valim Owner

FWIW, the sleep is required because File.mtime just changes after 1s. So if we don't sleep, the reloader cannot pick up file system changes.

Thanks for explanation :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
app_file "app/models/user.rb", <<-MODEL
class User
1  railties/test/application/loading_test.rb
View
@@ -66,6 +66,7 @@ class User < ActiveRecord::Base
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

12 comments on commit fa1d9a8

Santiago Pastorino

dirs = config.watchable_dirs.dup ?

José Valim

feel free to change it bro :) the line above as well.

Xavier Noria
Owner

:heart: :heart: :heart: :heart: :heart: :heart: :heart:

Guillermo Iguaran

LOVE THIS :heart:

Ryan Bigg

OMG BLOAT.

trollface.jpg

[<3<3<3<3<3]

Andrew Rosa

There is no sufficient :heart: to express myself.

Elliot Winkler

Awesomesauce. Makes sense to me.

Gavin Laking

I'm liking it! So when will see this bad boy in action?

Anders Hansen

Awesome

Piotr Okoński

! :)

Gabriel Sobrinho

Sleep Driven Development :P

José Valim

FWIW, the sleep is required because File.mtime just changes after 1s. So if we don't sleep, the reloader cannot pick up file system changes.

Thilo Utke

This would save so much time, thank you!

Igor Bozato

WOLOLO

Matthew Kanwisher

Finally !!!!! THX!!!!

Please sign in to comment.
Something went wrong with that request. Please try again.