Current section

Files

Jump to
snakepit lib snakepit diagnostics profile_inspector.ex
Raw

lib/snakepit/diagnostics/profile_inspector.ex

defmodule Snakepit.Diagnostics.ProfileInspector do
@moduledoc """
Programmatic inspection of pool profiles and worker statistics.
This module provides functions to analyze pool configurations, worker profiles,
capacity utilization, memory usage, and performance metrics for both process
and thread profile workers.
## Features
- Pool configuration inspection
- Worker profile analysis (process vs thread)
- Capacity utilization metrics
- Memory usage per worker
- Thread usage for thread profile pools
- Performance statistics
## Examples
# Get statistics for a specific pool
{:ok, stats} = ProfileInspector.get_pool_stats(:default)
# Analyze capacity utilization
{:ok, capacity} = ProfileInspector.get_capacity_stats(:hpc_pool)
# Get memory usage breakdown
{:ok, memory} = ProfileInspector.get_memory_stats(:default)
# Get comprehensive report for all pools
{:ok, report} = ProfileInspector.get_comprehensive_report()
"""
require Logger
alias Snakepit.Config
@type pool_name :: atom()
@type pool_stats :: %{
pool_name: pool_name(),
profile: :process | :thread,
worker_count: non_neg_integer(),
capacity_total: non_neg_integer(),
capacity_used: non_neg_integer(),
capacity_available: non_neg_integer(),
utilization_percent: float(),
workers: [worker_stats()]
}
@type worker_stats :: %{
worker_id: String.t(),
pid: pid(),
profile: :process | :thread,
capacity: pos_integer(),
load: non_neg_integer(),
memory_mb: float(),
status: :available | :busy | :unknown
}
@type capacity_stats :: %{
total_capacity: non_neg_integer(),
used_capacity: non_neg_integer(),
available_capacity: non_neg_integer(),
utilization_percent: float(),
thread_info: thread_info() | nil
}
@type thread_info :: %{
workers: non_neg_integer(),
threads_per_worker: pos_integer(),
total_threads: non_neg_integer(),
avg_load_per_worker: float()
}
@type memory_stats :: %{
total_memory_mb: float(),
avg_memory_per_worker_mb: float(),
max_memory_worker_mb: float(),
min_memory_worker_mb: float(),
workers: [{String.t(), float()}]
}
@doc """
Get comprehensive statistics for a specific pool.
Returns detailed information about the pool's configuration, workers,
capacity utilization, and performance metrics.
## Examples
iex> ProfileInspector.get_pool_stats(:default)
{:ok, %{
pool_name: :default,
profile: :process,
worker_count: 100,
capacity_total: 100,
capacity_used: 45,
capacity_available: 55,
utilization_percent: 45.0,
workers: [...]
}}
"""
@spec get_pool_stats(pool_name()) :: {:ok, pool_stats()} | {:error, term()}
def get_pool_stats(pool_name \\ :default) do
with {:ok, config} <- Config.get_pool_config(pool_name),
{:ok, workers} <- get_pool_workers(pool_name) do
profile = Map.get(config, :worker_profile, :process)
profile_module = Config.get_profile_module(config)
# Gather statistics for each worker
worker_stats =
Enum.map(workers, fn worker_id ->
get_worker_stats(worker_id, profile_module)
end)
|> Enum.reject(&is_nil/1)
# Calculate aggregate statistics
total_capacity = Enum.sum(Enum.map(worker_stats, & &1.capacity))
used_capacity = Enum.sum(Enum.map(worker_stats, & &1.load))
available_capacity = total_capacity - used_capacity
utilization =
if total_capacity > 0 do
used_capacity / total_capacity * 100
else
0.0
end
stats = %{
pool_name: pool_name,
profile: profile,
worker_count: length(worker_stats),
capacity_total: total_capacity,
capacity_used: used_capacity,
capacity_available: available_capacity,
utilization_percent: Float.round(utilization, 2),
workers: worker_stats
}
{:ok, stats}
end
end
@doc """
Get capacity statistics for a pool.
Returns information about total capacity, utilization, and thread-specific
metrics for thread profile pools.
## Examples
iex> ProfileInspector.get_capacity_stats(:hpc_pool)
{:ok, %{
total_capacity: 64,
used_capacity: 42,
available_capacity: 22,
utilization_percent: 65.6,
thread_info: %{
workers: 4,
threads_per_worker: 16,
total_threads: 64,
avg_load_per_worker: 10.5
}
}}
"""
@spec get_capacity_stats(pool_name()) :: {:ok, capacity_stats()} | {:error, term()}
def get_capacity_stats(pool_name \\ :default) do
with {:ok, pool_stats} <- get_pool_stats(pool_name) do
capacity = %{
total_capacity: pool_stats.capacity_total,
used_capacity: pool_stats.capacity_used,
available_capacity: pool_stats.capacity_available,
utilization_percent: pool_stats.utilization_percent,
thread_info: build_thread_info(pool_stats)
}
{:ok, capacity}
end
end
@doc """
Get memory statistics for a pool.
Returns aggregate and per-worker memory usage statistics.
## Examples
iex> ProfileInspector.get_memory_stats(:default)
{:ok, %{
total_memory_mb: 2048.5,
avg_memory_per_worker_mb: 20.5,
max_memory_worker_mb: 45.2,
min_memory_worker_mb: 15.8,
workers: [{"worker_1", 20.5}, ...]
}}
"""
@spec get_memory_stats(pool_name()) :: {:ok, memory_stats()} | {:error, term()}
def get_memory_stats(pool_name \\ :default) do
with {:ok, pool_stats} <- get_pool_stats(pool_name) do
worker_memories = Enum.map(pool_stats.workers, fn w -> {w.worker_id, w.memory_mb} end)
memory_values = Enum.map(worker_memories, fn {_id, mem} -> mem end)
total = Enum.sum(memory_values)
avg = if length(memory_values) > 0, do: total / length(memory_values), else: 0.0
max = if length(memory_values) > 0, do: Enum.max(memory_values), else: 0.0
min = if length(memory_values) > 0, do: Enum.min(memory_values), else: 0.0
memory = %{
total_memory_mb: Float.round(total, 2),
avg_memory_per_worker_mb: Float.round(avg, 2),
max_memory_worker_mb: Float.round(max, 2),
min_memory_worker_mb: Float.round(min, 2),
workers: worker_memories
}
{:ok, memory}
end
end
@doc """
Get comprehensive report for all configured pools.
Returns a map of pool names to their statistics.
## Examples
iex> ProfileInspector.get_comprehensive_report()
{:ok, %{
default: %{...},
hpc_pool: %{...}
}}
"""
@spec get_comprehensive_report() :: {:ok, %{pool_name() => pool_stats()}} | {:error, term()}
def get_comprehensive_report do
case Config.get_pool_configs() do
{:ok, configs} ->
report =
Enum.reduce(configs, %{}, fn config, acc ->
pool_name = Map.fetch!(config, :name)
case get_pool_stats(pool_name) do
{:ok, stats} -> Map.put(acc, pool_name, stats)
{:error, _} -> acc
end
end)
{:ok, report}
error ->
error
end
end
@doc """
Check if a pool is approaching capacity saturation.
Returns a warning if utilization is above the threshold (default 80%).
## Examples
iex> ProfileInspector.check_saturation(:default)
{:ok, :healthy}
iex> ProfileInspector.check_saturation(:busy_pool)
{:warning, :approaching_saturation, 85.5}
"""
@spec check_saturation(pool_name(), float()) ::
{:ok, :healthy} | {:warning, :approaching_saturation, float()} | {:error, term()}
def check_saturation(pool_name \\ :default, threshold \\ 80.0) do
with {:ok, stats} <- get_pool_stats(pool_name) do
if stats.utilization_percent >= threshold do
{:warning, :approaching_saturation, stats.utilization_percent}
else
{:ok, :healthy}
end
end
end
@doc """
Get recommendations for pool configuration based on current usage.
Analyzes current pool statistics and provides recommendations for
optimization.
## Examples
iex> ProfileInspector.get_recommendations(:default)
{:ok, [
"Pool utilization is high (85%). Consider adding more workers.",
"Average memory per worker is 45MB. Monitor for memory leaks."
]}
"""
@spec get_recommendations(pool_name()) :: {:ok, [String.t()]} | {:error, term()}
def get_recommendations(pool_name \\ :default) do
with {:ok, pool_stats} <- get_pool_stats(pool_name),
{:ok, memory_stats} <- get_memory_stats(pool_name) do
recommendations =
[]
|> add_utilization_recommendations(pool_stats)
|> add_memory_recommendations(memory_stats)
|> add_profile_recommendations(pool_stats)
{:ok, recommendations}
end
end
# Private functions
defp get_pool_workers(pool_name) do
# Try to get workers from the pool
try do
workers = Snakepit.Pool.list_workers(pool_name)
{:ok, workers}
rescue
_ -> {:error, :pool_not_found}
catch
:exit, _ -> {:error, :pool_not_running}
end
end
defp get_worker_stats(worker_id, profile_module) do
# Look up worker PID from registry
case Registry.lookup(Snakepit.Pool.Registry, worker_id) do
[{pid, _metadata}] ->
# Get profile-specific information
{:ok, metadata} = profile_module.get_metadata(worker_id)
capacity = profile_module.get_capacity(worker_id)
load = profile_module.get_load(worker_id)
# Get memory information
memory_mb = get_worker_memory(pid)
# Determine status
status =
cond do
load >= capacity -> :busy
load > 0 -> :busy
true -> :available
end
%{
worker_id: worker_id,
pid: pid,
profile: Map.get(metadata, :profile, :process),
capacity: capacity,
load: load,
memory_mb: memory_mb,
status: status
}
[] ->
nil
end
end
defp get_worker_memory(pid) when is_pid(pid) do
case Process.info(pid, :memory) do
{:memory, bytes} -> Float.round(bytes / (1024 * 1024), 2)
nil -> 0.0
end
end
defp build_thread_info(%{profile: :thread} = stats) do
# Calculate thread-specific metrics for thread profile pools
workers_with_threads =
Enum.filter(stats.workers, fn w -> w.capacity > 1 end)
if length(workers_with_threads) > 0 do
total_threads = stats.capacity_total
workers_count = length(workers_with_threads)
# Get threads per worker (should be consistent, but take first)
threads_per_worker =
case List.first(workers_with_threads) do
%{capacity: c} -> c
_ -> 1
end
avg_load =
if workers_count > 0 do
Enum.sum(Enum.map(workers_with_threads, & &1.load)) / workers_count
else
0.0
end
%{
workers: workers_count,
threads_per_worker: threads_per_worker,
total_threads: total_threads,
avg_load_per_worker: Float.round(avg_load, 2)
}
else
nil
end
end
defp build_thread_info(_stats), do: nil
defp add_utilization_recommendations(recommendations, stats) do
cond do
stats.utilization_percent >= 90.0 ->
[
"CRITICAL: Pool utilization is very high (#{stats.utilization_percent}%). Add more workers immediately."
| recommendations
]
stats.utilization_percent >= 80.0 ->
[
"WARNING: Pool utilization is high (#{stats.utilization_percent}%). Consider adding more workers."
| recommendations
]
stats.utilization_percent < 20.0 and stats.worker_count > 10 ->
[
"INFO: Pool utilization is low (#{stats.utilization_percent}%). Consider reducing worker count to save resources."
| recommendations
]
true ->
recommendations
end
end
defp add_memory_recommendations(recommendations, memory_stats) do
cond do
memory_stats.avg_memory_per_worker_mb >= 100.0 ->
[
"WARNING: Average memory per worker is high (#{memory_stats.avg_memory_per_worker_mb}MB). Monitor for memory leaks."
| recommendations
]
memory_stats.max_memory_worker_mb >= 200.0 ->
[
"WARNING: Peak worker memory usage is #{memory_stats.max_memory_worker_mb}MB. Consider worker recycling."
| recommendations
]
true ->
recommendations
end
end
defp add_profile_recommendations(recommendations, stats) do
case stats.profile do
:thread ->
# Thread-specific recommendations
if stats.capacity_total > 0 and stats.worker_count > 0 do
threads_per_worker = div(stats.capacity_total, stats.worker_count)
cond do
threads_per_worker < 4 ->
[
"INFO: Thread pool has #{threads_per_worker} threads per worker. Consider increasing for better CPU utilization."
| recommendations
]
threads_per_worker > 32 ->
[
"INFO: Thread pool has #{threads_per_worker} threads per worker. Consider reducing to avoid context switching overhead."
| recommendations
]
true ->
recommendations
end
else
recommendations
end
:process ->
# Process-specific recommendations
if stats.worker_count > 200 do
[
"INFO: Large number of process workers (#{stats.worker_count}). Consider using thread profile for better resource efficiency."
| recommendations
]
else
recommendations
end
end
end
end