- 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>
186 lines
5.4 KiB
Elixir
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
|