Scenic v0.9.0 Scenic.Cache View Source

In memory cache for larger assets.

Static assets such as fonts, images and more tend to be relatively large compared to other data. These assets are often used across multiple scenes and may need to be shared with multiple drivers.

These assets also tend to have a significant load cost. Fonts need to be rendered. Images interpreted into their final binary form, etc.

Goals

Given this situation, the Cache module has multiple goals.

  • Reuse - assets used by multiple scenes should only be stored in memory once
  • Load Time- loading cost should only be paid once
  • Copy time - assets are stored in ETS, so they don’t need to be copied as they are used
  • Pub/Sub - Consumers of static assets (drivers…) should be notified when an asset is loaded or changed. They should not poll the system.
  • Security - Static assets can become an attack vector. Helper modules are provided to assist in verifying these files.

Scope

When an asset is loaded into the cache, it is assigned a scope. The scope is used to determine how long to hold the asset in memory before it is unloaded. Scope is either the atom :global, or a pid.

The typical flow is that a scene will load an asset into the cache. A scope is automatically defined that tracks the asset against the pid of the scene that loaded it. When the scene is closed, the scope becomes empty and the asset is unloaded.

If, while that scene is loaded, another scene (or any process…) attempts to load the same asset into the cache, a second scope is added and the duplicate load is skipped. When the first scene closes, the asset stays in memory as long as the second scope remains valid.

When a scene closes, it’s scope stays valid for a short time in order to give the next scene a chance to load its assets (or claim a scope) and possibly re-use the already loaded assets.

This is also useful in the event of a scene crashing and being restarted. The delay in unloading the scope means that the replacement scene will use already loaded assets instead of loading the same files again for no real benefit.

When you load assets you can alternately provide your own scope instead of taking the default, which is your processes pid. If you provide :global, then the asset will stay in memory until you explicitly release it.

Keys

At its simplest, accessing the cache is a key-value store. When inserting assets via the main Cache module, you can supply any term you want as the key. However, in most cases this is not recommended.

The key for an item in the cache should almost always be a SHA hash of the item itself.

Why? Read below…

The main exception is dynamic assets, such as video frames coming from a camera.

Security

A lesson learned the hard way is that static assets (fonts, images, etc) that your app loads out of storage can easily become attack vectors.

These formats are complicated! There is no guarantee (on any system) that a malformed asset will not cause an error in the C code that interprets it. Again - these are complicated and the renderers need to be fast…

The solution is to compute a SHA hash of these files during build-time of your and to store the result in your applications code itself. Then during run time, you compare then pre-computed hash against the run-time of the asset being loaded.

Please take advantage of the helper modules Cache.File, Cache.Term, and Cache.Hash to do this for you. These modules load files and insert them into the cache while checking a precomputed hash.

These scheme is much stronger when the application code itself is also signed and verified, but that is an exercise for the packaging tools.

Full Example:

defmodule MyApp.MyScene do
  use Scenic.Scene

  # build the path to the static asset file (compile time)
  @asset_path :code.priv_dir(:my_app) |> Path.join("/static/images/asset.jpg")

  # pre-compute the hash (compile time)
  @asset_hash Scenic.Cache.Hash.file!( @asset_path, :sha )

  # build a graph that uses the asset (compile time)
  @graph Scenic.Graph.build()
  |> rect( {100, 100}, fill: {:image, @asset_hash} )


  def init( _, _ ) {
    # load the asset into the cache (run time)
    Scenic.Cache.File.load(@asset_path, @asset_hash)

    # push the graph. (run time)
    push_graph(@graph)

    {:ok, @graph}
  end

end

When assets are loaded this way, the @asset_hash term is also used as the key in the cache. This has the additional benefit of allowing you to pre-compute the graph itself, using the correct keys for the correct assets.

Pub/Sub

Drivers (or any process…) listen to the Cache via a simple pub/sub api.

Because the graph, may be computed during compile time and pushed at some other time than the assets are loaded, the drivers need to know when the assets become available.

Whenever any asset is loaded into the cache, messages are sent to any subscribing processes along with the affected keys. This allows them to react in a loosely-coupled way to how the assets are managed in your scene.

Link to this section Summary

Functions

Returns a specification to start this module under a supervisor

Add a scope to an existing asset in the cache

Retrieve an item from the cache and wrap it in an {:ok, _} tuple

Retrieve an item from the Cache and raises an error if it doesn’t exist

Retrieve an item from the Cache

Returns a list of asset keys claimed by the given scope

Tests if a key is claimed by the current scope

Insert an item into the Cache

Release a scope claim on an asset

Get the current status of an asset in the cache

Subscribe the calling process to cache messages

Unsubscribe the calling process from cache messages

Link to this section Functions

Returns a specification to start this module under a supervisor.

See Supervisor.

Link to this function claim(key, scope \\ nil) View Source

Add a scope to an existing asset in the cache.

Claiming an asset in the cache adds a lifetime scope to it. This is essentially a refcount that is bound to a pid.

Returns true if the item is loaded and the scope is added. Returns false if the asset is not loaded into the cache.

Retrieve an item from the cache and wrap it in an {:ok, _} tuple.

This function ideal if you need to pattern match on the result of getting from the cache.

Examples

iex> Scenic.Cache.fetch("test_key")
{:error, :not_found}

iex> :ets.insert(:scenic_cache_key_table, {"test_key", 1, :test_data})
...> true
...> Scenic.Cache.fetch("test_key")
{:ok, :test_data}

Retrieve an item from the Cache and raises an error if it doesn’t exist.

This function accepts a key and a default both being any term in Elixir.

If there is no item in the Cache that corresponds to the key the function will return nil else the function returns the term stored in the cache with the using the provided key

Examples

iex> Scenic.Cache.get("test_key")
nil

iex> :ets.insert(:scenic_cache_key_table, {"test_key", 1, :test_data})
...> true
...> Scenic.Cache.get("test_key")
:test_data
Link to this function get(key, default \\ nil) View Source
get(term(), term()) :: term() | nil

Retrieve an item from the Cache.

This function accepts a key and a default both being any term in Elixir.

If there is no item in the Cache that corresponds to the key the function will return nil else the function returns the term stored in the cache with the using the provided key

Examples

iex> Scenic.Cache.get("test_key")
nil

iex> :ets.insert(:scenic_cache_key_table, {"test_key", 1, :test_data})
...> true
...> Scenic.Cache.get("test_key")
:test_data

Returns a list of asset keys claimed by the given scope.

Link to this function member?(key, scope \\ nil) View Source

Tests if a key is claimed by the current scope.

Link to this function put(key, data, scope \\ nil) View Source

Insert an item into the Cache.

Parameters:

  • key - term to use as the retrieval key. Typically a hash of the data itself.
  • data - term to use as the stored data
  • scope - Optional scope to track the lifetime of this asset against. Can be :global but is usually nil, which defaults to the pid of the calling process.

Examples

iex> Scenic.Cache.get("test_key")
nil

iex> :ets.insert(:scenic_cache_key_table, {"test_key", 1, :test_data})
...> true
...> Scenic.Cache.get("test_key")
:test_data
Link to this function release(key, opts \\ []) View Source

Release a scope claim on an asset.

Usually the scope is released automatically when a process shuts down. However if you want to manually clean up, or unload an asset with the :global scope, then you should use release.

Parameters:

  • key - the key to release.
  • options - options list

Options:

  • scope - set to :global to release the global scope.
  • delay - add a delay of n milliseconds before releasing. This allows starting processes a chance to claim a scope before it is unloaded.
Link to this function request_notification(message_type) View Source
This function is deprecated. Use Cache.subscribe/1 instead.
Link to this function status(key, scope \\ nil) View Source

Get the current status of an asset in the cache.

This is used to test if the current process has claimed a scope on an asset.

Link to this function stop_notification(message_type \\ :all) View Source
This function is deprecated. Use Cache.unsubscribe/1 instead.

Subscribe the calling process to cache messages.

Pass in the type of messages you want to subscribe to.

  • :cache_put - sent when assets are put into the cache
  • :cache_delete - sent when assets are fully unloaded from the cache
  • :cache_claim - sent when a scope is claimed
  • :cache_release - sent when a scope is released
  • :all - all of the above message types
Link to this function unsubscribe(message_type \\ :all) View Source

Unsubscribe the calling process from cache messages.

Pass in the type of messages you want to unsubscribe from.

  • :cache_put - sent when assets are put into the cache
  • :cache_delete - sent when assets are fully unloaded from the cache
  • :cache_claim - sent when a scope is claimed
  • :cache_release - sent when a scope is released
  • :all - all of the above message types