aprs.me/lib/aprsme_web/live/packets_live/index.ex
Graham McIntire 31ea64aa6c
perf: reduce per-packet allocations in ingest pipeline
Drop the pre-insert aprs_messages broadcast in Is.dispatch (subscribers
already get a richer payload via postgres:aprsme_packets after insert).
Switch PacketsLive.CallsignView to the per-callsign packets:<CS> topic
so it only receives relevant packets instead of filtering every one.

In PacketConsumer: reuse the received_at stamped in Is.dispatch instead
of calling DateTime.utc_now/0 per packet; drop the duplicate struct_to_map
pass (Is.dispatch already handled it); fold coordinate validation and
Geo.Point construction into set_lat_lon + create_location_geometry so
coords are normalized once; extract device_identifier from the already-
normalized attrs; pre-build the broadcast payload once (identifier pick,
lat/lon aliases, routing callsign) so the async broadcast task no longer
does Map.drop/Map.merge per packet.

Rewrite PacketSanitizer.sanitize_packet / sanitize_data_map with Map.new/2
instead of Enum.reduce + Map.put — one allocation per packet instead of N.

Also compute has_weather in Packet.changeset/2 so direct-changeset inserts
(tests, backfills) populate it the same way the GenStage pipeline does.
2026-04-19 13:33:16 -05:00

65 lines
2 KiB
Elixir

defmodule AprsmeWeb.PacketsLive.Index do
@moduledoc false
use AprsmeWeb, :live_view
use Gettext, backend: AprsmeWeb.Gettext
alias Aprsme.EncodingUtils
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
end
{:ok, assign(socket, :packets, [])}
end
@impl true
def handle_info({:postgres_packet, payload}, socket) do
sanitized_payload = EncodingUtils.sanitize_packet(payload)
packets = Enum.take([sanitized_payload | socket.assigns.packets], 100)
socket = assign(socket, :packets, packets)
{:noreply, socket}
end
@doc """
Extract a coordinate (:lat or :lon) from a packet, checking multiple sources:
1. Direct :lat/:lon keys
2. Packet struct with PostGIS location
3. data_extended map with :latitude/:longitude keys
"""
def extract_coordinate(packet, which) when which in [:lat, :lon] do
direct_key = which
extended_key = if which == :lat, do: :latitude, else: :longitude
Map.get(packet, direct_key) ||
extract_from_location(packet, which) ||
extract_from_data_extended(packet, extended_key)
end
@doc """
Format a coordinate value for display with up to 6 decimal places.
"""
def format_coordinate(nil), do: ""
def format_coordinate(value) when is_float(value) do
"~.6f" |> :io_lib.format([value]) |> List.to_string()
end
def format_coordinate(value) when is_binary(value) do
Regex.replace(~r/(\d+\.\d{1,6})\d*/, value, "\\1")
end
def format_coordinate(value), do: to_string(value)
defp extract_from_location(%Aprsme.Packet{location: %Geo.Point{coordinates: {_lon, lat}}}, :lat), do: lat
defp extract_from_location(%Aprsme.Packet{location: %Geo.Point{coordinates: {lon, _lat}}}, :lon), do: lon
defp extract_from_location(_, _), do: nil
defp extract_from_data_extended(packet, key) do
case Map.get(packet, :data_extended) do
%{} = data -> Map.get(data, key) || Map.get(data, to_string(key))
_ -> nil
end
end
end