Packages

Professional observability suite for Gleam: structured logging, metrics, and statistical benchmarking

Current section

Files

Jump to
viva_telemetry src viva_telemetry@bench.erl
Raw

src/viva_telemetry@bench.erl

-module(viva_telemetry@bench).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/viva_telemetry/bench.gleam").
-export([default_config/0, config/2, compare/2, to_json/1, to_json_string/1, to_markdown/1, to_markdown_table/1, print/1, print_comparison/1, run_with_config/3, run/2, run_all/2]).
-export_type([bench_result/0, stats/0, comparison/0, config/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" viva_telemetry/bench - Statistical benchmarking for Gleam\n"
"\n"
" Inspired by: criterion (Rust), benchee (Elixir), hyperfine\n"
"\n"
" ## Features\n"
" - Statistical analysis (mean, stddev, percentiles)\n"
" - Confidence intervals (bootstrapping)\n"
" - Multiple inputs and functions\n"
" - Comparison with speedup calculation\n"
" - Export to JSON/CSV/Markdown\n"
" - Regression detection\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import viva_telemetry/bench\n"
"\n"
" pub fn main() {\n"
" bench.run(\"my_function\", fn() { my_function() })\n"
" |> bench.print()\n"
" }\n"
" ```\n"
).
-type bench_result() :: {bench_result, binary(), list(integer()), stats()}.
-type stats() :: {stats,
float(),
float(),
integer(),
integer(),
integer(),
integer(),
integer(),
float(),
{float(), float()}}.
-type comparison() :: {comparison,
binary(),
binary(),
float(),
boolean(),
{float(), float()}}.
-type config() :: {config, integer(), integer(), float()}.
-file("src/viva_telemetry/bench.gleam", 76).
?DOC(" Default configuration\n").
-spec default_config() -> config().
default_config() ->
{config, 10, 100, 0.95}.
-file("src/viva_telemetry/bench.gleam", 81).
?DOC(" Config with custom iterations\n").
-spec config(integer(), integer()) -> config().
config(Warmup, Iterations) ->
{config, Warmup, Iterations, 0.95}.
-file("src/viva_telemetry/bench.gleam", 197).
?DOC(" Compare two benchmark results\n").
-spec compare(bench_result(), bench_result()) -> comparison().
compare(Baseline, Target) ->
Speedup = case erlang:element(2, erlang:element(4, Target)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:element(2, erlang:element(4, Baseline)) / Gleam@denominator
end,
{Base_lo, Base_hi} = erlang:element(10, erlang:element(4, Baseline)),
{Tgt_lo, Tgt_hi} = erlang:element(10, erlang:element(4, Target)),
Significant = (Base_hi < Tgt_lo) orelse (Tgt_hi < Base_lo),
Lo = case Tgt_hi of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> Base_lo / Gleam@denominator@1
end,
Hi = case Tgt_lo of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> Base_hi / Gleam@denominator@2
end,
{comparison,
erlang:element(2, Baseline),
erlang:element(2, Target),
Speedup,
Significant,
{Lo, Hi}}.
-file("src/viva_telemetry/bench.gleam", 280).
?DOC(" Export to JSON\n").
-spec to_json(bench_result()) -> gleam@json:json().
to_json(Result) ->
S = erlang:element(4, Result),
{Ci_lo, Ci_hi} = erlang:element(10, S),
gleam@json:object(
[{<<"name"/utf8>>, gleam@json:string(erlang:element(2, Result))},
{<<"mean_us"/utf8>>, gleam@json:float(erlang:element(2, S))},
{<<"stddev_us"/utf8>>, gleam@json:float(erlang:element(3, S))},
{<<"min_us"/utf8>>, gleam@json:int(erlang:element(4, S))},
{<<"max_us"/utf8>>, gleam@json:int(erlang:element(5, S))},
{<<"p50_us"/utf8>>, gleam@json:int(erlang:element(6, S))},
{<<"p95_us"/utf8>>, gleam@json:int(erlang:element(7, S))},
{<<"p99_us"/utf8>>, gleam@json:int(erlang:element(8, S))},
{<<"ips"/utf8>>, gleam@json:float(erlang:element(9, S))},
{<<"ci_95_lo"/utf8>>, gleam@json:float(Ci_lo)},
{<<"ci_95_hi"/utf8>>, gleam@json:float(Ci_hi)},
{<<"samples"/utf8>>,
gleam@json:int(erlang:length(erlang:element(3, Result)))}]
).
-file("src/viva_telemetry/bench.gleam", 301).
?DOC(" Export to JSON string\n").
-spec to_json_string(bench_result()) -> binary().
to_json_string(Result) ->
_pipe = to_json(Result),
gleam@json:to_string(_pipe).
-file("src/viva_telemetry/bench.gleam", 346).
-spec format_number(float()) -> binary().
format_number(N) ->
Rounded = erlang:float(erlang:round(N * 100.0)) / 100.0,
gleam_stdlib:float_to_string(Rounded).
-file("src/viva_telemetry/bench.gleam", 335).
-spec format_time(integer()) -> binary().
format_time(Us) ->
case Us >= 1000000 of
true ->
<<(format_number(erlang:float(Us) / 1000000.0))/binary, "s"/utf8>>;
false ->
case Us >= 1000 of
true ->
<<(format_number(erlang:float(Us) / 1000.0))/binary,
"ms"/utf8>>;
false ->
<<(erlang:integer_to_binary(Us))/binary, "μs"/utf8>>
end
end.
-file("src/viva_telemetry/bench.gleam", 306).
?DOC(" Export to Markdown table row\n").
-spec to_markdown(bench_result()) -> binary().
to_markdown(Result) ->
S = erlang:element(4, Result),
<<<<<<<<<<<<<<<<<<<<"| "/utf8, (erlang:element(2, Result))/binary>>/binary,
" | "/utf8>>/binary,
(format_time(
erlang:round(erlang:element(2, S))
))/binary>>/binary,
" | "/utf8>>/binary,
(format_time(erlang:element(6, S)))/binary>>/binary,
" | "/utf8>>/binary,
(format_time(erlang:element(8, S)))/binary>>/binary,
" | "/utf8>>/binary,
(format_number(erlang:element(9, S)))/binary>>/binary,
" |"/utf8>>.
-file("src/viva_telemetry/bench.gleam", 322).
?DOC(" Export multiple results to Markdown table\n").
-spec to_markdown_table(list(bench_result())) -> binary().
to_markdown_table(Results) ->
Header = <<"| Name | Mean | p50 | p99 | IPS |"/utf8>>,
Separator = <<"|------|------|-----|-----|-----|"/utf8>>,
Rows = gleam@list:map(Results, fun to_markdown/1),
_pipe = [Header, Separator | Rows],
gleam@string:join(_pipe, <<"\n"/utf8>>).
-file("src/viva_telemetry/bench.gleam", 352).
-spec pad_to(integer()) -> binary().
pad_to(N) ->
case N =< 0 of
true ->
<<""/utf8>>;
false ->
gleam@string:repeat(<<" "/utf8>>, N)
end.
-file("src/viva_telemetry/bench.gleam", 223).
?DOC(" Print benchmark result to console\n").
-spec print(bench_result()) -> nil.
print(Result) ->
S = erlang:element(4, Result),
gleam_stdlib:println(
<<"\n╔══════════════════════════════════════════════════════════════╗"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ BENCHMARK: "/utf8, (erlang:element(2, Result))/binary>>/binary,
(pad_to(50 - string:length(erlang:element(2, Result))))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<"╠══════════════════════════════════════════════════════════════╣"/utf8>>
),
gleam_stdlib:println(
<<<<<<<<<<"║ Mean: "/utf8,
(format_time(erlang:round(erlang:element(2, S))))/binary>>/binary,
" ± "/utf8>>/binary,
(format_time(erlang:round(erlang:element(3, S))))/binary>>/binary,
(pad_to(32))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<<<"║ IPS: "/utf8,
(format_number(erlang:element(9, S)))/binary>>/binary,
" iterations/sec"/utf8>>/binary,
(pad_to(26))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ Min: "/utf8, (format_time(erlang:element(4, S)))/binary>>/binary,
(pad_to(44))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ p50: "/utf8, (format_time(erlang:element(6, S)))/binary>>/binary,
(pad_to(44))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ p95: "/utf8, (format_time(erlang:element(7, S)))/binary>>/binary,
(pad_to(44))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ p99: "/utf8, (format_time(erlang:element(8, S)))/binary>>/binary,
(pad_to(44))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<<<<<"║ Max: "/utf8, (format_time(erlang:element(5, S)))/binary>>/binary,
(pad_to(44))/binary>>/binary,
"║"/utf8>>
),
gleam_stdlib:println(
<<"╚══════════════════════════════════════════════════════════════╝"/utf8>>
).
-file("src/viva_telemetry/bench.gleam", 359).
-spec bool_to_string(boolean()) -> binary().
bool_to_string(B) ->
case B of
true ->
<<"Yes"/utf8>>;
false ->
<<"No"/utf8>>
end.
-file("src/viva_telemetry/bench.gleam", 260).
?DOC(" Print comparison result\n").
-spec print_comparison(comparison()) -> nil.
print_comparison(Cmp) ->
Arrow = case erlang:element(4, Cmp) >= 1.0 of
true ->
<<"🚀"/utf8>>;
false ->
<<"🐢"/utf8>>
end,
gleam_stdlib:println(
<<<<<<"\n"/utf8, (erlang:element(2, Cmp))/binary>>/binary, " vs "/utf8>>/binary,
(erlang:element(3, Cmp))/binary>>
),
gleam_stdlib:println(
<<<<<<" Speedup: "/utf8,
(format_number(erlang:element(4, Cmp)))/binary>>/binary,
"x "/utf8>>/binary,
Arrow/binary>>
),
gleam_stdlib:println(
<<" Significant: "/utf8,
(bool_to_string(erlang:element(5, Cmp)))/binary>>
),
{Lo, Hi} = erlang:element(6, Cmp),
gleam_stdlib:println(
<<<<<<<<" 95% CI: ["/utf8, (format_number(Lo))/binary>>/binary,
"x, "/utf8>>/binary,
(format_number(Hi))/binary>>/binary,
"x]"/utf8>>
).
-file("src/viva_telemetry/bench.gleam", 366).
-spec result_unwrap({ok, ELM} | {error, any()}, ELM) -> ELM.
result_unwrap(R, Default) ->
case R of
{ok, V} ->
V;
{error, _} ->
Default
end.
-file("src/viva_telemetry/bench.gleam", 373).
-spec list_at(list(ELQ), integer()) -> {ok, ELQ} | {error, nil}.
list_at(L, Idx) ->
case Idx < 0 of
true ->
{error, nil};
false ->
case L of
[] ->
{error, nil};
[X | Rest] ->
case Idx =:= 0 of
true ->
{ok, X};
false ->
list_at(Rest, Idx - 1)
end
end
end.
-file("src/viva_telemetry/bench.gleam", 186).
-spec percentile(list(integer()), float()) -> integer().
percentile(Sorted, P) ->
N = erlang:length(Sorted),
Idx = erlang:round(erlang:float(N - 1) * P),
_pipe = list_at(Sorted, Idx),
result_unwrap(_pipe, 0).
-file("src/viva_telemetry/bench.gleam", 132).
-spec calculate_stats(list(integer()), float()) -> stats().
calculate_stats(Samples, _) ->
N = erlang:length(Samples),
Floats = gleam@list:map(Samples, fun erlang:float/1),
Sum = gleam@list:fold(Floats, +0.0, fun(Acc, X) -> Acc + X end),
Mean = case erlang:float(N) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Sum / Gleam@denominator
end,
Variance = case erlang:float(N - 1) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> gleam@list:fold(
Floats,
+0.0,
fun(Acc@1, X@1) ->
Diff = X@1 - Mean,
Acc@1 + (Diff * Diff)
end
)
/ Gleam@denominator@1
end,
Stddev = math:sqrt(Variance),
Sorted = gleam@list:sort(Samples, fun gleam@int:compare/2),
Min = begin
_pipe = gleam@list:first(Sorted),
result_unwrap(_pipe, 0)
end,
Max = begin
_pipe@1 = gleam@list:last(Sorted),
result_unwrap(_pipe@1, 0)
end,
P50 = percentile(Sorted, 0.5),
P95 = percentile(Sorted, 0.95),
P99 = percentile(Sorted, 0.99),
Ips = case Mean > +0.0 of
true ->
case Mean of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> 1000000.0 / Gleam@denominator@2
end;
false ->
+0.0
end,
T_value = 1.96,
Margin = case math:sqrt(erlang:float(N)) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@3 -> T_value * Stddev / Gleam@denominator@3
end,
Ci_95 = {Mean - Margin, Mean + Margin},
{stats, Mean, Stddev, Min, Max, P50, P95, P99, Ips, Ci_95}.
-file("src/viva_telemetry/bench.gleam", 95).
?DOC(" Run benchmark with custom config\n").
-spec run_with_config(binary(), fun(() -> any()), config()) -> bench_result().
run_with_config(Name, F, Cfg) ->
_pipe = gleam@list:range(1, erlang:element(2, Cfg)),
gleam@list:each(
_pipe,
fun(_) ->
_ = F(),
nil
end
),
Samples = begin
_pipe@1 = gleam@list:range(1, erlang:element(3, Cfg)),
gleam@list:map(
_pipe@1,
fun(_) ->
{Time, _} = timer:tc(F),
Time
end
)
end,
Stats = calculate_stats(Samples, erlang:element(4, Cfg)),
{bench_result, Name, Samples, Stats}.
-file("src/viva_telemetry/bench.gleam", 90).
?DOC(" Run a single benchmark with default config\n").
-spec run(binary(), fun(() -> any())) -> bench_result().
run(Name, F) ->
run_with_config(Name, F, default_config()).
-file("src/viva_telemetry/bench.gleam", 118).
?DOC(" Run multiple benchmarks\n").
-spec run_all(list({binary(), fun(() -> any())}), config()) -> list(bench_result()).
run_all(Benchmarks, Cfg) ->
gleam@list:map(
Benchmarks,
fun(Bench) ->
{Name, F} = Bench,
run_with_config(Name, F, Cfg)
end
).