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 benchmark storage.ex
Raw

lib/raxol/benchmark/storage.ex

defmodule Raxol.Benchmark.Storage do
alias Raxol.Core.Runtime.Log
@moduledoc """
Handles storage and retrieval of benchmark results for historical analysis.
Provides functionality for saving benchmarks, creating baselines, and
tracking performance over time.
"""
@storage_path "bench/results"
@baseline_path "bench/baselines"
@snapshot_path "bench/snapshots"
@doc """
Saves benchmark results to storage.
"""
def save_results(suite_name, results, duration) do
ensure_directories()
timestamp = DateTime.utc_now()
# Save raw results
save_raw_results(suite_name, results, timestamp)
# Save summary for quick access
save_summary(suite_name, results, duration, timestamp)
# Update baseline if requested
maybe_update_baseline(suite_name, results)
:ok
end
@doc """
Loads historical results for a suite.
"""
def load_historical(suite_name, opts \\ []) do
days = Keyword.get(opts, :days, 30)
limit = Keyword.get(opts, :limit, 100)
cutoff_date =
DateTime.add(DateTime.utc_now(), -days * 24 * 60 * 60, :second)
list_result_files(suite_name)
|> Enum.filter(&after_date?(&1, cutoff_date))
|> Enum.sort(:desc)
|> Enum.take(limit)
|> Enum.map(&load_result_file/1)
|> Enum.filter(& &1)
end
@doc """
Saves a baseline for comparison.
"""
def save_baseline(suite_name, results) do
ensure_directories()
path = baseline_path(suite_name)
data = serialize_baseline(results)
File.write!(path, data)
Log.info("Baseline saved for #{suite_name}")
:ok
end
@doc """
Loads a baseline for comparison.
"""
def load_baseline(suite_name) do
path = baseline_path(suite_name)
case File.exists?(path) do
true -> deserialize_baseline(File.read!(path))
false -> nil
end
end
@doc """
Creates a performance snapshot for version tracking.
"""
def create_snapshot(version, results) do
ensure_directories()
snapshot = %{
version: version,
timestamp: DateTime.utc_now(),
results: results,
metadata: collect_metadata()
}
path = snapshot_path(version)
File.write!(path, :erlang.term_to_binary(snapshot))
Log.info("Snapshot created for version #{version}")
:ok
end
@doc """
Loads a snapshot by version.
"""
def load_snapshot(version) do
path = snapshot_path(version)
case File.exists?(path) do
true -> :erlang.binary_to_term(File.read!(path))
false -> nil
end
end
@doc """
Lists available snapshots.
"""
def list_snapshots do
ensure_directories()
Path.wildcard(Path.join(@snapshot_path, "*.snapshot"))
|> Enum.map(fn path ->
version = Path.basename(path, ".snapshot")
{version, File.stat!(path).mtime}
end)
|> Enum.sort_by(&elem(&1, 1), :desc)
end
@doc """
Exports results to CSV format.
"""
def export_to_csv(suite_name, output_file) do
results = load_historical(suite_name, days: 365)
csv_content = generate_csv(results)
File.write!(output_file, csv_content)
Log.info("Results exported to #{output_file}")
:ok
end
@doc """
Cleans up old benchmark data.
"""
def cleanup(days_to_keep \\ 90) do
cutoff_date =
DateTime.add(DateTime.utc_now(), -days_to_keep * 24 * 60 * 60, :second)
deleted_count =
Path.wildcard(Path.join(@storage_path, "**/*.benchee"))
|> Enum.filter(&before_date?(&1, cutoff_date))
|> Enum.map(&File.rm/1)
|> Enum.count(&(&1 == :ok))
Log.info("Cleaned up #{deleted_count} old benchmark files")
{:ok, deleted_count}
end
# Private functions
defp ensure_directories do
[@storage_path, @baseline_path, @snapshot_path]
|> Enum.each(&File.mkdir_p!/1)
end
defp save_raw_results(suite_name, results, timestamp) do
suite_dir = Path.join(@storage_path, suite_name)
File.mkdir_p!(suite_dir)
filename = "#{format_timestamp(timestamp)}.benchee"
path = Path.join(suite_dir, filename)
File.write!(path, :erlang.term_to_binary(results))
end
defp save_summary(suite_name, results, duration, timestamp) do
summary = %{
suite_name: suite_name,
timestamp: timestamp,
duration: duration,
scenarios: Enum.map(results.scenarios, &extract_scenario_summary/1),
environment: collect_environment()
}
suite_dir = Path.join(@storage_path, suite_name)
filename = "#{format_timestamp(timestamp)}_summary.json"
path = Path.join(suite_dir, filename)
File.write!(path, Jason.encode!(summary, pretty: true))
end
defp extract_scenario_summary(scenario) do
%{
name: scenario.name,
average: scenario.run_time_data.statistics.average,
min: scenario.run_time_data.statistics.minimum,
max: scenario.run_time_data.statistics.maximum,
std_dev: scenario.run_time_data.statistics.std_dev,
memory_average: scenario.memory_usage_data.statistics.average
}
end
defp maybe_update_baseline(suite_name, results) do
case should_update_baseline?(suite_name) do
true -> update_baseline(suite_name, results)
false -> :ok
end
end
defp should_update_baseline?(suite_name) do
# Check if baseline update was requested via environment variable
System.get_env("UPDATE_BASELINE") == "true" ||
System.get_env("UPDATE_BASELINE_#{suite_name}") == "true"
end
defp update_baseline(suite_name, results) do
# Backup existing baseline
backup_baseline(suite_name)
# Save new baseline
save_baseline(suite_name, results)
end
defp backup_baseline(suite_name) do
current_path = baseline_path(suite_name)
case File.exists?(current_path) do
true ->
backup_path =
"#{current_path}.backup.#{DateTime.utc_now() |> DateTime.to_unix()}"
File.rename!(current_path, backup_path)
false ->
:ok
end
end
defp baseline_path(suite_name) do
Path.join(@baseline_path, "#{suite_name}.baseline")
end
defp snapshot_path(version) do
Path.join(@snapshot_path, "#{version}.snapshot")
end
defp serialize_baseline(results) do
:erlang.term_to_binary(results)
end
defp deserialize_baseline(data) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
:erlang.binary_to_term(data)
end) do
{:ok, result} -> result
{:error, _} -> nil
end
end
defp list_result_files(suite_name) do
suite_dir = Path.join(@storage_path, suite_name)
case File.dir?(suite_dir) do
true -> Path.wildcard(Path.join(suite_dir, "*.benchee"))
false -> []
end
end
defp after_date?(file_path, date) do
case extract_timestamp_from_path(file_path) do
{:ok, file_date} -> DateTime.compare(file_date, date) == :gt
_ -> false
end
end
defp before_date?(file_path, date) do
case extract_timestamp_from_path(file_path) do
{:ok, file_date} -> DateTime.compare(file_date, date) == :lt
_ -> false
end
end
defp extract_timestamp_from_path(path) do
filename = Path.basename(path, ".benchee")
case parse_timestamp(filename) do
{:ok, datetime} -> {:ok, datetime}
_ -> {:error, :invalid_timestamp}
end
end
defp load_result_file(path) do
case File.read(path) do
{:ok, content} ->
parse_binary_term(content)
_ ->
nil
end
end
defp collect_metadata do
%{
elixir_version: System.version(),
otp_version: :erlang.system_info(:otp_release) |> List.to_string(),
git_sha: get_git_sha(),
branch: get_git_branch()
}
end
defp collect_environment do
{family, name} = :os.type()
%{
os: "#{family}_#{name}",
cpu_count: System.schedulers_online(),
total_memory: :erlang.memory(:total)
}
end
defp get_git_sha do
case System.cmd("git", ["rev-parse", "HEAD"]) do
{sha, 0} -> String.trim(sha)
_ -> "unknown"
end
end
defp get_git_branch do
case System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]) do
{branch, 0} -> String.trim(branch)
_ -> "unknown"
end
end
defp format_timestamp(datetime) do
datetime
|> DateTime.to_string()
|> String.replace(~r/[^\d]/, "_")
end
defp parse_timestamp(string) do
# Parse timestamp from format: "2024_01_15_10_30_45_123456Z"
case Regex.run(~r/(\d{4})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d{2})/, string) do
[_, year, month, day, hour, minute, second] ->
DateTime.new(
Date.new!(
String.to_integer(year),
String.to_integer(month),
String.to_integer(day)
),
Time.new!(
String.to_integer(hour),
String.to_integer(minute),
String.to_integer(second)
)
)
_ ->
:error
end
end
defp generate_csv(results) do
headers = "Timestamp,Suite,Scenario,Average,Min,Max,StdDev,Memory\n"
rows =
Enum.flat_map(results, fn result ->
Enum.map(result.scenarios, fn scenario ->
[
result.timestamp,
result.suite_name,
scenario.name,
scenario.run_time_data.statistics.average,
scenario.run_time_data.statistics.minimum,
scenario.run_time_data.statistics.maximum,
scenario.run_time_data.statistics.std_dev,
scenario.memory_usage_data.statistics.average
]
|> Enum.join(",")
end)
end)
headers <> Enum.join(rows, "\n")
end
defp parse_binary_term(content) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
:erlang.binary_to_term(content)
end) do
{:ok, result} -> result
{:error, _} -> nil
end
end
end