Optimize viewport-based loading with zoom-aware batching

Key improvements:
- Fix spatial query order: Apply bounds filtering BEFORE limit/offset
- Add zoom-level based batch sizing (20-100 packets per batch)
- Dynamic batch count based on zoom level (2-5 batches)
- Faster loading delays for high zoom levels (25ms vs 50ms)
- Include zoom level in cache keys for better cache efficiency

Performance benefits:
- Zoomed in far (zoom 15+): 2 batches of 20 packets each
- Moderately zoomed (zoom 12-14): 3 batches of 35 packets each
- Medium zoom (zoom 8-11): 4 batches of 50 packets each
- Zoomed out (zoom <8): 5 batches of 75-100 packets each

This ensures packets are loaded from the current viewport first,
dramatically reducing load times when zoomed in.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-09 09:14:12 -05:00
parent 330fe27eec
commit 7318d2dfbc
No known key found for this signature in database
2 changed files with 60 additions and 15 deletions

View file

@ -420,22 +420,23 @@ defmodule Aprsme.Packets do
limit = Map.get(opts, :limit, 200)
offset = Map.get(opts, :offset, 0)
# Use a more efficient query that leverages the partial indexes
# Order by received_at DESC to get the most recent packets first
# Build base query with time and position filters
base_query =
from(p in Packet,
where: p.has_position == true,
where: p.received_at >= ^one_hour_ago,
order_by: [desc: p.received_at],
limit: ^limit,
offset: ^offset
where: p.received_at >= ^one_hour_ago
)
# Apply spatial and other filters BEFORE limiting
# This ensures we get the most recent packets within the specified bounds
query =
base_query
|> filter_by_region(opts)
|> filter_by_callsign(opts)
|> filter_by_map_bounds(opts)
|> order_by(desc: :received_at)
|> limit(^limit)
|> offset(^offset)
|> select_with_virtual_coordinates()
Repo.all(query)

View file

@ -1173,10 +1173,14 @@ defmodule AprsmeWeb.MapLive.Index do
historical_packets =
if packets_module == Aprsme.Packets do
# Use cached queries for better performance
# Include zoom level in cache key for better cache efficiency
zoom = socket.assigns.map_zoom || 5
Aprsme.CachedQueries.get_recent_packets_cached(%{
bounds: bounds,
# Reduced limit for faster initial load
limit: 200
limit: 200,
zoom: zoom
})
else
# Fallback for testing
@ -1197,20 +1201,55 @@ defmodule AprsmeWeb.MapLive.Index do
# Progressive loading functions using LiveView's efficient update mechanisms
@spec start_progressive_historical_loading(Socket.t()) :: Socket.t()
defp start_progressive_historical_loading(socket) do
# Calculate optimal batch count based on zoom level
zoom = socket.assigns.map_zoom || 5
total_batches = calculate_batch_count_for_zoom(zoom)
# Start with a small batch for immediate visual feedback
socket =
socket
|> assign(loading_batch: 0, total_batches: 4)
|> assign(loading_batch: 0, total_batches: total_batches)
|> load_historical_batch(0)
# Schedule next batches using LiveView's async processing
Process.send_after(self(), {:load_historical_batch, 1}, 50)
Process.send_after(self(), {:load_historical_batch, 2}, 150)
Process.send_after(self(), {:load_historical_batch, 3}, 300)
# Use shorter delays for higher zoom levels (fewer batches)
base_delay = if zoom >= 12, do: 25, else: 50
Enum.each(1..(total_batches - 1), fn batch_index ->
delay = base_delay * (batch_index * 2)
# Progressive delays: 50ms, 100ms, 150ms, etc.
Process.send_after(self(), {:load_historical_batch, batch_index}, delay)
end)
socket
end
# Calculate optimal batch size based on zoom level
# Higher zoom = smaller viewport = fewer packets needed = smaller batches for faster response
@spec calculate_batch_size_for_zoom(integer()) :: integer()
# Very zoomed in - small batches
defp calculate_batch_size_for_zoom(zoom) when zoom >= 15, do: 20
# Moderately zoomed in
defp calculate_batch_size_for_zoom(zoom) when zoom >= 12, do: 35
# Medium zoom
defp calculate_batch_size_for_zoom(zoom) when zoom >= 8, do: 50
# Zoomed out
defp calculate_batch_size_for_zoom(zoom) when zoom >= 5, do: 75
# Very zoomed out
defp calculate_batch_size_for_zoom(_), do: 100
# Calculate optimal number of batches based on zoom level
# Higher zoom = fewer batches needed since viewport is smaller
@spec calculate_batch_count_for_zoom(integer()) :: integer()
# Very zoomed in - fewer batches
defp calculate_batch_count_for_zoom(zoom) when zoom >= 15, do: 2
# Moderately zoomed in
defp calculate_batch_count_for_zoom(zoom) when zoom >= 12, do: 3
# Medium zoom
defp calculate_batch_count_for_zoom(zoom) when zoom >= 8, do: 4
# Zoomed out - more batches
defp calculate_batch_count_for_zoom(_), do: 5
@spec load_historical_batch(Socket.t(), integer()) :: Socket.t()
defp load_historical_batch(socket, batch_offset) do
if socket.assigns.map_bounds do
@ -1221,8 +1260,9 @@ defmodule AprsmeWeb.MapLive.Index do
socket.assigns.map_bounds.north
]
# Load smaller batches with offset for progressive loading
batch_size = 50
# Calculate zoom-based batch size - higher zoom = smaller batches for faster response
zoom = socket.assigns.map_zoom || 5
batch_size = calculate_batch_size_for_zoom(zoom)
offset = batch_offset * batch_size
packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
@ -1230,10 +1270,12 @@ defmodule AprsmeWeb.MapLive.Index do
historical_packets =
if packets_module == Aprsme.Packets do
# Use cached queries for better performance
# Include zoom level in cache key for better cache efficiency
Aprsme.CachedQueries.get_recent_packets_cached(%{
bounds: bounds,
limit: batch_size,
offset: offset
offset: offset,
zoom: zoom
})
else
# Fallback for testing
@ -1250,11 +1292,13 @@ defmodule AprsmeWeb.MapLive.Index do
if Enum.any?(packet_data_list) do
# Use LiveView's efficient push_event for incremental updates
total_batches = socket.assigns.total_batches || 4
socket =
push_event(socket, "add_historical_packets_batch", %{
packets: packet_data_list,
batch: batch_offset,
is_final: batch_offset >= 3
is_final: batch_offset >= total_batches - 1
})
# Update progress for user feedback