Port fast reloadable templates from rails-dev-boost.

This commit is contained in:
thedarkone
2009-02-12 19:35:14 +01:00
committed by Joshua Peek
parent b1d41bdfb0
commit 3942cb406e
17 changed files with 275 additions and 130 deletions

View File

@@ -19,6 +19,7 @@ ActionView::Template.register_template_handler :bak, lambda { |template| "Lame b
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
ActionView::Base.cache_template_loading = true
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionMailer::Base.template_root = FIXTURE_LOAD_PATH

View File

@@ -975,7 +975,7 @@ end
class InheritableTemplateRootTest < Test::Unit::TestCase
def test_attr
expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
expected = ("#{File.dirname(__FILE__)}/fixtures/path.with.dots").sub(/\.\//, '')
assert_equal expected, FunkyPathMailer.template_root.to_s
sub = Class.new(FunkyPathMailer)

View File

@@ -38,7 +38,7 @@ module ActionController #:nodoc:
'ActionView::TemplateError' => 'template_error'
}
RESCUES_TEMPLATE_PATH = ActionView::Template::Path.new(
RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new_and_loaded(
File.join(File.dirname(__FILE__), "templates"))
def self.included(base) #:nodoc:

View File

@@ -44,6 +44,7 @@ module ActionView
autoload :Renderable, 'action_view/renderable'
autoload :RenderablePartial, 'action_view/renderable_partial'
autoload :Template, 'action_view/template'
autoload :ReloadableTemplate, 'action_view/reloadable_template'
autoload :TemplateError, 'action_view/template_error'
autoload :TemplateHandler, 'action_view/template_handler'
autoload :TemplateHandlers, 'action_view/template_handlers'

View File

@@ -182,10 +182,15 @@ module ActionView #:nodoc:
# that alert()s the caught exception (and then re-raises it).
cattr_accessor :debug_rjs
# Specify whether to check whether modified templates are recompiled without a restart
# Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed.
# Automaticaly reloading templates are not thread safe and should only be used in development mode.
@@cache_template_loading = false
cattr_accessor :cache_template_loading
def self.cache_template_loading?
ActionController::Base.allow_concurrency || cache_template_loading
end
attr_internal :request
delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers,
@@ -226,6 +231,8 @@ module ActionView #:nodoc:
def view_paths=(paths)
@view_paths = self.class.process_view_paths(paths)
# we might be using ReloadableTemplates, so we need to let them know this a new request
@view_paths.load!
end
# Returns the result of a render that's dictated by the options hash. The primary options are:
@@ -247,8 +254,8 @@ module ActionView #:nodoc:
if options[:layout]
_render_with_layout(options, local_assigns, &block)
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
tempalte = self.view_paths.find_template(options[:file], template_format)
tempalte.render_template(self, options[:locals])
elsif options[:partial]
render_partial(options)
elsif options[:inline]

View File

@@ -235,5 +235,6 @@ module ActionView
self.view_paths.find_template(path, self.template_format)
end
memoize :_pick_partial_template
end
end

View File

@@ -2,12 +2,16 @@ module ActionView #:nodoc:
class PathSet < Array #:nodoc:
def self.type_cast(obj)
if obj.is_a?(String)
Template::Path.new(obj)
if Base.cache_template_loading?
Template::EagerPath.new(obj.to_s)
else
ReloadableTemplate::ReloadablePath.new(obj.to_s)
end
else
obj
end
end
def initialize(*args)
super(*args).map! { |obj| self.class.type_cast(obj) }
end
@@ -31,9 +35,14 @@ module ActionView #:nodoc:
def unshift(*objs)
super(*objs.map { |obj| self.class.type_cast(obj) })
end
def load!
each(&:load!)
end
def find_template(template_path, format = nil)
return template_path if template_path.respond_to?(:render)
def find_template(original_template_path, format = nil)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
each do |load_path|
if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
@@ -52,11 +61,9 @@ module ActionView #:nodoc:
end
end
if File.exist?(template_path)
return Template.new(template_path, template_path[0] == 47 ? "" : ".")
end
return Template.new(original_template_path, original_template_path =~ /\A\// ? "" : ".") if File.file?(original_template_path)
raise MissingTemplate.new(self, template_path, format)
raise MissingTemplate.new(self, original_template_path, format)
end
end
end

View File

@@ -0,0 +1,120 @@
module ActionView #:nodoc:
class ReloadableTemplate < Template
class TemplateDeleted < ActionView::ActionViewError
end
class ReloadablePath < Template::Path
def initialize(path)
super
@paths = {}
new_request!
end
def new_request!
@disk_cache = {}
end
alias_method :load!, :new_request!
def [](path)
if found_template = @paths[path]
begin
found_template.reset_cache_if_stale!
rescue TemplateDeleted
unregister_template(found_template)
self[path]
end
else
load_all_templates_from_dir(templates_dir_from_path(path))
@paths[path]
end
end
def register_template_from_file(template_file_path)
if !@paths[template_relative_path = template_file_path.split("#{@path}/").last] && File.file?(template_file_path)
register_template(ReloadableTemplate.new(template_relative_path, self))
end
end
def register_template(template)
template.accessible_paths.each do |path|
@paths[path] = template
end
end
# remove (probably deleted) template from cache
def unregister_template(template)
template.accessible_paths.each do |template_path|
@paths.delete(template_path) if @paths[template_path] == template
end
# fill in any newly created gaps
@paths.values.uniq.each do |template|
template.accessible_paths.each {|path| @paths[path] ||= template}
end
end
# load all templates from the directory of the requested template
def load_all_templates_from_dir(dir)
# hit disk only once per template-dir/request
@disk_cache[dir] ||= template_files_from_dir(dir).each {|template_file| register_template_from_file(template_file)}
end
def templates_dir_from_path(path)
dirname = File.dirname(path)
File.join(@path, dirname == '.' ? '' : dirname)
end
# get all the template filenames from the dir
def template_files_from_dir(dir)
Dir.glob(File.join(dir, '*'))
end
end
module Unfreezable
def freeze; self; end
end
def initialize(*args)
super
@compiled_methods = []
# we don't ever want to get frozen
extend Unfreezable
end
def mtime
File.mtime(filename)
end
attr_accessor :previously_last_modified
def stale?
previously_last_modified.nil? || previously_last_modified < mtime
rescue Errno::ENOENT => e
undef_my_compiled_methods!
raise TemplateDeleted
end
def reset_cache_if_stale!
if stale?
flush_cache 'source', 'compiled_source'
undef_my_compiled_methods!
@previously_last_modified = mtime
end
self
end
def undef_my_compiled_methods!
@compiled_methods.each {|comp_method| ActionView::Base::CompiledTemplates.send(:remove_method, comp_method)}
@compiled_methods.clear
end
def compile!(render_symbol, local_assigns)
super
@compiled_methods << render_symbol
end
end
end

View File

@@ -16,18 +16,8 @@ module ActionView
memoize :handler
def compiled_source
@compiled_at = Time.now
handler.call(self)
end
memoize :compiled_source
def compiled_at
@compiled_at
end
def defined_at
@defined_at ||= {}
end
def method_name_without_locals
['_run', extension, method_segment].compact.join('_')
@@ -71,12 +61,8 @@ module ActionView
def compile(local_assigns)
render_symbol = method_name(local_assigns)
if self.is_a?(InlineTemplate)
if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
compile!(render_symbol, local_assigns)
else
if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?(render_symbol)
recompile!(render_symbol, local_assigns)
end
end
end
@@ -93,7 +79,6 @@ module ActionView
begin
ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
defined_at[render_symbol] = Time.now if respond_to?(:reloadable?) && reloadable?
rescue Errno::ENOENT => e
raise e # Missing template file, re-raise for Base to rescue
rescue Exception => e # errors from template code
@@ -107,17 +92,8 @@ module ActionView
end
end
def recompile?(render_symbol)
!cached? || redefine?(render_symbol) || stale?
end
def recompile!(render_symbol, local_assigns)
compiled_source(:reload) if compiled_at.nil? || compiled_at < mtime
compile!(render_symbol, local_assigns)
end
def redefine?(render_symbol)
compiled_at && defined_at[render_symbol] && compiled_at > defined_at[render_symbol]
def recompile?
false
end
end
end

View File

@@ -6,14 +6,12 @@ module ActionView #:nodoc:
def initialize(path)
raise ArgumentError, "path already is a Path class" if path.is_a?(Path)
@path = path.freeze
@path = expand_path(path).freeze
end
def load!
@paths = {}
templates_in_path do |template|
load_template(template)
end
def expand_path(path)
# collapse any directory dots in path ('.' or '..')
path.starts_with?('/') ? File.expand_path(path) : File.expand_path(path, '/').from(1)
end
def to_s
@@ -46,10 +44,42 @@ module ActionView #:nodoc:
# etc. A format must be supplied to match a formated file. +hello/index+
# will never match +hello/index.html.erb+.
def [](path)
load! if @paths.nil?
@paths[path] || find_template(path)
end
def load!
end
def self.new_and_loaded(path)
returning new(path) do |path|
path.load!
end
end
end
class EagerPath < Path
def initialize(path)
super
end
def load!
return if @loaded
@paths = {}
templates_in_path do |template|
template.load!
template.accessible_paths.each do |path|
@paths[path] = template
end
end
@paths.freeze
@loaded = true
end
def [](path)
load! unless @loaded
@paths[path]
end
private
def templates_in_path
(Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
@@ -60,30 +90,6 @@ module ActionView #:nodoc:
def create_template(file)
Template.new(file.split("#{self}/").last, self)
end
def load_template(template)
template.load!
template.accessible_paths.each do |path|
@paths[path] = template
end
end
def matching_templates(template_path)
Dir.glob("#{@path}/#{template_path}.*").each do |file|
yield create_template(file) unless File.directory?(file)
end
end
def find_template(path)
return nil if Base.cache_template_loading || ActionController::Base.allow_concurrency
matching_templates(path) do |template|
if template.accessible_paths.include?(path)
load_template(template)
return template
end
end
nil
end
end
extend TemplateHandlers
@@ -171,14 +177,10 @@ module ActionView #:nodoc:
@@exempt_from_layout.any? { |exempted| path =~ exempted }
end
def mtime
File.mtime(filename)
end
memoize :mtime
def source
File.read(filename)
end
memoize :source
def method_segment
relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }
@@ -192,27 +194,13 @@ module ActionView #:nodoc:
if TemplateError === e
e.sub_template_of(self)
raise e
elsif Errno::ENOENT === e
raise MissingTemplate.new(view.view_paths, filename.sub("#{RAILS_ROOT}/#{load_path}/", ""))
else
raise TemplateError.new(self, view.assigns, e)
end
end
def stale?
reloadable? && (mtime < mtime(:reload))
end
def load!
reloadable? ? memoize_all : freeze
end
def reloadable?
!(Base.cache_template_loading || ActionController::Base.allow_concurrency)
end
def cached?
ActionController::Base.perform_caching || !reloadable?
freeze
end
private
@@ -262,5 +250,5 @@ module ActionView #:nodoc:
[base_path, name, locale, format, extension]
end
end
end
end

View File

@@ -38,4 +38,8 @@ I18n.backend.store_translations 'pt-BR', {}
ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionView::Base.cache_template_loading = true
ActionController::Base.view_paths = FIXTURE_LOAD_PATH
CACHED_VIEW_PATHS = ActionView::Base.cache_template_loading? ?
ActionController::Base.view_paths :
ActionController::Base.view_paths.map {|path| ActionView::Template::EagerPath.new(path.to_s)}

View File

@@ -41,31 +41,35 @@ class ViewLoadPathsTest < ActionController::TestCase
def teardown
ActiveSupport::Deprecation.behavior = @old_behavior
end
def assert_view_path_strings_are_equal(expected, actual)
assert_equal(expected.map {|path| path.sub(/\.\//, '')}, actual)
end
def test_template_load_path_was_set_correctly
assert_equal [FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
end
def test_controller_appends_view_path_correctly
@controller.append_view_path 'foo'
assert_equal [FIXTURE_LOAD_PATH, 'foo'], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo'], @controller.view_paths.map(&:to_s)
@controller.append_view_path(%w(bar baz))
assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths.map(&:to_s)
@controller.append_view_path(FIXTURE_LOAD_PATH)
assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
end
def test_controller_prepends_view_path_correctly
@controller.prepend_view_path 'baz'
assert_equal ['baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal ['baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
@controller.prepend_view_path(%w(foo bar))
assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
@controller.prepend_view_path(FIXTURE_LOAD_PATH)
assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
end
def test_template_appends_view_path_correctly
@@ -73,10 +77,10 @@ class ViewLoadPathsTest < ActionController::TestCase
class_view_paths = TestController.view_paths
@controller.append_view_path 'foo'
assert_equal [FIXTURE_LOAD_PATH, 'foo'], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo'], @controller.view_paths.map(&:to_s)
@controller.append_view_path(%w(bar baz))
assert_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal [FIXTURE_LOAD_PATH, 'foo', 'bar', 'baz'], @controller.view_paths.map(&:to_s)
assert_equal class_view_paths, TestController.view_paths
end
@@ -85,10 +89,10 @@ class ViewLoadPathsTest < ActionController::TestCase
class_view_paths = TestController.view_paths
@controller.prepend_view_path 'baz'
assert_equal ['baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal ['baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
@controller.prepend_view_path(%w(foo bar))
assert_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_view_path_strings_are_equal ['foo', 'bar', 'baz', FIXTURE_LOAD_PATH], @controller.view_paths.map(&:to_s)
assert_equal class_view_paths, TestController.view_paths
end

View File

@@ -2,8 +2,21 @@ require 'abstract_unit'
require 'controller/fake_models'
class CompiledTemplatesTest < Test::Unit::TestCase
def setup
@compiled_templates = ActionView::Base::CompiledTemplates
# first, if we are running the whole test suite with ReloadableTemplates
# try to undef all the methods through ReloadableTemplate's interfaces
unless ActionView::Base.cache_template_loading?
ActionController::Base.view_paths.each do |view_path|
view_path.paths.values.uniq!.each do |reloadable_template|
reloadable_template.undef_my_compiled_methods!
end
end
end
# just purge anything that's left
@compiled_templates.instance_methods.each do |m|
@compiled_templates.send(:remove_method, m) if m =~ /^_run_/
end
@@ -35,17 +48,6 @@ class CompiledTemplatesTest < Test::Unit::TestCase
end
end
def test_compiled_template_will_always_be_recompiled_when_template_is_not_cached
with_caching(false) do
ActionView::Template.any_instance.expects(:recompile?).times(3).returns(true)
assert_equal 0, @compiled_templates.instance_methods.size
assert_equal "Hello world!", render(:file => "#{FIXTURE_LOAD_PATH}/test/hello_world.erb")
ActionView::Template.any_instance.expects(:compile!).times(3)
3.times { assert_equal "Hello world!", render(:file => "#{FIXTURE_LOAD_PATH}/test/hello_world.erb") }
assert_equal 1, @compiled_templates.instance_methods.size
end
end
def test_template_changes_are_not_reflected_with_cached_template_loading
with_caching(true) do
with_reloading(false) do
@@ -63,9 +65,10 @@ class CompiledTemplatesTest < Test::Unit::TestCase
with_reloading(true) do
assert_equal "Hello world!", render(:file => "test/hello_world.erb")
modify_template "test/hello_world.erb", "Goodbye world!" do
reset_mtime_of('test/hello_world.erb')
assert_equal "Goodbye world!", render(:file => "test/hello_world.erb")
sleep(1) # Need to sleep so that the timestamp actually changes
end
reset_mtime_of('test/hello_world.erb')
assert_equal "Hello world!", render(:file => "test/hello_world.erb")
end
end
@@ -74,10 +77,15 @@ class CompiledTemplatesTest < Test::Unit::TestCase
private
def render(*args)
view_paths = ActionController::Base.view_paths
assert_equal ActionView::Template::Path, view_paths.first.class
ActionView::Base.new(view_paths, {}).render(*args)
end
def reset_mtime_of(template_name)
unless ActionView::Base.cache_template_loading?
ActionController::Base.view_paths.find_template(template_name).previously_last_modified = 10.seconds.ago
end
end
def modify_template(template, content)
filename = "#{FIXTURE_LOAD_PATH}/#{template}"
old_content = File.read(filename)
@@ -100,12 +108,18 @@ class CompiledTemplatesTest < Test::Unit::TestCase
end
def with_reloading(reload_templates)
old_cache_template_loading = ActionView::Base.cache_template_loading
old_view_paths, old_cache_templates = ActionController::Base.view_paths, ActionView::Base.cache_template_loading
begin
ActionView::Base.cache_template_loading = !reload_templates
ActionController::Base.view_paths = view_paths_for(reload_templates)
yield
ensure
ActionView::Base.cache_template_loading = old_cache_template_loading
ActionController::Base.view_paths, ActionView::Base.cache_template_loading = old_view_paths, old_cache_templates
end
end
def view_paths_for(reload_templates)
# reloadable paths are cheap to create
reload_templates ? ActionView::PathSet.new(CACHED_VIEW_PATHS.map(&:to_s)) : CACHED_VIEW_PATHS
end
end

View File

@@ -243,13 +243,34 @@ module RenderTestCases
end
end
class CachedRenderTest < Test::Unit::TestCase
include RenderTestCases
# Ensure view path cache is primed
def setup
view_paths = ActionController::Base.view_paths
assert_equal ActionView::Template::Path, view_paths.first.class
module TemplatesSetupTeardown
def setup_view_paths_for(new_cache_template_loading)
@previous_cache_template_loading, ActionView::Base.cache_template_loading = ActionView::Base.cache_template_loading, new_cache_template_loading
view_paths = new_cache_template_loading ? CACHED_VIEW_PATHS : ActionView::Base.process_view_paths(CACHED_VIEW_PATHS.map(&:to_s))
assert_equal(new_cache_template_loading ? ActionView::Template::EagerPath : ActionView::ReloadableTemplate::ReloadablePath, view_paths.first.class)
setup_view(view_paths)
end
def teardown
ActionView::Base.cache_template_loading = @previous_cache_template_loading
end
end
class CachedRenderTest < Test::Unit::TestCase
include TemplatesSetupTeardown
include RenderTestCases
def setup
setup_view_paths_for(cache_templates = true)
end
end
class ReloadableRenderTest < Test::Unit::TestCase
include TemplatesSetupTeardown
include RenderTestCases
def setup
setup_view_paths_for(cache_templates = false)
end
end

View File

@@ -12,6 +12,7 @@ config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_view.cache_template_loading = true
# Disable request forgery protection in test environment
config.action_controller.allow_forgery_protection = false

View File

@@ -369,8 +369,8 @@ Run `rake gems:install` to install the missing gems.
def load_view_paths
if configuration.frameworks.include?(:action_view)
ActionController::Base.view_paths.each { |path| path.load! } if configuration.frameworks.include?(:action_controller)
ActionMailer::Base.template_root.load! if configuration.frameworks.include?(:action_mailer)
ActionController::Base.view_paths.load! if configuration.frameworks.include?(:action_controller)
ActionMailer::Base.view_paths.load! if configuration.frameworks.include?(:action_mailer)
end
end
@@ -478,7 +478,7 @@ Run `rake gems:install` to install the missing gems.
# set to use Configuration#view_path.
def initialize_framework_views
if configuration.frameworks.include?(:action_view)
view_path = ActionView::Template::Path.new(configuration.view_path)
view_path = ActionView::PathSet.type_cast(configuration.view_path)
ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer)
ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty?
end

View File

@@ -130,7 +130,7 @@ class TestPluginLoader < Test::Unit::TestCase
@loader.send :add_engine_view_paths
assert_equal [ File.join(plugin_fixture_path('engines/engine'), 'app', 'views') ], ActionController::Base.view_paths
assert_equal [ File.join(plugin_fixture_path('engines/engine'), 'app', 'views').sub(/\A\.\//, '') ], ActionController::Base.view_paths
end
def test_should_add_plugin_load_paths_to_Dependencies_load_once_paths