aprs.me/lib/aprsme/performance/insert_optimizer.ex
Graham McIntire 8811298eed
Fix packet buffer overflow and system load adjustment issues
- Fixed InsertOptimizer returning map instead of keyword list for Ecto
- Increased batch size ranges to handle high load (100-800 packets)
- Improved consumer responsiveness:
  - Reduced batch timeout from 1000ms to 500ms
  - Process batches at 80% capacity for better throughput
  - Reduced adjustment interval from 10s to 5s
- Added comprehensive logging for debugging buffer status
- Created integration tests for packet pipeline
- Coordinated batch sizing between SystemMonitor and InsertOptimizer

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 08:16:11 -05:00

186 lines
5.4 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
# Calculate throughput (packets per second)
throughput = if duration_ms > 0, do: success_count / (duration_ms / 1000), else: 0
metric = %{
batch_size: batch_size,
duration_ms: duration_ms,
success_count: success_count,
throughput: throughput,
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
@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
defp optimize_based_on_performance(state) do
if length(state.performance_history) >= 3 do
# Analyze recent performance
recent_metrics = Enum.take(state.performance_history, 5)
total_throughput = recent_metrics |> Enum.map(& &1.throughput) |> Enum.sum()
avg_throughput = total_throughput / length(recent_metrics)
total_duration = recent_metrics |> Enum.map(& &1.duration_ms) |> Enum.sum()
avg_duration = total_duration / length(recent_metrics)
# Adjust batch size based on performance
new_batch_size = calculate_optimal_batch_size(avg_throughput, avg_duration, state.current_batch_size)
# Adjust INSERT options based on system load
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"
)
# Emit telemetry for monitoring
: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}, %{})
if new_batch_size != state.current_batch_size do
:telemetry.execute([:aprsme, :insert_optimizer, :optimizations], %{count: 1}, %{})
end
%{
state
| current_batch_size: new_batch_size,
insert_options: new_insert_options,
last_optimization: System.monotonic_time(:millisecond)
}
else
state
end
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
defp calculate_insert_options(avg_duration) do
base_options = default_insert_options()
# If INSERTs are taking too long, use more aggressive optimizations
if avg_duration > 3000 do
Keyword.merge(base_options,
returning: false,
on_conflict: :nothing,
timeout: 30_000
)
else
base_options
end
end
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