192 lines
5.8 KiB
Elixir
192 lines
5.8 KiB
Elixir
defmodule AprsmeWeb.MapLive.PacketManager do
|
|
@moduledoc """
|
|
Optimized packet management for LiveView with reduced memory usage.
|
|
|
|
This module provides efficient packet storage and retrieval optimized for
|
|
LiveView performance by:
|
|
- Storing minimal data in LiveView assigns
|
|
- Using sliding window for packet management
|
|
- Providing just-in-time data fetching
|
|
- Batching operations for better performance
|
|
"""
|
|
|
|
alias AprsmeWeb.MapLive.PacketStore
|
|
|
|
# Maximum packets to keep in memory per category
|
|
@max_visible_packets 1000
|
|
@max_historical_packets 2000
|
|
# Clean up when over limit by this amount
|
|
@packet_cleanup_threshold 50
|
|
|
|
@doc """
|
|
Initialize packet management state for a socket.
|
|
Returns minimal state optimized for memory usage.
|
|
"""
|
|
def init_packet_state do
|
|
%{
|
|
visible_packet_ids: [],
|
|
historical_packet_ids: [],
|
|
packet_count: %{visible: 0, historical: 0},
|
|
last_cleanup: System.monotonic_time(:millisecond)
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Add packets to visible storage with memory management.
|
|
Returns updated state and packet IDs for broadcasting.
|
|
"""
|
|
def add_visible_packets(packet_state, packets) when is_list(packets) do
|
|
# Store packets and get IDs
|
|
packet_ids = PacketStore.store_packets(packets)
|
|
|
|
# Update visible packet tracking
|
|
new_visible_ids = packet_ids ++ packet_state.visible_packet_ids
|
|
|
|
# Apply memory management
|
|
{managed_ids, cleanup_needed} = apply_memory_limits(new_visible_ids, @max_visible_packets)
|
|
|
|
updated_state = %{
|
|
packet_state
|
|
| visible_packet_ids: managed_ids,
|
|
packet_count: Map.put(packet_state.packet_count, :visible, length(managed_ids))
|
|
}
|
|
|
|
# Clean up if needed
|
|
final_state =
|
|
if cleanup_needed do
|
|
perform_cleanup(updated_state, :visible)
|
|
else
|
|
updated_state
|
|
end
|
|
|
|
{final_state, packet_ids}
|
|
end
|
|
|
|
@doc """
|
|
Add packets to historical storage with sliding window management.
|
|
"""
|
|
def add_historical_packets(packet_state, packets) when is_list(packets) do
|
|
packet_ids = PacketStore.store_packets(packets)
|
|
|
|
new_historical_ids = packet_ids ++ packet_state.historical_packet_ids
|
|
{managed_ids, cleanup_needed} = apply_memory_limits(new_historical_ids, @max_historical_packets)
|
|
|
|
updated_state = %{
|
|
packet_state
|
|
| historical_packet_ids: managed_ids,
|
|
packet_count: Map.put(packet_state.packet_count, :historical, length(managed_ids))
|
|
}
|
|
|
|
final_state =
|
|
if cleanup_needed do
|
|
perform_cleanup(updated_state, :historical)
|
|
else
|
|
updated_state
|
|
end
|
|
|
|
{final_state, packet_ids}
|
|
end
|
|
|
|
@doc """
|
|
Get visible packets for rendering. Only fetches data when needed.
|
|
"""
|
|
def get_visible_packets(packet_state, limit \\ @max_visible_packets) do
|
|
packet_ids = Enum.take(packet_state.visible_packet_ids, limit)
|
|
packet_ids |> PacketStore.get_packets() |> Map.values()
|
|
end
|
|
|
|
@doc """
|
|
Get historical packets for rendering.
|
|
"""
|
|
def get_historical_packets(packet_state, limit \\ @max_historical_packets) do
|
|
packet_ids = Enum.take(packet_state.historical_packet_ids, limit)
|
|
packet_ids |> PacketStore.get_packets() |> Map.values()
|
|
end
|
|
|
|
@doc """
|
|
Remove packets matching a predicate (e.g., expired packets).
|
|
"""
|
|
def remove_packets_where(packet_state, predicate_fn) do
|
|
# Get visible packets and apply predicate
|
|
visible_packets = get_visible_packets(packet_state)
|
|
{keep_visible, remove_visible} = Enum.split_with(visible_packets, predicate_fn)
|
|
|
|
# Get historical packets and apply predicate
|
|
historical_packets = get_historical_packets(packet_state)
|
|
{keep_historical, remove_historical} = Enum.split_with(historical_packets, predicate_fn)
|
|
|
|
# Remove from storage
|
|
remove_ids = extract_packet_ids(remove_visible ++ remove_historical)
|
|
PacketStore.remove_packets(remove_ids)
|
|
|
|
# Update state with remaining IDs
|
|
keep_visible_ids = extract_packet_ids(keep_visible)
|
|
keep_historical_ids = extract_packet_ids(keep_historical)
|
|
|
|
%{
|
|
packet_state
|
|
| visible_packet_ids: keep_visible_ids,
|
|
historical_packet_ids: keep_historical_ids,
|
|
packet_count: %{
|
|
visible: length(keep_visible_ids),
|
|
historical: length(keep_historical_ids)
|
|
}
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Get current memory usage statistics.
|
|
"""
|
|
def get_memory_stats(packet_state) do
|
|
store_stats = PacketStore.get_stats()
|
|
|
|
%{
|
|
visible_count: packet_state.packet_count.visible,
|
|
historical_count: packet_state.packet_count.historical,
|
|
total_stored: store_stats.total_packets,
|
|
memory_bytes: store_stats.memory_bytes,
|
|
avg_packet_size:
|
|
if(store_stats.total_packets > 0,
|
|
do: store_stats.memory_bytes / store_stats.total_packets,
|
|
else: 0
|
|
)
|
|
}
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp apply_memory_limits(packet_ids, max_count) do
|
|
if length(packet_ids) > max_count + @packet_cleanup_threshold do
|
|
# Keep most recent packets
|
|
managed_ids = Enum.take(packet_ids, max_count)
|
|
{managed_ids, true}
|
|
else
|
|
{packet_ids, false}
|
|
end
|
|
end
|
|
|
|
defp perform_cleanup(packet_state, _type) do
|
|
current_time = System.monotonic_time(:millisecond)
|
|
|
|
# Only cleanup if enough time has passed to avoid excessive cleanup
|
|
# 30 seconds
|
|
if current_time - packet_state.last_cleanup > 30_000 do
|
|
%{packet_state | last_cleanup: current_time}
|
|
else
|
|
packet_state
|
|
end
|
|
end
|
|
|
|
defp extract_packet_ids(packets) do
|
|
Enum.map(packets, fn packet ->
|
|
packet[:id] || packet["id"] || generate_packet_id(packet)
|
|
end)
|
|
end
|
|
|
|
defp generate_packet_id(packet) do
|
|
# Generate deterministic ID from packet data
|
|
sender = packet[:sender] || packet["sender"] || ""
|
|
timestamp = packet[:received_at] || packet["received_at"] || DateTime.utc_now()
|
|
"#{sender}_#{DateTime.to_unix(timestamp, :microsecond)}"
|
|
end
|
|
end
|