From e56c63542780fe2fb804636a875f95cae08ab3f4 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 15 Feb 2015 20:34:18 +0100 Subject: Merge multi_fetch_fragments. Makes caching a collection of template partials faster using `read_multi` on the Rails cache store. Some caching implementations have optimized `read_multi` so we don't have to check in the cache store for every template. --- actionview/lib/action_view/helpers/cache_helper.rb | 8 +++ actionview/lib/action_view/railtie.rb | 6 +++ .../lib/action_view/renderer/partial_renderer.rb | 8 ++- .../partial_renderer/collection_caching.rb | 57 ++++++++++++++++++++++ .../test/actionpack/controller/render_test.rb | 4 ++ actionview/test/template/render_test.rb | 14 ++++++ 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 4db8930a26..56b1c5049c 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -161,6 +161,14 @@ module ActionView end end + # Given a key (as described in ActionController::Caching::Fragments.expire_fragment), + # returns a key suitable for use in reading, writing, or expiring a + # cached fragment. All keys are prefixed with views/ and uses + # ActiveSupport::Cache.expand_cache_key for the expansion. + def fragment_cache_key(key) + ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + end + private def fragment_name_with_digest(name) #:nodoc: diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index ee3dce24b7..9a26cba574 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -36,6 +36,12 @@ module ActionView end end + initializer "action_view.collection_caching" do |app| + ActiveSupport.on_load(:action_controller) do + PartialRenderer.collection_cache = app.config.action_controller.cache_store + end + end + initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 5ff15411cf..56b8ab1e2d 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,3 +1,4 @@ +require 'action_view/renderer/partial_renderer/collection_caching' require 'thread_safe' module ActionView @@ -280,6 +281,8 @@ module ActionView # <%- end -%> # <% end %> class PartialRenderer < AbstractRenderer + include CollectionCaching + PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k| h[k] = ThreadSafe::Cache.new end @@ -321,8 +324,9 @@ module ActionView spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) end - result = @template ? collection_with_template : collection_without_template - result.join(spacer).html_safe + cache_collection_render do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe 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 new file mode 100644 index 0000000000..0f2ab887bc --- /dev/null +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -0,0 +1,57 @@ +require 'active_support/core_ext/object/try' + +module ActionView + module CollectionCaching # :nodoc: + extend ActiveSupport::Concern + + included do + # Fallback cache store if Action View is used without Rails. + # Otherwise overriden in Railtie to use Rails.cache. + mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new } + end + + private + def cache_collection_render + return yield unless cache_collection? + + keyed_collection = collection_by_cache_keys + partial_cache = collection_cache.read_multi(*keyed_collection.keys) + + @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values + rendered_partials = @collection.any? ? yield.dup : [] + + fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do + rendered_partials.shift + end + end + + def cache_collection? + @options[:cache].present? + end + + def collection_by_cache_keys + seed = @options[:cache].respond_to?(:call) ? @options[:cache] : ->(i) { i } + + @collection.each_with_object({}) do |item, hash| + hash[expanded_cache_key(seed.call(item))] = item + end + end + + def expanded_cache_key(key) + key = @view.fragment_cache_key(@view.cache_fragment_name(key)) + key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. + end + + def fetch_or_cache_partial(cached_partials, order_by:) + cache_options = @options[:cache_options] || @locals[:cache_options] || {} + + order_by.map do |key| + cached_partials.fetch(key) do + yield.tap do |rendered_partial| + collection_cache.write(key, rendered_partial, cache_options) + end + end + end + end + end +end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index fe4cf3688a..63ee5e14ec 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -31,6 +31,10 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end + + def cache_key + name.to_s + end end module Quiz diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index f77b81f0ee..73871cf974 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -597,3 +597,17 @@ class LazyViewRenderTest < ActiveSupport::TestCase silence_warnings { Encoding.default_external = old } end end + +class CachedCollectionViewRenderTest < CachedViewRenderTest + test "with custom key" do + customer = Customer.new("david") + key = ActionController::Base.new.fragment_cache_key([customer, 'key']) + + ActionView::PartialRenderer.collection_cache.write(key, 'Hello') + + assert_equal "Hello", + @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] }) + + ActionView::PartialRenderer.collection_cache.clear + end +end -- cgit v1.2.3