-module(gleamy@bench). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([run/3, do_repeat/3, repeat/2, table/2]). -export_type([input/1, function_/2, set/0, stat/0, option/0, options/0]). -type input(FJS) :: {input, binary(), FJS}. -type function_(FJT, FJU) :: {function, binary(), fun((FJT) -> FJU)}. -type set() :: {set, binary(), binary(), list(float())}. -type stat() :: {p, integer()} | ips | min | max | mean | sd | sd_percent | {stat, binary(), fun((set()) -> float())}. -type option() :: {warmup, integer()} | {duration, integer()} | quiet. -type options() :: {options, integer(), integer(), boolean()}. -spec now() -> float(). now() -> Ns = os:perf_counter(1000000000), gleam@int:to_float(Ns) / 1000000.0. -spec mean(list(float())) -> float(). mean(Data) -> Count = gleam@int:to_float(gleam@list:length(Data)), case Count of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> gleam@float:sum(Data) / Gleam@denominator end. -spec standard_deviation(list(float())) -> float(). standard_deviation(Data) -> Count = gleam@int:to_float(gleam@list:length(Data)), Mean = mean(Data), _assert_subject = begin _pipe = (case Count of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> gleam@float:sum( gleam@list:map( Data, fun(X) -> Y = X - Mean, Y * Y end ) ) / Gleam@denominator end), gleam@float:square_root(_pipe) end, {ok, Value} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"gleamy/bench"/utf8>>, function => <<"standard_deviation"/utf8>>, line => 50}) end, Value. -spec min(list(float())) -> float(). min(Data) -> First = case Data of [X | _] -> X; _ -> +0.0 end, gleam@list:fold(Data, First, fun(A, X@1) -> gleam@float:min(A, X@1) end). -spec percentile(integer(), list(float())) -> float(). percentile(N, Data) -> Data@1 = begin _pipe = Data, _pipe@1 = gleam@list:sort(_pipe, fun gleam@float:compare/2), gleam@list:drop(_pipe@1, (N * gleam@list:length(Data)) div 100) end, case Data@1 of [X | _] -> X; _ -> +0.0 end. -spec max(list(float())) -> float(). max(Data) -> First = case Data of [X | _] -> X; _ -> +0.0 end, gleam@list:fold(Data, First, fun(A, X@1) -> gleam@float:max(A, X@1) end). -spec do_repeat_until(list(float()), float(), FKC, fun((FKC) -> any())) -> list(float()). do_repeat_until(Acc, Stop, Value, Fun) -> Start = now(), _ = Fun(Value), End = now(), case End of _ when End < Stop -> do_repeat_until([End - Start | Acc], Stop, Value, Fun); _ -> Acc end. -spec repeat_until(float(), FKF, fun((FKF) -> any())) -> list(float()). repeat_until(Duration, Value, Fun) -> do_repeat_until([], now() + Duration, Value, Fun). -spec default_options() -> options(). default_options() -> {options, 500, 2000, false}. -spec apply_options(options(), list(option())) -> options(). apply_options(Default, Options) -> case Options of [] -> Default; [X | Xs] -> case X of {warmup, Ms} -> apply_options(erlang:setelement(2, Default, Ms), Xs); {duration, Ms@1} -> apply_options(erlang:setelement(3, Default, Ms@1), Xs); quiet -> apply_options(erlang:setelement(4, Default, true), Xs) end end. -spec run(list(input(FKJ)), list(function_(FKJ, any())), list(option())) -> list(set()). run(Inputs, Functions, Options) -> Options@1 = apply_options(default_options(), Options), gleam@list:flat_map( Inputs, fun(_use0) -> {input, Input_label, Input} = _use0, gleam@list:map(Functions, fun(Function) -> case Function of {function, Fun_label, Fun} -> case erlang:element(4, Options@1) of true -> nil; false -> gleam@io:println( <<<<<<"benching set "/utf8, Input_label/binary>>/binary, " "/utf8>>/binary, Fun_label/binary>> ) end, _ = repeat_until( gleam@int:to_float(erlang:element(2, Options@1)), Input, Fun ), Timings = repeat_until( gleam@int:to_float(erlang:element(3, Options@1)), Input, Fun ), {set, Input_label, Fun_label, Timings} end end) end ). -spec do_repeat(integer(), FKS, fun((FKS) -> any())) -> nil. do_repeat(N, Input, Fun) -> case N of 0 -> nil; _ -> _ = Fun(Input), do_repeat(N - 1, Input, Fun) end. -spec repeat(integer(), fun((FKV) -> any())) -> fun((FKV) -> nil). repeat(N, Fun) -> fun(Input) -> do_repeat(N, Input, Fun) end. -spec format_float(float(), integer()) -> binary(). format_float(F, Decimals) -> _assert_subject = gleam@int:power(10, gleam@int:to_float(Decimals)), {ok, Factor} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"gleamy/bench"/utf8>>, function => <<"format_float"/utf8>>, line => 180}) end, Whole = gleam@float:truncate(F), Decimal = gleam@float:truncate(F * Factor) - (Whole * gleam@float:truncate( Factor )), gleam@string:concat( [gleam@string:pad_left( gleam@int:to_string(Whole), (14 - Decimals) - 1, <<" "/utf8>> ), <<"."/utf8>>, gleam@string:pad_left( gleam@int:to_string(Decimal), Decimals, <<"0"/utf8>> )] ). -spec header_row(list(stat())) -> binary(). header_row(Stats) -> _pipe = [gleam@string:pad_right(<<"Input"/utf8>>, 20, <<" "/utf8>>), gleam@string:pad_right(<<"Function"/utf8>>, 20, <<" "/utf8>>) | gleam@list:map( Stats, fun(Stat) -> Stat@1 = case Stat of {p, N} -> <<"P"/utf8, (gleam@int:to_string(N))/binary>>; ips -> <<"IPS"/utf8>>; min -> <<"Min"/utf8>>; max -> <<"Max"/utf8>>; mean -> <<"Mean"/utf8>>; sd -> <<"SD"/utf8>>; sd_percent -> <<"SD%"/utf8>>; {stat, Name, _} -> Name end, gleam@string:pad_left(Stat@1, 14, <<" "/utf8>>) end )], gleam@string:join(_pipe, <<"\t"/utf8>>). -spec stat_row(set(), list(stat())) -> binary(). stat_row(Set, Stats) -> _pipe@2 = [gleam@string:pad_right(erlang:element(2, Set), 20, <<" "/utf8>>), gleam@string:pad_right(erlang:element(3, Set), 20, <<" "/utf8>>) | gleam@list:map( Stats, fun(Stat) -> Stat@1 = case Stat of {p, N} -> percentile(N, erlang:element(4, Set)); ips -> case gleam@float:sum(erlang:element(4, Set)) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> 1000.0 * gleam@int:to_float( gleam@list:length(erlang:element(4, Set)) ) / Gleam@denominator end; min -> min(erlang:element(4, Set)); max -> max(erlang:element(4, Set)); mean -> mean(erlang:element(4, Set)); sd -> standard_deviation(erlang:element(4, Set)); sd_percent -> case mean(erlang:element(4, Set)) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator@1 -> 100.0 * standard_deviation( erlang:element(4, Set) ) / Gleam@denominator@1 end; {stat, _, Calc} -> Calc(Set) end, _pipe = Stat@1, _pipe@1 = format_float(_pipe, 4), gleam@string:pad_left(_pipe@1, 14, <<" "/utf8>>) end )], gleam@string:join(_pipe@2, <<"\t"/utf8>>). -spec table(list(set()), list(stat())) -> binary(). table(Sets, Stats) -> Header = header_row(Stats), Body = gleam@list:map(Sets, fun(_capture) -> stat_row(_capture, Stats) end), _pipe = [Header | Body], gleam@string:join(_pipe, <<"\n"/utf8>>).