- ScoreCache stores {band, valid_time} as %{{lat, lon} => score} map so
point lookups are O(1); adds fetch_point/4 and valid_times/1
- available_valid_times/1 reads directly from ScoreCache when warm,
falls back to DB on cold start
- point_forecast/3 iterates cached valid_times and uses fetch_point/4
instead of hitting the DB per click
- NexradCache: node-local ETS cache of decoded n0q PNG pixel buffers
keyed by 5-minute rounded timestamp; skips ~1-5s HTTP+decode on
concurrent/repeat clicks within the same window
- MapLive: start_async the rain_scatter fetch so point_detail renders
immediately with a pending marker; push rain_scatter_update when
NEXRAD resolves
- MapLive: preload all 18 remaining forecast hours for the current
viewport after mount/band change/propagation_updated; client caches
them and renders timeline scrubs instantly without a server roundtrip.
Adds set_selected_time event for fast-path state sync.
- Propagation map JS: forecastCache map + drawScatterMarkers helper,
timeline click uses preloaded cache when available
55 lines
1.6 KiB
Elixir
55 lines
1.6 KiB
Elixir
defmodule Microwaveprop.Weather.NexradCache do
|
|
@moduledoc """
|
|
Node-local ETS cache of decoded NEXRAD n0q composite reflectivity frames.
|
|
Keyed by a 5-minute rounded timestamp. Stores the raw pixel buffer + image
|
|
width so per-point rain-cell extraction can skip the HTTP fetch + PNG decode
|
|
(which take 1-5 seconds for a ~5 MB CONUS-wide image).
|
|
|
|
The cache is populated by `Microwaveprop.Weather.NexradClient.fetch_rain_cells/4`
|
|
on its first call per 5-min window, then reused by every concurrent click
|
|
until the window rolls over.
|
|
"""
|
|
use GenServer
|
|
|
|
@table :nexrad_frame_cache
|
|
|
|
@type pixels :: binary()
|
|
@type width :: pos_integer()
|
|
|
|
@spec start_link(keyword()) :: GenServer.on_start()
|
|
def start_link(opts) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@spec fetch(DateTime.t()) :: {:ok, pixels(), width()} | :miss
|
|
def fetch(rounded_ts) do
|
|
case :ets.lookup(@table, rounded_ts) do
|
|
[{_, pixels, width}] -> {:ok, pixels, width}
|
|
[] -> :miss
|
|
end
|
|
end
|
|
|
|
@spec put(DateTime.t(), pixels(), width()) :: :ok
|
|
def put(rounded_ts, pixels, width) do
|
|
:ets.insert(@table, {rounded_ts, pixels, width})
|
|
:ok
|
|
end
|
|
|
|
@spec prune_older_than(DateTime.t()) :: non_neg_integer()
|
|
def prune_older_than(cutoff_ts) do
|
|
match_spec = [{{:"$1", :_, :_}, [{:<, :"$1", {:const, cutoff_ts}}], [true]}]
|
|
:ets.select_delete(@table, match_spec)
|
|
end
|
|
|
|
@spec clear() :: :ok
|
|
def clear do
|
|
:ets.delete_all_objects(@table)
|
|
:ok
|
|
end
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
:ets.new(@table, [:set, :named_table, :public, read_concurrency: true])
|
|
{:ok, %{}}
|
|
end
|
|
end
|