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 mix tasks raxol.memory.stability.ex
Raw

lib/mix/tasks/raxol.memory.stability.ex

defmodule Mix.Tasks.Raxol.Memory.Stability do
alias Raxol.Utils.MemoryFormatter
@moduledoc """
Long-running memory stability tests for detecting memory leaks and performance degradation.
This task runs extended memory tests that simulate real-world usage patterns
over extended periods to detect memory leaks, performance degradation, and
stability issues.
## Usage
mix raxol.memory.stability
mix raxol.memory.stability --duration 3600
mix raxol.memory.stability --scenario vim_session
mix raxol.memory.stability --with-profiling
## Options
* `--duration` - Test duration in seconds (default: 1800 = 30 minutes)
* `--scenario` - Specific scenario to test (vim_session, log_streaming, interactive_shell)
* `--with-profiling` - Enable detailed memory profiling
* `--output` - Output directory for results (default: bench/stability)
* `--interval` - Measurement interval in seconds (default: 30)
* `--memory-threshold` - Memory growth threshold in MB (default: 10)
## Test Scenarios
### vim_session
Simulates a Vim editing session with:
- File editing operations
- Syntax highlighting
- Buffer management
- Search and replace operations
### log_streaming
Simulates continuous log streaming with:
- High-frequency text updates
- ANSI color processing
- Buffer scrolling
- Pattern matching
### interactive_shell
Simulates interactive shell usage with:
- Command execution
- Output processing
- History management
- Tab completion
## Exit Codes
- 0: Stability test passed
- 1: Memory leak detected
- 2: Performance degradation detected
- 3: Test execution failed
"""
use Mix.Task
alias Raxol.Terminal.{ANSI.Utils.AnsiParser, Cursor.Manager}
@shortdoc "Run long-running memory stability tests"
@spec run(list()) :: no_return()
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
switches: [
duration: :integer,
scenario: :string,
with_profiling: :boolean,
output: :string,
interval: :integer,
memory_threshold: :float,
help: :boolean
],
aliases: [d: :duration, s: :scenario, o: :output, h: :help]
)
if opts[:help] do
print_help()
System.halt(0)
end
_ = Application.ensure_all_started(:raxol)
config = build_config(opts)
Mix.shell().info("Memory Stability Test")
Mix.shell().info("====================")
Mix.shell().info(
"Duration: #{config.duration}s (#{Float.round(config.duration / 60, 1)} minutes)"
)
Mix.shell().info("Scenario: #{config.scenario}")
Mix.shell().info("Measurement interval: #{config.interval}s")
Mix.shell().info("Memory threshold: #{config.memory_threshold}MB growth")
try do
results = run_stability_test(config)
exit_code = analyze_results(results, config)
save_results(results, config)
System.halt(exit_code)
rescue
error ->
Mix.shell().error("Stability test failed: #{inspect(error)}")
System.halt(3)
end
end
defp build_config(opts) do
%{
duration: Keyword.get(opts, :duration, 1800),
scenario: Keyword.get(opts, :scenario, "vim_session"),
with_profiling: Keyword.get(opts, :with_profiling, false),
output: Keyword.get(opts, :output, "bench/stability"),
interval: Keyword.get(opts, :interval, 30),
memory_threshold: Keyword.get(opts, :memory_threshold, 10.0)
}
end
defp run_stability_test(config) do
_ = File.mkdir_p(config.output)
initial_memory = get_memory_info()
start_time = System.monotonic_time(:millisecond)
Mix.shell().info("Starting stability test...")
Mix.shell().info("Initial memory: #{format_memory(initial_memory.total)}")
# Start the scenario task
scenario_task =
Task.async(fn -> run_scenario(config.scenario, config.duration) end)
# Start memory monitoring
monitoring_task = Task.async(fn -> monitor_memory(config, start_time) end)
# Wait for both tasks to complete
scenario_result = Task.await(scenario_task, (config.duration + 60) * 1000)
memory_measurements =
Task.await(monitoring_task, (config.duration + 60) * 1000)
final_memory = get_memory_info()
end_time = System.monotonic_time(:millisecond)
%{
config: config,
start_time: start_time,
end_time: end_time,
duration: end_time - start_time,
initial_memory: initial_memory,
final_memory: final_memory,
memory_measurements: memory_measurements,
scenario_result: scenario_result
}
end
defp run_scenario("vim_session", duration), do: simulate_vim_session(duration)
defp run_scenario("log_streaming", duration),
do: simulate_log_streaming(duration)
defp run_scenario("interactive_shell", duration),
do: simulate_interactive_shell(duration)
defp run_scenario(scenario, _duration) do
Mix.shell().error("Unknown scenario: #{scenario}")
%{error: "unknown_scenario"}
end
defp simulate_vim_session(duration) do
Mix.shell().info("Simulating Vim session...")
buffer = Raxol.Terminal.ScreenBuffer.new(120, 40)
deadline = System.monotonic_time(:millisecond) + duration * 1000
run_timed_simulation(
buffer,
deadline,
&random_vim_operation/0,
"vim_session"
)
end
defp random_vim_operation do
vim_operations = [
&vim_write_text/1,
&vim_navigate/1,
&vim_search_replace/1,
&vim_syntax_highlight/1,
&vim_buffer_management/1
]
Enum.random(vim_operations)
end
defp run_timed_simulation(buffer, deadline, operation_fn, type) do
Stream.repeatedly(operation_fn)
|> Stream.scan({buffer, 0}, fn operation, {current_buffer, count} ->
if System.monotonic_time(:millisecond) < deadline do
execute_vim_operation(operation, current_buffer, count)
else
{current_buffer, count}
end
end)
|> Stream.take_while(fn _ ->
System.monotonic_time(:millisecond) < deadline
end)
|> Enum.to_list()
|> List.last()
|> format_simulation_result(type)
end
defp format_simulation_result({final_buffer, final_count}, type) do
%{buffer: final_buffer, operations: final_count, type: type}
end
defp format_simulation_result(_, type) do
%{operations: 0, type: type}
end
defp simulate_log_streaming(duration) do
Mix.shell().info("Simulating log streaming...")
buffer = Raxol.Terminal.ScreenBuffer.new(100, 50)
start_time = System.monotonic_time(:millisecond)
_lines_processed = 0
log_levels = ["INFO", "WARN", "ERROR", "DEBUG"]
# Green, Yellow, Red, Cyan
color_codes = [32, 33, 31, 36]
Stream.iterate(0, &(&1 + 1))
|> Stream.map(fn line_num ->
if System.monotonic_time(:millisecond) - start_time < duration * 1000 do
process_log_line(buffer, line_num, log_levels, color_codes)
else
{buffer, line_num}
end
end)
|> Stream.take_while(fn {_buffer, _count} ->
System.monotonic_time(:millisecond) - start_time < duration * 1000
end)
|> Enum.to_list()
|> List.last()
|> case do
{final_buffer, final_count} ->
%{buffer: final_buffer, lines: final_count, type: "log_streaming"}
_ ->
%{lines: 0, type: "log_streaming"}
end
end
defp simulate_interactive_shell(duration) do
Mix.shell().info("Simulating interactive shell...")
buffer = Raxol.Terminal.ScreenBuffer.new(80, 24)
start_time = System.monotonic_time(:millisecond)
_commands_executed = 0
shell_commands = [
"ls -la",
"ps aux",
"find . -name '*.ex'",
"grep -r 'defmodule' lib/",
"cat README.md",
"tail -f /var/log/system.log",
"top -l 1"
]
Stream.iterate(0, &(&1 + 1))
|> Stream.map(fn cmd_num ->
if System.monotonic_time(:millisecond) - start_time < duration * 1000 do
execute_shell_command(buffer, cmd_num, shell_commands)
else
{buffer, cmd_num}
end
end)
|> Stream.take_while(fn {_buffer, _count} ->
System.monotonic_time(:millisecond) - start_time < duration * 1000
end)
|> Enum.to_list()
|> List.last()
|> case do
{final_buffer, final_count} ->
%{
buffer: final_buffer,
commands: final_count,
type: "interactive_shell"
}
_ ->
%{commands: 0, type: "interactive_shell"}
end
end
# Vim operation helpers
defp vim_write_text(buffer) do
text =
"def function_#{:rand.uniform(1000)}(param) do\n # Implementation here\nend"
case Raxol.Terminal.ScreenBuffer.write(buffer, text) do
{:ok, updated_buffer} -> updated_buffer
_ -> buffer
end
end
defp vim_navigate(buffer) do
# Simulate cursor movements (doesn't modify buffer)
cursor = Manager.new()
_moved_cursor =
cursor
|> Manager.move_to(:rand.uniform(120), :rand.uniform(40))
|> Manager.move_to(:rand.uniform(80), :rand.uniform(40))
buffer
end
defp vim_search_replace(buffer) do
# Simulate search and replace operation
_search_term = "function_#{:rand.uniform(100)}"
_replace_term = "method_#{:rand.uniform(100)}"
# This would normally search and replace in buffer
# For simulation, just return the buffer
buffer
end
defp vim_syntax_highlight(buffer) do
# Simulate syntax highlighting by parsing ANSI sequences
highlight_sequences = [
# Blue for keywords
"\e[34mdef\e[0m",
# Green for strings
"\e[32m'string'\e[0m",
# Yellow for comments
"\e[33m# comment\e[0m"
]
Enum.each(highlight_sequences, &AnsiParser.parse/1)
buffer
end
defp vim_buffer_management(buffer) do
# Simulate buffer operations like clear/resize
case :rand.uniform(3) do
1 ->
Raxol.Terminal.ScreenBuffer.clear(buffer)
_ ->
buffer
end
end
defp generate_command_output(command, cmd_num) do
case command do
"ls -la" ->
[
"total 48",
"drwxr-xr-x 8 user staff 256 Sep 16 10:30 .",
"drwxr-xr-x 3 user staff 96 Sep 16 10:29 .."
]
"ps aux" ->
[
"USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND",
"user #{1000 + cmd_num} 0.0 0.1 12345 1234 pts/0 S 10:30 0:00 bash"
]
_ ->
[
"Command output line 1 for #{command}",
"Command output line 2 for #{command}"
]
end
end
defp monitor_memory(config, start_time) do
measurements = []
monitor_memory_loop(config, start_time, measurements)
end
defp monitor_memory_loop(config, start_time, measurements) do
current_time = System.monotonic_time(:millisecond)
elapsed = (current_time - start_time) / 1000
if elapsed < config.duration do
memory_info = get_memory_info()
measurement = %{
timestamp: current_time,
elapsed: elapsed,
memory: memory_info
}
updated_measurements = [measurement | measurements]
if config.with_profiling do
Mix.shell().info(
"Memory at #{Float.round(elapsed, 1)}s: #{format_memory(memory_info.total)}"
)
end
Process.sleep(config.interval * 1000)
monitor_memory_loop(config, start_time, updated_measurements)
else
Enum.reverse(measurements)
end
end
defp get_memory_info do
%{
total: :erlang.memory(:total),
processes: :erlang.memory(:processes),
system: :erlang.memory(:system),
atom: :erlang.memory(:atom),
binary: :erlang.memory(:binary),
ets: :erlang.memory(:ets)
}
end
defp analyze_results(results, config) do
Mix.shell().info("\nAnalyzing stability test results...")
memory_analysis = analyze_memory_trend(results.memory_measurements)
leak_detection = detect_memory_leak(results, config)
performance_analysis = analyze_performance(results)
Mix.shell().info("\nMemory Analysis:")
Mix.shell().info(
" Initial: #{format_memory(results.initial_memory.total)}"
)
Mix.shell().info(" Final: #{format_memory(results.final_memory.total)}")
Mix.shell().info(" Growth: #{format_memory(memory_analysis.total_growth)}")
Mix.shell().info(
" Growth rate: #{Float.round(memory_analysis.growth_rate_mb_per_hour, 2)} MB/hour"
)
case {leak_detection.status, performance_analysis.status} do
{:leak_detected, _} ->
Mix.shell().info("Result: Memory leak detected!")
1
{_, :degradation_detected} ->
Mix.shell().info("Result: Performance degradation detected!")
2
_ ->
Mix.shell().info("Result: Stability test passed")
0
end
end
defp analyze_memory_trend(measurements) do
if length(measurements) < 2 do
%{total_growth: 0, growth_rate_mb_per_hour: 0.0}
else
first = List.first(measurements)
last = List.last(measurements)
total_growth = last.memory.total - first.memory.total
elapsed_hours = (last.elapsed - first.elapsed) / 3600
growth_rate_mb_per_hour =
if elapsed_hours > 0 do
total_growth / 1_000_000 / elapsed_hours
else
0.0
end
%{
total_growth: total_growth,
growth_rate_mb_per_hour: growth_rate_mb_per_hour,
measurements_count: length(measurements)
}
end
end
defp detect_memory_leak(results, config) do
growth_mb =
(results.final_memory.total - results.initial_memory.total) / 1_000_000
if growth_mb > config.memory_threshold do
%{
status: :leak_detected,
growth_mb: growth_mb,
threshold_mb: config.memory_threshold
}
else
%{
status: :no_leak,
growth_mb: growth_mb,
threshold_mb: config.memory_threshold
}
end
end
defp analyze_performance(results) do
# Analyze if operations per second degraded over time
# This is a simplified analysis - could be more sophisticated
case results.scenario_result do
%{operations: ops} when ops > 0 ->
duration_hours = results.duration / (1000 * 3600)
ops_per_hour = ops / duration_hours
# If less than 100 operations per hour, consider it degraded
if ops_per_hour < 100 do
%{status: :degradation_detected, ops_per_hour: ops_per_hour}
else
%{status: :normal, ops_per_hour: ops_per_hour}
end
_ ->
%{status: :unknown}
end
end
defp save_results(results, config) do
timestamp =
DateTime.utc_now() |> DateTime.to_iso8601() |> String.replace(":", "-")
filename = "stability_#{config.scenario}_#{timestamp}.json"
output_path = Path.join(config.output, filename)
content = Jason.encode!(results, pretty: true)
File.write!(output_path, content)
Mix.shell().info("Results saved to: #{output_path}")
end
defp format_memory(bytes), do: MemoryFormatter.format_memory(bytes)
defp print_help do
Mix.shell().info(@moduledoc)
end
defp execute_vim_operation(operation, current_buffer, count) do
updated_buffer = operation.(current_buffer)
# Periodic garbage collection
if rem(count, 100) == 0 do
:erlang.garbage_collect()
end
{updated_buffer, count + 1}
end
defp process_log_line(buffer, line_num, log_levels, color_codes) do
level = Enum.random(log_levels)
color = Enum.random(color_codes)
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
log_line = "\e[#{color}m[#{timestamp}] #{level}: Log entry #{line_num}\e[0m"
# Process ANSI sequences
_parsed = AnsiParser.parse(log_line)
# Write to buffer (simulate scrolling)
_row = rem(line_num, 50)
case Raxol.Terminal.ScreenBuffer.write(buffer, log_line) do
{:ok, updated_buffer} -> {updated_buffer, line_num + 1}
_ -> {buffer, line_num + 1}
end
end
defp execute_shell_command(buffer, cmd_num, shell_commands) do
command = Enum.random(shell_commands)
# Simulate command prompt
prompt = "$ #{command}"
# Simulate command output
output_lines = generate_command_output(command, cmd_num)
# Write to buffer
updated_buffer =
write_lines_to_buffer([prompt | output_lines], buffer, cmd_num)
# Simulate typing delay
Process.sleep(100)
{updated_buffer, cmd_num + 1}
end
defp write_lines_to_buffer(lines, buffer, cmd_num) do
Enum.reduce(lines, buffer, fn line, acc_buffer ->
# Each command takes ~3 lines
_row = rem(cmd_num * 3, 24)
case Raxol.Terminal.ScreenBuffer.write(acc_buffer, line) do
{:ok, new_buffer} -> new_buffer
_ -> acc_buffer
end
end)
end
end