Skip to content
  •  
  •  
  •  
89 changes: 89 additions & 0 deletions bench/pool_call_many_bench.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Benchmark: Pool.call_many vs repeated Pool.call
#
# Demonstrates the overhead savings from batching multiple function calls
# into a single checkout/checkin cycle.
#
# Run: mix run bench/pool_call_many_bench.exs

math_wasm = "fixtures/math.wasm"
{:ok, pool} = Firebird.Pool.start_link(wasm: math_wasm, size: 4)

# Build call lists of various sizes
calls_5 = for i <- 1..5, do: {:add, [i, i]}
calls_20 = for i <- 1..20, do: {:add, [i, i]}
calls_100 = for i <- 1..100, do: {:add, [i, i]}

defmodule BenchHelper do
def repeated_calls(pool, calls) do
Enum.map(calls, fn {func, args} ->
{:ok, result} = Firebird.Pool.call(pool, func, args)
result
end)
end
end

IO.puts("=" |> String.duplicate(70))
IO.puts("Pool.call_many vs repeated Pool.call benchmark")
IO.puts("=" |> String.duplicate(70))

for {label, calls} <- [{"5 calls", calls_5}, {"20 calls", calls_20}, {"100 calls", calls_100}] do
IO.puts("\n--- #{label} ---")

# Warmup
for _ <- 1..10 do
{:ok, _} = Firebird.Pool.call_many(pool, calls)
BenchHelper.repeated_calls(pool, calls)
end

iterations = 200

# Benchmark call_many
many_times = for _ <- 1..iterations do
{time, {:ok, _results}} = :timer.tc(fn -> Firebird.Pool.call_many(pool, calls) end)
time
end

# Benchmark repeated call
repeat_times = for _ <- 1..iterations do
{time, _results} = :timer.tc(fn -> BenchHelper.repeated_calls(pool, calls) end)
time
end

many_avg = Enum.sum(many_times) / iterations
repeat_avg = Enum.sum(repeat_times) / iterations
speedup = repeat_avg / max(many_avg, 0.1)

many_p50 = Enum.sort(many_times) |> Enum.at(div(iterations, 2))
repeat_p50 = Enum.sort(repeat_times) |> Enum.at(div(iterations, 2))

IO.puts(" call_many: avg=#{Float.round(many_avg, 1)}μs p50=#{many_p50}μs")
IO.puts(" repeated call: avg=#{Float.round(repeat_avg, 1)}μs p50=#{repeat_p50}μs")
IO.puts(" speedup: #{Float.round(speedup, 2)}x faster with call_many")
end

# Also benchmark against pure Elixir equivalent
IO.puts("\n--- call_many vs native Elixir (100 additions) ---")

native_times = for _ <- 1..200 do
{time, _} = :timer.tc(fn ->
for i <- 1..100, do: i + i
end)
time
end

wasm_times = for _ <- 1..200 do
{time, {:ok, _}} = :timer.tc(fn ->
Firebird.Pool.call_many(pool, calls_100)
end)
time
end

native_avg = Enum.sum(native_times) / 200
wasm_avg = Enum.sum(wasm_times) / 200

IO.puts(" WASM call_many: avg=#{Float.round(wasm_avg, 1)}μs")
IO.puts(" Native Elixir: avg=#{Float.round(native_avg, 1)}μs")
IO.puts(" Overhead ratio: #{Float.round(wasm_avg / max(native_avg, 0.1), 1)}x")

Firebird.Pool.stop(pool)
IO.puts("\nDone.")
47 changes: 47 additions & 0 deletions bench/wasm_collection.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Benchmark: WasmRunner collection helpers vs manual loops
#
# Run: mix run bench/wasm_collection.exs
#
# Compares the overhead of WasmRunner.map/4, reduce/5, and filter/4
# against manual with_instance + Enum loops.

alias Firebird.WasmRunner

math_wasm = "fixtures/math.wasm"
{:ok, compiled} = WasmRunner.precompile(math_wasm)

inputs = Enum.to_list(1..50)

Benchee.run(
%{
"WasmRunner.map (from file)" => fn ->
{:ok, _} = WasmRunner.map(math_wasm, :fibonacci, inputs)
end,
"WasmRunner.map (precompiled)" => fn ->
{:ok, _} = WasmRunner.map(compiled, :fibonacci, inputs)
end,
"manual with_instance + Enum.map" => fn ->
{:ok, _results} =
WasmRunner.with_instance(math_wasm, fn pid ->
Enum.map(inputs, fn n ->
WasmRunner.call_single!(pid, :fibonacci, [n])
end)
end)
end,
"WasmRunner.reduce (from file)" => fn ->
{:ok, _} = WasmRunner.reduce(math_wasm, :add, inputs, 0)
end,
"WasmRunner.reduce (precompiled)" => fn ->
{:ok, _} = WasmRunner.reduce(compiled, :add, inputs, 0)
end,
"WasmRunner.filter (from file)" => fn ->
{:ok, _} = WasmRunner.filter(math_wasm, :is_prime, inputs)
end,
"WasmRunner.filter (precompiled)" => fn ->
{:ok, _} = WasmRunner.filter(compiled, :is_prime, inputs)
end
},
warmup: 1,
time: 3,
memory_time: 1
)
75 changes: 75 additions & 0 deletions bench/wasm_pipe.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Benchmark: WasmRunner.pipe/2 vs sequential WasmRunner.run/2
#
# Compares the performance of:
# 1. pipe/2 - loads once, chains calls
# 2. Multiple run/2 - loads per call (baseline)
# 3. with_instance + manual chaining
#
# Run: mix run bench/wasm_pipe.exs

alias Firebird.WasmRunner

math_wasm = "fixtures/math.wasm"

IO.puts("=== WasmRunner.pipe/2 Benchmark ===\n")

# Warmup
WasmRunner.pipe(math_wasm, [{:add, [5, 3]}, {:fibonacci, []}, {:multiply, [:pipe, 2]}])

# Benchmark pipe/2 (3-stage pipeline)
pipe_result = WasmRunner.benchmark(math_wasm, :add, [[5, 3]], iterations: 100)
IO.puts("Single call baseline: avg=#{pipe_result.avg_us}μs")

# Manual timing of pipe vs separate runs
iterations = 100

pipe_times =
for _ <- 1..iterations do
{elapsed, _} =
:timer.tc(fn ->
WasmRunner.pipe!(math_wasm, [
{:add, [5, 3]},
{:fibonacci, []},
{:multiply, [:pipe, 2]}
])
end)

elapsed
end

separate_times =
for _ <- 1..iterations do
{elapsed, _} =
:timer.tc(fn ->
r1 = WasmRunner.run!(math_wasm, add: [5, 3])
r2 = WasmRunner.run!(math_wasm, fibonacci: [r1])
WasmRunner.run!(math_wasm, multiply: [r2, 2])
end)

elapsed
end

with_instance_times =
for _ <- 1..iterations do
{elapsed, _} =
:timer.tc(fn ->
WasmRunner.with_instance(math_wasm, fn pid ->
r1 = WasmRunner.call_single!(pid, :add, [5, 3])
r2 = WasmRunner.call_single!(pid, :fibonacci, [r1])
WasmRunner.call_single!(pid, :multiply, [r2, 2])
end)
end)

elapsed
end

pipe_avg = Enum.sum(pipe_times) / iterations
separate_avg = Enum.sum(separate_times) / iterations
with_instance_avg = Enum.sum(with_instance_times) / iterations

IO.puts("\n3-stage pipeline (#{iterations} iterations):")
IO.puts(" pipe/2: avg=#{Float.round(pipe_avg, 1)}μs")
IO.puts(" 3x run/2: avg=#{Float.round(separate_avg, 1)}μs")
IO.puts(" with_instance: avg=#{Float.round(with_instance_avg, 1)}μs")
IO.puts(" pipe speedup vs separate: #{Float.round(separate_avg / pipe_avg, 2)}x")
IO.puts(" pipe vs with_instance: #{Float.round(pipe_avg / with_instance_avg, 2)}x overhead")
149 changes: 149 additions & 0 deletions bench/wasm_runner_run.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Benchmark: WasmRunner.run/2 vs native Elixir
#
# Run: mix run bench/wasm_runner_run.exs
#
# Compares the one-shot WasmRunner.run/2 API against:
# 1. Pure Elixir equivalent functions
# 2. Pre-loaded WASM instance (to isolate call overhead from load overhead)
# 3. Precompiled module (to show compilation cache benefit)

defmodule NativeElixir do
def add(a, b), do: a + b
def multiply(a, b), do: a * b

def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n) when n > 1, do: fibonacci(n - 1) + fibonacci(n - 2)
end

wasm_path = "fixtures/math.wasm"

IO.puts("=" |> String.duplicate(70))
IO.puts("WasmRunner.run/2 Benchmark")
IO.puts("=" |> String.duplicate(70))

# --- Section 1: One-shot run/2 vs Elixir ---
IO.puts("\n## 1. One-shot execution: run/2 vs pure Elixir\n")

iterations = 100

# Warmup
for _ <- 1..5 do
Firebird.WasmRunner.run(wasm_path, add: [5, 3])
NativeElixir.add(5, 3)
end

# Benchmark run/2 (includes load + call + cleanup each time)
run_times =
for _ <- 1..iterations do
{us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(wasm_path, add: [5, 3]) end)
us
end

# Benchmark Elixir
elixir_times =
for _ <- 1..iterations do
{us, _} = :timer.tc(fn -> NativeElixir.add(5, 3) end)
us
end

run_avg = Enum.sum(run_times) / iterations
elixir_avg = Enum.sum(elixir_times) / iterations
run_p50 = Enum.sort(run_times) |> Enum.at(div(iterations, 2))
elixir_p50 = Enum.sort(elixir_times) |> Enum.at(div(iterations, 2))

IO.puts(" WasmRunner.run/2 (add): avg=#{Float.round(run_avg, 1)}μs p50=#{run_p50}μs")
IO.puts(" Pure Elixir (add): avg=#{Float.round(elixir_avg, 1)}μs p50=#{elixir_p50}μs")
IO.puts(" Overhead ratio: #{Float.round(run_avg / max(elixir_avg, 0.1), 1)}x")

# --- Section 2: run/2 with multiple calls ---
IO.puts("\n## 2. Multiple calls in one run/2 (amortizes load cost)\n")

multi_times =
for _ <- 1..iterations do
{us, _} =
:timer.tc(fn ->
Firebird.WasmRunner.run(wasm_path,
add: [5, 3],
multiply: [4, 7],
fibonacci: [10]
)
end)
us
end

multi_avg = Enum.sum(multi_times) / iterations
per_call = multi_avg / 3

IO.puts(" 3 calls in one run/2: avg=#{Float.round(multi_avg, 1)}μs total")
IO.puts(" Per-call amortized: avg=#{Float.round(per_call, 1)}μs")
IO.puts(" vs single run/2: #{Float.round(multi_avg / run_avg, 2)}x (ideal: 1.0x + call overhead)")

# --- Section 3: Precompiled vs cold run/2 ---
IO.puts("\n## 3. Precompiled module vs cold start\n")

{:ok, compiled} = Firebird.WasmRunner.precompile(wasm_path)

precompiled_times =
for _ <- 1..iterations do
{us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(compiled, add: [5, 3]) end)
us
end

pre_avg = Enum.sum(precompiled_times) / iterations
pre_p50 = Enum.sort(precompiled_times) |> Enum.at(div(iterations, 2))

IO.puts(" Cold run/2: avg=#{Float.round(run_avg, 1)}μs p50=#{run_p50}μs")
IO.puts(" Precompiled run/2: avg=#{Float.round(pre_avg, 1)}μs p50=#{pre_p50}μs")
IO.puts(" Speedup: #{Float.round(run_avg / max(pre_avg, 0.1), 1)}x")

# --- Section 4: Pre-loaded instance (call overhead only) ---
IO.puts("\n## 4. Pre-loaded instance (isolates call overhead)\n")

{:ok, pid} = Firebird.WasmRunner.start(wasm_path)

loaded_times =
for _ <- 1..iterations do
{us, _} = :timer.tc(fn -> Firebird.WasmRunner.call_single(pid, :add, [5, 3]) end)
us
end

Firebird.stop(pid)

loaded_avg = Enum.sum(loaded_times) / iterations
loaded_p50 = Enum.sort(loaded_times) |> Enum.at(div(iterations, 2))

IO.puts(" Pre-loaded call_single: avg=#{Float.round(loaded_avg, 1)}μs p50=#{loaded_p50}μs")
IO.puts(" run/2 overhead (load): ~#{Float.round(run_avg - loaded_avg, 1)}μs per call")

# --- Section 5: fibonacci(30) - compute-heavy ---
IO.puts("\n## 5. Compute-heavy: fibonacci(30)\n")

fib_wasm_times =
for _ <- 1..50 do
{us, _} = :timer.tc(fn -> Firebird.WasmRunner.run(wasm_path, fibonacci: [30]) end)
us
end

fib_elixir_times =
for _ <- 1..50 do
{us, _} = :timer.tc(fn -> NativeElixir.fibonacci(30) end)
us
end

fib_wasm_avg = Enum.sum(fib_wasm_times) / 50
fib_elixir_avg = Enum.sum(fib_elixir_times) / 50

IO.puts(" WASM run/2 fib(30): avg=#{Float.round(fib_wasm_avg, 1)}μs")
IO.puts(" Elixir fib(30): avg=#{Float.round(fib_elixir_avg, 1)}μs")

if fib_wasm_avg < fib_elixir_avg do
IO.puts(" → WASM wins by #{Float.round(fib_elixir_avg / fib_wasm_avg, 1)}x")
else
IO.puts(" → Elixir wins by #{Float.round(fib_wasm_avg / fib_elixir_avg, 1)}x (WASM has load overhead)")
end

IO.puts("\n" <> String.duplicate("=", 70))
IO.puts("Summary: run/2 is the simplest API for one-shot WASM execution.")
IO.puts("For hot paths, use precompiled modules or pools to amortize load cost.")
IO.puts(String.duplicate("=", 70))
Loading
Loading