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, nil), do: grid_to_list(grid)
|
||||||
|
|
||||||
defp grid_to_filtered_list(grid, %{"south" => s, "north" => n, "west" => w, "east" => e}) do
|
defp grid_to_filtered_list(grid, %{"south" => s, "north" => n, "west" => w, "east" => e}) do
|
||||||
grid
|
# Single pass over 92k cells: the former Enum.filter |> Enum.map
|
||||||
|> Enum.filter(fn {{lat, lon}, _score} ->
|
# walked the map twice and allocated an intermediate list of
|
||||||
lat >= s and lat <= n and lon >= w and lon <= e
|
# {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)
|
end)
|
||||||
|> Enum.map(fn {{lat, lon}, score} -> %{lat: lat, lon: lon, score: score} end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,8 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
grid_visible: false,
|
grid_visible: false,
|
||||||
radar_visible: false,
|
radar_visible: false,
|
||||||
deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"),
|
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
|
end
|
||||||
|
|
||||||
|
|
@ -202,7 +203,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
selected_time = closest_to_now(valid_times)
|
selected_time = closest_to_now(valid_times)
|
||||||
scores = Propagation.scores_at(band, selected_time, socket.assigns.bounds)
|
scores = Propagation.scores_at(band, selected_time, socket.assigns.bounds)
|
||||||
|
|
||||||
send(self(), :preload_forecast)
|
socket = schedule_preload_forecast(socket)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|
|
@ -303,15 +304,12 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
def handle_event("map_bounds", bounds, socket) do
|
def handle_event("map_bounds", bounds, socket) do
|
||||||
scores = Propagation.scores_at(socket.assigns.selected_band, socket.assigns.selected_time, bounds)
|
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 =
|
||||||
socket
|
socket
|
||||||
|> assign(:bounds, bounds)
|
|> assign(:bounds, bounds)
|
||||||
|> maybe_assign_center_zoom(bounds)
|
|> maybe_assign_center_zoom(bounds)
|
||||||
|> push_event("update_scores", %{scores: scores})
|
|> push_event("update_scores", %{scores: scores})
|
||||||
|
|> schedule_preload_forecast()
|
||||||
|> patch_map_url(replace: true)
|
|> patch_map_url(replace: true)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -356,7 +354,10 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
%{time: DateTime.to_iso8601(t), scores: Propagation.scores_at(band, t, bounds)}
|
%{time: DateTime.to_iso8601(t), scores: Propagation.scores_at(band, t, bounds)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:noreply, push_event(socket, "preload_forecast", %{hours: hours})}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:preload_forecast_timer, nil)
|
||||||
|
|> push_event("preload_forecast", %{hours: hours})}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:propagation_updated, _valid_times}, socket) do
|
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),
|
do: Propagation.scores_at_fresh(band, selected_time, socket.assigns.bounds),
|
||||||
else: []
|
else: []
|
||||||
|
|
||||||
send(self(), :preload_forecast)
|
socket = schedule_preload_forecast(socket)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|
|
@ -518,6 +519,27 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
|
|
||||||
defp maybe_assign_center_zoom(socket, _), do: socket
|
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
|
defp patch_map_url(socket, opts \\ []) do
|
||||||
push_patch(socket, to: map_url(socket.assigns), replace: Keyword.get(opts, :replace, false))
|
push_patch(socket, to: map_url(socket.assigns), replace: Keyword.get(opts, :replace, false))
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue