Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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