Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/cache/system.ex
defmodule Raxol.Terminal.Cache.System do
@moduledoc """
Unified caching system for the Raxol terminal emulator.
This module provides a centralized caching mechanism that consolidates all caching
operations across the terminal system, including:
- Buffer caching
- Animation caching
- Scroll caching
- Clipboard caching
- General purpose caching
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Terminal.Cache.EvictionHelpers
@type namespace :: :buffer | :animation | :scroll | :clipboard | :general
@type cache_key :: term()
@type cache_value :: term()
@type cache_entry :: %{
value: cache_value(),
size: non_neg_integer(),
created_at: integer(),
last_access: integer(),
access_count: non_neg_integer(),
ttl: integer() | nil,
metadata: map()
}
@type cache_stats :: %{
size: non_neg_integer(),
max_size: non_neg_integer(),
hit_count: non_neg_integer(),
miss_count: non_neg_integer(),
hit_ratio: float(),
eviction_count: non_neg_integer()
}
# BaseManager provides start_link
@doc """
Gets a value from the cache.
## Parameters
* `key` - The cache key
* `opts` - Get options
* `:namespace` - Cache namespace (default: :general)
"""
def get(key, opts \\ []) do
namespace = Keyword.get(opts, :namespace, :general)
GenServer.call(__MODULE__, {:get, namespace, key})
end
@doc """
Puts a value in the cache.
## Parameters
* `key` - The cache key
* `value` - The value to cache
* `opts` - Put options
* `:namespace` - Cache namespace (default: :general)
* `:ttl` - Time-to-live in seconds
* `:metadata` - Additional metadata
"""
def put(key, value, opts \\ []) do
namespace = Keyword.get(opts, :namespace, :general)
ttl = Keyword.get(opts, :ttl)
metadata = Keyword.get(opts, :metadata, %{})
GenServer.call(__MODULE__, {:put, namespace, key, value, ttl, metadata})
end
@doc """
Invalidates a cache entry.
## Parameters
* `key` - The cache key
* `opts` - Invalidate options
* `:namespace` - Cache namespace (default: :general)
"""
def invalidate(key, opts \\ []) do
namespace = Keyword.get(opts, :namespace, :general)
GenServer.call(__MODULE__, {:invalidate, namespace, key})
end
@doc """
Gets cache statistics.
## Parameters
* `opts` - Stats options
* `:namespace` - Cache namespace (default: :general)
"""
def stats(opts \\ []) do
namespace = Keyword.get(opts, :namespace, :general)
GenServer.call(__MODULE__, {:stats, namespace})
end
@doc """
Clears the cache.
## Parameters
* `opts` - Clear options
* `:namespace` - Cache namespace (default: :general)
"""
def clear(opts \\ []) do
namespace = Keyword.get(opts, :namespace, :general)
GenServer.call(__MODULE__, {:clear, namespace})
end
@doc """
Returns the current monotonic time in milliseconds.
This is used for cache expiration and timing operations.
"""
@spec monotonic_time() :: integer()
def monotonic_time do
System.monotonic_time(:millisecond)
end
@impl Raxol.Core.Behaviours.BaseManager
def init_manager(opts) do
max_size = Keyword.get(opts, :max_size, Raxol.Core.Defaults.cache_max_bytes())
default_ttl = Keyword.get(opts, :default_ttl, Raxol.Core.Defaults.cache_ttl_seconds())
eviction_policy = Keyword.get(opts, :eviction_policy, :lru)
compression_enabled = Keyword.get(opts, :compression_enabled, true)
namespace_configs = Keyword.get(opts, :namespace_configs, %{})
namespaces = initialize_namespaces(namespace_configs, max_size)
state = %{
namespaces: namespaces,
default_ttl: default_ttl,
eviction_policy: eviction_policy,
compression_enabled: compression_enabled
}
{:ok, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call({:get, namespace, key}, _from, state) do
case get_namespace(state, namespace) do
nil ->
{:reply, {:error, :namespace_not_found}, state}
namespace_state ->
handle_cache_entry(namespace_state, key, state, namespace)
end
end
def handle_manager_call(
{:put, namespace, key, value, ttl, metadata},
_from,
state
) do
case get_namespace(state, namespace) do
nil ->
{:reply, {:error, :namespace_not_found}, state}
namespace_state ->
entry_size = calculate_size(value)
{updated_cache, updated_size} =
case namespace_state.size + entry_size > namespace_state.max_size do
true ->
{cache_after_eviction, size_after_eviction} =
evict_entries(
namespace_state.cache,
namespace_state.size,
entry_size,
state.eviction_policy,
namespace_state.max_size
)
{cache_after_eviction, size_after_eviction}
false ->
{namespace_state.cache, namespace_state.size}
end
entry = %{
value: value,
size: entry_size,
created_at: System.system_time(:second),
last_access: System.monotonic_time(:millisecond),
access_count: 1,
ttl: ttl || state.default_ttl,
metadata: metadata
}
updated_cache = Map.put(updated_cache, key, entry)
updated_size = updated_size + entry_size
updated_state =
update_namespace(state, namespace, %{
namespace_state
| cache: updated_cache,
size: updated_size
})
{:reply, :ok, updated_state}
end
end
def handle_manager_call({:invalidate, namespace, key}, _from, state) do
case get_namespace(state, namespace) do
nil ->
{:reply, {:error, :namespace_not_found}, state}
namespace_state ->
case Map.pop(namespace_state.cache, key) do
{nil, _} ->
{:reply, :ok, state}
{entry, updated_cache} ->
updated_size = namespace_state.size - entry.size
updated_state =
update_namespace(state, namespace, %{
namespace_state
| cache: updated_cache,
size: updated_size
})
{:reply, :ok, updated_state}
end
end
end
def handle_manager_call({:stats, namespace}, _from, state) do
case get_namespace(state, namespace) do
nil ->
{:reply, {:error, :namespace_not_found}, state}
namespace_state ->
stats = %{
size: namespace_state.size,
max_size: namespace_state.max_size,
hit_count: namespace_state.hit_count,
miss_count: namespace_state.miss_count,
hit_ratio: calculate_hit_ratio(namespace_state),
eviction_count: namespace_state.eviction_count
}
{:reply, {:ok, stats}, state}
end
end
def handle_manager_call({:clear, namespace}, _from, state) do
case get_namespace(state, namespace) do
nil ->
{:reply, {:error, :namespace_not_found}, state}
namespace_state ->
updated_state =
update_namespace(state, namespace, %{
namespace_state
| cache: %{},
size: 0,
hit_count: 0,
miss_count: 0,
eviction_count: 0
})
{:reply, :ok, updated_state}
end
end
defp handle_cache_entry(namespace_state, key, state, namespace) do
case Map.get(namespace_state.cache, key) do
nil ->
updated_state =
update_namespace(state, namespace, %{
namespace_state
| miss_count: namespace_state.miss_count + 1
})
{:reply, {:error, :not_found}, updated_state}
entry ->
case expired?(entry) do
true ->
handle_expired_entry(entry, key, namespace_state, state, namespace)
false ->
handle_valid_entry(entry, key, namespace_state, state, namespace)
end
end
end
defp handle_expired_entry(entry, key, namespace_state, state, namespace) do
updated_cache = Map.delete(namespace_state.cache, key)
updated_size = namespace_state.size - entry.size
updated_state =
update_namespace(state, namespace, %{
namespace_state
| cache: updated_cache,
size: updated_size,
miss_count: namespace_state.miss_count + 1
})
{:reply, {:error, :expired}, updated_state}
end
defp handle_valid_entry(entry, key, namespace_state, state, namespace) do
updated_entry = %{
entry
| last_access: System.monotonic_time(:millisecond),
access_count: entry.access_count + 1
}
updated_cache = Map.put(namespace_state.cache, key, updated_entry)
updated_state =
update_namespace(state, namespace, %{
namespace_state
| cache: updated_cache,
hit_count: namespace_state.hit_count + 1
})
{:reply, {:ok, entry.value}, updated_state}
end
defp initialize_namespaces(configs, default_max_size) do
default_namespace = %{
cache: %{},
size: 0,
max_size: default_max_size,
hit_count: 0,
miss_count: 0,
eviction_count: 0
}
[:buffer, :animation, :scroll, :clipboard, :general]
|> Enum.reduce(%{}, fn namespace, acc ->
config = Map.get(configs, namespace, %{})
max_size = Map.get(config, :max_size, default_max_size)
namespace_state = Map.put(default_namespace, :namespace, namespace)
Map.put(acc, namespace, %{namespace_state | max_size: max_size})
end)
end
defp get_namespace(state, namespace) do
Map.get(state.namespaces, namespace)
end
defp update_namespace(state, namespace, namespace_state) do
%{state | namespaces: Map.put(state.namespaces, namespace, namespace_state)}
end
defp expired?(entry) do
case entry.ttl do
nil -> false
ttl -> System.system_time(:second) - entry.created_at > ttl
end
end
defp calculate_size(value) do
:erlang.external_size(value)
end
defp calculate_hit_ratio(namespace_state) do
total = namespace_state.hit_count + namespace_state.miss_count
case total > 0 do
true -> namespace_state.hit_count / total
false -> 0.0
end
end
defp evict_entries(cache, current_size, needed_size, policy, max_size) do
case policy do
:lru ->
EvictionHelpers.evict_lru(cache, current_size, needed_size, max_size)
:lfu ->
EvictionHelpers.evict_lfu(cache, current_size, needed_size, max_size)
:fifo ->
EvictionHelpers.evict_fifo(cache, current_size, needed_size, max_size)
end
end
end