Current section
Files
Jump to
Current section
Files
examples/bench_channel_async.erl
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa _build/default/lib/erlang_python/ebin
%%% @doc Benchmark script for Channel API: Sync vs Async comparison.
%%%
%%% Run with:
%%% rebar3 compile && escript examples/bench_channel_async.erl
-mode(compile).
main(_Args) ->
io:format("~n========================================~n"),
io:format("Channel Benchmark: Sync vs Async~n"),
io:format("========================================~n~n"),
%% Start the application
{ok, _} = application:ensure_all_started(erlang_python),
{ok, _} = py:start_contexts(),
ok = py_channel:register_callbacks(),
%% Initialize event loop for async operations (gen_server)
%% Already started by application, just ensure it's running
case py_event_loop:start_link() of
{ok, _} -> ok;
{error, {already_started, _}} -> ok
end,
%% 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"),
%% Setup Python async channel receiver
setup_python_async_receiver(),
%% Run benchmarks
run_sync_channel_bench(),
run_async_channel_bench(),
run_comparison_bench(),
io:format("~n========================================~n"),
io:format("Benchmark Complete~n"),
io:format("========================================~n"),
halt(0).
setup_python_async_receiver() ->
io:format("Python channel helpers ready.~n~n").
run_sync_channel_bench() ->
io:format("--- Sync Channel Benchmark ---~n"),
io:format("(Erlang send + NIF try_receive - pure Erlang)~n~n"),
Sizes = [64, 1024, 16384],
Iterations = 5000,
io:format("~8s | ~12s | ~12s~n",
["Size", "Throughput", "Avg (us)"]),
io:format("~s~n", [string:copies("-", 38)]),
lists:foreach(fun(Size) ->
{ok, Ch} = py_channel:new(),
Data = binary:copy(<<0>>, Size),
%% Fill channel
lists:foreach(fun(_) ->
ok = py_channel:send(Ch, Data)
end, lists:seq(1, Iterations)),
%% Time receiving all messages via NIF
Start = erlang:monotonic_time(microsecond),
receive_all_sync(Ch, Iterations),
End = erlang:monotonic_time(microsecond),
TotalTime = (End - Start) / 1000000,
AvgUs = (TotalTime / Iterations) * 1000000,
Throughput = round(Iterations / TotalTime),
io:format("~8B | ~12w | ~12.2f~n", [Size, Throughput, AvgUs]),
py_channel:close(Ch)
end, Sizes),
ok.
receive_all_sync(_Ch, 0) -> ok;
receive_all_sync(Ch, N) ->
{ok, _} = py_nif:channel_try_receive(Ch),
receive_all_sync(Ch, N - 1).
run_async_channel_bench() ->
io:format("~n--- Async Task API Benchmark ---~n"),
io:format("(py_event_loop:create_task + await using stdlib)~n~n"),
Iterations = 1000,
io:format("~15s | ~12s | ~12s~n",
["Operation", "Throughput", "Avg (us)"]),
io:format("~s~n", [string:copies("-", 44)]),
%% Test math.sqrt via async task API
Start1 = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
Ref = py_event_loop:create_task(math, sqrt, [2.0]),
{ok, _} = py_event_loop:await(Ref, 5000)
end, lists:seq(1, Iterations)),
End1 = erlang:monotonic_time(microsecond),
TotalTime1 = (End1 - Start1) / 1000000,
AvgUs1 = (TotalTime1 / Iterations) * 1000000,
Throughput1 = round(Iterations / TotalTime1),
io:format("~15s | ~12w | ~12.2f~n", ["math.sqrt", Throughput1, AvgUs1]),
%% Test concurrent tasks (20 processes, 50 each)
NumProcs = 20,
TasksPerProc = 50,
TotalTasks = NumProcs * TasksPerProc,
Start2 = erlang:monotonic_time(microsecond),
Parent = self(),
lists:foreach(fun(_) ->
spawn(fun() ->
lists:foreach(fun(_) ->
Ref = py_event_loop:create_task(math, sqrt, [2.0]),
{ok, _} = py_event_loop:await(Ref, 5000)
end, lists:seq(1, TasksPerProc)),
Parent ! done
end)
end, lists:seq(1, NumProcs)),
wait_all(NumProcs),
End2 = erlang:monotonic_time(microsecond),
TotalTime2 = (End2 - Start2) / 1000000,
AvgUs2 = (TotalTime2 / TotalTasks) * 1000000,
Throughput2 = round(TotalTasks / TotalTime2),
io:format("~15s | ~12w | ~12.2f~n", ["concurrent", Throughput2, AvgUs2]),
ok.
wait_all(0) -> ok;
wait_all(N) ->
receive done -> wait_all(N - 1) end.
run_comparison_bench() ->
io:format("~n--- Sync vs Async Comparison ---~n"),
io:format("(Channel operations: NIF sync vs py:call)~n~n"),
Size = 1024,
Iterations = 1000,
io:format("Message size: ~B bytes, Iterations: ~B~n~n", [Size, Iterations]),
io:format("~15s | ~12s | ~12s~n",
["Method", "Time (ms)", "Throughput"]),
io:format("~s~n", [string:copies("-", 45)]),
Data = binary:copy(<<0>>, Size),
%% NIF-level sync (fastest - no Python)
{ok, NifCh} = py_channel:new(),
lists:foreach(fun(_) -> ok = py_channel:send(NifCh, Data) end, lists:seq(1, Iterations)),
NifStart = erlang:monotonic_time(microsecond),
receive_all_sync(NifCh, Iterations),
NifEnd = erlang:monotonic_time(microsecond),
NifTime = (NifEnd - NifStart) / 1000,
NifThroughput = round(Iterations / (NifTime / 1000)),
io:format("~15s | ~12.2f | ~12w~n", ["NIF sync", NifTime, NifThroughput]),
py_channel:close(NifCh),
%% py:call sync (Python stdlib function)
PyStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
{ok, _} = py:call(math, sqrt, [2.0])
end, lists:seq(1, Iterations)),
PyEnd = erlang:monotonic_time(microsecond),
PyTime = (PyEnd - PyStart) / 1000,
PyThroughput = round(Iterations / (PyTime / 1000)),
io:format("~15s | ~12.2f | ~12w~n", ["py:call sync", PyTime, PyThroughput]),
%% Async task API (sequential)
AsyncStart = erlang:monotonic_time(microsecond),
lists:foreach(fun(_) ->
Ref = py_event_loop:create_task(math, sqrt, [2.0]),
{ok, _} = py_event_loop:await(Ref, 5000)
end, lists:seq(1, Iterations)),
AsyncEnd = erlang:monotonic_time(microsecond),
AsyncTime = (AsyncEnd - AsyncStart) / 1000,
AsyncThroughput = round(Iterations / (AsyncTime / 1000)),
io:format("~15s | ~12.2f | ~12w~n", ["async task", AsyncTime, AsyncThroughput]),
%% Spawn task (fire-and-forget, then collect)
SpawnStart = erlang:monotonic_time(microsecond),
Refs = lists:map(fun(_) ->
py_event_loop:create_task(math, sqrt, [2.0])
end, lists:seq(1, Iterations)),
%% Await all
lists:foreach(fun(R) ->
{ok, _} = py_event_loop:await(R, 5000)
end, Refs),
SpawnEnd = erlang:monotonic_time(microsecond),
SpawnTime = (SpawnEnd - SpawnStart) / 1000,
SpawnThroughput = round(Iterations / (SpawnTime / 1000)),
io:format("~15s | ~12.2f | ~12w~n", ["spawn batch", SpawnTime, SpawnThroughput]),
%% Print summary
io:format("~n"),
io:format("NIF sync is ~.1fx faster than py:call~n", [PyTime / NifTime]),
io:format("NIF sync is ~.1fx faster than async task~n", [AsyncTime / NifTime]),
io:format("Spawn batch is ~.1fx faster than sequential async~n", [AsyncTime / SpawnTime]),
ok.