perf: fix remaining medium/low severity inefficiencies
- Radio.group_reciprocals: replace O(n²) nested Enum.filter with O(n) Enum.group_by using a hash key (sorted station pair + band + hour) - contact_live/index.ex: cache monthly bars Repo.all(Contact) query with 60s TTL - ScoreCache.valid_times: replace :ets.select with :ets.foldl to avoid intermediate list allocation - Accounts.list_users: add limit(100) to unbounded query - Accounts.backfill_missing_home_qth: batch individual Oban.insert calls into single Oban.insert_all
This commit is contained in:
parent
9cce257db7
commit
15f4175cce
4 changed files with 35 additions and 47 deletions
|
|
@ -90,10 +90,10 @@ defmodule Microwaveprop.Accounts do
|
|||
|
||||
## Admin user management
|
||||
|
||||
@doc "Returns all users ordered by callsign."
|
||||
@doc "Returns users ordered by callsign, limited to 100."
|
||||
@spec list_users() :: [User.t()]
|
||||
def list_users do
|
||||
Repo.all(from u in User, order_by: [asc: u.callsign])
|
||||
Repo.all(from u in User, order_by: [asc: u.callsign], limit: 100)
|
||||
end
|
||||
|
||||
@doc "Updates admin-managed user fields (callsign, name, email, is_admin)."
|
||||
|
|
@ -168,9 +168,8 @@ defmodule Microwaveprop.Accounts do
|
|||
|> where([u], is_nil(u.home_grid))
|
||||
|> select([u], u.id)
|
||||
|> Repo.all()
|
||||
|> Enum.each(fn id ->
|
||||
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
||||
end)
|
||||
|> Enum.map(fn id -> %{user_id: id} |> worker.new() end)
|
||||
|> then(&if &1 != [], do: Oban.insert_all(&1))
|
||||
end
|
||||
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -149,10 +149,15 @@ defmodule Microwaveprop.Propagation.ScoreCache do
|
|||
@doc "Returns the sorted list of valid_times cached for `band_mhz`."
|
||||
@spec valid_times(non_neg_integer()) :: [DateTime.t()]
|
||||
def valid_times(band_mhz) do
|
||||
match_spec = [{{{band_mhz, :"$1"}, :_}, [], [:"$1"]}]
|
||||
|
||||
@table
|
||||
|> :ets.select(match_spec)
|
||||
|> :ets.foldl(
|
||||
fn
|
||||
{{^band_mhz, vt}, _}, acc -> [vt | acc]
|
||||
_, acc -> acc
|
||||
end,
|
||||
[],
|
||||
@table
|
||||
)
|
||||
|> Enum.sort({:asc, DateTime})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -230,42 +230,20 @@ defmodule Microwaveprop.Radio do
|
|||
"""
|
||||
@spec group_reciprocals([Contact.t()]) :: [{Contact.t(), [Contact.t()]}]
|
||||
def group_reciprocals(contacts) do
|
||||
{groups, _seen_ids} =
|
||||
Enum.reduce(contacts, {[], MapSet.new()}, fn contact, {groups, seen} ->
|
||||
group_contact(contact, contacts, groups, seen)
|
||||
end)
|
||||
|
||||
Enum.reverse(groups)
|
||||
contacts
|
||||
|> Enum.group_by(&reciprocal_key/1)
|
||||
|> Enum.map(fn {_key, [primary | rest]} -> {primary, rest} end)
|
||||
end
|
||||
|
||||
defp group_contact(contact, contacts, groups, seen) do
|
||||
if MapSet.member?(seen, contact.id) do
|
||||
{groups, seen}
|
||||
else
|
||||
reciprocals =
|
||||
Enum.filter(contacts, fn other ->
|
||||
other.id != contact.id and
|
||||
not MapSet.member?(seen, other.id) and
|
||||
other.band == contact.band and
|
||||
reciprocal_pair?(contact, other)
|
||||
end)
|
||||
|
||||
new_seen = Enum.reduce(reciprocals, MapSet.put(seen, contact.id), &MapSet.put(&2, &1.id))
|
||||
{[{contact, reciprocals} | groups], new_seen}
|
||||
end
|
||||
defp reciprocal_key(contact) do
|
||||
stations = Enum.sort([contact.station1, contact.station2])
|
||||
{stations, contact.band, reciprocal_hour_key(contact.qso_timestamp)}
|
||||
end
|
||||
|
||||
defp reciprocal_pair?(a, b) do
|
||||
same_stations =
|
||||
(a.station1 == b.station2 and a.station2 == b.station1) or
|
||||
(a.station1 == b.station1 and a.station2 == b.station2)
|
||||
defp reciprocal_hour_key(nil), do: nil
|
||||
|
||||
same_stations and same_hour?(a.qso_timestamp, b.qso_timestamp)
|
||||
end
|
||||
|
||||
defp same_hour?(t1, t2) do
|
||||
t1 |> NaiveDateTime.truncate(:second) |> Map.take([:year, :month, :day, :hour]) ==
|
||||
t2 |> NaiveDateTime.truncate(:second) |> Map.take([:year, :month, :day, :hour])
|
||||
defp reciprocal_hour_key(%_{} = ts) do
|
||||
Map.take(ts, [:year, :month, :day, :hour])
|
||||
end
|
||||
|
||||
# Only cache the unscoped, unsearched total — scope-dependent counts
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
|||
|
||||
alias Microwaveprop.Accounts.Scope
|
||||
alias Microwaveprop.Accounts.User
|
||||
alias Microwaveprop.Cache
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
|
|
@ -77,11 +78,15 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
|||
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [scope_token(scope)]})}
|
||||
end
|
||||
|
||||
@monthly_bars_cache_key {__MODULE__, :monthly_bars}
|
||||
@monthly_bars_ttl_ms 60_000
|
||||
|
||||
# Counts every contact in the corpus regardless of viewer scope —
|
||||
# the chart is a public-facing seasonal summary, not a personal
|
||||
# view, so private contacts roll into the per-month totals like
|
||||
# any other.
|
||||
# any other. Cached for 60s since data only changes on new inserts.
|
||||
defp monthly_bars do
|
||||
Cache.fetch_or_store(@monthly_bars_cache_key, @monthly_bars_ttl_ms, fn ->
|
||||
by_month =
|
||||
Contact
|
||||
|> where([c], not is_nil(c.qso_timestamp))
|
||||
|
|
@ -106,6 +111,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
|||
height: height
|
||||
}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp bar_height(_count, 0), do: 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue