Permalink
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...
josevalim committed Dec 12, 2011
1 parent 62cda03 commit fa1d9a884c0d5b70c97442e3360ac98ca5fa4340
@@ -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 }
@@ -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
@@ -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(",")}}"
@@ -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
@@ -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 }
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*
@@ -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+.
@@ -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,17 +121,43 @@ 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
# An app dependencies hook that is used to setup to_cleanup callbacks.
# 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

This comment has been minimized.

Show comment
Hide comment
@spastorino

spastorino Dec 12, 2011

Member

dirs = config.watchable_dirs.dup ?

@spastorino

spastorino Dec 12, 2011

Member

dirs = config.watchable_dirs.dup ?

This comment has been minimized.

Show comment
Hide comment
@josevalim

josevalim Dec 12, 2011

Contributor

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

@josevalim

josevalim Dec 12, 2011

Contributor

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

+ 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
@@ -7,37 +7,38 @@ 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
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
@@ -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
@@ -64,18 +64,18 @@ module Finisher
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
@@ -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
@@ -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
@@ -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
@@ -61,7 +61,8 @@ class User
load_environment
assert User.new.respond_to?(:name)
- assert !User.new.respond_to?(:age)
+
+ sleep(1)

This comment has been minimized.

Show comment
Hide comment
@sobrinho

sobrinho Dec 13, 2011

Contributor

Sleep Driven Development :P

@sobrinho

sobrinho Dec 13, 2011

Contributor

Sleep Driven Development :P

This comment has been minimized.

Show comment
Hide comment
@josevalim

josevalim Dec 13, 2011

Contributor

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.

@josevalim

josevalim Dec 13, 2011

Contributor

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.

This comment has been minimized.

Show comment
Hide comment
@sobrinho

sobrinho Dec 13, 2011

Contributor

Thanks for explanation :)

@sobrinho

sobrinho Dec 13, 2011

Contributor

Thanks for explanation :)

app_file "app/models/user.rb", <<-MODEL
class User
Oops, something went wrong.

12 comments on commit fa1d9a8

@fxn

This comment has been minimized.

Show comment
Hide comment
@fxn

fxn Dec 12, 2011

Member

❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️

Member

fxn replied Dec 12, 2011

❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️

@guilleiguaran

This comment has been minimized.

Show comment
Hide comment
@guilleiguaran

guilleiguaran Dec 12, 2011

Member

LOVE THIS ❤️

Member

guilleiguaran replied Dec 12, 2011

LOVE THIS ❤️

@radar

This comment has been minimized.

Show comment
Hide comment
@radar

radar Dec 12, 2011

Contributor

OMG BLOAT.

trollface.jpg

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

Contributor

radar replied Dec 12, 2011

OMG BLOAT.

trollface.jpg

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

@andrewhr

This comment has been minimized.

Show comment
Hide comment
@andrewhr

andrewhr Dec 12, 2011

There is no sufficient ❤️ to express myself.

There is no sufficient ❤️ to express myself.

@mcmire

This comment has been minimized.

Show comment
Hide comment
@mcmire

mcmire Dec 12, 2011

Contributor

Awesomesauce. Makes sense to me.

Contributor

mcmire replied Dec 12, 2011

Awesomesauce. Makes sense to me.

@gavinlaking

This comment has been minimized.

Show comment
Hide comment
@gavinlaking

gavinlaking Dec 12, 2011

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

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

@dmitriy-kiriyenko

This comment has been minimized.

Show comment
Hide comment
@dmitriy-kiriyenko

dmitriy-kiriyenko Dec 13, 2011

Contributor

Wow.

Contributor

dmitriy-kiriyenko replied Dec 13, 2011

Wow.

@xiplias

This comment has been minimized.

Show comment
Hide comment

Awesome

@pokonski

This comment has been minimized.

Show comment
Hide comment
@pokonski

pokonski Dec 13, 2011

Contributor

! :)

Contributor

pokonski replied Dec 13, 2011

! :)

@thilo

This comment has been minimized.

Show comment
Hide comment
@thilo

thilo Dec 13, 2011

Contributor

This would save so much time, thank you!

Contributor

thilo replied Dec 13, 2011

This would save so much time, thank you!

@igorbozato

This comment has been minimized.

Show comment
Hide comment

WOLOLO

@mattkanwisher

This comment has been minimized.

Show comment
Hide comment
@mattkanwisher

mattkanwisher Dec 16, 2011

Finally !!!!! THX!!!!

Finally !!!!! THX!!!!

Please sign in to comment.