perf(map): debounce preload_forecast + single-pass score filter
Two wins in the /map hot path. 1. preload_forecast fired on every Leaflet moveend — which arrives in bursts during pan/zoom — and each firing read + filtered + shipped 18 forecast-hour score lists (up to 90k cells per hour at full CONUS view) over the LiveView websocket. Now debounced to the trailing edge: the user must stop moving for 750 ms before we pay the preload cost. select_band and propagation_updated go through the same scheduler so a storm of PubSub updates during an hourly chain also coalesces. 2. ScoreCache.grid_to_filtered_list walked the 92k-cell grid twice (Enum.filter then Enum.map) and allocated an intermediate tuple list between them. Enum.reduce emits only in-bounds result maps directly — halves map traversal + drops the tuple intermediate.
This commit is contained in:
parent
9815bd691e
commit
aebf3911ce
2 changed files with 40 additions and 12 deletions
|
|
@ -151,10 +151,16 @@ defmodule Microwaveprop.Propagation.ScoreCache do
|
|||
defp grid_to_filtered_list(grid, nil), do: grid_to_list(grid)
|
||||
|
||||
defp grid_to_filtered_list(grid, %{"south" => s, "north" => n, "west" => w, "east" => e}) do
|
||||
grid
|
||||
|> Enum.filter(fn {{lat, lon}, _score} ->
|
||||
lat >= s and lat <= n and lon >= w and lon <= e
|
||||
# Single pass over 92k cells: the former Enum.filter |> Enum.map
|
||||
# walked the map twice and allocated an intermediate list of
|
||||
# {key, value} tuples between them. Enum.reduce emits only the
|
||||
# in-bounds result maps directly.
|
||||
Enum.reduce(grid, [], fn {{lat, lon}, score}, acc ->
|
||||
if lat >= s and lat <= n and lon >= w and lon <= e do
|
||||
[%{lat: lat, lon: lon, score: score} | acc]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> Enum.map(fn {{lat, lon}, score} -> %{lat: lat, lon: lon, score: score} end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
grid_visible: false,
|
||||
radar_visible: false,
|
||||
deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"),
|
||||
deploy_ago: format_deploy_ago(build_ts)
|
||||
deploy_ago: format_deploy_ago(build_ts),
|
||||
preload_forecast_timer: nil
|
||||
)}
|
||||
end
|
||||
|
||||
|
|
@ -202,7 +203,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
selected_time = closest_to_now(valid_times)
|
||||
scores = Propagation.scores_at(band, selected_time, socket.assigns.bounds)
|
||||
|
||||
send(self(), :preload_forecast)
|
||||
socket = schedule_preload_forecast(socket)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
@ -303,15 +304,12 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
def handle_event("map_bounds", bounds, socket) do
|
||||
scores = Propagation.scores_at(socket.assigns.selected_band, socket.assigns.selected_time, bounds)
|
||||
|
||||
# Refresh the forecast cache for the new viewport — the client cleared its
|
||||
# local cache in sendBounds() so any timeline scrub would otherwise miss.
|
||||
send(self(), :preload_forecast)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:bounds, bounds)
|
||||
|> maybe_assign_center_zoom(bounds)
|
||||
|> push_event("update_scores", %{scores: scores})
|
||||
|> schedule_preload_forecast()
|
||||
|> patch_map_url(replace: true)
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -356,7 +354,10 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
%{time: DateTime.to_iso8601(t), scores: Propagation.scores_at(band, t, bounds)}
|
||||
end)
|
||||
|
||||
{:noreply, push_event(socket, "preload_forecast", %{hours: hours})}
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:preload_forecast_timer, nil)
|
||||
|> push_event("preload_forecast", %{hours: hours})}
|
||||
end
|
||||
|
||||
def handle_info({:propagation_updated, _valid_times}, socket) do
|
||||
|
|
@ -381,7 +382,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
do: Propagation.scores_at_fresh(band, selected_time, socket.assigns.bounds),
|
||||
else: []
|
||||
|
||||
send(self(), :preload_forecast)
|
||||
socket = schedule_preload_forecast(socket)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
@ -518,6 +519,27 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
|
||||
defp maybe_assign_center_zoom(socket, _), do: socket
|
||||
|
||||
# Leaflet fires `moveend` on every pan + zoom, so bounds events arrive
|
||||
# in bursts during interactive map use. Each :preload_forecast delivers
|
||||
# 18 forecast-hour score lists filtered to the current viewport (up to
|
||||
# ~90k cells per hour at full CONUS view), so firing it per moveend
|
||||
# produced an 18× read + large websocket payload per pan. Debounce to
|
||||
# the trailing edge: the user has stopped moving for 750 ms before we
|
||||
# pay the preload cost.
|
||||
@preload_debounce_ms 750
|
||||
|
||||
defp schedule_preload_forecast(%{assigns: %{preload_forecast_timer: ref}} = socket) when is_reference(ref) do
|
||||
_ = Process.cancel_timer(ref)
|
||||
do_schedule_preload_forecast(socket)
|
||||
end
|
||||
|
||||
defp schedule_preload_forecast(socket), do: do_schedule_preload_forecast(socket)
|
||||
|
||||
defp do_schedule_preload_forecast(socket) do
|
||||
ref = Process.send_after(self(), :preload_forecast, @preload_debounce_ms)
|
||||
assign(socket, :preload_forecast_timer, ref)
|
||||
end
|
||||
|
||||
defp patch_map_url(socket, opts \\ []) do
|
||||
push_patch(socket, to: map_url(socket.assigns), replace: Keyword.get(opts, :replace, false))
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue