Skip to content

Commit

Permalink
add SetupFunction feature
Browse files Browse the repository at this point in the history
  • Loading branch information
schurhammer committed Oct 24, 2024
1 parent fb31909 commit 7d9e0b3
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 32 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
73 changes: 47 additions & 26 deletions src/gleamy/bench.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand All @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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, " "),
Expand All @@ -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")
}
35 changes: 31 additions & 4 deletions test/gleamy_bench_test.gleam
Original file line number Diff line number Diff line change
@@ -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")
}

0 comments on commit 7d9e0b3

Please sign in to comment.