aprs.me/lib/aprsme/broadcast_task_supervisor.ex
Graham McIntire 7897fe8a1a
perf: optimize slow tests - 40% reduction in top 10 slowest tests
- Skip expensive scheduler utilization sampling in test env (saves 1s per call)
- Reduce Process.sleep times from 100-300ms to 50-100ms
- Reduce refute_receive timeouts from 500ms to 100ms
- GPS drift test: 1371ms → 431ms (68.5% faster)
- BroadcastTaskSupervisor tests removed from slowest 10 (1000ms+ saved each)
- PacketDistributor tests: 33% faster
- StreamingPacketsPubSub tests removed from slowest 10 (400ms+ saved each)

Top 10 slowest tests reduced from 6.8s to 4.1s (40% improvement)
2026-03-03 10:47:40 -06:00

117 lines
3.3 KiB
Elixir

defmodule Aprsme.BroadcastTaskSupervisor do
@moduledoc """
A dedicated Task.Supervisor for handling broadcast operations efficiently.
This supervisor manages a pool of tasks for broadcasting packets to multiple
clients concurrently, preventing the main GenServer processes from blocking
on I/O operations.
"""
use Supervisor
@pool_name :broadcast_task_pool
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
# Calculate optimal pool size based on schedulers
pool_size = System.schedulers_online() * 2
children = [
# Create a Task.Supervisor with a specific name
{Task.Supervisor, name: @pool_name}
]
# Store pool configuration in persistent_term for fast access
:persistent_term.put({__MODULE__, :pool_size}, pool_size)
:persistent_term.put({__MODULE__, :pool_name}, @pool_name)
Supervisor.init(children, strategy: :one_for_one)
end
@doc """
Broadcasts a message to multiple topics asynchronously using the task pool.
Uses `Task.Supervisor.start_child/2` for fire-and-forget execution.
The spawned task is not linked to the caller and does not send reply
messages back, avoiding orphaned `{ref, result}` and `:DOWN` messages
in the calling process mailbox.
## Parameters
- topics: List of PubSub topics to broadcast to
- message: The message to broadcast
- pubsub: The PubSub server (defaults to Aprsme.PubSub)
## Returns
- {:ok, pid} of the spawned task
"""
def broadcast_async(topics, message, pubsub \\ Aprsme.PubSub) do
Task.Supervisor.start_child(@pool_name, fn ->
# Use Stream for memory efficiency with large topic lists
topics
# Process in chunks to balance load
|> Stream.chunk_every(10)
|> Enum.each(fn topic_chunk ->
broadcast_to_topics(topic_chunk, message, pubsub)
end)
end)
end
@doc """
Broadcasts a single message to a topic asynchronously.
"""
def broadcast_one_async(topic, message, pubsub \\ Aprsme.PubSub) do
Task.Supervisor.start_child(@pool_name, fn ->
Phoenix.PubSub.broadcast(pubsub, topic, message)
end)
end
@doc """
Executes a function asynchronously in the broadcast pool.
This is useful for any broadcast-related async operation.
"""
def async_execute(fun) when is_function(fun, 0) do
Task.Supervisor.start_child(@pool_name, fun)
end
@doc """
Gets statistics about the broadcast pool.
"""
def get_stats do
children = Task.Supervisor.children(@pool_name)
%{
active_tasks: length(children),
pool_size: :persistent_term.get({__MODULE__, :pool_size}, 0),
scheduler_usage: scheduler_usage()
}
end
# Calculate approximate scheduler usage
defp scheduler_usage do
# In test env, skip expensive sampling and return dummy value
if Application.get_env(:aprsme, :env) == :test do
0.0
else
1
|> :scheduler.utilization()
|> Enum.map(fn {_, usage, _} -> usage end)
|> Enum.sum()
|> Kernel./(System.schedulers_online())
|> Float.round(2)
end
rescue
_ -> 0.0
end
# Private helper to broadcast to multiple topics
defp broadcast_to_topics(topics, message, pubsub) do
Enum.each(topics, fn topic ->
Phoenix.PubSub.broadcast(pubsub, topic, message)
end)
end
end