diff options
Diffstat (limited to 'actionview/lib/action_view')
-rw-r--r-- | actionview/lib/action_view/digestor.rb | 30 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers/cache_helper.rb | 45 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers/tag_helper.rb | 1 | ||||
-rw-r--r-- | actionview/lib/action_view/helpers/url_helper.rb | 40 | ||||
-rw-r--r-- | actionview/lib/action_view/log_subscriber.rb | 18 | ||||
-rw-r--r-- | actionview/lib/action_view/lookup_context.rb | 16 | ||||
-rw-r--r-- | actionview/lib/action_view/renderer/abstract_renderer.rb | 8 | ||||
-rw-r--r-- | actionview/lib/action_view/renderer/partial_renderer.rb | 24 | ||||
-rw-r--r-- | actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb | 41 | ||||
-rw-r--r-- | actionview/lib/action_view/tasks/dependencies.rake | 4 | ||||
-rw-r--r-- | actionview/lib/action_view/template.rb | 23 | ||||
-rw-r--r-- | actionview/lib/action_view/template/error.rb | 4 | ||||
-rw-r--r-- | actionview/lib/action_view/template/handlers/erb.rb | 25 |
13 files changed, 133 insertions, 146 deletions
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 359b2f810c..f3c5d6c8df 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -4,13 +4,11 @@ require 'monitor' module ActionView class Digestor - cattr_reader(:cache) - @@cache = Concurrent::Map.new @@digest_monitor = Monitor.new class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: def call(env) - ActionView::Digestor.cache.clear + ActionView::LookupContext::DetailsKey.clear app.call(env) end end @@ -22,42 +20,42 @@ module ActionView # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(options) - options.assert_valid_keys(:name, :finder, :dependencies, :partial) + def digest(name:, finder:, **options) + options.assert_valid_keys(:dependencies, :partial) - cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') + cache_key = ([ name ].compact + Array.wrap(options[:dependencies])).join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) - @@cache[cache_key] || @@digest_monitor.synchronize do - @@cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, options) + finder.digest_cache[cache_key] || @@digest_monitor.synchronize do + finder.digest_cache.fetch(cache_key) do # re-check under lock + compute_and_store_digest(cache_key, name, finder, options) end end end private - def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock - klass = if options[:partial] || options[:name].include?("/_") + def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock + klass = if options[:partial] || name.include?("/_") # Prevent re-entry or else recursive templates will blow the stack. # There is no need to worry about other threads seeing the +false+ value, # as they will then have to wait for this thread to let go of the @@digest_monitor lock. - pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion + pre_stored = finder.digest_cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion PartialDigestor else Digestor end - @@cache[cache_key] = stored_digest = klass.new(options).digest + finder.digest_cache[cache_key] = stored_digest = klass.new(name, finder, options).digest ensure # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest + finder.digest_cache.delete_pair(cache_key, false) if pre_stored && !stored_digest end end attr_reader :name, :finder, :options - def initialize(name:, finder:, **options) + def initialize(name, finder, options = {}) @name, @finder = name, finder @options = options end @@ -80,7 +78,7 @@ module ActionView def nested_dependencies dependencies.collect do |dependency| - dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies + dependencies = PartialDigestor.new(dependency, finder).nested_dependencies dependencies.any? ? { dependency => dependencies } : dependency end end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 401f398721..4c7c4b91c6 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -126,44 +126,29 @@ module ActionView # # Now all you have to do is change that timestamp when the helper method changes. # - # === Automatic Collection Caching + # === Collection Caching # - # When rendering collections such as: + # When rendering a collection of objects that each use the same partial, a `cached` + # option can be passed. + # For collections rendered such: # - # <%= render @notifications %> - # <%= render partial: 'notifications/notification', collection: @notifications %> + # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %> # - # If the notifications/_notification partial starts with a cache call as: + # The `cached: true` will make Action View's rendering read several templates + # from cache at once instead of one call per template. # - # <% cache notification do %> - # <%= notification.name %> - # <% end %> - # - # The collection can then automatically use any cached renders for that - # template by reading them at once instead of one by one. - # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for - # more information on what cache calls make a template eligible for this - # collection caching. - # - # The automatic cache multi read can be turned off like so: + # Templates in the collection not already cached are written to cache. # - # <%= render @notifications, cache: false %> + # Works great alongside individual template fragment caching. + # For instance if the template the collection renders is cached like: # - # === Explicit Collection Caching - # - # If the partial template doesn't start with a clean cache call as - # mentioned above, you can still benefit from collection caching by - # adding a special comment format anywhere in the template, like: - # - # <%# Template Collection: notification %> - # <% my_helper_that_calls_cache(some_arg, notification) do %> - # <%= notification.name %> + # # notifications/_notification.html.erb + # <% cache notification do %> + # <%# ... %> # <% end %> # - # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>, - # so it's important that you type it out just so. - # You can only declare one collection in a partial template file. + # Any collection renders will find those cached templates when attempting + # to read multiple templates at once. def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching name_options = options.slice(:skip_digest, :virtual_path) diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 2562504896..42e7358a1d 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -154,6 +154,7 @@ module ActionView options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| + next if v.nil? output << sep output << prefix_tag_option(key, k, v, escape) end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 3a4561a083..87218821ed 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -329,8 +329,8 @@ module ActionView inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) if params - params.each do |param_name, value| - inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param) + to_form_params(params).each do |param| + inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value]) end end content_tag('form', inner_tags, form_options) @@ -595,6 +595,42 @@ module ActionView def method_tag(method) tag('input', type: 'hidden', name: '_method', value: method.to_s) end + + # Returns an array of hashes each containing :name and :value keys + # suitable for use as the names and values of form input fields: + # + # to_form_params(name: 'David', nationality: 'Danish') + # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # + # to_form_params(country: {name: 'Denmark'}) + # # => [{name: 'country[name]', value: 'Denmark'}] + # + # to_form_params(countries: ['Denmark', 'Sweden']}) + # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}] + # + # An optional namespace can be passed to enclose key names: + # + # to_form_params({ name: 'Denmark' }, 'country') + # # => [{name: 'country[name]', value: 'Denmark'}] + def to_form_params(attribute, namespace = nil) # :nodoc: + params = [] + case attribute + when Hash + attribute.each do |key, value| + prefix = namespace ? "#{namespace}[#{key}]" : key + params.push(*to_form_params(value, prefix)) + end + when Array + array_prefix = "#{namespace}[]" + attribute.each do |value| + params.push(*to_form_params(value, array_prefix)) + end + else + params << { name: namespace, value: attribute.to_param } + end + + params.sort_by { |pair| pair[:name] } + end end end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 9047dbdd85..aa38db2a3a 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -20,7 +20,15 @@ module ActionView end end alias :render_partial :render_template - alias :render_collection :render_template + + def render_collection(event) + identifier = event.payload[:identifier] || 'templates' + + info do + " Rendered collection of #{from_rails_root(identifier)}" \ + " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + end + end def logger ActionView::Base.logger @@ -38,6 +46,14 @@ module ActionView def rails_root @root ||= "#{Rails.root}/" end + + def render_count(payload) + if payload[:cache_hits] + "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" + else + "[#{payload[:count]} times]" + end + end end end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 126f289f55..86afedaa2d 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -55,9 +55,7 @@ module ActionView class DetailsKey #:nodoc: alias :eql? :equal? - alias :object_hash :hash - attr_reader :hash @details_keys = Concurrent::Map.new def self.get(details) @@ -72,8 +70,16 @@ module ActionView @details_keys.clear end + def self.empty?; @details_keys.empty?; end + + def self.digest_caches + @details_keys.values.map(&:digest_cache) + end + + attr_reader :digest_cache + def initialize - @hash = object_hash + @digest_cache = Concurrent::Map.new end end @@ -200,6 +206,10 @@ module ActionView self.view_paths = view_paths end + def digest_cache + details_key.digest_cache + end + def initialize_details(target, details) registered_details.each do |k| target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index aa77a77acf..23e672a95f 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -35,8 +35,12 @@ module ActionView end end - def instrument(name, options={}) - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } + def instrument(name, **options) + options[:identifier] ||= (@template && @template.identifier) || @path + + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| + yield payload + end end def prepend_formats(formats) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index bdbf03191a..514804b08e 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -294,7 +294,7 @@ module ActionView def render(context, options, block) setup(context, options, block) - identifier = (@template = find_partial) ? @template.identifier : @path + @template = find_partial @lookup_context.rendered_format ||= begin if @template && @template.formats.present? @@ -305,11 +305,9 @@ module ActionView end if @collection - instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end + render_collection else - instrument(:partial, :identifier => identifier) do + instrument(:partial) do render_partial end end @@ -318,15 +316,17 @@ module ActionView private def render_collection - return nil if @collection.blank? + instrument(:collection, count: @collection.size) do |payload| + return nil if @collection.blank? - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) - end + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + end - cache_collection_render do - @template ? collection_with_template : collection_without_template - end.join(spacer).html_safe + cache_collection_render(payload) do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe + end end def render_partial diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 5a294744e8..f7deba94ce 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - module ActionView module CollectionCaching # :nodoc: extend ActiveSupport::Concern @@ -11,38 +9,25 @@ module ActionView end private - def cache_collection_render - return yield unless cache_collection? + def cache_collection_render(instrumentation_payload) + return yield unless @options[:cached] keyed_collection = collection_by_cache_keys - partial_cache = collection_cache.read_multi(*keyed_collection.keys) + cached_partials = collection_cache.read_multi(*keyed_collection.keys) + instrumentation_payload[:cache_hits] = cached_partials.size - @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values - rendered_partials = @collection.any? ? yield.dup : [] + @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + rendered_partials = @collection.empty? ? [] : yield - fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do - rendered_partials.shift + index = 0 + fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + rendered_partials[index].tap { index += 1 } end end - def cache_collection? - @options.fetch(:cache, automatic_cache_eligible?) - end - - def automatic_cache_eligible? - @template && !callable_cache_key? && - @template.eligible_for_collection_caching?(as: @options[:as]) - end - - def callable_cache_key? - @options[:cache].respond_to?(:call) - end - def collection_by_cache_keys - seed = callable_cache_key? ? @options[:cache] : ->(i) { i } - @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item))] = item + hash[expanded_cache_key(item)] = item end end @@ -52,10 +37,10 @@ module ActionView end def fetch_or_cache_partial(cached_partials, order_by:) - order_by.map do |key| - cached_partials.fetch(key) do + order_by.map do |cache_key| + cached_partials.fetch(cache_key) do yield.tap do |rendered_partial| - collection_cache.write(key, rendered_partial, @options[:cache_options]) + collection_cache.write(cache_key, rendered_partial) end end end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index f394c319c1..9932ff8b6d 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,13 +2,13 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).nested_dependencies end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).dependencies end class CacheDigests diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 15fc2b71a3..169ee55fdc 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -130,7 +130,6 @@ module ActionView @source = source @identifier = identifier @handler = handler - @cache_name = extract_resource_cache_name @compiled = false @original_encoding = nil @locals = details[:locals] || [] @@ -166,10 +165,6 @@ module ActionView @type ||= Types[@formats.first] if @formats.first end - def eligible_for_collection_caching?(as: nil) - @cache_name == (as || inferred_cache_name).to_s - end - # Receives a view object and return a template similar to self by using @virtual_path. # # This method is useful if you have a template object but it does not contain its source @@ -355,23 +350,5 @@ module ActionView ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) end end - - EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/ - - def extract_resource_cache_name - if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match - match[:resource_name] - end - end - - def resource_cache_call_match - if @handler.respond_to?(:resource_cache_call_pattern) - @source.match(@handler.resource_cache_call_pattern) - end - end - - def inferred_cache_name - @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze) - end end end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index ccee785d3e..3f38c3d2b9 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -135,13 +135,13 @@ module ActionView end def formatted_code_for(source_code, line_counter, indent, output) - start_value = (output == :html) ? {} : "" + start_value = (output == :html) ? {} : [] source_code.inject(start_value) do |result, line| line_counter += 1 if output == :html result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line]) else - result << "%#{indent}s: %s\n" % [line_counter, line] + result << "%#{indent}s: %s" % [line_counter, line] end end end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 1f8459c24b..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -123,31 +123,6 @@ module ActionView ).src end - # Returns Regexp to extract a cached resource's name from a cache call at the - # first line of a template. - # The extracted cache name is captured as :resource_name. - # - # <% cache notification do %> # => notification - # - # The pattern should support templates with a beginning comment: - # - # <%# Still extractable even though there's a comment %> - # <% cache notification do %> # => notification - # - # But fail to extract a name if a resource association is cached. - # - # <% cache notification.event do %> # => nil - def resource_cache_call_pattern - /\A - (?:<%\#.*%>)* # optional initial comment - \s* # followed by optional spaces or newlines - <%\s*cache[\(\s] # followed by an ERB call to cache - \s* # followed by optional spaces or newlines - (?<resource_name>\w+) # capture the cache call argument as :resource_name - [\s\)] # followed by a space or close paren - /xm - end - private def valid_encoding(string, encoding) |