Scenic v0.10.2 Scenic.Cache.Base behaviour View Source

Base module for static asset caches.

This module is not intended to be used directly. To access items in the static cache, refer to the modules for the type of data you are interested in.

Asset ClassModule
FontsScenic.Cache.Static.Font
Font MetricsScenic.Cache.Static.FontMetrics
Textures (images in a fill)Scenic.Cache.Static.Texture
Raw Pixel MapsScenic.Cache.Dynamic.Texture

Some of the Cache support modules have moved

Old ModuleNew Module
Scenic.Cache.HashScenic.Cache.Support.Hash
Scenic.Cache.FileScenic.Cache.Support.File
Scenic.Cache.SupervisorScenic.Cache.Support.Supervisor

Overview

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
  import Scenic.Primitives

  # 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( _, _ ) do
    # load the asset into the cache (run time)
    Scenic.Cache.Static.Texture.load(@asset_path, @asset_hash)

    {:ok, :some_state, push: @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

Tests if a key is claimed by the scope

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

Retrieve an item from the Cache

Retrieve an item from the Cache and raise an error if it doesn't exist

Returns a list of asset keys claimed by the given scope

Tests if a key is claimed by any scope

Insert an item into the Cache. If it is already in the cache, then it overwrites the data {:ok, hash}

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 Types

Link to this type

sub_types() View Source
sub_types() :: :put | :delete | :all

Link to this section Functions

Returns a specification to start this module under a supervisor.

See Supervisor.

Link to this function

claim(service, key, scope \\ nil) View Source
claim(
  service :: atom(),
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: {:ok, Scenic.Cache.Base.hash()}

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.

Link to this function

claimed?(service, key, scope \\ nil) View Source
claimed?(
  service :: atom(),
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: true | false

Tests if a key is claimed by the scope.

Pass in the service, hash, and scope.

Returns true or false.

Link to this function

fetch(service, hash) View Source
fetch(service :: atom(), hash :: Scenic.Cache.Base.hash()) ::
  term() | {:error, :not_found}

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.

Link to this function

get(service, hash, default \\ nil) View Source
get(service :: atom(), hash :: Scenic.Cache.Base.hash(), default :: term()) ::
  term() | nil

Retrieve an item from the Cache.

If there is no item in the Cache that corresponds to the hash the function will return either nil or the supplied default value

Link to this function

get!(service, hash) View Source
get!(service :: atom(), hash :: Scenic.Cache.Base.hash()) :: term()

Retrieve an item from the Cache and raise an error if it doesn't exist.

If there is no item in the Cache that corresponds to the hash the function will raise an error.

Link to this function

keys(service, scope \\ nil) View Source
keys(service :: atom(), scope :: :global | nil | GenServer.server()) :: list()

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

Pass in the service and a scope.

Returns a list of claimed keys.

Link to this function

member?(service, key) View Source
member?(service :: atom(), hash :: Scenic.Cache.Base.hash()) :: true | false

Tests if a key is claimed by any scope.

Pass in the service and a hash.

Returns true or false.

Link to this function

put(service, key, data, scope \\ nil) View Source
put(
  service :: atom(),
  hash :: Scenic.Cache.Base.hash(),
  data :: term(),
  scope :: :global | nil | GenServer.server()
) :: term()

Insert an item into the Cache. If it is already in the cache, then it overwrites the data {:ok, hash}

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(service, hash, opts \\ []) View Source
release(service :: atom(), hash :: Scenic.Cache.Base.hash(), opts :: list()) ::
  :ok

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

status(service, hash, scope \\ nil) View Source
status(
  service :: atom(),
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: :ok

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.

Pass in the service, hash, and a scope.

Returns one of:

{:ok, hash}           # it is claimed by the given scope
{:ok, :global}        # it is NOT claimed by the given scope, but is :global
{:error, :not_found}  # it is not in the cache at all
Link to this function

subscribe(service, hash, sub_type \\ :all) View Source
subscribe(service :: atom(), hash :: hash() | :all, sub_type :: sub_types()) ::
  :ok

Subscribe the calling process to cache messages.

Parameters

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

    • :put - sent when assets are put into the cache
    • :delete - sent when assets are fully unloaded from the cache
    • :claim - sent when a scope is claimed
    • :release - sent when a scope is released
    • :all - all of the above message types
  • hash - The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keys
Link to this function

unsubscribe(service, sub_type, hash \\ :all) View Source
unsubscribe(service :: atom(), hash :: hash() | :all, sub_type :: sub_types()) ::
  :ok

Unsubscribe the calling process from cache messages.

Parameters

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

    • :put - sent when assets are put into the cache
    • :delete - sent when assets are fully unloaded from the cache
    • :claim - sent when a scope is claimed
    • :release - sent when a scope is released
    • :all - all of the above message types
  • hash - The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keys

Link to this section Callbacks

Link to this callback

load(file_path, hash_name, options) View Source
load(file_path :: String.t(), hash_name :: String.t(), options :: list()) ::
  {:ok, data :: any()} | {:error, error :: atom()}

Link to this callback

load!(file_path, hash_name, options) View Source
load!(file_path :: String.t(), hash_name :: String.t(), options :: list()) ::
  data :: any()