diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9e5346..867b7a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: - otp-version: "26.0.2" - gleam-version: "0.34.1" + otp-version: "27.1.2" + gleam-version: "1.5.1" rebar3-version: "3" # elixir-version: "1.15.4" - run: gleam format --check src test diff --git a/README.md b/README.md index b11cf73..2cdf99e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,46 @@ pre-sorted list list.sort() 37.8532 22.4190 reversed list list.sort() 34.0101 27.0734 31.0618 ``` +## Function with additional setup + +Sometimes you need to do some additional setup before you can call your function, instead of having it called directly with the input data. +For this use case you can use bench.SetupFunction + +`SetupFunction(label: String, setup_function: fn(a) -> fn(a) -> b)` + +The setup function is executed once at the start of the run, and should return the function that will be benchmarked. +Both the setup function and the benchmark function will be passed the input data. + +For example, you might be testing the speed of a certain operation on a range of data structures. +To do this you will need to create each data structure beforehand with the given input data so you can run the operation on it. + +```gleam +bench.run( + [ + bench.Input("100", list.range(1, 100)), + bench.Input("1000", list.reverse(list.range(1, 1000))), + ], + [ + bench.SetupFunction("dict.get", fn(items) { + // This section will not be measured in the benchmark. + // We fill a dictionary with the input items to use later. + let d = list.fold(items, dict.new(), fn(d, i) { + dict.insert(d, i, i) + })m + + // The returned function will be measured for the benchmark. + // It tries to "get" each item in the input from the dictionary. + fn(items) { + list.each(items, fn(i) { dict.get(d, i) }) + } + }) + + // ... + ], + [bench.Duration(1000), bench.Warmup(100)], +) +``` + ## Contributing Suggestions and pull requests are welcome! diff --git a/src/gleamy/bench.gleam b/src/gleamy/bench.gleam index d86ffc1..8913887 100644 --- a/src/gleamy/bench.gleam +++ b/src/gleamy/bench.gleam @@ -11,7 +11,7 @@ fn perf_counter(_resolution: Int) -> Int { /// timestamp in milliseconds @external(javascript, "../gleamy_bench_ffi.mjs", "now") -fn now() -> Float { +pub fn now() -> Float { let ns = perf_counter(1_000_000_000) int.to_float(ns) /. 1_000_000.0 } @@ -22,6 +22,7 @@ pub type Input(a) { pub type Function(a, b) { Function(label: String, function: fn(a) -> b) + SetupFunction(label: String, setup_function: fn(a) -> fn(a) -> b) } pub type Set { @@ -110,15 +111,20 @@ fn repeat_until(duration: Float, value: a, fun: fn(a) -> b) { pub type Option { Warmup(ms: Int) Duration(ms: Int) + Decimals(n: Int) Quiet } -type Options { - Options(warmup: Int, duration: Int, quiet: Bool) +pub type Options { + Options(warmup: Int, duration: Int, decimals: Int, quiet: Bool) +} + +pub type BenchResults { + BenchResults(options: Options, sets: List(Set)) } fn default_options() -> Options { - Options(warmup: 500, duration: 2000, quiet: False) + Options(warmup: 500, duration: 2000, decimals: 4, quiet: False) } fn apply_options(default: Options, options: List(Option)) -> Options { @@ -128,6 +134,7 @@ fn apply_options(default: Options, options: List(Option)) -> Options { case x { Warmup(ms) -> apply_options(Options(..default, warmup: ms), xs) Duration(ms) -> apply_options(Options(..default, duration: ms), xs) + Decimals(n) -> apply_options(Options(..default, decimals: n), xs) Quiet -> apply_options(Options(..default, quiet: True), xs) } } @@ -137,23 +144,39 @@ pub fn run( inputs: List(Input(a)), functions: List(Function(a, b)), options: List(Option), -) -> List(Set) { +) -> BenchResults { let options = apply_options(default_options(), options) - use Input(input_label, input) <- list.flat_map(inputs) - use function <- list.map(functions) - case function { - Function(fun_label, fun) -> { - case options.quiet { - True -> Nil - False -> { - io.println("benching set " <> input_label <> " " <> fun_label) + let results = + list.flat_map(inputs, fn(input) { + let Input(input_label, input) = input + use function <- list.map(functions) + case function { + Function(fun_label, fun) -> { + case options.quiet { + True -> Nil + False -> { + io.println("benching set " <> input_label <> " " <> fun_label) + } + } + let _warmup = repeat_until(int.to_float(options.warmup), input, fun) + let timings = repeat_until(int.to_float(options.duration), input, fun) + Set(input_label, fun_label, timings) + } + SetupFunction(fun_label, setup_fun) -> { + case options.quiet { + True -> Nil + False -> { + io.println("benching set " <> input_label <> " " <> fun_label) + } + } + let fun = setup_fun(input) + let _warmup = repeat_until(int.to_float(options.warmup), input, fun) + let timings = repeat_until(int.to_float(options.duration), input, fun) + Set(input_label, fun_label, timings) } } - let _warmup = repeat_until(int.to_float(options.warmup), input, fun) - let timings = repeat_until(int.to_float(options.duration), input, fun) - Set(input_label, fun_label, timings) - } - } + }) + BenchResults(options, results) } pub fn do_repeat(n: Int, input: a, fun: fn(a) -> b) { @@ -174,8 +197,6 @@ const name_pad = 20 const stat_pad = 14 -const stat_decimal = 4 - fn format_float(f: Float, decimals: Int) { let assert Ok(factor) = int.power(10, int.to_float(decimals)) let whole = float.truncate(f) @@ -205,10 +226,10 @@ fn header_row(stats: List(Stat)) -> String { string.pad_left(stat, stat_pad, " ") }) ] - |> string.join("\t") + |> string.join("") } -fn stat_row(set: Set, stats: List(Stat)) -> String { +fn stat_row(set: Set, stats: List(Stat), options: Options) -> String { [ string.pad_right(set.input, name_pad, " "), string.pad_right(set.function, name_pad, " "), @@ -225,16 +246,16 @@ fn stat_row(set: Set, stats: List(Stat)) -> String { Stat(_, calc) -> calc(set) } stat - |> format_float(stat_decimal) + |> format_float(options.decimals) |> string.pad_left(stat_pad, " ") }) ] - |> string.join("\t") + |> string.join("") } -pub fn table(sets: List(Set), stats: List(Stat)) -> String { +pub fn table(result: BenchResults, stats: List(Stat)) -> String { let header = header_row(stats) - let body = list.map(sets, stat_row(_, stats)) + let body = list.map(result.sets, stat_row(_, stats, result.options)) [header, ..body] |> string.join("\n") } diff --git a/test/gleamy_bench_test.gleam b/test/gleamy_bench_test.gleam index 3831e7a..abd6c96 100644 --- a/test/gleamy_bench_test.gleam +++ b/test/gleamy_bench_test.gleam @@ -1,12 +1,39 @@ +import gleam/int +import gleamy/bench import gleeunit import gleeunit/should +fn do_busy_sleep(until: Float) { + case bench.now() { + now if now >. until -> Nil + _ -> do_busy_sleep(until) + } +} + +fn sleep(ms: Int) -> Nil { + do_busy_sleep(bench.now() +. int.to_float(ms)) +} + pub fn main() { gleeunit.main() } -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) +pub fn bench_run_test() { + bench.run( + [bench.Input("10ms", 10), bench.Input("20ms", 20)], + [ + bench.Function("sleep1", fn(ms) { sleep(ms) }), + bench.SetupFunction("sleep2", fn(ms) { fn(_) { sleep(ms) } }), + bench.SetupFunction("sleep3", fn(_) { fn(ms) { sleep(ms) } }), + ], + [bench.Duration(100), bench.Warmup(10), bench.Decimals(0)], + ) + |> bench.table([bench.IPS, bench.Min, bench.P(99)]) + |> should.equal("Input Function IPS Min P99 +10ms sleep1 99.0 10.0 10.0 +10ms sleep2 99.0 10.0 10.0 +10ms sleep3 99.0 10.0 10.0 +20ms sleep1 49.0 20.0 20.0 +20ms sleep2 49.0 20.0 20.0 +20ms sleep3 49.0 20.0 20.0") }