Skip to content

Commit 2578445

Browse files
committed
Benchmarks
1 parent 955d7f4 commit 2578445

14 files changed

Lines changed: 1697 additions & 125 deletions

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ Wrap your untrusted NIFs so that they can never crash your node.
1818
> For right now, it can be used to wrap any function or MFA that might cause some sort of crash on the BEAM node in order to keep that function safe and isolated.
1919
> It currently carries performance penalties of peer node startup and code loading, however warm node pooling is in development to optimize this performance penalty.
2020
21+
## Benchmarks
22+
23+
Benchmarks can be found in the [`bench`](./bench/) directory. As of v0.1.0, SafeNIF has not implemented pooling of peer nodes.
24+
This means that it currently incurs the high cost of starting up a peer node for every call, which can take anywhere from 100ms to over a second,
25+
depending on how much code needs to be loaded onto the peer node. You can see from the benchmarks that out of the three methods benchmarked (CLI+Port, NIF, SafeNIF),
26+
SafeNIF is currently the slowest due to this incurred cost.
27+
28+
Adding pooling will be implemented in v0.2.0 and should make this far more efficient as we will only need to incur the cost once per node created.
29+
It should be noted that pooling will incur different costs - namely memory and CPU since it spins up a node on the same machine.
30+
2131
**The following information was generated by Claude and Reviewed by @probably-not. If issues in this README are found, feel free to open up a PR to fix them!**
2232

2333
## The Problem

bench/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SafeNIF Benchmarks
2+
3+
Most of the code in this directory was generated by Claude and reviewed by @probably-not.
4+
5+
This directory contains benchmarks comparing SafeNIF's performance characteristics.
6+
7+
## Running Benchmarks
8+
9+
Ensure dependencies are installed and NIF/Port are compiled:
10+
11+
```bash
12+
mix deps.get
13+
mix compile
14+
```
15+
16+
Then run individual benchmarks (must use `--sname` for distribution):
17+
18+
```bash
19+
# Main comparison: NIF vs SafeNIF vs Port
20+
elixir --sname bench -S mix run bench/nif_vs_port.exs
21+
22+
# Payload size impact
23+
elixir --sname bench -S mix run bench/payload_sizes.exs
24+
```
25+
26+
## Benchmark Files
27+
28+
| File | Description |
29+
| ------------------- | ------------------------------------------------- |
30+
| `nif_vs_port.exs` | Compares Direct NIF, SafeNIF, and Port approaches |
31+
| `payload_sizes.exs` | Tests how data size affects overhead |
32+
33+
## Support Modules
34+
35+
| File | Description |
36+
| ----------------- | ----------------------------------------------- |
37+
| `helpers.ex` | Shared setup/teardown and Benchee configuration |
38+
| `port_wrapper.ex` | GenServer wrapping the Port executable |
39+
40+
## Results
41+
42+
Results are saved to `bench/results/` as Markdown files. These are gitignored
43+
by default, but you may want to commit significant baseline results.
44+
45+
## Understanding the Results
46+
47+
### Direct NIF (Unsafe)
48+
- **Fastest** - No isolation overhead
49+
- **Dangerous** - Crashes take down the entire node
50+
- Use as baseline for "best possible" performance
51+
52+
### Port
53+
- **Traditional safe approach** - Isolation via OS process
54+
- **Overhead**: Process spawn + stdio + serialization
55+
- Good for: Stateless operations, moderate throughput needs
56+
57+
### SafeNIF
58+
- **Peer node isolation** - Each call spawns a BEAM node
59+
- **Overhead**: BEAM startup + code loading + distribution
60+
- Good for: Untrusted code, crashy NIFs, safety > speed
61+
62+
### When to Use What
63+
64+
| Approach | Use When |
65+
| ---------- | ----------------------------------------------------------------- |
66+
| Direct NIF | Trusted code, high performance needed, crashes acceptable |
67+
| Port | Moderate isolation needs, stateless ops, reasonable throughput |
68+
| SafeNIF | Untrusted code, must not crash main node, throughput not critical |

bench/helpers.ex

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule SafeNIF.Bench.Helpers do
2+
@moduledoc """
3+
Shared helpers for SafeNIF benchmarks.
4+
5+
Generated by Claude and reviewed by @probably-not
6+
"""
7+
8+
alias SafeNIF.Bench.PortWrapper
9+
alias SafeNIFTest.TestNIF
10+
11+
@doc """
12+
Ensures all benchmark dependencies are ready.
13+
Returns {:ok, context} or {:error, reason}.
14+
"""
15+
def setup do
16+
with :ok <- ensure_distributed(),
17+
:ok <- ensure_nif_loaded(),
18+
:ok <- ensure_port_exists() do
19+
{:ok, %{}}
20+
end
21+
end
22+
23+
@doc """
24+
Cleans up benchmark resources.
25+
"""
26+
def teardown(_ctx), do: :ok
27+
28+
defp ensure_distributed do
29+
if Node.alive?() do
30+
:ok
31+
else
32+
{:error, :not_distributed}
33+
end
34+
end
35+
36+
defp ensure_nif_loaded do
37+
case TestNIF.load_nif() do
38+
:ok -> :ok
39+
{:error, reason} -> {:error, {:nif_not_loaded, reason}}
40+
end
41+
end
42+
43+
defp ensure_port_exists do
44+
if File.exists?(PortWrapper.port_path()) do
45+
:ok
46+
else
47+
{:error, {:port_not_found, PortWrapper.port_path()}}
48+
end
49+
end
50+
51+
@doc """
52+
Standard Benchee configuration.
53+
"""
54+
def benchee_config(opts \\ []) do
55+
markdown_file = Keyword.get(opts, :markdown_file, "bench/results/latest.md")
56+
57+
[
58+
warmup: 2,
59+
time: 10,
60+
memory_time: 2,
61+
reduction_time: 2,
62+
formatters: [
63+
Benchee.Formatters.Console,
64+
{Benchee.Formatters.Markdown, file: markdown_file}
65+
]
66+
]
67+
end
68+
end

bench/nif_vs_port.exs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# SafeNIF Benchmark: NIF vs SafeNIF vs CLI Port
2+
#
3+
# Compares three approaches to running native code:
4+
# 1. Direct NIF call - fastest, but crashes take down the node
5+
# 2. SafeNIF wrapped - isolated via peer nodes (new BEAM per call)
6+
# 3. CLI Port - isolated via OS process (new process per call)
7+
#
8+
# Run with:
9+
# elixir --sname bench -S mix run bench/nif_vs_port.exs
10+
# Generated by Claude and reviewed by @probably-not
11+
12+
alias SafeNIF.Bench.Helpers
13+
alias SafeNIF.Bench.PortWrapper
14+
alias SafeNIFTest.TestNIF
15+
16+
IO.puts("""
17+
================================================================================
18+
SafeNIF Benchmark: NIF vs SafeNIF vs CLI Port
19+
================================================================================
20+
21+
This benchmark compares execution overhead for three isolation strategies:
22+
23+
• Direct NIF: No isolation - crashes kill the node
24+
• CLI Port: OS process isolation - spawns new process per call
25+
• SafeNIF: Peer node isolation - spawns new BEAM node per call
26+
27+
Both CLI Port and SafeNIF provide true isolation (fresh process per call).
28+
29+
""")
30+
31+
# Setup
32+
case Helpers.setup() do
33+
{:ok, _ctx} ->
34+
IO.puts("✓ Setup complete\n")
35+
36+
# Verify everything works before benchmarking
37+
IO.puts("Verifying implementations...")
38+
:ok = TestNIF.safe_noop()
39+
:ok = PortWrapper.noop()
40+
{:ok, :ok} = SafeNIF.wrap({TestNIF, :safe_noop, []})
41+
42+
42 = TestNIF.safe_add(17, 25)
43+
42 = PortWrapper.add(17, 25)
44+
{:ok, 42} = SafeNIF.wrap({TestNIF, :safe_add, [17, 25]})
45+
46+
IO.puts("✓ All implementations working\n")
47+
48+
# Run benchmarks
49+
IO.puts("Running benchmarks...\n")
50+
51+
Benchee.run(
52+
%{
53+
"Direct NIF (unsafe)" => fn -> TestNIF.safe_noop() end,
54+
"CLI Port" => fn -> PortWrapper.noop() end,
55+
"SafeNIF" => fn -> SafeNIF.wrap({TestNIF, :safe_noop, []}) end
56+
},
57+
Helpers.benchee_config(markdown_file: "bench/results/noop_comparison.md")
58+
)
59+
60+
IO.puts("\n" <> String.duplicate("=", 80) <> "\n")
61+
62+
Benchee.run(
63+
%{
64+
"Direct NIF (unsafe)" => fn -> TestNIF.safe_add(17, 25) end,
65+
"CLI Port" => fn -> PortWrapper.add(17, 25) end,
66+
"SafeNIF" => fn -> SafeNIF.wrap({TestNIF, :safe_add, [17, 25]}) end
67+
},
68+
Helpers.benchee_config(markdown_file: "bench/results/add_comparison.md")
69+
)
70+
71+
IO.puts("""
72+
73+
================================================================================
74+
Benchmark complete!
75+
76+
Results saved to:
77+
• bench/results/noop_comparison.md
78+
• bench/results/add_comparison.md
79+
80+
Key insights:
81+
• Direct NIF shows the baseline (no isolation overhead)
82+
• CLI Port shows OS process spawn overhead (~1-10ms typical)
83+
• SafeNIF shows BEAM node spawn overhead (~500-2000ms typical)
84+
85+
Note: SafeNIF is designed for SAFETY, not speed. Use it when you need
86+
BEAM-level isolation (crash protection, code loading, distribution).
87+
================================================================================
88+
""")
89+
90+
{:error, :not_distributed} ->
91+
IO.puts("""
92+
❌ Node is not running in distributed mode.
93+
94+
Run benchmarks with:
95+
elixir --sname bench -S mix run bench/nif_vs_port.exs
96+
""")
97+
98+
System.halt(1)
99+
100+
{:error, reason} ->
101+
IO.puts("❌ Setup failed: #{inspect(reason)}")
102+
IO.puts("\nMake sure to run 'mix compile' first to build the NIF and Port.")
103+
System.halt(1)
104+
end

bench/payload_sizes.exs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# SafeNIF Benchmark: Payload Size Impact
2+
#
3+
# Tests how different payload sizes affect overhead for each approach.
4+
# Important for understanding when SafeNIF is appropriate.
5+
#
6+
# Run with:
7+
# elixir --sname bench -S mix run bench/payload_sizes.exs
8+
# Generated by Claude and reviewed by @probably-not
9+
10+
alias SafeNIF.Bench.Helpers
11+
alias SafeNIF.Bench.PortWrapper
12+
alias SafeNIFTest.TestNIF
13+
14+
IO.puts("""
15+
================================================================================
16+
SafeNIF Benchmark: Payload Size Impact
17+
================================================================================
18+
19+
Tests how data size affects overhead for NIF vs CLI Port vs SafeNIF.
20+
All three use the same echo operation for fair comparison.
21+
22+
""")
23+
24+
case Helpers.setup() do
25+
{:ok, _ctx} ->
26+
IO.puts("✓ Setup complete\n")
27+
28+
# Test payloads
29+
payloads = %{
30+
"tiny (10 B)" => :crypto.strong_rand_bytes(10),
31+
"small (100 B)" => :crypto.strong_rand_bytes(100),
32+
"medium (1 KB)" => :crypto.strong_rand_bytes(1_000),
33+
"large (10 KB)" => :crypto.strong_rand_bytes(10_000),
34+
"xlarge (100 KB)" => :crypto.strong_rand_bytes(100_000)
35+
}
36+
37+
# Verify all implementations work with all payload sizes
38+
IO.puts("Verifying echo implementations...")
39+
40+
for {name, data} <- payloads do
41+
^data = TestNIF.safe_echo(data)
42+
^data = PortWrapper.echo(data)
43+
{:ok, ^data} = SafeNIF.wrap({TestNIF, :safe_echo, [data]})
44+
IO.puts(" ✓ #{name}")
45+
end
46+
47+
IO.puts("")
48+
49+
# Benchmark each payload size
50+
for {size_name, data} <- payloads do
51+
IO.puts("Benchmarking #{size_name}...\n")
52+
53+
safe_filename =
54+
size_name
55+
|> String.downcase()
56+
|> String.replace(~r/[^a-z0-9]/, "_")
57+
|> String.replace(~r/_+/, "_")
58+
|> String.trim("_")
59+
60+
Benchee.run(
61+
%{
62+
"Direct NIF" => fn -> TestNIF.safe_echo(data) end,
63+
"CLI Port" => fn -> PortWrapper.echo(data) end,
64+
"SafeNIF" => fn -> SafeNIF.wrap({TestNIF, :safe_echo, [data]}) end
65+
},
66+
Helpers.benchee_config(markdown_file: "bench/results/payload_#{safe_filename}.md")
67+
)
68+
69+
IO.puts("\n" <> String.duplicate("-", 80) <> "\n")
70+
end
71+
72+
IO.puts("""
73+
74+
================================================================================
75+
Payload size benchmark complete!
76+
77+
Results saved to bench/results/payload_*.md
78+
79+
Key insights:
80+
• Direct NIF is constant time (just returns the term)
81+
• CLI Port overhead scales with payload (data through stdio + process spawn)
82+
• SafeNIF overhead is dominated by node startup, payload is minor factor
83+
================================================================================
84+
""")
85+
86+
{:error, :not_distributed} ->
87+
IO.puts("""
88+
❌ Node is not running in distributed mode.
89+
90+
Run benchmarks with:
91+
elixir --sname bench -S mix run bench/payload_sizes.exs
92+
""")
93+
94+
System.halt(1)
95+
96+
{:error, reason} ->
97+
IO.puts("❌ Setup failed: #{inspect(reason)}")
98+
System.halt(1)
99+
end

0 commit comments

Comments
 (0)