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 Class | Module |
---|---|
Fonts | Scenic.Cache.Static.Font |
Font Metrics | Scenic.Cache.Static.FontMetrics |
Textures (images in a fill) | Scenic.Cache.Static.Texture |
Raw Pixel Maps | Scenic.Cache.Dynamic.Texture |
Some of the Cache support modules have moved
Old Module | New Module |
---|---|
Scenic.Cache.Hash | Scenic.Cache.Support.Hash |
Scenic.Cache.File | Scenic.Cache.Support.File |
Scenic.Cache.Supervisor | Scenic.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
hash()
View Source
hash() :: String.t()
hash() :: String.t()
sub_types()
View Source
sub_types() :: :put | :delete | :all
sub_types() :: :put | :delete | :all
Link to this section Functions
child_spec(init_arg) View Source
Returns a specification to start this module under a supervisor.
See Supervisor
.
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()}
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.
claimed?(service, key, scope \\ nil)
View Source
claimed?(
service :: atom(),
hash :: Scenic.Cache.Base.hash(),
scope :: :global | nil | GenServer.server()
) :: true | false
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
.
fetch(service, hash)
View Source
fetch(service :: atom(), hash :: Scenic.Cache.Base.hash()) ::
term() | {:error, :not_found}
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.
get(service, hash, default \\ nil)
View Source
get(service :: atom(), hash :: Scenic.Cache.Base.hash(), default :: term()) ::
term() | nil
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
get!(service, hash)
View Source
get!(service :: atom(), hash :: Scenic.Cache.Base.hash()) :: term()
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.
keys(service, scope \\ nil)
View Source
keys(service :: atom(), scope :: :global | nil | GenServer.server()) :: list()
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.
member?(service, key)
View Source
member?(service :: atom(), hash :: Scenic.Cache.Base.hash()) :: true | false
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
.
put(service, key, data, scope \\ nil)
View Source
put(
service :: atom(),
hash :: Scenic.Cache.Base.hash(),
data :: term(),
scope :: :global | nil | GenServer.server()
) :: term()
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 datascope
- 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
release(service, hash, opts \\ [])
View Source
release(service :: atom(), hash :: Scenic.Cache.Base.hash(), opts :: list()) ::
:ok
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.
status(service, hash, scope \\ nil)
View Source
status(
service :: atom(),
hash :: Scenic.Cache.Base.hash(),
scope :: :global | nil | GenServer.server()
) :: :ok
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
subscribe(service, hash, sub_type \\ :all) View Source
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
unsubscribe(service, sub_type, hash \\ :all) View Source
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