-module(bench@bench). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/bench/bench.gleam"). -export([default_config/0, quick_config/0, thorough_config/0, config/3, new_suite/1, add_result/2, benchmark_with_config/3, benchmark/2, benchmark_detailed/3, benchmark_with_setup/5, to_markdown/1, print_suite_summary/1, print_result/1, run_suite/3, compare_results/2, within_threshold/3]). -export_type([bench_config/0, bench_result/0, bench_suite/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. -type bench_config() :: {bench_config, integer(), integer(), boolean()}. -type bench_result() :: {bench_result, binary(), integer(), integer(), float(), gleam@option:option(float()), gleam@option:option(float())}. -type bench_suite() :: {bench_suite, binary(), list(bench_result())}. -file("src/bench/bench.gleam", 58). ?DOC(" Default benchmark configuration\n"). -spec default_config() -> bench_config(). default_config() -> {bench_config, 10, 1000, false}. -file("src/bench/bench.gleam", 63). ?DOC(" Quick benchmark configuration (fewer iterations)\n"). -spec quick_config() -> bench_config(). quick_config() -> {bench_config, 5, 100, false}. -file("src/bench/bench.gleam", 68). ?DOC(" Thorough benchmark configuration (more iterations)\n"). -spec thorough_config() -> bench_config(). thorough_config() -> {bench_config, 100, 10000, false}. -file("src/bench/bench.gleam", 73). ?DOC(" Create a custom configuration\n"). -spec config(integer(), integer(), boolean()) -> bench_config(). config(Warmup_iterations, Iterations, Verbose) -> {bench_config, Warmup_iterations, Iterations, Verbose}. -file("src/bench/bench.gleam", 190). ?DOC(" Create a new benchmark suite\n"). -spec new_suite(binary()) -> bench_suite(). new_suite(Name) -> {bench_suite, Name, []}. -file("src/bench/bench.gleam", 195). ?DOC(" Add a benchmark result to a suite\n"). -spec add_result(bench_suite(), bench_result()) -> bench_suite(). add_result(Suite, Result) -> {bench_suite, erlang:element(2, Suite), lists:append(erlang:element(3, Suite), [Result])}. -file("src/bench/bench.gleam", 314). ?DOC(" Run a function n times\n"). -spec run_iterations(integer(), fun(() -> any())) -> nil. run_iterations(N, F) -> case N > 0 of false -> nil; true -> _ = F(), run_iterations(N - 1, F) end. -file("src/bench/bench.gleam", 91). ?DOC(" Run a benchmark with custom configuration\n"). -spec benchmark_with_config(binary(), bench_config(), fun(() -> any())) -> bench_result(). benchmark_with_config(Name, Config, F) -> run_iterations(erlang:element(2, Config), F), Start = bench_ffi:monotonic_time_us(), run_iterations(erlang:element(3, Config), F), End = bench_ffi:monotonic_time_us(), Total_us = End - Start, Total_ms = Total_us div 1000, Avg_us = case erlang:float(erlang:element(3, Config)) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> erlang:float(Total_us) / Gleam@denominator end, {bench_result, Name, Total_ms, erlang:element(3, Config), Avg_us, none, none}. -file("src/bench/bench.gleam", 86). ?DOC(" Run a benchmark with default configuration\n"). -spec benchmark(binary(), fun(() -> any())) -> bench_result(). benchmark(Name, F) -> benchmark_with_config(Name, default_config(), F). -file("src/bench/bench.gleam", 325). ?DOC(" Measure each iteration individually\n"). -spec measure_each_iteration(integer(), fun(() -> any()), list(integer())) -> list(integer()). measure_each_iteration(N, F, Times) -> case N > 0 of false -> lists:reverse(Times); true -> Start = bench_ffi:monotonic_time_us(), _ = F(), End = bench_ffi:monotonic_time_us(), measure_each_iteration(N - 1, F, [End - Start | Times]) end. -file("src/bench/bench.gleam", 119). ?DOC(" Run a benchmark capturing min/max times (slower but more detailed)\n"). -spec benchmark_detailed(binary(), bench_config(), fun(() -> any())) -> bench_result(). benchmark_detailed(Name, Config, F) -> run_iterations(erlang:element(2, Config), F), Times = measure_each_iteration(erlang:element(3, Config), F, []), Total_us = gleam@list:fold(Times, 0, fun gleam@int:add/2), Total_ms = Total_us div 1000, Avg_us = case erlang:float(erlang:element(3, Config)) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> erlang:float(Total_us) / Gleam@denominator end, Min_us = begin _pipe = Times, _pipe@1 = gleam@list:reduce(_pipe, fun gleam@int:min/2), _pipe@2 = gleam@result:map(_pipe@1, fun erlang:float/1), gleam@option:from_result(_pipe@2) end, Max_us = begin _pipe@3 = Times, _pipe@4 = gleam@list:reduce(_pipe@3, fun gleam@int:max/2), _pipe@5 = gleam@result:map(_pipe@4, fun erlang:float/1), gleam@option:from_result(_pipe@5) end, {bench_result, Name, Total_ms, erlang:element(3, Config), Avg_us, Min_us, Max_us}. -file("src/bench/bench.gleam", 338). ?DOC(" Run iterations with setup/teardown\n"). -spec run_iterations_with_setup( integer(), fun(() -> KYH), fun((KYH) -> KYI), fun((KYH, KYI) -> nil) ) -> nil. run_iterations_with_setup(N, Setup, F, Teardown) -> case N > 0 of false -> nil; true -> State = Setup(), Result = F(State), Teardown(State, Result), run_iterations_with_setup(N - 1, Setup, F, Teardown) end. -file("src/bench/bench.gleam", 156). ?DOC(" Run a function that needs setup/teardown for each iteration\n"). -spec benchmark_with_setup( binary(), bench_config(), fun(() -> KXZ), fun((KXZ) -> KYA), fun((KXZ, KYA) -> nil) ) -> bench_result(). benchmark_with_setup(Name, Config, Setup, F, Teardown) -> run_iterations_with_setup(erlang:element(2, Config), Setup, F, Teardown), Start = bench_ffi:monotonic_time_us(), run_iterations_with_setup(erlang:element(3, Config), Setup, F, Teardown), End = bench_ffi:monotonic_time_us(), Total_us = End - Start, Total_ms = Total_us div 1000, Avg_us = case erlang:float(erlang:element(3, Config)) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> erlang:float(Total_us) / Gleam@denominator end, {bench_result, Name, Total_ms, erlang:element(3, Config), Avg_us, none, none}. -file("src/bench/bench.gleam", 380). -spec power_of_10(integer()) -> float(). power_of_10(N) -> case N of 0 -> 1.0; 1 -> 10.0; 2 -> 100.0; 3 -> 1000.0; _ -> 10.0 * power_of_10(N - 1) end. -file("src/bench/bench.gleam", 368). ?DOC(" Convert float to string with limited precision\n"). -spec float_to_string_precision(float(), integer()) -> binary(). float_to_string_precision(F, Precision) -> Multiplier = power_of_10(Precision), Rounded = case Multiplier of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> erlang:float(erlang:round(F * Multiplier)) / Gleam@denominator end, Str = gleam_stdlib:float_to_string(Rounded), case gleam_stdlib:contains_string(Str, <<"."/utf8>>) of true -> Str; false -> <> end. -file("src/bench/bench.gleam", 356). ?DOC(" Format microseconds as a human-readable duration\n"). -spec format_duration_us(float()) -> binary(). format_duration_us(Us) -> case Us < 1000.0 of true -> <<(float_to_string_precision(Us, 2))/binary, "us"/utf8>>; false -> case Us < 1000000.0 of true -> <<(float_to_string_precision(Us / 1000.0, 2))/binary, "ms"/utf8>>; false -> <<(float_to_string_precision(Us / 1000000.0, 2))/binary, "s"/utf8>> end end. -file("src/bench/bench.gleam", 284). ?DOC(" Format a benchmark suite as markdown table\n"). -spec to_markdown(bench_suite()) -> binary(). to_markdown(Suite) -> Header = <<"| Benchmark | Avg/Op | Total | Iterations |\n"/utf8>>, Separator = <<"|-----------|--------|-------|------------|\n"/utf8>>, Rows = begin _pipe = gleam@list:map( erlang:element(3, Suite), fun(Result) -> <<<<<<<<<<<<<<<<"| "/utf8, (erlang:element(2, Result))/binary>>/binary, " | "/utf8>>/binary, (format_duration_us( erlang:element(5, Result) ))/binary>>/binary, " | "/utf8>>/binary, (erlang:integer_to_binary( erlang:element(3, Result) ))/binary>>/binary, "ms | "/utf8>>/binary, (erlang:integer_to_binary(erlang:element(4, Result)))/binary>>/binary, " |\n"/utf8>> end ), erlang:list_to_binary(_pipe) end, <<<<<<<<<<"### "/utf8, (erlang:element(2, Suite))/binary>>/binary, "\n\n"/utf8>>/binary, Header/binary>>/binary, Separator/binary>>/binary, Rows/binary>>. -file("src/bench/bench.gleam", 391). ?DOC(" Pad a string on the right to a given length\n"). -spec pad_right(binary(), integer()) -> binary(). pad_right(S, Len) -> Current_len = string:length(S), case Current_len >= Len of true -> S; false -> <>, Len - Current_len))/binary>> end. -file("src/bench/bench.gleam", 264). ?DOC(" Print summary of a benchmark suite\n"). -spec print_suite_summary(bench_suite()) -> nil. print_suite_summary(Suite) -> gleam_stdlib:println(<<""/utf8>>), gleam_stdlib:println( begin _pipe = <<"="/utf8>>, gleam@string:repeat(_pipe, 60) end ), gleam_stdlib:println( <<"Summary: "/utf8, (erlang:element(2, Suite))/binary>> ), gleam_stdlib:println( begin _pipe@1 = <<"="/utf8>>, gleam@string:repeat(_pipe@1, 60) end ), gleam@list:each( erlang:element(3, Suite), fun(Result) -> gleam_stdlib:println( <<<<<<<<" "/utf8, (pad_right(erlang:element(2, Result), 30))/binary>>/binary, ": "/utf8>>/binary, (format_duration_us(erlang:element(5, Result)))/binary>>/binary, "/op"/utf8>> ) end ), gleam_stdlib:println(<<""/utf8>>). -file("src/bench/bench.gleam", 400). ?DOC(" Pad a string on the left to a given length\n"). -spec pad_left(binary(), integer()) -> binary(). pad_left(S, Len) -> Current_len = string:length(S), case Current_len >= Len of true -> S; false -> <<(gleam@string:repeat(<<" "/utf8>>, Len - Current_len))/binary, S/binary>> end. -file("src/bench/bench.gleam", 237). ?DOC(" Print a single benchmark result\n"). -spec print_result(bench_result()) -> nil. print_result(Result) -> Avg_str = format_duration_us(erlang:element(5, Result)), Total_str = <<(erlang:integer_to_binary(erlang:element(3, Result)))/binary, "ms"/utf8>>, Iter_str = <<(erlang:integer_to_binary(erlang:element(4, Result)))/binary, " iters"/utf8>>, Extra = case {erlang:element(6, Result), erlang:element(7, Result)} of {{some, Min}, {some, Max}} -> <<<<<<<<" (min: "/utf8, (format_duration_us(Min))/binary>>/binary, ", max: "/utf8>>/binary, (format_duration_us(Max))/binary>>/binary, ")"/utf8>>; {_, _} -> <<""/utf8>> end, gleam_stdlib:println( <<<<<<<<<<<<(pad_right(erlang:element(2, Result), 35))/binary, (pad_left(Avg_str, 12))/binary>>/binary, "/op "/utf8>>/binary, (pad_left(Total_str, 8))/binary>>/binary, " total "/utf8>>/binary, (pad_left(Iter_str, 12))/binary>>/binary, Extra/binary>> ). -file("src/bench/bench.gleam", 200). ?DOC(" Run all benchmarks in a suite\n"). -spec run_suite(binary(), list({binary(), fun(() -> any())}), bench_config()) -> bench_suite(). run_suite(Suite_name, Benchmarks, Config) -> gleam_stdlib:println(<<""/utf8>>), gleam_stdlib:println( begin _pipe = <<"="/utf8>>, gleam@string:repeat(_pipe, 60) end ), gleam_stdlib:println(<<"Benchmark Suite: "/utf8, Suite_name/binary>>), gleam_stdlib:println( begin _pipe@1 = <<"="/utf8>>, gleam@string:repeat(_pipe@1, 60) end ), gleam_stdlib:println( <<<<<<<<"Config: "/utf8, (erlang:integer_to_binary(erlang:element(3, Config)))/binary>>/binary, " iterations, "/utf8>>/binary, (erlang:integer_to_binary(erlang:element(2, Config)))/binary>>/binary, " warmup"/utf8>> ), gleam_stdlib:println( begin _pipe@2 = <<"-"/utf8>>, gleam@string:repeat(_pipe@2, 60) end ), Results = gleam@list:map( Benchmarks, fun(Bench) -> {Name, F} = Bench, Result = benchmark_with_config(Name, Config, F), print_result(Result), Result end ), gleam_stdlib:println( begin _pipe@3 = <<"-"/utf8>>, gleam@string:repeat(_pipe@3, 60) end ), gleam_stdlib:println(<<""/utf8>>), {bench_suite, Suite_name, Results}. -file("src/bench/bench.gleam", 413). ?DOC(" Compare two benchmark results and return percentage change\n"). -spec compare_results(bench_result(), bench_result()) -> float(). compare_results(Baseline, Current) -> (case erlang:element(5, Baseline) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> (erlang:element(5, Current) - erlang:element( 5, Baseline )) / Gleam@denominator end) * 100.0. -file("src/bench/bench.gleam", 418). ?DOC(" Check if a benchmark result is within acceptable threshold of baseline\n"). -spec within_threshold(bench_result(), bench_result(), float()) -> boolean(). within_threshold(Baseline, Current, Threshold_percent) -> Change = compare_results(Baseline, Current), Change < Threshold_percent.