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