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%.
153 lines
5.5 KiB
Elixir
153 lines
5.5 KiB
Elixir
defmodule Aprsme.Performance.InsertOptimizerTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Aprsme.Performance.InsertOptimizer
|
|
|
|
defp fresh_state do
|
|
%{
|
|
current_batch_size: 200,
|
|
insert_options: [returning: false, on_conflict: :nothing, timeout: 15_000],
|
|
performance_history: [],
|
|
last_optimization: System.monotonic_time(:millisecond)
|
|
}
|
|
end
|
|
|
|
defp metric(throughput, duration_ms) do
|
|
%{
|
|
batch_size: 200,
|
|
duration_ms: duration_ms,
|
|
success_count: 100,
|
|
throughput: throughput,
|
|
timestamp: System.monotonic_time(:millisecond)
|
|
}
|
|
end
|
|
|
|
describe "GenServer callbacks" do
|
|
test "handle_call :get_batch_size returns current batch size" do
|
|
state = fresh_state()
|
|
|
|
assert {:reply, 200, ^state} =
|
|
InsertOptimizer.handle_call(:get_batch_size, self(), state)
|
|
end
|
|
|
|
test "handle_call :get_insert_options returns insert_options" do
|
|
state = fresh_state()
|
|
opts = state.insert_options
|
|
assert {:reply, ^opts, ^state} = InsertOptimizer.handle_call(:get_insert_options, self(), state)
|
|
end
|
|
|
|
test "handle_cast records a new metric in performance_history" do
|
|
state = fresh_state()
|
|
|
|
{:noreply, new_state} =
|
|
InsertOptimizer.handle_cast({:record_metrics, 200, 1_000, 100}, state)
|
|
|
|
assert [entry | _] = new_state.performance_history
|
|
assert entry.batch_size == 200
|
|
assert entry.duration_ms == 1_000
|
|
assert entry.success_count == 100
|
|
# Throughput = 100 / (1000/1000) = 100 pps
|
|
assert entry.throughput == 100.0
|
|
end
|
|
|
|
test "handle_cast with zero duration avoids div-by-0, throughput is 0" do
|
|
state = fresh_state()
|
|
|
|
{:noreply, new_state} =
|
|
InsertOptimizer.handle_cast({:record_metrics, 100, 0, 50}, state)
|
|
|
|
assert [entry | _] = new_state.performance_history
|
|
assert entry.throughput == 0
|
|
end
|
|
|
|
test "performance history is capped at 20 entries" do
|
|
state = fresh_state()
|
|
|
|
final =
|
|
Enum.reduce(1..25, state, fn _, s ->
|
|
{:noreply, s2} = InsertOptimizer.handle_cast({:record_metrics, 200, 1000, 100}, s)
|
|
s2
|
|
end)
|
|
|
|
assert length(final.performance_history) == 20
|
|
end
|
|
end
|
|
|
|
describe "handle_info :optimize_settings" do
|
|
test "with fewer than 3 metrics, state is unchanged except for re-scheduled timer" do
|
|
state = %{fresh_state() | performance_history: [metric(100, 1000), metric(90, 1100)]}
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
# Batch size shouldn't have moved.
|
|
assert new_state.current_batch_size == state.current_batch_size
|
|
assert new_state.performance_history == state.performance_history
|
|
end
|
|
|
|
test "grows batch size when throughput is high and duration low" do
|
|
# With avg_throughput > 200 and avg_duration < 2000, batch size should scale up
|
|
# from 200 → min(@max, round(200 * 1.2)) = 240.
|
|
history =
|
|
Enum.map(1..5, fn _ -> metric(300, 1000) end)
|
|
|
|
state = %{fresh_state() | performance_history: history}
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
assert new_state.current_batch_size == 240
|
|
# Fast INSERTs keep default options.
|
|
assert Keyword.get(new_state.insert_options, :timeout) == 15_000
|
|
end
|
|
|
|
test "shrinks batch size when throughput is low and duration high" do
|
|
# avg_throughput < 50 and avg_duration > 5000 → batch_size drops by 20%.
|
|
history = Enum.map(1..5, fn _ -> metric(10, 6000) end)
|
|
state = %{fresh_state() | performance_history: history}
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
assert new_state.current_batch_size == 160
|
|
# Slow INSERTs bump timeout.
|
|
assert Keyword.get(new_state.insert_options, :timeout) == 30_000
|
|
end
|
|
|
|
test "keeps batch size when performance is middling" do
|
|
# 100 pps at 3000ms falls in neither bucket → no change to batch size.
|
|
history = Enum.map(1..5, fn _ -> metric(100, 3000) end)
|
|
state = %{fresh_state() | performance_history: history}
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
assert new_state.current_batch_size == 200
|
|
end
|
|
|
|
test "never drops below the minimum batch size" do
|
|
state = %{
|
|
fresh_state()
|
|
| current_batch_size: 110,
|
|
performance_history: Enum.map(1..5, fn _ -> metric(5, 10_000) end)
|
|
}
|
|
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
assert new_state.current_batch_size == 100
|
|
end
|
|
|
|
test "never exceeds the maximum batch size" do
|
|
state = %{
|
|
fresh_state()
|
|
| current_batch_size: 750,
|
|
performance_history: Enum.map(1..5, fn _ -> metric(500, 500) end)
|
|
}
|
|
|
|
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
|
|
assert new_state.current_batch_size == 800
|
|
end
|
|
end
|
|
|
|
describe "client API when GenServer isn't running" do
|
|
test "get_optimal_batch_size falls back to base size on :noproc" do
|
|
# The supervised instance isn't started in the app tree, so this should
|
|
# hit the catch clause and return the base batch size (200).
|
|
assert InsertOptimizer.get_optimal_batch_size() == 200
|
|
end
|
|
|
|
test "get_insert_options falls back to default options on :noproc" do
|
|
opts = InsertOptimizer.get_insert_options()
|
|
assert Keyword.get(opts, :returning) == false
|
|
assert Keyword.get(opts, :on_conflict) == :nothing
|
|
assert Keyword.get(opts, :timeout) == 15_000
|
|
end
|
|
end
|
|
end
|