erlang: ring benchmark + results — Phase 3 closed
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
86
lib/erlang/bench_ring.sh
Executable file
86
lib/erlang/bench_ring.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Erlang-on-SX ring benchmark.
|
||||
#
|
||||
# Spawns N processes in a ring, passes a token N hops (one full round),
|
||||
# and reports wall-clock time + throughput. Aspirational target from
|
||||
# the plan is 1M processes; current sync-scheduler architecture caps out
|
||||
# orders of magnitude lower — this script measures honestly across a
|
||||
# range of N so the result/scaling is recorded.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/erlang/bench_ring.sh # default ladder
|
||||
# bash lib/erlang/bench_ring.sh 100 1000 5000 # custom Ns
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
NS=("$@")
|
||||
else
|
||||
NS=(10 100 500 1000)
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
# One-line Erlang program. Replaces __N__ with the size for each run.
|
||||
PROGRAM='Me = self(), N = __N__, Spawner = fun () -> receive {setup, Next} -> Loop = fun () -> receive {token, 0, Parent} -> Parent ! done; {token, K, Parent} -> Next ! {token, K-1, Parent}, Loop() end end, Loop() end end, BuildRing = fun (K, Acc) -> if K =:= 0 -> Acc; true -> BuildRing(K-1, [spawn(Spawner) | Acc]) end end, Pids = BuildRing(N, []), Wire = fun (Ps) -> case Ps of [P, Q | _] -> P ! {setup, Q}, Wire(tl(Ps)); [Last] -> Last ! {setup, hd(Pids)} end end, Wire(Pids), hd(Pids) ! {token, N, Me}, receive done -> done end'
|
||||
|
||||
run_n() {
|
||||
local n="$1"
|
||||
local prog="${PROGRAM//__N__/$n}"
|
||||
cat > "$TMPFILE" <<EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/erlang/tokenizer.sx")
|
||||
(load "lib/erlang/parser.sx")
|
||||
(load "lib/erlang/parser-core.sx")
|
||||
(load "lib/erlang/parser-expr.sx")
|
||||
(load "lib/erlang/parser-module.sx")
|
||||
(load "lib/erlang/transpile.sx")
|
||||
(load "lib/erlang/runtime.sx")
|
||||
(epoch 2)
|
||||
(eval "(erlang-eval-ast \"${prog//\"/\\\"}\")")
|
||||
EPOCHS
|
||||
|
||||
local start_s start_ns end_s end_ns elapsed_ms
|
||||
start_s=$(date +%s)
|
||||
start_ns=$(date +%N)
|
||||
out=$(timeout 300 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
end_s=$(date +%s)
|
||||
end_ns=$(date +%N)
|
||||
|
||||
local ok="false"
|
||||
if echo "$out" | grep -q ':name "done"'; then ok="true"; fi
|
||||
|
||||
# ms = (end_s - start_s)*1000 + (end_ns - start_ns)/1e6
|
||||
elapsed_ms=$(awk -v s1="$start_s" -v n1="$start_ns" -v s2="$end_s" -v n2="$end_ns" \
|
||||
'BEGIN { printf "%d", (s2 - s1) * 1000 + (n2 - n1) / 1000000 }')
|
||||
|
||||
if [ "$ok" = "true" ]; then
|
||||
local hops_per_s
|
||||
hops_per_s=$(awk -v n="$n" -v ms="$elapsed_ms" \
|
||||
'BEGIN { if (ms == 0) ms = 1; printf "%.0f", n * 1000 / ms }')
|
||||
printf " N=%-8s hops=%-8s %sms (%s hops/s)\n" "$n" "$n" "$elapsed_ms" "$hops_per_s"
|
||||
else
|
||||
printf " N=%-8s FAILED %sms\n" "$n" "$elapsed_ms"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Ring benchmark — sx_server.exe (synchronous scheduler)"
|
||||
echo
|
||||
for n in "${NS[@]}"; do
|
||||
run_n "$n"
|
||||
done
|
||||
echo
|
||||
echo "Note: 1M-process target from the plan is aspirational; the synchronous"
|
||||
echo "scheduler with shift-based suspension and dict-based env copies is not"
|
||||
echo "engineered for that scale. Numbers above are honest baselines."
|
||||
35
lib/erlang/bench_ring_results.md
Normal file
35
lib/erlang/bench_ring_results.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Ring Benchmark Results
|
||||
|
||||
Generated by `lib/erlang/bench_ring.sh` against `sx_server.exe` on the
|
||||
synchronous Erlang-on-SX scheduler.
|
||||
|
||||
| N (processes) | Hops | Wall-clock | Throughput |
|
||||
|---|---|---|---|
|
||||
| 10 | 10 | 907ms | 11 hops/s |
|
||||
| 50 | 50 | 2107ms | 24 hops/s |
|
||||
| 100 | 100 | 3827ms | 26 hops/s |
|
||||
| 500 | 500 | 17004ms | 29 hops/s |
|
||||
| 1000 | 1000 | 29832ms | 34 hops/s |
|
||||
|
||||
(Each `Nm` row spawns N processes connected in a ring and passes a
|
||||
single token N hops total — i.e. the token completes one full lap.)
|
||||
|
||||
## Status of the 1M-process target
|
||||
|
||||
Phase 3's stretch goal in `plans/erlang-on-sx.md` is a million-process
|
||||
ring benchmark. **That target is not met** in the current synchronous
|
||||
scheduler; extrapolating from the table above, 1M hops would take
|
||||
~30 000 s. Correctness is fine — the program runs at every measured
|
||||
size — but throughput is bound by per-hop overhead.
|
||||
|
||||
Per-hop cost is dominated by:
|
||||
- `er-env-copy` per fun clause attempt (whole-dict copy each time)
|
||||
- `call/cc` capture + `raise`/`guard` unwind on every `receive`
|
||||
- `er-q-delete-at!` rebuilds the mailbox backing list on every match
|
||||
- `dict-set!`/`dict-has?` lookups in the global processes table
|
||||
|
||||
To reach 1M-process throughput in this architecture would need at
|
||||
least: persistent (path-copying) envs, an inline scheduler that
|
||||
doesn't call/cc on the common path (msg-already-in-mailbox), and a
|
||||
linked-list mailbox. None of those are in scope for the Phase 3
|
||||
checkbox — captured here as the floor we're starting from.
|
||||
Reference in New Issue
Block a user