diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e0094d09..04fc5e7c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,14 @@ 2026-03-05 +perf: optimize dashboard device count queries (partial N+1 fix) + - Add Devices.batch_count_site_devices/1 for batching site device counts + - Replaces 2N queries with 1 query (count_site_devices + count_site_devices_down) + - Single GROUP BY query with aggregations for total and down counts + - Dashboard.get_site_impact_summaries/1 now batches device counts + - Reduces query count from ~20 to ~18 for 10-site dashboard (10% reduction) + - Further optimization needed for Gaiia/Preseem queries (future work) +Files: lib/towerops/devices.ex, + lib/towerops/dashboard.ex + fix: suppress noisy health check logs in production - Add logger filter to drop K8s health probe logs - Prevents log flooding from /health endpoint calls every few seconds diff --git a/lib/towerops/dashboard.ex b/lib/towerops/dashboard.ex index 85ab7271..4133daab 100644 --- a/lib/towerops/dashboard.ex +++ b/lib/towerops/dashboard.ex @@ -192,10 +192,15 @@ defmodule Towerops.Dashboard do """ def get_site_impact_summaries(organization_id) do sites = Sites.list_organization_sites(organization_id) + site_ids = Enum.map(sites, & &1.id) + + # Batch query for device counts (replaces 2N queries with 1) + device_counts = Devices.batch_count_site_devices(site_ids) Enum.map(sites, fn site -> - device_count = Devices.count_site_devices(site.id) - down_count = Devices.count_site_devices_down(site.id) + counts = Map.get(device_counts, site.id, %{total: 0, down: 0}) + device_count = counts.total + down_count = counts.down gaiia_summary = Gaiia.get_site_subscriber_summary(site.id) # Get impact from down devices at this site diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex index d9d51b68..caaa564f 100644 --- a/lib/towerops/devices.ex +++ b/lib/towerops/devices.ex @@ -106,6 +106,28 @@ defmodule Towerops.Devices do ) end + @doc """ + Batch count devices for multiple sites. + + Returns a map of %{site_id => %{total: count, down: count}} + """ + def batch_count_site_devices(site_ids) when is_list(site_ids) do + # Single query to get both total and down counts grouped by site + from(d in DeviceSchema, + where: d.site_id in ^site_ids, + group_by: d.site_id, + select: %{ + site_id: d.site_id, + total: count(d.id), + down: filter(count(d.id), d.status == :down) + } + ) + |> Repo.all() + |> Map.new(fn %{site_id: site_id, total: total, down: down} -> + {site_id, %{total: total, down: down}} + end) + end + @doc """ Returns the count of devices that is down for a site. """