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
|
## Admin user management
|
||||||
|
|
||||||
@doc "Returns all users ordered by callsign."
|
@doc "Returns users ordered by callsign, limited to 100."
|
||||||
@spec list_users() :: [User.t()]
|
@spec list_users() :: [User.t()]
|
||||||
def list_users do
|
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
|
end
|
||||||
|
|
||||||
@doc "Updates admin-managed user fields (callsign, name, email, is_admin)."
|
@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))
|
|> where([u], is_nil(u.home_grid))
|
||||||
|> select([u], u.id)
|
|> select([u], u.id)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Enum.each(fn id ->
|
|> Enum.map(fn id -> %{user_id: id} |> worker.new() end)
|
||||||
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
|> then(&if &1 != [], do: Oban.insert_all(&1))
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|
|
||||||
|
|
@ -149,10 +149,15 @@ defmodule Microwaveprop.Propagation.ScoreCache do
|
||||||
@doc "Returns the sorted list of valid_times cached for `band_mhz`."
|
@doc "Returns the sorted list of valid_times cached for `band_mhz`."
|
||||||
@spec valid_times(non_neg_integer()) :: [DateTime.t()]
|
@spec valid_times(non_neg_integer()) :: [DateTime.t()]
|
||||||
def valid_times(band_mhz) do
|
def valid_times(band_mhz) do
|
||||||
match_spec = [{{{band_mhz, :"$1"}, :_}, [], [:"$1"]}]
|
|
||||||
|
|
||||||
@table
|
@table
|
||||||
|> :ets.select(match_spec)
|
|> :ets.foldl(
|
||||||
|
fn
|
||||||
|
{{^band_mhz, vt}, _}, acc -> [vt | acc]
|
||||||
|
_, acc -> acc
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
@table
|
||||||
|
)
|
||||||
|> Enum.sort({:asc, DateTime})
|
|> Enum.sort({:asc, DateTime})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,42 +230,20 @@ defmodule Microwaveprop.Radio do
|
||||||
"""
|
"""
|
||||||
@spec group_reciprocals([Contact.t()]) :: [{Contact.t(), [Contact.t()]}]
|
@spec group_reciprocals([Contact.t()]) :: [{Contact.t(), [Contact.t()]}]
|
||||||
def group_reciprocals(contacts) do
|
def group_reciprocals(contacts) do
|
||||||
{groups, _seen_ids} =
|
contacts
|
||||||
Enum.reduce(contacts, {[], MapSet.new()}, fn contact, {groups, seen} ->
|
|> Enum.group_by(&reciprocal_key/1)
|
||||||
group_contact(contact, contacts, groups, seen)
|
|> Enum.map(fn {_key, [primary | rest]} -> {primary, rest} end)
|
||||||
end)
|
|
||||||
|
|
||||||
Enum.reverse(groups)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp group_contact(contact, contacts, groups, seen) do
|
defp reciprocal_key(contact) do
|
||||||
if MapSet.member?(seen, contact.id) do
|
stations = Enum.sort([contact.station1, contact.station2])
|
||||||
{groups, seen}
|
{stations, contact.band, reciprocal_hour_key(contact.qso_timestamp)}
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp reciprocal_pair?(a, b) do
|
defp reciprocal_hour_key(nil), do: nil
|
||||||
same_stations =
|
|
||||||
(a.station1 == b.station2 and a.station2 == b.station1) or
|
|
||||||
(a.station1 == b.station1 and a.station2 == b.station2)
|
|
||||||
|
|
||||||
same_stations and same_hour?(a.qso_timestamp, b.qso_timestamp)
|
defp reciprocal_hour_key(%_{} = ts) do
|
||||||
end
|
Map.take(ts, [:year, :month, :day, :hour])
|
||||||
|
|
||||||
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])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Only cache the unscoped, unsearched total — scope-dependent counts
|
# 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.Scope
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
|
alias Microwaveprop.Cache
|
||||||
alias Microwaveprop.Radio.Contact
|
alias Microwaveprop.Radio.Contact
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
|
||||||
|
|
@ -77,18 +78,22 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [scope_token(scope)]})}
|
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [scope_token(scope)]})}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@monthly_bars_cache_key {__MODULE__, :monthly_bars}
|
||||||
|
@monthly_bars_ttl_ms 60_000
|
||||||
|
|
||||||
# Counts every contact in the corpus regardless of viewer scope —
|
# Counts every contact in the corpus regardless of viewer scope —
|
||||||
# the chart is a public-facing seasonal summary, not a personal
|
# the chart is a public-facing seasonal summary, not a personal
|
||||||
# view, so private contacts roll into the per-month totals like
|
# 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
|
defp monthly_bars do
|
||||||
by_month =
|
Cache.fetch_or_store(@monthly_bars_cache_key, @monthly_bars_ttl_ms, fn ->
|
||||||
Contact
|
by_month =
|
||||||
|> where([c], not is_nil(c.qso_timestamp))
|
Contact
|
||||||
|> group_by([c], fragment("EXTRACT(MONTH FROM ?)::int", c.qso_timestamp))
|
|> where([c], not is_nil(c.qso_timestamp))
|
||||||
|> select([c], {fragment("EXTRACT(MONTH FROM ?)::int", c.qso_timestamp), count(c.id)})
|
|> group_by([c], fragment("EXTRACT(MONTH FROM ?)::int", c.qso_timestamp))
|
||||||
|> Repo.all()
|
|> select([c], {fragment("EXTRACT(MONTH FROM ?)::int", c.qso_timestamp), count(c.id)})
|
||||||
|> Map.new()
|
|> Repo.all()
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
counts = Enum.map(1..12, fn m -> {m, Map.get(by_month, m, 0)} end)
|
counts = Enum.map(1..12, fn m -> {m, Map.get(by_month, m, 0)} end)
|
||||||
max_count = counts |> Enum.map(&elem(&1, 1)) |> Enum.max()
|
max_count = counts |> Enum.map(&elem(&1, 1)) |> Enum.max()
|
||||||
|
|
@ -106,6 +111,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
height: height
|
height: height
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp bar_height(_count, 0), do: 0
|
defp bar_height(_count, 0), do: 0
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue