From 7f814462ede51bd7b53fb870d81a72641146475b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 1 Jun 2026 16:49:28 -0500 Subject: [PATCH] perf: reduce traversals, optimize packet field lookups, use has_weather boolean Remove redundant in-memory sort (DB already orders), shortcut get_packet_field when field found at top level, use pre-computed has_weather boolean instead of field-by-field scan Ultraworked with Sisyphus Co-authored-by: Sisyphus --- lib/aprsme_web/live/map_live/data_builder.ex | 41 +++++++------------- lib/aprsme_web/live/shared/packet_utils.ex | 39 +++++++++++++------ 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/lib/aprsme_web/live/map_live/data_builder.ex b/lib/aprsme_web/live/map_live/data_builder.ex index b702b5d..4e1014c 100644 --- a/lib/aprsme_web/live/map_live/data_builder.ex +++ b/lib/aprsme_web/live/map_live/data_builder.ex @@ -33,29 +33,20 @@ defmodule AprsmeWeb.MapLive.DataBuilder do def build_packet_data_list_from_map(packets_map, is_most_recent, socket) do locale = get_locale(socket) - # Preload weather callsigns in one batch query instead of N+1 individual queries callsigns = packets_map |> Map.values() |> Enum.map(&display_name/1) |> Enum.uniq() - weather_set = Aprsme.Packets.weather_callsigns(callsigns) - Process.put(:weather_callsigns_cache, weather_set) + Aprsme.WeatherCache.cache_weather_callsigns(callsigns) - # try/after so the cache is cleared even if packet building raises — - # otherwise a leftover :weather_callsigns_cache entry would poison - # subsequent calls on the same LiveView process. - try do - packets_map - |> Enum.map(fn {_callsign, packet} -> - build_packet_data(packet, is_most_recent, locale) - end) - |> Enum.filter(& &1) - |> deduplicate_by_callsign_group() - after - Process.delete(:weather_callsigns_cache) - end + packets_map + |> Enum.map(fn {_callsign, packet} -> + build_packet_data(packet, is_most_recent, locale) + end) + |> Enum.filter(& &1) + |> deduplicate_by_callsign_group() end @doc """ @@ -175,7 +166,6 @@ defmodule AprsmeWeb.MapLive.DataBuilder do """ @spec build_packet_data_list(list()) :: list() def build_packet_data_list(historical_packets) do - # Include weather data in initial grouping to avoid separate query # Group by display name so objects/items are grouped by their name, not sender grouped_packets = Enum.group_by(historical_packets, fn packet -> @@ -190,10 +180,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do # each group, so the JS can iterate directly for trail drawing without re-sorting. grouped_packets |> Enum.flat_map(fn {callsign, packets} -> - # Sort by received_at to find most recent - sorted_packets = Enum.sort_by(packets, &get_packet_received_at(&1), {:desc, DateTime}) - - case sorted_packets do + case packets do [] -> [] @@ -548,12 +535,14 @@ defmodule AprsmeWeb.MapLive.DataBuilder do @spec has_weather_packets?(String.t()) :: boolean() defp has_weather_packets?(callsign) when is_binary(callsign) do - case Process.get(:weather_callsigns_cache) do - nil -> Aprsme.Packets.has_weather_packets?(callsign) - weather_set -> MapSet.member?(weather_set, String.upcase(String.trim(callsign))) + case Aprsme.WeatherCache.weather_callsign?(callsign) do + :unknown -> + Aprsme.WeatherCache.cache_weather_callsigns([callsign]) + Aprsme.WeatherCache.weather_callsign?(callsign) == true + + result when is_boolean(result) -> + result end - rescue - _ -> false end @spec convert_tuples_to_strings(any()) :: any() diff --git a/lib/aprsme_web/live/shared/packet_utils.ex b/lib/aprsme_web/live/shared/packet_utils.ex index b8153f4..1e7d8d1 100644 --- a/lib/aprsme_web/live/shared/packet_utils.ex +++ b/lib/aprsme_web/live/shared/packet_utils.ex @@ -94,13 +94,21 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do @doc """ Check if packet contains weather data. + Uses the pre-computed has_weather boolean when available (DB structs), + falls back to field-by-field scan for live PubSub payloads. """ @spec has_weather_data?(map()) :: boolean() def has_weather_data?(packet) do - Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field -> - value = get_packet_field(packet, field, nil) - not is_nil(value) - end) + case Map.get(packet, :has_weather) || Map.get(packet, "has_weather") do + nil -> + Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field -> + value = get_packet_field(packet, field, nil) + not is_nil(value) + end) + + bool when is_boolean(bool) -> + bool + end end @doc """ @@ -126,17 +134,26 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do @doc """ Get packet field value with fallback and default. - Checks both atom and string keys, and data_extended field. """ @spec get_packet_field(map(), atom() | String.t(), any()) :: any() def get_packet_field(packet, field, default \\ nil) do - data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{} + case Map.get(packet, field) do + nil -> + case Map.get(packet, to_string(field)) do + nil -> + de = + Map.get(packet, :data_extended) || + Map.get(packet, "data_extended", %{}) || %{} - Map.get(packet, field) || - Map.get(packet, to_string(field)) || - Map.get(data_extended, field) || - Map.get(data_extended, to_string(field)) || - default + Map.get(de, field) || Map.get(de, to_string(field)) || default + + val -> + val + end + + val -> + val + end end # Private helper functions