Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

ActionView cache helper is inefficient

#5506

I have a simple app that displays the schedule and session details for a large conference. With potentially hundreds of partials displayed on a single page, it was a natural candidate for fragment caching. The caching works fine, but I noticed this redundancy in the logs:

Started GET "/sessions?type=full&scope=am" for 127.0.0.1 at 2010-08-30 23:36:51 -0400
  Processing by SessionsController#index as HTML
  Parameters: {"type"=>"full", "scope"=>"am"}
Read fragment views/localhost:3000/sessions?type=full&scope=am (0.8ms)
Rendered sessions/_header.haml (2.7ms)
Rendered sessions/_search.haml (171.8ms)
Exist fragment? views/localhost:3000/sessions?id=4c5c40f0609c843161000001 (0.9ms)
Read fragment views/localhost:3000/sessions?id=4c5c40f0609c843161000001 (1.5ms)
Exist fragment? views/localhost:3000/sessions?id=4c5c40f3609c843161000003 (0.8ms)
Read fragment views/localhost:3000/sessions?id=4c5c40f3609c843161000003 (0.7ms)
Exist fragment? views/localhost:3000/sessions?id=4c5c40f6609c843161000005 (0.7ms)
Read fragment views/localhost:3000/sessions?id=4c5c40f6609c843161000005 (0.6ms)
Exist fragment? views/localhost:3000/sessions?id=4c5c40f7609c843161000006 (0.9ms)
Read fragment views/localhost:3000/sessions?id=4c5c40f7609c843161000006 (1.1ms)
Exist fragment? views/localhost:3000/sessions?id=4c5c40f9609c843161000007 (0.8ms)
Read fragment views/localhost:3000/sessions?id=4c5c40f9609c843161000007 (0.7ms)

...And so forth. My observation is that on a cache hit, the ActionView::Helpers::CacheHelper#cache method has two steps (underneath various layers of abstraction) of approximately equal duration:

  1. Call the underlying cache store's #exist? method
  2. Perform a #read to get the content

However, this is redundant because the #exist? method calls the same lower-level #read_entry method that #read does! We're retrieving the exact same value from the cache store twice. The first time we return a true or false and throw the value away; the second time, we keep the value.

This is an obvious candidate for refactoring. Every cache store has consistent behavior on #read that returns nil in the event of a cache miss. It'd be very easy to skip the existence test and simply call the execute-and-write code if the read doesn't return a value.

I'll submit a patch for this presently. I just wanted to register the issue first. For complex pages with many fragment hits, this has the potential to nearly double rendering performance.

Reported by Stephen Eley · August 31st, 2010 @ 05:12 AM

State: stale
Milestone: none
Assigned to: nobody
Importance: Low

Activity

  1. José Valim
    José Valim
    • Importance changed from to Low

    Patch please!

    September 1st, 2010 @ 11:29 PM

  2. Neeraj Singh
    Neeraj Singh
    • State changed from new to open

    Stephen : nice catch.

          # Return true if the cache contains an entry for the given key.
          #
          # Options are passed to the underlying cache implementation.
          def exist?(name, options = nil)
            options = merged_options(options)
            instrument(:exist?, name) do |payload|
              entry = read_entry(namespaced_key(name, options), options)
              puts entry.inspect
              if entry && !entry.expired?
                true
              else
                false
              end
            end
          end
    

    Above method which is declared in AS cache.rb does get hold of the entire cached payload.

    However there is one thing to notice. This is an API for these storage types.

    #   config.action_controller.cache_store = :memory_store
    #   config.action_controller.cache_store = :file_store, "/path/to/cache/directory"
    #   config.action_controller.cache_store = :drb_store, "druby://localhost:9192"
    #   config.action_controller.cache_store = :mem_cache_store, "localhost"
    #   config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211")
    #   config.action_controller.cache_store = MyOwnStore.new("parameter")
    

    If you change the code in cache.rb to return the payload instead of true if there is content and use that value going forward then there is a possibility that a custom implementation of cache storage might be overriding 'def exist?' and might be returning true or false. In that case be using 'true' as the payload.

    I guess this fix is very much needed. However we should publish this change in API so that custom impelementation of cache is not caught off guard.

    September 2nd, 2010 @ 04:48 AM

  3. Santiago Pastorino
    Santiago Pastorino

    This issue has been automatically marked as stale because it has not been commented on for at least three months.

    The resources of the Rails core team are limited, and so we are asking for your help. If you can still reproduce this error on the 3-0-stable branch or on master, please reply with all of the information you have about it and add "[state:open]" to your comment. This will reopen the ticket for review. Likewise, if you feel that this is a very important feature for Rails to include, please reply with your explanation so we can consider it.

    Thank you for all your contributions, and we hope you will understand this step to focus our efforts where they are most helpful.

    February 2nd, 2011 @ 04:49 PM

  4. Santiago Pastorino
    Santiago Pastorino
    • State changed from open to stale

    February 2nd, 2011 @ 04:49 PM