From 7318d2dfbc410a6538e04bab19811721868e9810 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 9 Jul 2025 09:14:12 -0500 Subject: [PATCH] Optimize viewport-based loading with zoom-aware batching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/aprsme/packets.ex | 13 +++--- lib/aprsme_web/live/map_live/index.ex | 62 +++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lib/aprsme/packets.ex b/lib/aprsme/packets.ex index 71f6cfa..e7bd21c 100644 --- a/lib/aprsme/packets.ex +++ b/lib/aprsme/packets.ex @@ -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) diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index 196deca..8e8517f 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -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