Packages
fnord
0.8.73
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/model_performance_tracker.ex
defmodule Services.ModelPerformanceTracker do
@moduledoc """
A GenServer that tracks AI model performance metrics during sessions.
This service tracks request-level timing and token usage to help evaluate
different model configurations and their effectiveness.
"""
use GenServer
defstruct [
:session_id,
:active_requests,
:completed_requests
]
@type tracking_id :: String.t()
@type model :: AI.Model.t()
@type usage_data :: map()
@type request_data :: %{
id: tracking_id(),
model: model(),
start_time: integer(),
end_time: integer() | nil,
usage: usage_data() | nil
}
@type t :: %__MODULE__{
session_id: String.t(),
active_requests: %{tracking_id() => request_data()},
completed_requests: [request_data()]
}
# Client API
@spec start_link() :: {:ok, pid()}
def start_link do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@spec start_session() :: String.t()
def start_session do
GenServer.call(__MODULE__, :start_session)
end
@spec begin_tracking(model()) :: tracking_id()
def begin_tracking(model) do
GenServer.call(__MODULE__, {:begin_tracking, model})
end
@spec end_tracking(tracking_id(), usage_data()) :: :ok
def end_tracking(tracking_id, usage_data) do
GenServer.call(__MODULE__, {:end_tracking, tracking_id, usage_data})
end
@spec generate_report() :: String.t()
def generate_report do
GenServer.call(__MODULE__, :generate_report)
end
@spec reset_session() :: :ok
def reset_session do
GenServer.call(__MODULE__, :reset_session)
end
# Server Callbacks
@impl GenServer
def init(_args) do
{:ok,
%__MODULE__{
session_id: generate_session_id(),
active_requests: %{},
completed_requests: []
}}
end
@impl GenServer
def handle_call(:start_session, _from, state) do
new_session_id = generate_session_id()
new_state = %{state | session_id: new_session_id, completed_requests: []}
{:reply, new_session_id, new_state}
end
@impl GenServer
def handle_call({:begin_tracking, model}, _from, state) do
tracking_id = generate_tracking_id()
request_data = %{
id: tracking_id,
model: model,
start_time: System.monotonic_time(:millisecond),
end_time: nil,
usage: nil
}
new_active_requests = Map.put(state.active_requests, tracking_id, request_data)
new_state = %{state | active_requests: new_active_requests}
{:reply, tracking_id, new_state}
end
@impl GenServer
def handle_call({:end_tracking, tracking_id, usage_data}, _from, state) do
case Map.get(state.active_requests, tracking_id) do
nil ->
{:reply, :ok, state}
request_data ->
completed_request = %{
request_data
| end_time: System.monotonic_time(:millisecond),
usage: usage_data
}
new_active_requests = Map.delete(state.active_requests, tracking_id)
new_completed_requests = [completed_request | state.completed_requests]
new_state = %{
state
| active_requests: new_active_requests,
completed_requests: new_completed_requests
}
{:reply, :ok, new_state}
end
end
@impl GenServer
def handle_call(:generate_report, _from, state) do
report = build_performance_report(state.completed_requests)
{:reply, report, state}
end
@impl GenServer
def handle_call(:reset_session, _from, state) do
new_state = %{
state
| session_id: generate_session_id(),
active_requests: %{},
completed_requests: []
}
{:reply, :ok, new_state}
end
# Private Functions
defp generate_session_id do
:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
end
defp generate_tracking_id do
:crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
defp build_performance_report([]), do: ""
defp build_performance_report(requests) do
total_requests = length(requests)
if total_requests == 0 do
""
else
model_stats = calculate_model_statistics(requests)
overall_stats = calculate_overall_statistics(requests)
"""
### Model Performance Report
**Session Summary:**
- Total API Requests: #{total_requests}
- Total Time: #{overall_stats.total_time_ms}ms
- Total Tokens: #{overall_stats.total_tokens}
#{format_model_breakdown(model_stats)}
#{format_detailed_metrics(model_stats)}
"""
end
end
defp calculate_overall_statistics(requests) do
total_time_ms =
requests
|> Enum.map(fn req -> req.end_time - req.start_time end)
|> Enum.sum()
total_tokens =
requests
|> Enum.map(fn req -> get_total_tokens(req.usage) end)
|> Enum.sum()
%{
total_time_ms: total_time_ms,
total_tokens: total_tokens
}
end
defp calculate_model_statistics(requests) do
requests
|> Enum.group_by(fn req ->
%{
model: req.model.model,
reasoning: req.model.reasoning
}
end)
|> Enum.map(fn {model_config, model_requests} ->
total_time_ms =
model_requests
|> Enum.map(fn req -> req.end_time - req.start_time end)
|> Enum.sum()
request_count = length(model_requests)
avg_time_ms = if request_count > 0, do: total_time_ms / request_count, else: 0
total_input_tokens =
model_requests
|> Enum.map(fn req -> get_input_tokens(req.usage) end)
|> Enum.sum()
total_output_tokens =
model_requests
|> Enum.map(fn req -> get_output_tokens(req.usage) end)
|> Enum.sum()
total_reasoning_tokens =
model_requests
|> Enum.map(fn req -> get_reasoning_tokens(req.usage) end)
|> Enum.sum()
total_tokens = total_input_tokens + total_output_tokens + total_reasoning_tokens
# Calculate tokens per minute
total_time_minutes = total_time_ms / 1000 / 60
tokens_per_minute =
if total_time_minutes > 0, do: total_tokens / total_time_minutes, else: 0
output_tokens_per_minute =
if total_time_minutes > 0, do: total_output_tokens / total_time_minutes, else: 0
# Calculate input token analysis
input_analysis = calculate_input_analysis(model_requests)
%{
model_config: model_config,
request_count: request_count,
total_time_ms: total_time_ms,
avg_time_ms: avg_time_ms,
total_input_tokens: total_input_tokens,
total_output_tokens: total_output_tokens,
total_reasoning_tokens: total_reasoning_tokens,
total_tokens: total_tokens,
tokens_per_minute: tokens_per_minute,
output_tokens_per_minute: output_tokens_per_minute,
input_analysis: input_analysis
}
end)
|> Enum.sort_by(fn stat ->
{stat.model_config.model, reasoning_level_to_int(stat.model_config.reasoning)}
end)
end
defp calculate_input_analysis(requests) do
if length(requests) == 0 do
%{
avg_input_size: 0,
input_processing_speed_ms_per_token: 0.0,
scaling_analysis: %{},
input_correlation: 0.0
}
else
# Basic input metrics
input_sizes = Enum.map(requests, fn req -> get_input_tokens(req.usage) end)
processing_times = Enum.map(requests, fn req -> req.end_time - req.start_time end)
avg_input_size = Enum.sum(input_sizes) / length(input_sizes)
# Calculate input processing speed (ms per input token)
total_input_tokens = Enum.sum(input_sizes)
total_processing_time = Enum.sum(processing_times)
input_processing_speed =
if total_input_tokens > 0 do
total_processing_time / total_input_tokens
else
0.0
end
# Input size bucketing analysis
scaling_analysis = calculate_scaling_analysis(requests)
# Calculate correlation between input size and processing time
input_correlation = calculate_correlation(input_sizes, processing_times)
%{
avg_input_size: avg_input_size,
input_processing_speed_ms_per_token: input_processing_speed,
scaling_analysis: scaling_analysis,
input_correlation: input_correlation
}
end
end
defp calculate_scaling_analysis(requests) do
# Group requests by input size buckets
buckets = %{
# < 2000 tokens
small: [],
# 2000-10000 tokens
medium: [],
# > 10000 tokens
large: []
}
bucketed_requests =
Enum.reduce(requests, buckets, fn req, acc ->
input_tokens = get_input_tokens(req.usage)
processing_time = req.end_time - req.start_time
request_data = %{input_tokens: input_tokens, processing_time: processing_time}
cond do
input_tokens < 2000 ->
%{acc | small: [request_data | acc.small]}
input_tokens <= 10000 ->
%{acc | medium: [request_data | acc.medium]}
true ->
%{acc | large: [request_data | acc.large]}
end
end)
# Calculate metrics for each bucket
bucket_stats = %{
small: calculate_bucket_stats(bucketed_requests.small),
medium: calculate_bucket_stats(bucketed_requests.medium),
large: calculate_bucket_stats(bucketed_requests.large)
}
# Calculate scaling factors
small_speed = bucket_stats.small.avg_processing_speed_ms_per_token
medium_speed = bucket_stats.medium.avg_processing_speed_ms_per_token
large_speed = bucket_stats.large.avg_processing_speed_ms_per_token
scaling_factors = %{
medium_vs_small: if(small_speed > 0, do: medium_speed / small_speed, else: 0.0),
large_vs_small: if(small_speed > 0, do: large_speed / small_speed, else: 0.0),
large_vs_medium: if(medium_speed > 0, do: large_speed / medium_speed, else: 0.0)
}
%{
buckets: bucket_stats,
scaling_factors: scaling_factors
}
end
defp calculate_bucket_stats([]),
do: %{
count: 0,
avg_input_size: 0,
avg_processing_time: 0,
avg_processing_speed_ms_per_token: 0.0
}
defp calculate_bucket_stats(bucket_data) do
count = length(bucket_data)
total_input = Enum.sum(Enum.map(bucket_data, & &1.input_tokens))
total_time = Enum.sum(Enum.map(bucket_data, & &1.processing_time))
avg_input_size = if count > 0, do: total_input / count, else: 0
avg_processing_time = if count > 0, do: total_time / count, else: 0
avg_processing_speed = if total_input > 0, do: total_time / total_input, else: 0.0
%{
count: count,
avg_input_size: avg_input_size,
avg_processing_time: avg_processing_time,
avg_processing_speed_ms_per_token: avg_processing_speed
}
end
defp calculate_correlation([], []), do: 0.0
# Need at least 2 points
defp calculate_correlation([_], [_]), do: 0.0
defp calculate_correlation(x_values, y_values) when length(x_values) != length(y_values),
do: 0.0
defp calculate_correlation(x_values, y_values) do
n = length(x_values)
if n < 2 do
0.0
else
# Calculate means
x_mean = Enum.sum(x_values) / n
y_mean = Enum.sum(y_values) / n
# Calculate correlation coefficient
numerator =
Enum.zip(x_values, y_values)
|> Enum.map(fn {x, y} -> (x - x_mean) * (y - y_mean) end)
|> Enum.sum()
x_variance =
x_values
|> Enum.map(fn x -> (x - x_mean) * (x - x_mean) end)
|> Enum.sum()
y_variance =
y_values
|> Enum.map(fn y -> (y - y_mean) * (y - y_mean) end)
|> Enum.sum()
denominator = :math.sqrt(x_variance * y_variance)
if denominator > 0 do
numerator / denominator
else
0.0
end
end
end
defp format_model_breakdown(model_stats) do
if length(model_stats) <= 1 do
""
else
breakdown =
model_stats
|> Enum.map(fn stat ->
"- #{format_model_name(stat.model_config)}: #{stat.request_count} requests, #{stat.total_time_ms}ms"
end)
|> Enum.join("\n")
"""
**By Model:**
#{breakdown}
"""
end
end
defp format_detailed_metrics(model_stats) do
model_stats
|> Enum.map(fn stat ->
input_analysis_text = format_input_analysis(stat.input_analysis)
"""
**#{format_model_name(stat.model_config)}:**
- Requests: #{stat.request_count}, Avg Input: #{format_number(stat.input_analysis.avg_input_size)} tokens
- Avg Response Time: #{Float.round(stat.avg_time_ms, 1)}ms (#{Float.round(stat.input_analysis.input_processing_speed_ms_per_token, 2)}ms/token input)
- Total Tokens: #{stat.total_tokens} (Input: #{stat.total_input_tokens}, Output: #{stat.total_output_tokens}#{format_reasoning_tokens(stat.total_reasoning_tokens)})
- Throughput: #{Float.round(stat.tokens_per_minute, 1)} tokens/min (#{Float.round(stat.output_tokens_per_minute, 1)} output/min)#{input_analysis_text}
"""
end)
|> Enum.join("\n")
end
defp format_model_name(%{model: model, reasoning: reasoning}) do
case reasoning do
:none -> model
reasoning_level -> "#{model} (reasoning: #{reasoning_level})"
end
end
defp format_reasoning_tokens(0), do: ""
defp format_reasoning_tokens(count), do: ", Reasoning: #{count}"
defp format_input_analysis(%{
scaling_analysis: scaling_analysis,
input_correlation: correlation
}) do
scaling_text = format_scaling_analysis(scaling_analysis)
correlation_text = format_correlation(correlation)
case {scaling_text, correlation_text} do
{"", ""} -> ""
{scaling, ""} -> "\n#{scaling}"
{"", corr} -> "\n#{corr}"
{scaling, corr} -> "\n#{scaling}\n#{corr}"
end
end
defp format_scaling_analysis(%{buckets: buckets, scaling_factors: factors}) do
# Only show scaling analysis if we have meaningful data in multiple buckets
bucket_counts = [
buckets.small.count,
buckets.medium.count,
buckets.large.count
]
active_buckets = Enum.count(bucket_counts, fn count -> count > 0 end)
if active_buckets < 2 do
""
else
parts = []
# Show bucket breakdown
bucket_info =
[
if buckets.small.count > 0 do
"Small (<2K): #{buckets.small.count} requests, #{Float.round(buckets.small.avg_processing_time, 0)}ms avg"
end,
if buckets.medium.count > 0 do
"Medium (2-10K): #{buckets.medium.count} requests, #{Float.round(buckets.medium.avg_processing_time, 0)}ms avg"
end,
if buckets.large.count > 0 do
"Large (>10K): #{buckets.large.count} requests, #{Float.round(buckets.large.avg_processing_time, 0)}ms avg"
end
]
|> Enum.filter(& &1)
|> Enum.join(", ")
parts =
if bucket_info != "", do: ["- Input Size Analysis: #{bucket_info}" | parts], else: parts
# Show most significant scaling factor
{significant_factor, factor_value} =
[
{"Large vs Small", factors.large_vs_small},
{"Large vs Medium", factors.large_vs_medium},
{"Medium vs Small", factors.medium_vs_small}
]
# Only show significant differences
|> Enum.filter(fn {_name, value} -> value > 1.2 end)
|> Enum.max_by(fn {_name, value} -> value end, fn -> {nil, 0.0} end)
scaling_info =
if significant_factor do
"- Scaling Impact: #{significant_factor} inputs are #{Float.round(factor_value, 1)}x slower"
else
nil
end
parts = if scaling_info, do: [scaling_info | parts], else: parts
if length(parts) > 0 do
Enum.reverse(parts) |> Enum.join("\n")
else
""
end
end
end
defp format_correlation(correlation) when correlation > 0.7 do
"- Input Size Impact: Strong correlation (#{Float.round(correlation, 2)}) - larger inputs significantly slower"
end
defp format_correlation(correlation) when correlation > 0.4 do
"- Input Size Impact: Moderate correlation (#{Float.round(correlation, 2)}) - some scaling effect observed"
end
defp format_correlation(_), do: ""
defp format_number(num) when is_float(num), do: format_number(round(num))
defp format_number(num) when num >= 1_000_000 do
"#{Float.round(num / 1_000_000, 1)}M"
end
defp format_number(num) when num >= 1_000 do
# Format with commas for readability
num
|> Integer.to_string()
|> String.graphemes()
|> Enum.reverse()
|> Enum.chunk_every(3)
|> Enum.map(&Enum.reverse/1)
|> Enum.reverse()
|> Enum.join(",")
end
defp format_number(num), do: Integer.to_string(num)
# Convert reasoning levels to integers for sorting (least to most effort)
defp reasoning_level_to_int(:none), do: 0
defp reasoning_level_to_int(:minimal), do: 1
defp reasoning_level_to_int(:low), do: 2
defp reasoning_level_to_int(:medium), do: 3
defp reasoning_level_to_int(:high), do: 4
defp get_total_tokens(%{"total_tokens" => total}), do: total
defp get_total_tokens(%{total_tokens: total}), do: total
defp get_total_tokens(_), do: 0
defp get_input_tokens(%{"prompt_tokens" => prompt}), do: prompt
defp get_input_tokens(%{prompt_tokens: prompt}), do: prompt
defp get_input_tokens(_), do: 0
defp get_output_tokens(%{"completion_tokens" => completion}), do: completion
defp get_output_tokens(%{completion_tokens: completion}), do: completion
defp get_output_tokens(_), do: 0
defp get_reasoning_tokens(%{"reasoning_tokens" => reasoning}), do: reasoning
defp get_reasoning_tokens(%{reasoning_tokens: reasoning}), do: reasoning
defp get_reasoning_tokens(_), do: 0
end