| 1 | import benchmark |
| 2 | import context |
| 3 | import os |
| 4 | import time |
| 5 | import x.async as xasync |
| 6 | |
| 7 | const default_group_rounds = 8 |
| 8 | const default_group_jobs = 16 |
| 9 | const default_task_rounds = 32 |
| 10 | const default_pool_jobs = 64 |
| 11 | const default_pool_workers = 4 |
| 12 | const default_pool_bounded_rounds = 8 |
| 13 | const default_pool_bounded_timeout_ms = 2 |
| 14 | const default_timeout_rounds = 32 |
| 15 | const default_every_iterations = 5 |
| 16 | const default_every_interval_ms = 1 |
| 17 | |
| 18 | fn main() { |
| 19 | run_benchmarks() or { |
| 20 | eprintln('x.async benchmark failed: ${err.msg()}') |
| 21 | exit(1) |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | fn run_benchmarks() ! { |
| 26 | group_rounds := env_int('XASYNC_BENCH_GROUP_ROUNDS', default_group_rounds, 1, 200) |
| 27 | group_jobs := env_int('XASYNC_BENCH_GROUP_JOBS', default_group_jobs, 1, 512) |
| 28 | task_rounds := env_int('XASYNC_BENCH_TASK_ROUNDS', default_task_rounds, 1, 500) |
| 29 | pool_jobs := env_int('XASYNC_BENCH_POOL_JOBS', default_pool_jobs, 1, 1000) |
| 30 | pool_workers := env_int('XASYNC_BENCH_POOL_WORKERS', default_pool_workers, 1, 64) |
| 31 | pool_bounded_rounds := env_int('XASYNC_BENCH_POOL_BOUNDED_ROUNDS', default_pool_bounded_rounds, |
| 32 | 1, 100) |
| 33 | pool_bounded_timeout_ms := env_int('XASYNC_BENCH_POOL_BOUNDED_TIMEOUT_MS', |
| 34 | default_pool_bounded_timeout_ms, 1, 100) |
| 35 | timeout_rounds := env_int('XASYNC_BENCH_TIMEOUT_ROUNDS', default_timeout_rounds, 1, 500) |
| 36 | every_iterations := env_int('XASYNC_BENCH_EVERY_ITERATIONS', default_every_iterations, 1, 100) |
| 37 | every_interval_ms := |
| 38 | env_int('XASYNC_BENCH_EVERY_INTERVAL_MS', default_every_interval_ms, 1, 100) |
| 39 | |
| 40 | println('x.async cautious benchmark') |
| 41 | println('Override sizes with XASYNC_BENCH_* environment variables.') |
| 42 | |
| 43 | mut checksum := 0 |
| 44 | mut b := benchmark.start() |
| 45 | checksum += bench_group(group_rounds, group_jobs)! |
| 46 | b.measure('Group: rounds=${group_rounds}, jobs_per_round=${group_jobs}, checksum=${checksum}') |
| 47 | |
| 48 | checksum += bench_task(task_rounds)! |
| 49 | b.measure('Task: rounds=${task_rounds}, checksum=${checksum}') |
| 50 | |
| 51 | checksum += bench_pool(pool_jobs, pool_workers)! |
| 52 | b.measure('Pool: jobs=${pool_jobs}, workers=${pool_workers}, checksum=${checksum}') |
| 53 | |
| 54 | checksum += bench_pool_bounded_admission(pool_bounded_rounds, pool_bounded_timeout_ms)! |
| 55 | b.measure('Pool bounded admission: rounds=${pool_bounded_rounds}, timeout_ms=${pool_bounded_timeout_ms}, checksum=${checksum}') |
| 56 | |
| 57 | checksum += bench_timeout(timeout_rounds)! |
| 58 | b.measure('with_timeout: rounds=${timeout_rounds}, checksum=${checksum}') |
| 59 | |
| 60 | checksum += bench_every(every_iterations, every_interval_ms)! |
| 61 | b.measure('every: iterations=${every_iterations}, interval_ms=${every_interval_ms}, checksum=${checksum}') |
| 62 | } |
| 63 | |
| 64 | fn env_int(name string, default_value int, min_value int, max_value int) int { |
| 65 | raw := os.getenv(name) |
| 66 | if raw == '' { |
| 67 | return default_value |
| 68 | } |
| 69 | mut value := raw.int() |
| 70 | if value < min_value { |
| 71 | value = min_value |
| 72 | } |
| 73 | if value > max_value { |
| 74 | value = max_value |
| 75 | } |
| 76 | return value |
| 77 | } |
| 78 | |
| 79 | fn bench_group(rounds int, jobs_per_round int) !int { |
| 80 | mut total := 0 |
| 81 | for _ in 0 .. rounds { |
| 82 | done := chan int{cap: jobs_per_round} |
| 83 | mut group := xasync.new_group(context.background()) |
| 84 | for _ in 0 .. jobs_per_round { |
| 85 | group.go(fn [done] (mut ctx context.Context) ! { |
| 86 | _ = ctx |
| 87 | done <- 1 |
| 88 | })! |
| 89 | } |
| 90 | group.wait()! |
| 91 | for _ in 0 .. jobs_per_round { |
| 92 | total += <-done |
| 93 | } |
| 94 | } |
| 95 | return total |
| 96 | } |
| 97 | |
| 98 | fn bench_task(rounds int) !int { |
| 99 | mut total := 0 |
| 100 | for i in 0 .. rounds { |
| 101 | mut task := xasync.run[int](fn [i] (mut ctx context.Context) !int { |
| 102 | _ = ctx |
| 103 | return i + 1 |
| 104 | })! |
| 105 | total += task.wait()! |
| 106 | } |
| 107 | return total |
| 108 | } |
| 109 | |
| 110 | fn bench_pool(jobs int, workers int) !int { |
| 111 | done := chan int{cap: jobs} |
| 112 | mut pool := xasync.new_pool(workers: workers, queue_size: jobs)! |
| 113 | for _ in 0 .. jobs { |
| 114 | pool.try_submit(fn [done] (mut ctx context.Context) ! { |
| 115 | _ = ctx |
| 116 | done <- 1 |
| 117 | })! |
| 118 | } |
| 119 | pool.close()! |
| 120 | mut total := 0 |
| 121 | for _ in 0 .. jobs { |
| 122 | total += <-done |
| 123 | } |
| 124 | return total |
| 125 | } |
| 126 | |
| 127 | fn bench_pool_bounded_admission(rounds int, timeout_ms int) !int { |
| 128 | mut total := 0 |
| 129 | admission_timeout := time.Duration(timeout_ms) * time.millisecond |
| 130 | for _ in 0 .. rounds { |
| 131 | mut pool := xasync.new_pool(workers: 1, queue_size: 1)! |
| 132 | started := chan bool{cap: 2} |
| 133 | release := chan bool{cap: 3} |
| 134 | finished := chan bool{cap: 2} |
| 135 | attempting := chan bool{cap: 1} |
| 136 | accepted := chan bool{cap: 1} |
| 137 | ran := chan int{cap: 1} |
| 138 | blocking_job := fn [started, release, finished] (mut ctx context.Context) ! { |
| 139 | _ = ctx |
| 140 | started <- true |
| 141 | _ := <-release |
| 142 | finished <- true |
| 143 | } |
| 144 | |
| 145 | pool.try_submit(blocking_job)! |
| 146 | wait_for_benchmark_bool(started, 'bounded admission pool job did not start')! |
| 147 | pool.try_submit(blocking_job)! |
| 148 | |
| 149 | mut timed_out := false |
| 150 | pool.submit_with_timeout(admission_timeout, fn (mut ctx context.Context) ! { |
| 151 | _ = ctx |
| 152 | }) or { |
| 153 | if err.msg() != 'async: timeout' { |
| 154 | return err |
| 155 | } |
| 156 | timed_out = true |
| 157 | } |
| 158 | if !timed_out { |
| 159 | release <- true |
| 160 | release <- true |
| 161 | pool.close()! |
| 162 | return error('submit_with_timeout accepted while the pool was full') |
| 163 | } |
| 164 | |
| 165 | submit_thread := spawn fn [mut pool, attempting, accepted, ran] () { |
| 166 | attempting <- true |
| 167 | pool.submit_with_context(context.background(), fn [ran] (mut ctx context.Context) ! { |
| 168 | _ = ctx |
| 169 | ran <- 1 |
| 170 | }) or { |
| 171 | accepted <- false |
| 172 | return |
| 173 | } |
| 174 | accepted <- true |
| 175 | }() |
| 176 | wait_for_benchmark_bool(attempting, 'bounded admission submitter did not start')! |
| 177 | |
| 178 | release <- true |
| 179 | wait_for_benchmark_bool(finished, 'bounded admission first job did not finish')! |
| 180 | wait_for_benchmark_bool(accepted, |
| 181 | 'submit_with_context did not accept after capacity opened')! |
| 182 | release <- true |
| 183 | total += read_benchmark_int(ran, 'bounded admission accepted job did not run')! |
| 184 | pool.close()! |
| 185 | submit_thread.wait() |
| 186 | } |
| 187 | return total |
| 188 | } |
| 189 | |
| 190 | fn bench_timeout(rounds int) !int { |
| 191 | mut total := 0 |
| 192 | for _ in 0 .. rounds { |
| 193 | xasync.with_timeout(1 * time.second, fn (mut ctx context.Context) ! { |
| 194 | _ = ctx |
| 195 | })! |
| 196 | total++ |
| 197 | } |
| 198 | return total |
| 199 | } |
| 200 | |
| 201 | fn bench_every(iterations int, interval_ms int) !int { |
| 202 | ctx, cancel := xasync.with_cancel() |
| 203 | ticks := chan int{cap: iterations + 4} |
| 204 | result := chan string{cap: 1} |
| 205 | interval := time.Duration(interval_ms) * time.millisecond |
| 206 | worker := spawn fn [ctx, ticks, result, interval] () { |
| 207 | xasync.every(ctx, interval, fn [ticks] (mut ctx context.Context) ! { |
| 208 | _ = ctx |
| 209 | ticks <- 1 |
| 210 | }) or { |
| 211 | result <- err.msg() |
| 212 | return |
| 213 | } |
| 214 | result <- 'ok' |
| 215 | }() |
| 216 | |
| 217 | mut total := 0 |
| 218 | for _ in 0 .. iterations { |
| 219 | select { |
| 220 | value := <-ticks { |
| 221 | total += value |
| 222 | } |
| 223 | 1 * time.second { |
| 224 | cancel() |
| 225 | worker.wait() |
| 226 | return error('every benchmark did not receive a tick') |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | cancel() |
| 231 | select { |
| 232 | msg := <-result { |
| 233 | if msg != 'context canceled' { |
| 234 | worker.wait() |
| 235 | return error('unexpected every benchmark result: ${msg}') |
| 236 | } |
| 237 | } |
| 238 | 1 * time.second { |
| 239 | worker.wait() |
| 240 | return error('every benchmark did not stop after cancellation') |
| 241 | } |
| 242 | } |
| 243 | worker.wait() |
| 244 | return total |
| 245 | } |
| 246 | |
| 247 | fn wait_for_benchmark_bool(signal chan bool, message string) ! { |
| 248 | select { |
| 249 | ok := <-signal { |
| 250 | if !ok { |
| 251 | return error(message) |
| 252 | } |
| 253 | } |
| 254 | 1 * time.second { |
| 255 | return error(message) |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | fn read_benchmark_int(signal chan int, message string) !int { |
| 261 | select { |
| 262 | value := <-signal { |
| 263 | return value |
| 264 | } |
| 265 | 1 * time.second { |
| 266 | return error(message) |
| 267 | } |
| 268 | } |
| 269 | return 0 |
| 270 | } |
| 271 | |