Current section
Files
Jump to
Current section
Files
examples/bench_channel.erl
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa _build/default/lib/erlang_python/ebin
%%% @doc Benchmark script for Channel API vs Reactor comparison.
%%%
%%% Run with:
%%% rebar3 compile && escript examples/bench_channel.erl
-mode(compile).
main(_Args) ->
io:format("~n========================================~n"),
io:format("Channel API Benchmark~n"),
io:format("========================================~n~n"),
%% Start the application
{ok, _} = application:ensure_all_started(erlang_python),
{ok, _} = py:start_contexts(),
ok = py_channel:register_callbacks(),
%% Print system info
io:format("System Information:~n"),
io:format(" Erlang/OTP: ~s~n", [erlang:system_info(otp_release)]),
{ok, PyVer} = py:version(),
io:format(" Python: ~s~n", [PyVer]),
io:format("~n"),
%% Run benchmarks
run_channel_throughput_bench(),
run_channel_vs_reactor_bench(),
run_channel_latency_bench(),
io:format("~n========================================~n"),
io:format("Benchmark Complete~n"),
io:format("========================================~n"),
halt(0).
run_channel_throughput_bench() ->
io:format("~n--- Channel Throughput Benchmark ---~n"),
io:format("Messages per batch: 1000~n~n"),
Sizes = [64, 256, 1024, 4096, 16384],
io:format("~8s | ~12s | ~12s | ~12s~n",
["Size", "Send (msg/s)", "Recv (msg/s)", "Round (msg/s)"]),
io:format("~s~n", [string:copies("-", 52)]),
lists:foreach(fun(Size) ->
{ok, Ch} = py_channel:new(),
Data = binary:copy(<<0>>, Size),
Iterations = 1000,
%% Benchmark send
SendStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data)
end, lists:seq(1, Iterations)),
SendEnd = erlang:monotonic_time(microsecond),
SendTime = (SendEnd - SendStart) / 1000000,
SendRate = Iterations / SendTime,
%% Benchmark receive (drain the queue)
RecvStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
{ok, _} = py_nif:channel_try_receive(Ch)
end, lists:seq(1, Iterations)),
RecvEnd = erlang:monotonic_time(microsecond),
RecvTime = (RecvEnd - RecvStart) / 1000000,
RecvRate = Iterations / RecvTime,
%% Benchmark round-trip (send + receive)
RoundStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data),
{ok, _} = py_nif:channel_try_receive(Ch)
end, lists:seq(1, Iterations)),
RoundEnd = erlang:monotonic_time(microsecond),
RoundTime = (RoundEnd - RoundStart) / 1000000,
RoundRate = Iterations / RoundTime,
io:format("~8B | ~12w | ~12w | ~12w~n",
[Size, round(SendRate), round(RecvRate), round(RoundRate)]),
py_channel:close(Ch)
end, Sizes),
ok.
run_channel_vs_reactor_bench() ->
io:format("~n--- Channel vs Reactor Comparison ---~n"),
io:format("Iterations: 5000~n~n"),
Code = <<"
import time
import socket
import erlang
import erlang.reactor as reactor
def bench_channel_python(iterations=5000):
'''Benchmark Channel from Python side'''
results = {}
sizes = [64, 256, 1024, 4096, 16384]
for size in sizes:
test_data = b'X' * size
# We can't easily create channels from Python yet,
# so we benchmark try_receive on pre-filled channels
# This measures Python-side overhead
# Measure try_receive with None ref (measures call overhead)
start = time.perf_counter()
for _ in range(iterations):
try:
# Just measure the erlang.call overhead
pass
except:
pass
call_time = time.perf_counter() - start
results[size] = {
'call_overhead_ms': call_time * 1000,
'ops_per_sec': iterations / max(call_time, 0.0001),
}
return results
def bench_reactor_protocol(iterations=200):
'''Benchmark Reactor Protocol pattern'''
results = {}
sizes = [64, 256, 1024, 4096, 16384]
class EchoProtocol(reactor.Protocol):
def data_received(self, data):
self.write_buffer.extend(data)
return 'write_pending'
def write_ready(self):
if not self.write_buffer:
return 'read_pending'
written = self.write(bytes(self.write_buffer))
del self.write_buffer[:written]
return 'continue' if self.write_buffer else 'read_pending'
for size in sizes:
test_data = b'X' * size
times = []
for _ in range(iterations):
s1, s2 = socket.socketpair()
s1.setblocking(False)
s2.setblocking(False)
try:
reactor.set_protocol_factory(EchoProtocol)
reactor.init_connection(s1.fileno(), {'type': 'test'})
s2.send(test_data)
start = time.perf_counter()
action = reactor.on_read_ready(s1.fileno())
elapsed = time.perf_counter() - start
times.append(elapsed)
reactor.close_connection(s1.fileno())
finally:
s1.close()
s2.close()
import statistics
avg_time = statistics.mean(times)
results[size] = {
'avg_time_ms': avg_time * 1000,
'ops_per_sec': 1.0 / avg_time,
}
return results
_reactor_results = bench_reactor_protocol()
">>,
ok = py:exec(Code),
{ok, ReactorResults} = py:eval(<<"_reactor_results">>),
io:format("Reactor Protocol (echo pattern):~n"),
io:format("~8s | ~12s | ~12s~n",
["Size", "Avg (ms)", "Ops/sec"]),
io:format("~s~n", [string:copies("-", 36)]),
Sizes = [64, 256, 1024, 4096, 16384],
lists:foreach(fun(Size) ->
Data = maps:get(Size, ReactorResults),
AvgMs = maps:get(<<"avg_time_ms">>, Data),
OpsPerSec = maps:get(<<"ops_per_sec">>, Data),
io:format("~8B | ~12.3f | ~12w~n",
[Size, AvgMs, round(OpsPerSec)])
end, Sizes),
%% Now compare with Channel round-trip
io:format("~nChannel Round-trip (Erlang send + NIF receive):~n"),
io:format("~8s | ~12s | ~12s | ~8s~n",
["Size", "Avg (ms)", "Ops/sec", "vs React"]),
io:format("~s~n", [string:copies("-", 48)]),
lists:foreach(fun(Size) ->
{ok, Ch} = py_channel:new(),
Data = binary:copy(<<0>>, Size),
Iterations = 1000,
Start = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data),
{ok, _} = py_nif:channel_try_receive(Ch)
end, lists:seq(1, Iterations)),
End = erlang:monotonic_time(microsecond),
TotalTime = (End - Start) / 1000000,
AvgMs = (TotalTime / Iterations) * 1000,
OpsPerSec = Iterations / TotalTime,
ReactorData = maps:get(Size, ReactorResults),
ReactorOps = maps:get(<<"ops_per_sec">>, ReactorData),
Ratio = OpsPerSec / ReactorOps,
io:format("~8B | ~12.3f | ~12w | ~.1fx~n",
[Size, AvgMs, round(OpsPerSec), Ratio]),
py_channel:close(Ch)
end, Sizes),
ok.
run_channel_latency_bench() ->
io:format("~n--- Channel Latency Distribution ---~n"),
io:format("Iterations: 10000~n~n"),
Sizes = [64, 1024, 16384],
io:format("~8s | ~10s | ~10s | ~10s | ~10s~n",
["Size", "Min (us)", "Avg (us)", "P99 (us)", "Max (us)"]),
io:format("~s~n", [string:copies("-", 56)]),
lists:foreach(fun(Size) ->
{ok, Ch} = py_channel:new(),
Data = binary:copy(<<0>>, Size),
Iterations = 10000,
%% Collect latencies
Latencies = lists:map(fun(_) ->
Start = erlang:monotonic_time(microsecond),
ok = py_channel:send(Ch, Data),
{ok, _} = py_nif:channel_try_receive(Ch),
End = erlang:monotonic_time(microsecond),
End - Start
end, lists:seq(1, Iterations)),
Sorted = lists:sort(Latencies),
Min = hd(Sorted),
Max = lists:last(Sorted),
Avg = lists:sum(Latencies) / Iterations,
P99Idx = round(Iterations * 0.99),
P99 = lists:nth(P99Idx, Sorted),
io:format("~8B | ~10.1f | ~10.1f | ~10.1f | ~10.1f~n",
[Size, float(Min), Avg, float(P99), float(Max)]),
py_channel:close(Ch)
end, Sizes),
ok.