Swaps `if`/`cond` branches for multi-clause function heads or head-pattern matching, making the control flow visible in the function signatures: - Navigation.determine_map_location: resolve_location/4 now dispatches on the shape of the geolocation input and whether URL params are explicit. - Navigation.handle_callsign_tracking: per-case clauses for empty callsign and explicit-URL-params short-circuits, plus a final clause that does the DB lookup and rescues once. - Navigation.handle_callsign_search: dispatch_callsign_search/3 separates the valid/invalid callsign branches. - InsertOptimizer.optimize_based_on_performance: pattern-match on [_, _, _ | _] to require ≥3 samples without calling length/1. - InsertOptimizer.calculate_insert_options and throughput helpers split into guarded clauses instead of inline `if`. - maybe_emit_optimization uses same-var pattern matching to skip the telemetry hit when nothing changed. Adds unit tests covering every branch of both modules plus SignalHandler, LocaleHook, and the InsertOptimizer's :noproc fallback path. Coverage 64.69% → 65.72%.
185 lines
5.7 KiB
Elixir
185 lines
5.7 KiB
Elixir
defmodule Aprsme.Performance.InsertOptimizer do
|
|
@moduledoc """
|
|
Optimizations specifically for INSERT performance.
|
|
|
|
This module contains strategies to improve packet insertion performance:
|
|
- Optimized batch sizing based on system load
|
|
- Reduced index maintenance overhead
|
|
- Streamlined packet preparation
|
|
- Connection pool optimizations
|
|
"""
|
|
|
|
use GenServer
|
|
|
|
require Logger
|
|
|
|
# Configuration for INSERT optimization
|
|
@base_batch_size 200
|
|
@max_batch_size 800
|
|
@min_batch_size 100
|
|
# 30 seconds
|
|
@optimization_check_interval 30_000
|
|
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Get the optimal batch size for current system conditions.
|
|
"""
|
|
def get_optimal_batch_size do
|
|
GenServer.call(__MODULE__, :get_batch_size)
|
|
catch
|
|
:exit, {:noproc, _} -> @base_batch_size
|
|
end
|
|
|
|
@doc """
|
|
Get INSERT optimization settings.
|
|
"""
|
|
def get_insert_options do
|
|
GenServer.call(__MODULE__, :get_insert_options)
|
|
catch
|
|
:exit, {:noproc, _} -> default_insert_options()
|
|
end
|
|
|
|
@doc """
|
|
Record INSERT performance metrics for optimization.
|
|
"""
|
|
def record_insert_metrics(batch_size, duration_ms, success_count) do
|
|
GenServer.cast(__MODULE__, {:record_metrics, batch_size, duration_ms, success_count})
|
|
end
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
schedule_optimization_check()
|
|
|
|
state = %{
|
|
current_batch_size: @base_batch_size,
|
|
insert_options: default_insert_options(),
|
|
performance_history: [],
|
|
last_optimization: System.monotonic_time(:millisecond)
|
|
}
|
|
|
|
{:ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_batch_size, _from, state) do
|
|
{:reply, state.current_batch_size, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_insert_options, _from, state) do
|
|
{:reply, state.insert_options, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:record_metrics, batch_size, duration_ms, success_count}, state) do
|
|
metric = %{
|
|
batch_size: batch_size,
|
|
duration_ms: duration_ms,
|
|
success_count: success_count,
|
|
throughput: throughput(success_count, duration_ms),
|
|
timestamp: System.monotonic_time(:millisecond)
|
|
}
|
|
|
|
# Keep last 20 metrics for analysis
|
|
new_history = Enum.take([metric | state.performance_history], 20)
|
|
|
|
{:noreply, %{state | performance_history: new_history}}
|
|
end
|
|
|
|
# Packets-per-second, or 0 when the batch completed instantly (avoid div-by-0).
|
|
defp throughput(_count, 0), do: 0
|
|
defp throughput(_count, duration_ms) when duration_ms < 0, do: 0
|
|
defp throughput(count, duration_ms), do: count / (duration_ms / 1000)
|
|
|
|
@impl true
|
|
def handle_info(:optimize_settings, state) do
|
|
new_state = optimize_based_on_performance(state)
|
|
schedule_optimization_check()
|
|
{:noreply, new_state}
|
|
end
|
|
|
|
defp schedule_optimization_check do
|
|
Process.send_after(self(), :optimize_settings, @optimization_check_interval)
|
|
end
|
|
|
|
# Require at least three samples before doing anything — match directly on the
|
|
# list prefix instead of calling length/1 so we short-circuit for short lists.
|
|
defp optimize_based_on_performance(%{performance_history: [_, _, _ | _] = history} = state) do
|
|
recent_metrics = Enum.take(history, 5)
|
|
sample_size = length(recent_metrics)
|
|
avg_throughput = average(recent_metrics, & &1.throughput, sample_size)
|
|
avg_duration = average(recent_metrics, & &1.duration_ms, sample_size)
|
|
|
|
new_batch_size = calculate_optimal_batch_size(avg_throughput, avg_duration, state.current_batch_size)
|
|
new_insert_options = calculate_insert_options(avg_duration)
|
|
|
|
Logger.debug(
|
|
"INSERT optimization: batch_size=#{new_batch_size}, avg_throughput=#{Float.round(avg_throughput, 2)} pps"
|
|
)
|
|
|
|
:telemetry.execute([:aprsme, :insert_optimizer, :batch_size], %{value: new_batch_size}, %{})
|
|
:telemetry.execute([:aprsme, :insert_optimizer, :throughput], %{value: avg_throughput}, %{})
|
|
:telemetry.execute([:aprsme, :insert_optimizer, :duration], %{value: avg_duration}, %{})
|
|
maybe_emit_optimization(new_batch_size, state.current_batch_size)
|
|
|
|
%{
|
|
state
|
|
| current_batch_size: new_batch_size,
|
|
insert_options: new_insert_options,
|
|
last_optimization: System.monotonic_time(:millisecond)
|
|
}
|
|
end
|
|
|
|
defp optimize_based_on_performance(state), do: state
|
|
|
|
defp average(items, getter, count) do
|
|
items |> Enum.map(getter) |> Enum.sum() |> Kernel./(count)
|
|
end
|
|
|
|
defp maybe_emit_optimization(same, same), do: :ok
|
|
|
|
defp maybe_emit_optimization(_new, _old) do
|
|
:telemetry.execute([:aprsme, :insert_optimizer, :optimizations], %{count: 1}, %{})
|
|
end
|
|
|
|
defp calculate_optimal_batch_size(avg_throughput, avg_duration, current_batch_size) do
|
|
cond do
|
|
# If throughput is low and duration is high, reduce batch size
|
|
avg_throughput < 50 and avg_duration > 5000 ->
|
|
max(@min_batch_size, round(current_batch_size * 0.8))
|
|
|
|
# If throughput is good and duration is acceptable, increase batch size
|
|
avg_throughput > 200 and avg_duration < 2000 ->
|
|
min(@max_batch_size, round(current_batch_size * 1.2))
|
|
|
|
# Otherwise keep current size
|
|
true ->
|
|
current_batch_size
|
|
end
|
|
end
|
|
|
|
# When INSERTs are slow (>3s avg), use a longer timeout; otherwise stick with defaults.
|
|
defp calculate_insert_options(avg_duration) when avg_duration > 3_000 do
|
|
Keyword.merge(default_insert_options(),
|
|
returning: false,
|
|
on_conflict: :nothing,
|
|
timeout: 30_000
|
|
)
|
|
end
|
|
|
|
defp calculate_insert_options(_avg_duration), do: default_insert_options()
|
|
|
|
defp default_insert_options do
|
|
[
|
|
# Don't return IDs unless needed
|
|
returning: false,
|
|
# Skip conflicts instead of raising
|
|
on_conflict: :nothing,
|
|
# 15 second timeout
|
|
timeout: 15_000
|
|
]
|
|
end
|
|
end
|