Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# x.async benchmarks
|
||||
|
||||
This folder contains cautious local benchmarks for the public `x.async` API.
|
||||
They are observation tools, not correctness tests and not portable performance
|
||||
claims.
|
||||
|
||||
## Available benchmark
|
||||
|
||||
- `async_benchmark.v`: measures short default runs for `Group`, `Task[T]`,
|
||||
`Pool`, bounded pool admission, `with_timeout()`, and `every()`.
|
||||
- `run_async_benchmark.sh`: builds and runs the benchmark with the local `./v`,
|
||||
isolated `VTMP`/`VCACHE`, and a temporary output binary.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
sh vlib/x/async/benchmarks/run_async_benchmark.sh
|
||||
```
|
||||
|
||||
The script is serialized by design. Do not run multiple V benchmark or test
|
||||
runners against the same checkout/cache without isolating `VTMP`, `VCACHE`, and
|
||||
output paths.
|
||||
|
||||
## Parameters
|
||||
|
||||
The defaults are intentionally small. Tune them locally with:
|
||||
|
||||
- `XASYNC_BENCH_GROUP_ROUNDS`
|
||||
- `XASYNC_BENCH_GROUP_JOBS`
|
||||
- `XASYNC_BENCH_TASK_ROUNDS`
|
||||
- `XASYNC_BENCH_POOL_JOBS`
|
||||
- `XASYNC_BENCH_POOL_WORKERS`
|
||||
- `XASYNC_BENCH_POOL_BOUNDED_ROUNDS`
|
||||
- `XASYNC_BENCH_POOL_BOUNDED_TIMEOUT_MS`
|
||||
- `XASYNC_BENCH_TIMEOUT_ROUNDS`
|
||||
- `XASYNC_BENCH_EVERY_ITERATIONS`
|
||||
- `XASYNC_BENCH_EVERY_INTERVAL_MS`
|
||||
|
||||
The bounded pool admission case exercises `submit_with_timeout()` while the
|
||||
pool is full and `submit_with_context()` after capacity opens. The timeout only
|
||||
bounds admission; the benchmark does not make performance assertions.
|
||||
|
||||
Do not commit machine-specific benchmark results as permanent truth. Tests
|
||||
remain the authority for functional and concurrency-safety validation.
|
||||
@@ -0,0 +1,270 @@
|
||||
import benchmark
|
||||
import context
|
||||
import os
|
||||
import time
|
||||
import x.async as xasync
|
||||
|
||||
const default_group_rounds = 8
|
||||
const default_group_jobs = 16
|
||||
const default_task_rounds = 32
|
||||
const default_pool_jobs = 64
|
||||
const default_pool_workers = 4
|
||||
const default_pool_bounded_rounds = 8
|
||||
const default_pool_bounded_timeout_ms = 2
|
||||
const default_timeout_rounds = 32
|
||||
const default_every_iterations = 5
|
||||
const default_every_interval_ms = 1
|
||||
|
||||
fn main() {
|
||||
run_benchmarks() or {
|
||||
eprintln('x.async benchmark failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_benchmarks() ! {
|
||||
group_rounds := env_int('XASYNC_BENCH_GROUP_ROUNDS', default_group_rounds, 1, 200)
|
||||
group_jobs := env_int('XASYNC_BENCH_GROUP_JOBS', default_group_jobs, 1, 512)
|
||||
task_rounds := env_int('XASYNC_BENCH_TASK_ROUNDS', default_task_rounds, 1, 500)
|
||||
pool_jobs := env_int('XASYNC_BENCH_POOL_JOBS', default_pool_jobs, 1, 1000)
|
||||
pool_workers := env_int('XASYNC_BENCH_POOL_WORKERS', default_pool_workers, 1, 64)
|
||||
pool_bounded_rounds := env_int('XASYNC_BENCH_POOL_BOUNDED_ROUNDS', default_pool_bounded_rounds,
|
||||
1, 100)
|
||||
pool_bounded_timeout_ms := env_int('XASYNC_BENCH_POOL_BOUNDED_TIMEOUT_MS',
|
||||
default_pool_bounded_timeout_ms, 1, 100)
|
||||
timeout_rounds := env_int('XASYNC_BENCH_TIMEOUT_ROUNDS', default_timeout_rounds, 1, 500)
|
||||
every_iterations := env_int('XASYNC_BENCH_EVERY_ITERATIONS', default_every_iterations, 1, 100)
|
||||
every_interval_ms :=
|
||||
env_int('XASYNC_BENCH_EVERY_INTERVAL_MS', default_every_interval_ms, 1, 100)
|
||||
|
||||
println('x.async cautious benchmark')
|
||||
println('Override sizes with XASYNC_BENCH_* environment variables.')
|
||||
|
||||
mut checksum := 0
|
||||
mut b := benchmark.start()
|
||||
checksum += bench_group(group_rounds, group_jobs)!
|
||||
b.measure('Group: rounds=${group_rounds}, jobs_per_round=${group_jobs}, checksum=${checksum}')
|
||||
|
||||
checksum += bench_task(task_rounds)!
|
||||
b.measure('Task: rounds=${task_rounds}, checksum=${checksum}')
|
||||
|
||||
checksum += bench_pool(pool_jobs, pool_workers)!
|
||||
b.measure('Pool: jobs=${pool_jobs}, workers=${pool_workers}, checksum=${checksum}')
|
||||
|
||||
checksum += bench_pool_bounded_admission(pool_bounded_rounds, pool_bounded_timeout_ms)!
|
||||
b.measure('Pool bounded admission: rounds=${pool_bounded_rounds}, timeout_ms=${pool_bounded_timeout_ms}, checksum=${checksum}')
|
||||
|
||||
checksum += bench_timeout(timeout_rounds)!
|
||||
b.measure('with_timeout: rounds=${timeout_rounds}, checksum=${checksum}')
|
||||
|
||||
checksum += bench_every(every_iterations, every_interval_ms)!
|
||||
b.measure('every: iterations=${every_iterations}, interval_ms=${every_interval_ms}, checksum=${checksum}')
|
||||
}
|
||||
|
||||
fn env_int(name string, default_value int, min_value int, max_value int) int {
|
||||
raw := os.getenv(name)
|
||||
if raw == '' {
|
||||
return default_value
|
||||
}
|
||||
mut value := raw.int()
|
||||
if value < min_value {
|
||||
value = min_value
|
||||
}
|
||||
if value > max_value {
|
||||
value = max_value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fn bench_group(rounds int, jobs_per_round int) !int {
|
||||
mut total := 0
|
||||
for _ in 0 .. rounds {
|
||||
done := chan int{cap: jobs_per_round}
|
||||
mut group := xasync.new_group(context.background())
|
||||
for _ in 0 .. jobs_per_round {
|
||||
group.go(fn [done] (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
done <- 1
|
||||
})!
|
||||
}
|
||||
group.wait()!
|
||||
for _ in 0 .. jobs_per_round {
|
||||
total += <-done
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn bench_task(rounds int) !int {
|
||||
mut total := 0
|
||||
for i in 0 .. rounds {
|
||||
mut task := xasync.run[int](fn [i] (mut ctx context.Context) !int {
|
||||
_ = ctx
|
||||
return i + 1
|
||||
})!
|
||||
total += task.wait()!
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn bench_pool(jobs int, workers int) !int {
|
||||
done := chan int{cap: jobs}
|
||||
mut pool := xasync.new_pool(workers: workers, queue_size: jobs)!
|
||||
for _ in 0 .. jobs {
|
||||
pool.try_submit(fn [done] (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
done <- 1
|
||||
})!
|
||||
}
|
||||
pool.close()!
|
||||
mut total := 0
|
||||
for _ in 0 .. jobs {
|
||||
total += <-done
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn bench_pool_bounded_admission(rounds int, timeout_ms int) !int {
|
||||
mut total := 0
|
||||
admission_timeout := time.Duration(timeout_ms) * time.millisecond
|
||||
for _ in 0 .. rounds {
|
||||
mut pool := xasync.new_pool(workers: 1, queue_size: 1)!
|
||||
started := chan bool{cap: 2}
|
||||
release := chan bool{cap: 3}
|
||||
finished := chan bool{cap: 2}
|
||||
attempting := chan bool{cap: 1}
|
||||
accepted := chan bool{cap: 1}
|
||||
ran := chan int{cap: 1}
|
||||
blocking_job := fn [started, release, finished] (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
started <- true
|
||||
_ := <-release
|
||||
finished <- true
|
||||
}
|
||||
|
||||
pool.try_submit(blocking_job)!
|
||||
wait_for_benchmark_bool(started, 'bounded admission pool job did not start')!
|
||||
pool.try_submit(blocking_job)!
|
||||
|
||||
mut timed_out := false
|
||||
pool.submit_with_timeout(admission_timeout, fn (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
}) or {
|
||||
if err.msg() != 'async: timeout' {
|
||||
return err
|
||||
}
|
||||
timed_out = true
|
||||
}
|
||||
if !timed_out {
|
||||
release <- true
|
||||
release <- true
|
||||
pool.close()!
|
||||
return error('submit_with_timeout accepted while the pool was full')
|
||||
}
|
||||
|
||||
submit_thread := spawn fn [mut pool, attempting, accepted, ran] () {
|
||||
attempting <- true
|
||||
pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
ran <- 1
|
||||
}) or {
|
||||
accepted <- false
|
||||
return
|
||||
}
|
||||
accepted <- true
|
||||
}()
|
||||
wait_for_benchmark_bool(attempting, 'bounded admission submitter did not start')!
|
||||
|
||||
release <- true
|
||||
wait_for_benchmark_bool(finished, 'bounded admission first job did not finish')!
|
||||
wait_for_benchmark_bool(accepted,
|
||||
'submit_with_context did not accept after capacity opened')!
|
||||
release <- true
|
||||
total += read_benchmark_int(ran, 'bounded admission accepted job did not run')!
|
||||
pool.close()!
|
||||
submit_thread.wait()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn bench_timeout(rounds int) !int {
|
||||
mut total := 0
|
||||
for _ in 0 .. rounds {
|
||||
xasync.with_timeout(1 * time.second, fn (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
})!
|
||||
total++
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
fn bench_every(iterations int, interval_ms int) !int {
|
||||
ctx, cancel := xasync.with_cancel()
|
||||
ticks := chan int{cap: iterations + 4}
|
||||
result := chan string{cap: 1}
|
||||
interval := time.Duration(interval_ms) * time.millisecond
|
||||
worker := spawn fn [ctx, ticks, result, interval] () {
|
||||
xasync.every(ctx, interval, fn [ticks] (mut ctx context.Context) ! {
|
||||
_ = ctx
|
||||
ticks <- 1
|
||||
}) or {
|
||||
result <- err.msg()
|
||||
return
|
||||
}
|
||||
result <- 'ok'
|
||||
}()
|
||||
|
||||
mut total := 0
|
||||
for _ in 0 .. iterations {
|
||||
select {
|
||||
value := <-ticks {
|
||||
total += value
|
||||
}
|
||||
1 * time.second {
|
||||
cancel()
|
||||
worker.wait()
|
||||
return error('every benchmark did not receive a tick')
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
msg := <-result {
|
||||
if msg != 'context canceled' {
|
||||
worker.wait()
|
||||
return error('unexpected every benchmark result: ${msg}')
|
||||
}
|
||||
}
|
||||
1 * time.second {
|
||||
worker.wait()
|
||||
return error('every benchmark did not stop after cancellation')
|
||||
}
|
||||
}
|
||||
worker.wait()
|
||||
return total
|
||||
}
|
||||
|
||||
fn wait_for_benchmark_bool(signal chan bool, message string) ! {
|
||||
select {
|
||||
ok := <-signal {
|
||||
if !ok {
|
||||
return error(message)
|
||||
}
|
||||
}
|
||||
1 * time.second {
|
||||
return error(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_benchmark_int(signal chan int, message string) !int {
|
||||
select {
|
||||
value := <-signal {
|
||||
return value
|
||||
}
|
||||
1 * time.second {
|
||||
return error(message)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
repo_root=$(CDPATH= cd "$script_dir/../../../.." && pwd)
|
||||
cd "$repo_root"
|
||||
|
||||
if [ ! -x ./v ]; then
|
||||
echo "x.async benchmarks must be run from a V checkout with local ./v" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/xasync-benchmark.XXXXXX")
|
||||
cleanup() {
|
||||
rm -rf "$tmp_root"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
vtmp="$tmp_root/vtmp"
|
||||
vcache="$tmp_root/vcache"
|
||||
out="$tmp_root/out/async_benchmark"
|
||||
mkdir -p "$vtmp" "$vcache" "$(dirname "$out")"
|
||||
|
||||
echo "Running x.async benchmark with isolated VTMP and VCACHE."
|
||||
echo "Tune sizes with XASYNC_BENCH_* environment variables."
|
||||
echo "See vlib/x/async/benchmarks/README.md for the full list."
|
||||
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout 180s env VTMP="$vtmp" VCACHE="$vcache" ./v -prod -o "$out" run vlib/x/async/benchmarks/async_benchmark.v
|
||||
else
|
||||
env VTMP="$vtmp" VCACHE="$vcache" ./v -prod -o "$out" run vlib/x/async/benchmarks/async_benchmark.v
|
||||
fi
|
||||
Reference in new issue
Block a user