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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Graham McIntire 2026-06-01 16:49:28 -05:00
parent e02fafc8da
commit 7f814462ed
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 43 additions and 37 deletions

View file

@ -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()

View file

@ -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