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.
Private Instance Methods
# File lib/card/view/cache.rb, line 255 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 209 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 215 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 230 def cache_key @cache_key ||= [ card_cache_key, format.class, format.nest_mode, options_for_cache_key ].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 197 def cache_render &block cached_view = cache_fetch(&block) cache_active? ? cached_view : format.stub_render(cached_view) end
keep track of nested cache fetching
# File lib/card/view/cache.rb, line 223 def caching &block self.class.caching(self, &block) end
# File lib/card/view/cache.rb, line 236 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 241 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 180 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 249 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 263 def option_for_cache_key key, value "#{key}:#{option_value_to_string value}" end
# File lib/card/view/cache.rb, line 267 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 245 def options_for_cache_key hash_for_cache_key(live_options) + hash_for_cache_key(viz_hash) end