Scenic v0.10.2 Scenic.Cache.Static.FontMetrics View Source
In memory cache for static font_metrics assets.
Assets such as font_metrics 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.
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 - Base assets can become an attack vector. Helper modules are provided to assist in verifying these files.
Scope
When a font_metrics term 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 a font 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.
Hashes
At its simplest, accessing the cache is a key-value store. This cache is meant to be static in nature, so the key is be a hash of the data.
Why? Read below...
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 renderer needs 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.Support.File
and Cache.Support.Hash
to
build the hashes.
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)
@font_metrics :code.priv_dir(:my_app) |> Path.join("/static/fonts/my_font.ttf.metrics")
@font_metrics_hash Scenic.Cache.Hash.file!( @font_metrics, :sha )
def init( _, _ ) do
# load the asset into the cache (run time)
Scenic.Cache.Static.FontMetrics.load(@font_metrics, @font_metrics_hash)
{:ok, :some_state}
end
end
Pub/Sub
Drivers (or any process...) listen to the font 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
Add a scope to an existing font_metrics in the cache
Tests if a key is claimed by the given scope
Retrieve a fontmetrics from the cache and wrap it in an `{:ok, }` tuple
Retrieve a font_metrics from the Cache
Retrieve a font_metrics from the Cache and raise an error if it doesn't exist
Returns a list of keys claimed by the given scope
Callback implementation for Scenic.Cache.Base.load/3
Callback implementation for Scenic.Cache.Base.load!/3
Tests if a key is claimed by any scope
Insert a new font_metrics into the Cache
Release a scope claim on an font_metrics
Get the current status of a font_metrics in the cache
Subscribe the calling process to cache messages
Unsubscribe the calling process from cache messages
Link to this section Types
sys_fonts()
View Source
sys_fonts() :: :roboto | :roboto_mono
sys_fonts() :: :roboto | :roboto_mono
Link to this section Functions
claim(hash, scope \\ nil)
View Source
claim(
hash :: Scenic.Cache.Base.hash(),
scope :: :global | nil | GenServer.server()
) :: term()
claim( hash :: Scenic.Cache.Base.hash(), scope :: :global | nil | GenServer.server() ) :: term()
Add a scope to an existing font_metrics 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?(hash, scope \\ nil)
View Source
claimed?(
hash :: Scenic.Cache.Base.hash(),
scope :: :global | nil | GenServer.server()
) :: true | false
claimed?( hash :: Scenic.Cache.Base.hash(), scope :: :global | nil | GenServer.server() ) :: true | false
Tests if a key is claimed by the given scope.
fetch(hash)
View Source
fetch(hash :: Scenic.Cache.Base.hash()) :: term() | {:error, :not_found}
fetch(hash :: Scenic.Cache.Base.hash()) :: term() | {:error, :not_found}
Retrieve a fontmetrics from the cache and wrap it in an `{:ok, }` tuple.
This function ideal if you need to pattern match on the result.
Examples
iex> Elixir.Scenic.Cache.Static.FontMetrics.fetch("missing_hash")
...> {:error, :not_found}
iex> Elixir.Scenic.Cache.Static.FontMetrics.fetch("valid_hash")
...> {:ok, :test_data}
get(hash, default \\ nil)
View Source
get(hash :: Scenic.Cache.Base.hash(), default :: term()) :: term() | nil
get(hash :: Scenic.Cache.Base.hash(), default :: term()) :: term() | nil
Retrieve a font_metrics 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
Examples
iex> Elixir.Scenic.Cache.Static.FontMetrics.get("missing_hash")
nil
...> Elixir.Scenic.Cache.Static.FontMetrics.fetch("valid_hash")
{:ok, :test_data}
get!(hash)
View Source
get!(hash :: Scenic.Cache.Base.hash()) :: term()
get!(hash :: Scenic.Cache.Base.hash()) :: term()
Retrieve a font_metrics 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(scope \\ nil)
View Source
keys(scope :: :global | nil | GenServer.server()) :: list()
keys(scope :: :global | nil | GenServer.server()) :: list()
Returns a list of keys claimed by the given scope.
load(path, hash, opts \\ []) View Source
Callback implementation for Scenic.Cache.Base.load/3
.
load!(path, hash, opts \\ []) View Source
Callback implementation for Scenic.Cache.Base.load!/3
.
member?(hash)
View Source
member?(hash :: Scenic.Cache.Base.hash()) :: true | false
member?(hash :: Scenic.Cache.Base.hash()) :: true | false
Tests if a key is claimed by any scope.
put_new(hash, data, scope \\ nil)
View Source
put_new(
hash :: Scenic.Cache.Base.hash(),
data :: term(),
scope :: :global | nil | GenServer.server()
) :: term()
put_new( hash :: Scenic.Cache.Base.hash(), data :: term(), scope :: :global | nil | GenServer.server() ) :: term()
Insert a new font_metrics into the Cache.
If the font_metrics is already in the cache, put_new does nothing and just returns {:ok, hash}
Parameters:
hash
- term to use as the retrieval key. Typically a hash of the data itself. It will be required to be a hash of the data in the future.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.
Returns: {:ok, hash}
release(hash, opts \\ [])
View Source
release(hash :: Scenic.Cache.Base.hash(), opts :: list()) :: :ok
release(hash :: Scenic.Cache.Base.hash(), opts :: list()) :: :ok
Release a scope claim on an font_metrics.
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(hash, scope \\ nil)
View Source
status(
hash :: Scenic.Cache.Base.hash(),
scope :: :global | nil | GenServer.server()
) :: :ok
status( hash :: Scenic.Cache.Base.hash(), scope :: :global | nil | GenServer.server() ) :: :ok
Get the current status of a font_metrics in the cache.
This is used to test if the current process has claimed a scope on an asset.
subscribe(hash, sub_type \\ :all)
View Source
subscribe(
hash :: Scenic.Cache.Base.hash() | :all,
sub_type :: Scenic.Cache.Base.sub_types()
) :: :ok
subscribe( hash :: Scenic.Cache.Base.hash() | :all, sub_type :: Scenic.Cache.Base.sub_types() ) :: :ok
Subscribe the calling process to cache messages.
Parameters
hash
- The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keyssub_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
unsubscribe(hash, sub_type \\ :all)
View Source
unsubscribe(
hash :: Scenic.Cache.Base.hash() | :all,
sub_type :: Scenic.Cache.Base.sub_types()
) :: :ok
unsubscribe( hash :: Scenic.Cache.Base.hash() | :all, sub_type :: Scenic.Cache.Base.sub_types() ) :: :ok
Unsubscribe the calling process from cache messages.
Parameters
hash
- The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keyssub_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