Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol performance dev_profiler.ex
Raw

lib/raxol/performance/dev_profiler.ex

defmodule Raxol.Performance.DevProfiler do
alias Raxol.Core.Runtime.Log
@moduledoc """
Development-mode profiler for detailed performance analysis.
Provides detailed profiling capabilities specifically for development,
including function call tracing, memory analysis, and hot spot detection.
## Features
- Function call tracing with timing
- Memory allocation tracking
- Process hot spot detection
- Call graph generation
- Profiling reports with optimization suggestions
## Usage
# Profile a specific function
DevProfiler.profile(fn ->
SomeModule.expensive_function()
end)
# Profile with options
DevProfiler.profile([duration: 5000, memory: true], fn ->
run_workload()
end)
# Enable continuous profiling
DevProfiler.start_continuous(interval: 10_000)
"""
@type profile_opts :: [
duration: pos_integer(),
memory: boolean(),
processes: boolean(),
call_graph: boolean(),
output_format: :text | :html | :json
]
@default_opts [
# 10 seconds
duration: 10_000,
memory: true,
processes: true,
call_graph: false,
output_format: :text
]
@doc """
Profile a function call with detailed analysis.
## Options
- `:duration` - Maximum profiling duration in milliseconds
- `:memory` - Include memory profiling
- `:processes` - Include process analysis
- `:call_graph` - Generate call graph (expensive)
- `:output_format` - Output format (:text, :html, :json)
## Examples
# Basic profiling
result = DevProfiler.profile(fn ->
perform_complex_operation()
end)
# With memory analysis
DevProfiler.profile([memory: true], fn ->
process_large_buffer()
end)
"""
@spec profile(profile_opts() | (-> any()), (-> any()) | nil) :: any()
def profile(opts_or_fun, fun \\ nil)
def profile(fun, nil) when is_function(fun) do
profile(@default_opts, fun)
end
def profile(opts, fun) when is_list(opts) and is_function(fun) do
if Mix.env() != :dev do
Log.warning("DevProfiler: development mode only")
fun.()
end
opts = Keyword.merge(@default_opts, opts)
Log.info("Starting profiling")
start_time = System.monotonic_time(:microsecond)
memory_before = if opts[:memory], do: get_memory_info(), else: nil
# Start profiling tools
profiling_ref = start_profiling_tools(opts)
# Execute the function
result =
try do
fun.()
catch
kind, error ->
_ = stop_profiling_tools(profiling_ref)
:erlang.raise(kind, error, __STACKTRACE__)
end
# Stop profiling and collect data
profile_data = stop_profiling_tools(profiling_ref)
end_time = System.monotonic_time(:microsecond)
duration = end_time - start_time
memory_after = if opts[:memory], do: get_memory_info(), else: nil
# Generate report
report =
generate_report(%{
duration: duration,
memory_before: memory_before,
memory_after: memory_after,
profile_data: profile_data,
opts: opts
})
output_report(report, opts[:output_format])
result
end
@doc """
Start continuous profiling for ongoing performance monitoring.
## Options
- `:interval` - Profiling interval in milliseconds (default: 30000)
- `:duration` - Duration of each profiling session (default: 5000)
- `:auto_hints` - Enable automatic performance hints (default: true)
## Example
# Start continuous profiling every 30 seconds
DevProfiler.start_continuous(interval: 30_000, duration: 5_000)
"""
def start_continuous(opts \\ []) do
if Mix.env() != :dev do
Log.warning("Continuous profiling: development only")
:ignored
end
interval = Keyword.get(opts, :interval, 30_000)
duration = Keyword.get(opts, :duration, 5_000)
auto_hints = Keyword.get(opts, :auto_hints, true)
spawn_link(fn ->
continuous_profiling_loop(interval, duration, auto_hints)
end)
end
@doc """
Analyze current system performance and provide hints.
"""
def analyze_current_performance do
memory_info = get_memory_info()
process_info = get_process_info()
system_info = get_system_info()
analysis = %{
memory: analyze_memory(memory_info),
processes: analyze_processes(process_info),
system: analyze_system(system_info),
timestamp: System.system_time(:millisecond)
}
_ = generate_performance_hints(analysis)
analysis
end
@doc """
Profile memory usage over time.
"""
def profile_memory(duration \\ 10_000) do
Log.info("Memory profiling: #{duration}ms")
samples = []
start_time = System.monotonic_time(:millisecond)
end_time = start_time + duration
collect_memory_samples(samples, end_time)
end
# Private Functions
defp start_profiling_tools(opts) do
tools = %{}
# Start :fprof if available
tools =
if opts[:call_graph] do
_ = :fprof.start()
_ = :fprof.trace(:start)
Map.put(tools, :fprof, true)
else
tools
end
# Start process monitoring
tools =
if opts[:processes] do
Map.put(tools, :process_monitor, spawn_process_monitor())
else
tools
end
tools
end
defp stop_profiling_tools(tools) do
profile_data = %{}
# Stop :fprof
profile_data =
if Map.get(tools, :fprof) do
_ = :fprof.trace(:stop)
_ = :fprof.profile()
fprof_data = capture_fprof_analysis()
_ = :fprof.stop()
Map.put(profile_data, :fprof, fprof_data)
else
profile_data
end
# Stop process monitor
profile_data =
if monitor_pid = Map.get(tools, :process_monitor) do
send(monitor_pid, :stop)
receive do
{:process_data, data} -> Map.put(profile_data, :processes, data)
after
1000 -> profile_data
end
else
profile_data
end
profile_data
end
defp capture_fprof_analysis do
temp_file =
System.tmp_dir!() <> "/raxol_fprof_#{:os.system_time()}.analysis"
try do
_ = :fprof.analyse(dest: temp_file)
File.read!(temp_file)
catch
_, _ -> "fprof analysis failed"
after
_ = File.rm(temp_file)
end
end
defp spawn_process_monitor do
parent = self()
spawn(fn ->
process_samples = collect_process_samples([])
send(parent, {:process_data, process_samples})
end)
end
defp collect_process_samples(samples) do
receive do
:stop -> samples
after
100 ->
sample = %{
timestamp: System.monotonic_time(:millisecond),
process_count: length(Process.list()),
memory_usage: get_memory_info(),
top_processes: get_top_processes(5)
}
collect_process_samples([sample | samples])
end
end
defp get_memory_info do
%{
total: :erlang.memory(:total),
atom: :erlang.memory(:atom),
binary: :erlang.memory(:binary),
code: :erlang.memory(:code),
ets: :erlang.memory(:ets),
processes: :erlang.memory(:processes),
system: :erlang.memory(:system)
}
end
defp get_process_info do
processes = Process.list()
process_details =
processes
|> Enum.map(fn pid ->
info =
Process.info(pid, [
:memory,
:message_queue_len,
:current_function,
:initial_call
])
if info do
%{
pid: pid,
memory: info[:memory] || 0,
message_queue_len: info[:message_queue_len] || 0,
current_function: info[:current_function],
initial_call: info[:initial_call]
}
else
nil
end
end)
|> Enum.filter(&(&1 != nil))
%{
count: length(processes),
details: process_details,
top_by_memory:
Enum.sort_by(process_details, & &1.memory, :desc) |> Enum.take(10)
}
end
defp get_system_info do
%{
schedulers: :erlang.system_info(:schedulers),
scheduler_utilization:
try do
:scheduler.utilization(1)
catch
_, _ -> :not_available
end,
port_count: length(Port.list()),
atom_count: :erlang.system_info(:atom_count),
atom_limit: :erlang.system_info(:atom_limit)
}
end
defp get_top_processes(count) do
Process.list()
|> Enum.map(fn pid ->
case Process.info(pid, [:memory, :current_function]) do
nil ->
nil
info ->
%{
pid: pid,
memory: info[:memory] || 0,
current_function: info[:current_function]
}
end
end)
|> Enum.filter(&(&1 != nil))
|> Enum.sort_by(& &1.memory, :desc)
|> Enum.take(count)
end
defp analyze_memory(memory_info) do
total_mb = memory_info.total / (1024 * 1024)
issues = []
# Check for high memory usage
issues =
if total_mb > 100 do
["High total memory usage: #{Float.round(total_mb, 1)}MB" | issues]
else
issues
end
# Check binary memory
binary_percent = memory_info.binary / memory_info.total * 100
issues =
if binary_percent > 30 do
[
"High binary memory usage: #{Float.round(binary_percent, 1)}%"
| issues
]
else
issues
end
%{
total_mb: Float.round(total_mb, 2),
issues: issues,
breakdown: memory_info
}
end
defp analyze_processes(process_info) do
issues = []
# Check process count
issues =
if process_info.count > 1000 do
["High process count: #{process_info.count}" | issues]
else
issues
end
# Check for memory-heavy processes
heavy_processes =
Enum.filter(process_info.top_by_memory, &(&1.memory > 10 * 1024 * 1024))
issues =
if length(heavy_processes) > 0 do
["#{length(heavy_processes)} processes using >10MB memory" | issues]
else
issues
end
%{
count: process_info.count,
issues: issues,
top_by_memory: Enum.take(process_info.top_by_memory, 5)
}
end
defp analyze_system(system_info) do
issues = []
# Check atom usage
atom_usage_percent = system_info.atom_count / system_info.atom_limit * 100
issues =
if atom_usage_percent > 80 do
["High atom usage: #{Float.round(atom_usage_percent, 1)}%" | issues]
else
issues
end
%{
issues: issues,
atom_usage_percent: Float.round(atom_usage_percent, 2),
scheduler_count: system_info.schedulers
}
end
defp generate_performance_hints(analysis) do
hints = []
# Memory hints
hints = (hints ++ analysis.memory.issues) |> Enum.map(&"Memory: #{&1}")
# Process hints
hints = (hints ++ analysis.processes.issues) |> Enum.map(&"Process: #{&1}")
# System hints
hints = (hints ++ analysis.system.issues) |> Enum.map(&"System: #{&1}")
if length(hints) > 0 do
Log.warning("Analysis hints:")
Enum.each(hints, fn hint ->
Log.warning(" • #{hint}")
end)
else
Log.info("No issues detected")
end
hints
end
defp generate_report(data) do
duration_ms = data.duration / 1000
report = """
[STATS] Performance Profile Report
═══════════════════════════════════════
Execution Time: #{Float.round(duration_ms, 2)}ms
"""
# Add memory analysis
report =
if data.memory_before && data.memory_after do
memory_diff = data.memory_after.total - data.memory_before.total
memory_diff_mb = memory_diff / (1024 * 1024)
report <>
"""
Memory Usage:
- Before: #{Float.round(data.memory_before.total / (1024 * 1024), 2)}MB
- After: #{Float.round(data.memory_after.total / (1024 * 1024), 2)}MB
- Change: #{if memory_diff >= 0, do: "+", else: ""}#{Float.round(memory_diff_mb, 2)}MB
"""
else
report
end
# Add optimization suggestions
suggestions = generate_optimization_suggestions(data)
if length(suggestions) > 0 do
report <>
"""
[TIP] Optimization Suggestions:
#{Enum.map_join(suggestions, "\n", &" • #{&1}")}
"""
else
report <> "\n\n[OK] No obvious optimizations detected"
end
end
defp generate_optimization_suggestions(data) do
suggestions = []
duration_ms = data.duration / 1000
# Suggest optimizations based on execution time
suggestions =
if duration_ms > 1000 do
[
"Consider async execution or breaking into smaller operations (#{Float.round(duration_ms, 1)}ms)"
| suggestions
]
else
suggestions
end
# Memory-based suggestions
suggestions =
if data.memory_before && data.memory_after do
memory_growth = data.memory_after.total - data.memory_before.total
# 50MB growth
if memory_growth > 50 * 1024 * 1024 do
[
"High memory allocation detected - consider memory pooling or streaming"
| suggestions
]
else
suggestions
end
else
suggestions
end
suggestions
end
defp output_report(report, format) do
case format do
:text ->
Log.info(report)
:html ->
html_report = generate_html_report(report)
filename = "/tmp/raxol_profile_#{:os.system_time()}.html"
_ = File.write!(filename, html_report)
Log.info("HTML report written to: #{filename}")
:json ->
json_report = Jason.encode!(parse_report_to_map(report))
Log.info("JSON Report: #{json_report}")
end
end
defp generate_html_report(text_report) do
"""
<!DOCTYPE html>
<html>
<head>
<title>Raxol Performance Report</title>
<style>
body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
.report { background: #2d2d30; padding: 20px; border-radius: 8px; }
.metric { color: #4ec9b0; }
.suggestion { color: #ffd700; }
</style>
</head>
<body>
<div class="report">
<pre>#{text_report}</pre>
</div>
</body>
</html>
"""
end
defp parse_report_to_map(report) do
%{
type: "performance_report",
content: report,
timestamp: System.system_time(:millisecond)
}
end
defp continuous_profiling_loop(interval, duration, auto_hints) do
_ =
if auto_hints do
analyze_current_performance()
end
Process.sleep(interval)
continuous_profiling_loop(interval, duration, auto_hints)
end
defp collect_memory_samples(samples, end_time) do
current_time = System.monotonic_time(:millisecond)
if current_time < end_time do
sample = %{
timestamp: current_time,
memory: get_memory_info()
}
# Sample every 100ms
Process.sleep(100)
collect_memory_samples([sample | samples], end_time)
else
Log.info("Complete. #{length(samples)} samples.")
analyze_memory_samples(samples)
end
end
defp analyze_memory_samples(samples) do
if length(samples) > 1 do
first = List.last(samples)
last = List.first(samples)
growth = last.memory.total - first.memory.total
growth_mb = growth / (1024 * 1024)
duration = last.timestamp - first.timestamp
Log.info("Growth: #{Float.round(growth_mb, 2)}MB/#{duration}ms")
if growth_mb > 10 do
Log.warning("Significant memory growth")
end
end
samples
end
end