Current section

Files

Jump to
erlang_python examples bench_byte_channel.erl
Raw

examples/bench_byte_channel.erl

#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa _build/default/lib/erlang_python/ebin
%%% @doc Benchmark comparing Channel, ByteChannel, and PyBuffer.
%%%
%%% Run with:
%%% rebar3 compile && escript examples/bench_byte_channel.erl
-mode(compile).
main(_Args) ->
io:format("~n========================================~n"),
io:format("Channel vs ByteChannel vs PyBuffer~n"),
io:format("========================================~n~n"),
%% Start the application
{ok, _} = application:ensure_all_started(erlang_python),
{ok, _} = py:start_contexts(),
ok = py_channel:register_callbacks(),
ok = py_byte_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_erlang_send_bench(),
run_erlang_roundtrip_bench(),
run_python_receive_bench(),
run_streaming_bench(),
io:format("~n========================================~n"),
io:format("Benchmark Complete~n"),
io:format("========================================~n"),
halt(0).
%% Benchmark Erlang-side send performance (no Python involved)
run_erlang_send_bench() ->
io:format("~n--- Erlang Send Throughput (no Python) ---~n"),
io:format("Iterations: 10000~n~n"),
Sizes = [64, 256, 1024, 4096, 16384],
Iterations = 10000,
io:format("~8s | ~14s | ~14s | ~14s~n",
["Size", "Channel", "ByteChannel", "PyBuffer"]),
io:format("~8s | ~14s | ~14s | ~14s~n",
["(bytes)", "(msg/sec)", "(msg/sec)", "(writes/sec)"]),
io:format("~s~n", [string:copies("-", 58)]),
lists:foreach(fun(Size) ->
Data = binary:copy(<<0>>, Size),
%% Channel (term-based)
{ok, Ch} = py_channel:new(),
ChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data)
end, lists:seq(1, Iterations)),
ChEnd = erlang:monotonic_time(microsecond),
ChRate = Iterations / ((ChEnd - ChStart) / 1000000),
py_channel:close(Ch),
%% ByteChannel (raw bytes)
{ok, BCh} = py_byte_channel:new(),
BChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_byte_channel:send(BCh, Data)
end, lists:seq(1, Iterations)),
BChEnd = erlang:monotonic_time(microsecond),
BChRate = Iterations / ((BChEnd - BChStart) / 1000000),
py_byte_channel:close(BCh),
%% PyBuffer
{ok, Buf} = py_buffer:new(),
BufStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_buffer:write(Buf, Data)
end, lists:seq(1, Iterations)),
BufEnd = erlang:monotonic_time(microsecond),
BufRate = Iterations / ((BufEnd - BufStart) / 1000000),
py_buffer:close(Buf),
io:format("~8B | ~14w | ~14w | ~14w~n",
[Size, round(ChRate), round(BChRate), round(BufRate)])
end, Sizes),
ok.
%% Benchmark Erlang-side roundtrip (send + receive, no Python)
run_erlang_roundtrip_bench() ->
io:format("~n--- Erlang Roundtrip (send + receive, no Python) ---~n"),
io:format("Iterations: 10000~n~n"),
Sizes = [64, 256, 1024, 4096, 16384],
Iterations = 10000,
io:format("~8s | ~14s | ~14s~n",
["Size", "Channel", "ByteChannel"]),
io:format("~8s | ~14s | ~14s~n",
["(bytes)", "(roundtrip/s)", "(roundtrip/s)"]),
io:format("~s~n", [string:copies("-", 42)]),
lists:foreach(fun(Size) ->
Data = binary:copy(<<0>>, Size),
%% Channel roundtrip
{ok, Ch} = py_channel:new(),
ChStart = 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)),
ChEnd = erlang:monotonic_time(microsecond),
ChRate = Iterations / ((ChEnd - ChStart) / 1000000),
py_channel:close(Ch),
%% ByteChannel roundtrip
{ok, BCh} = py_byte_channel:new(),
BChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_byte_channel:send(BCh, Data),
{ok, _} = py_byte_channel:try_receive(BCh)
end, lists:seq(1, Iterations)),
BChEnd = erlang:monotonic_time(microsecond),
BChRate = Iterations / ((BChEnd - BChStart) / 1000000),
py_byte_channel:close(BCh),
io:format("~8B | ~14w | ~14w~n",
[Size, round(ChRate), round(BChRate)])
end, Sizes),
ok.
%% Benchmark Python receive performance
run_python_receive_bench() ->
io:format("~n--- Python Receive Performance ---~n"),
io:format("Pattern: Erlang send -> Python receive~n"),
io:format("Iterations: 1000~n~n"),
Ctx = py:context(1),
ok = py:exec(Ctx, <<"
from erlang import Channel, ByteChannel
def recv_channel(ch_ref):
ch = Channel(ch_ref)
return ch.try_receive()
def recv_byte_channel(ch_ref):
ch = ByteChannel(ch_ref)
return ch.try_receive_bytes()
def read_buffer(buf):
return buf.read()
">>),
Sizes = [64, 256, 1024, 4096, 16384],
Iterations = 1000,
io:format("~8s | ~12s | ~12s | ~12s~n",
["Size", "Channel", "ByteChannel", "PyBuffer"]),
io:format("~8s | ~12s | ~12s | ~12s~n",
["(bytes)", "(ops/sec)", "(ops/sec)", "(ops/sec)"]),
io:format("~s~n", [string:copies("-", 54)]),
lists:foreach(fun(Size) ->
Data = binary:copy(<<0>>, Size),
%% Channel: Erlang send -> Python receive
{ok, Ch} = py_channel:new(),
ChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data),
{ok, _} = py:eval(Ctx, <<"recv_channel(ch)">>, #{<<"ch">> => Ch})
end, lists:seq(1, Iterations)),
ChEnd = erlang:monotonic_time(microsecond),
ChRate = Iterations / ((ChEnd - ChStart) / 1000000),
py_channel:close(Ch),
%% ByteChannel: Erlang send -> Python receive
{ok, BCh} = py_byte_channel:new(),
BChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_byte_channel:send(BCh, Data),
{ok, _} = py:eval(Ctx, <<"recv_byte_channel(ch)">>, #{<<"ch">> => BCh})
end, lists:seq(1, Iterations)),
BChEnd = erlang:monotonic_time(microsecond),
BChRate = Iterations / ((BChEnd - BChStart) / 1000000),
py_byte_channel:close(BCh),
%% PyBuffer: Erlang write -> Python read
BufStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
{ok, Buf} = py_buffer:new(Size),
ok = py_buffer:write(Buf, Data),
ok = py_buffer:close(Buf),
{ok, _} = py:eval(Ctx, <<"read_buffer(buf)">>, #{<<"buf">> => Buf})
end, lists:seq(1, Iterations)),
BufEnd = erlang:monotonic_time(microsecond),
BufRate = Iterations / ((BufEnd - BufStart) / 1000000),
io:format("~8B | ~12w | ~12w | ~12w~n",
[Size, round(ChRate), round(BChRate), round(BufRate)])
end, Sizes),
ok.
%% Benchmark streaming (1MB transfer with varying chunk sizes)
run_streaming_bench() ->
io:format("~n--- Streaming Benchmark (1MB transfer) ---~n"),
io:format("Pattern: Erlang sends chunks -> Python receives all~n~n"),
Ctx = py:context(1),
ok = py:exec(Ctx, <<"
from erlang import Channel, ByteChannel
def stream_channel(ch_ref, num_chunks):
ch = Channel(ch_ref)
total = 0
for _ in range(num_chunks):
msg = ch.try_receive()
if msg is None:
break
total += len(msg)
return total
def stream_byte_channel(ch_ref, num_chunks):
ch = ByteChannel(ch_ref)
total = 0
for _ in range(num_chunks):
data = ch.try_receive_bytes()
if data is None:
break
total += len(data)
return total
def stream_buffer(buf):
total = 0
while True:
chunk = buf.read(8192)
if not chunk:
break
total += len(chunk)
return total
">>),
ChunkSizes = [1024, 4096, 16384, 65536],
TotalBytes = 1048576, % 1MB
io:format("~10s | ~12s | ~12s | ~12s~n",
["Chunk", "Channel", "ByteChannel", "PyBuffer"]),
io:format("~10s | ~12s | ~12s | ~12s~n",
["(bytes)", "(MB/sec)", "(MB/sec)", "(MB/sec)"]),
io:format("~s~n", [string:copies("-", 54)]),
lists:foreach(fun(ChunkSize) ->
NumChunks = TotalBytes div ChunkSize,
Chunk = binary:copy(<<0>>, ChunkSize),
%% Channel streaming
{ok, Ch} = py_channel:new(),
ChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Chunk)
end, lists:seq(1, NumChunks)),
{ok, _} = py:eval(Ctx, <<"stream_channel(ch, n)">>,
#{<<"ch">> => Ch, <<"n">> => NumChunks}),
ChEnd = erlang:monotonic_time(microsecond),
ChMBps = (TotalBytes / 1048576) / ((ChEnd - ChStart) / 1000000),
py_channel:close(Ch),
%% ByteChannel streaming
{ok, BCh} = py_byte_channel:new(),
BChStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_byte_channel:send(BCh, Chunk)
end, lists:seq(1, NumChunks)),
{ok, _} = py:eval(Ctx, <<"stream_byte_channel(ch, n)">>,
#{<<"ch">> => BCh, <<"n">> => NumChunks}),
BChEnd = erlang:monotonic_time(microsecond),
BChMBps = (TotalBytes / 1048576) / ((BChEnd - BChStart) / 1000000),
py_byte_channel:close(BCh),
%% PyBuffer streaming
{ok, Buf} = py_buffer:new(TotalBytes),
BufStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
ok = py_buffer:write(Buf, Chunk)
end, lists:seq(1, NumChunks)),
ok = py_buffer:close(Buf),
{ok, _} = py:eval(Ctx, <<"stream_buffer(buf)">>, #{<<"buf">> => Buf}),
BufEnd = erlang:monotonic_time(microsecond),
BufMBps = (TotalBytes / 1048576) / ((BufEnd - BufStart) / 1000000),
io:format("~10B | ~12.2f | ~12.2f | ~12.2f~n",
[ChunkSize, ChMBps, BChMBps, BufMBps])
end, ChunkSizes),
ok.