module Card::View::Cache
View::Cache
supports smart card view caching.
The basic idea is that when view caching is turned on (via ‘config.view_cache`), we try to cache a view whenever it’s “safe” to do so. We will include everything inside that view (including other views) until we find something that isn’t safe. When something isn’t safe, we render a {Stub stub}: a placeholder with all the info we need to come back and replace it with the correct content later. In this way it is possible to have many levels of cached views within cached views.
Here are some things that we never consider safe to cache:
-
a view explicitly configured never to be cached
-
a view of a card with view-relevant permission restrictions
-
a view other than the requested view (eg a denial view)
-
a card with unsaved content changes
We also consider it unsafe to cache a view of one card within a view of a different card, so nests are always handled with a stub.
## Cache
configuration
Cache
settings (#5) can be configured in the {Set::Format::AbstractFormat#view view definition} and (less commonly) as a {Card::View::Options view option}.
By far, the most common explicit caching configuration is ‘:never`. This setting is used to prevent over-caching, which becomes problematic when data changes do not clear the cache.
Generally speaking, a card is smart about clearing its own view caches when anything about the card itself. So when I update the card “Johnny”, all the cached views of “Johnny” are cleared. Similarly, changes to structure rules and other basic patterns are typically well managed by the caching system.
However, there are many other potential changes that views cannot detect. Views that are susceptible to these “cache hazards” should be configured with ‘cache: :never`.
## Cache
hazards
If a view contains any of the following cache hazards, it would be wise to consider a ‘cache: :never` configuration:
-
dynamic searches (eg ‘Card.search`) whose results may change
-
live timestamps (eg ‘Time.now`)
-
environmental variables (eg ‘Env.params`)
-
any variables altered in one view and used in another (eg ‘@myvar`)
-
other cards’ properties (eg ‘Card.content`)
What all of the above have in common is that they involve changes about which the view caching system is unaware. This means that whether the cache hazard is rendered directly in a view or just used in its logic, it can change in a way that should change the view but _won’t_ change the view if it’s cached.
## Altering cached views
Whereas ignoring cache hazards may cause over-caching, altering cached views may cause outright errors. If a view directly alters a rendered view, it may be dangerous to cache.
# obviously safe to cache view(:x) { "ABC" } # also safe, because x is NOT altered view(:y) { render_x + "DEF" } # unsafe and thus never cached, because x is altered view(:z, cache: :never) { render_x.reverse }
Specifically, the danger is that the inner view will be rendered as a stub, and the out view will end up altering the stub and not the view.
Although alterations should be considered dangerous, they are actually only problematic in situations where the inner view might sometimes render a stub. If the outer view is rendering a view of the _same card_ with all the _same view settings_ (perms, unknown, etc), there will be no stub and thus no error. Remember, however, that a view on a narrow set may inherit view settings from a general set. To be confident that a view alteration is safe, all inherited settings must be taken into account.
## Caching Best Practices
Here are some good rules of thumb to make good use of view caching:
-
*Use nests.* If you can show the content of a different card with a nest rather than by showing the content directly, the caching system will be much happier with you.
view :bad_idea, cache: :never do Card["random"].content end view :good_idea do nest :random, view: :core end
-
*Isolate the cache hazards.* Consider the following variants:
view :bad_idea, cache: :never do if morning_for_user? expensive_good_morning else expensive_good_afternoon end end view :good_idea, cache: :never do morning_for_user? ? render_good_morning : render_good_afternoon end In the first example, we have to generate expensive greetings every time we render the view. In the second, only the test is not cached.
-
If you must alter view results, consider *generating the view content in a separate method.*
# First Attempt view :hash_it_in do { cool: false } end view :bad_idea, cache: :never do render_badhash.merge sucks: true end #Second Attempt view :hash_it_out do hash_it_out end def hash_it_out { cool: true } end view :good_idea do hash_it_out.merge rocks: true end The first attempt will work fine with caching off but is risky with caching on. The second is safe with caching on.
## Optimizing with ‘:always`
It is never strictly necessary to use ‘cache: :always`, but this setting can help optimize your use of the caching system in some cases.
Consider the following views:
view(:hat) { "hat" } # ...but imagine this is computationally expensive view(:old_hat) { "old #{render_hat}" } view(:new_hat) { "new #{render_hat}" } view(:red_hat) { "red #{render_hat}" } view(:blue_hat) { "blue #{render_hat}" }
Whether “hat” uses ‘:standard` or `:always`, the hat varieties (old, new, etc…) will fully contain the rendered hat view in their cache. However, with `:standard`, the other views will each re-render hat without attempting to cache it separately or to find it in the cache. This could lead to man expensive renderings of the “hat” view. By contrast, if the cache setting is `:always`, then hat will be cached and retrieved even when it’s rendered inside another cached view.
Constants
- EXPIRE_VALUES
Private Instance Methods
# File lib/card/view/cache.rb, line 273 def array_for_cache_key array # TODO: needs better handling of edit_structure # currently we pass complete structure as nested array array.map do |item| item.is_a?(Array) ? item.join(":") : item.to_s end.sort.join "," end
Is there already a view cache in progress on which this one depends?
Note that if you create a brand new independent format object (ie, not a subformat) its activity will be treated as unrelated to this caching/rendering.
@return [true/false]
# File lib/card/view/cache.rb, line 211 def cache_active? deep_root? ? false : self.class.caching? end
If view is cached, retrieve it. Otherwise render and store it. Uses the primary cache API.
# File lib/card/view/cache.rb, line 217 def cache_fetch &block caching do ensure_cache_key self.class.cache.fetch cache_key, &block end end
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VIEW CACHE KEY
# File lib/card/view/cache.rb, line 232 def cache_key @cache_key ||= [ format.symbol, nest_mode, timestamp, card_cache_key, options_for_cache_key ].compact.map(&:to_s).join "-" end
Fetch
view via cache and, when appropriate, render its stubs
If this is a free cache action (see CacheAction
), we go through the stubs and render them now. If the cache is active (ie, we are inside another view), we do not worry about stubs but keep going, because the free cache we’re inside will take care of those stubs.
@return [String (usually)] rendered view
# File lib/card/view/cache.rb, line 199 def cache_render &block cached_view = cache_fetch(&block) cache_active? ? cached_view : format.stub_render(cached_view) end
# File lib/card/view/cache.rb, line 250 def cache_setting @cache_setting ||= format.view_cache_setting requested_view end
keep track of nested cache fetching
# File lib/card/view/cache.rb, line 225 def caching &block self.class.caching(cache_setting, &block) end
# File lib/card/view/cache.rb, line 254 def card_cache_key card.real? ? card.id : "#{card.key}-#{card.type_id}" end
Registers the cached view for later clearing in the event of related card changes
# File lib/card/view/cache.rb, line 259 def ensure_cache_key card.ensure_view_cache_key cache_key end
render or retrieve view (or stub) with current options @param block [Block] code block to render @return [rendered view or stub]
# File lib/card/view/cache.rb, line 182 def fetch &block case cache_action when :yield then yield # simple render when :cache_yield then cache_render(&block) # render to/from cache when :stub then stub # render stub end end
# File lib/card/view/cache.rb, line 267 def hash_for_cache_key hash hash.keys.sort.map do |key| option_for_cache_key key, hash[key] end.join ";" end
# File lib/card/view/cache.rb, line 238 def nest_mode mode = format.nest_mode mode == :normal ? nil : mode end
# File lib/card/view/cache.rb, line 281 def option_for_cache_key key, value "#{key}:#{option_value_to_string value}" end
# File lib/card/view/cache.rb, line 285 def option_value_to_string value case value when Hash then "{#{hash_for_cache_key value}}" when Array then array_for_cache_key(value) else value.to_s end end
# File lib/card/view/cache.rb, line 263 def options_for_cache_key hash_for_cache_key(live_options) + ";" + hash_for_cache_key(viz_hash) end
# File lib/card/view/cache.rb, line 243 def timestamp return unless (expire = format.view_setting :expire, requested_view) raise "invalid expire setting: #{expire}" unless EXPIRE_VALUES.include? expire Time.now.send("end_of_#{expire}").to_i end