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

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

Link to this type

sys_fonts() View Source
sys_fonts() :: :roboto | :roboto_mono

Link to this section Functions

Link to this function

claim(hash, scope \\ nil) View Source
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.

Link to this function

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

Tests if a key is claimed by the given scope.

Link to this function

fetch(hash) View Source
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}
Link to this function

get(hash, default \\ nil) View Source
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}

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.

Link to this function

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

Returns a list of keys claimed by the given scope.

Link to this function

load(path, hash, opts \\ []) View Source

Callback implementation for Scenic.Cache.Base.load/3.

Link to this function

load!(path, hash, opts \\ []) View Source

Callback implementation for Scenic.Cache.Base.load!/3.

Link to this function

member?(hash) View Source
member?(hash :: Scenic.Cache.Base.hash()) :: true | false

Tests if a key is claimed by any scope.

Link to this function

put_new(hash, data, scope \\ nil) View Source
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 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.

Returns: {:ok, hash}

Link to this function

release(hash, opts \\ []) View Source
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.
Link to this function

status(hash, scope \\ nil) View Source
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.

Link to this function

subscribe(hash, sub_type \\ :all) View Source
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 keys
  • 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
Link to this function

unsubscribe(hash, sub_type \\ :all) View Source
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 keys
  • 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