Skip to content

Commit 04fa898

Browse files
committed
Add PyBuffer vs Channel benchmark
1 parent 992072b commit 04fa898

1 file changed

Lines changed: 265 additions & 0 deletions

File tree

examples/bench_py_buffer.erl

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
#!/usr/bin/env escript
2+
%% -*- erlang -*-
3+
%%! -pa _build/default/lib/erlang_python/ebin
4+
5+
%%% @doc Benchmark script comparing PyBuffer with Channel API.
6+
%%%
7+
%%% Run with:
8+
%%% rebar3 compile && escript examples/bench_py_buffer.erl
9+
10+
-mode(compile).
11+
12+
main(_Args) ->
13+
io:format("~n========================================~n"),
14+
io:format("PyBuffer vs Channel Benchmark~n"),
15+
io:format("========================================~n~n"),
16+
17+
%% Start the application
18+
{ok, _} = application:ensure_all_started(erlang_python),
19+
{ok, _} = py:start_contexts(),
20+
ok = py_channel:register_callbacks(),
21+
22+
%% Print system info
23+
io:format("System Information:~n"),
24+
io:format(" Erlang/OTP: ~s~n", [erlang:system_info(otp_release)]),
25+
{ok, PyVer} = py:version(),
26+
io:format(" Python: ~s~n", [PyVer]),
27+
io:format("~n"),
28+
29+
%% Run benchmarks
30+
run_write_throughput_bench(),
31+
run_read_throughput_bench(),
32+
run_buffer_vs_channel_bench(),
33+
run_streaming_bench(),
34+
35+
io:format("~n========================================~n"),
36+
io:format("Benchmark Complete~n"),
37+
io:format("========================================~n"),
38+
39+
halt(0).
40+
41+
run_write_throughput_bench() ->
42+
io:format("~n--- PyBuffer Write Throughput ---~n"),
43+
io:format("Writes per batch: 1000~n~n"),
44+
45+
Sizes = [64, 256, 1024, 4096, 16384],
46+
47+
io:format("~8s | ~12s | ~12s~n",
48+
["Size", "Writes/sec", "MB/sec"]),
49+
io:format("~s~n", [string:copies("-", 38)]),
50+
51+
lists:foreach(fun(Size) ->
52+
{ok, Buf} = py_buffer:new(),
53+
Data = binary:copy(<<0>>, Size),
54+
Iterations = 1000,
55+
56+
Start = erlang:monotonic_time(microsecond),
57+
lists:foreach(fun(_) ->
58+
ok = py_buffer:write(Buf, Data)
59+
end, lists:seq(1, Iterations)),
60+
End = erlang:monotonic_time(microsecond),
61+
62+
TotalTime = (End - Start) / 1000000,
63+
WriteRate = Iterations / TotalTime,
64+
MBPerSec = (Iterations * Size) / TotalTime / 1048576,
65+
66+
io:format("~8B | ~12w | ~12.2f~n",
67+
[Size, round(WriteRate), MBPerSec]),
68+
69+
py_buffer:close(Buf)
70+
end, Sizes),
71+
ok.
72+
73+
run_read_throughput_bench() ->
74+
io:format("~n--- PyBuffer Read Throughput (Python) ---~n"),
75+
io:format("Reads per batch: 1000~n~n"),
76+
77+
Ctx = py:context(1),
78+
ok = py:exec(Ctx, <<"
79+
import time
80+
81+
def bench_buffer_reads(buf, size, iterations):
82+
'''Read from buffer and measure throughput.'''
83+
# Read all data
84+
start = time.perf_counter()
85+
total_read = 0
86+
while True:
87+
data = buf.read(size)
88+
if not data:
89+
break
90+
total_read += len(data)
91+
elapsed = time.perf_counter() - start
92+
93+
if elapsed > 0:
94+
reads_per_sec = (total_read / size) / elapsed
95+
mb_per_sec = total_read / elapsed / 1048576
96+
else:
97+
reads_per_sec = 0
98+
mb_per_sec = 0
99+
100+
return {
101+
'total_bytes': total_read,
102+
'reads_per_sec': reads_per_sec,
103+
'mb_per_sec': mb_per_sec,
104+
}
105+
">>),
106+
107+
Sizes = [64, 256, 1024, 4096, 16384],
108+
109+
io:format("~8s | ~12s | ~12s~n",
110+
["Size", "Reads/sec", "MB/sec"]),
111+
io:format("~s~n", [string:copies("-", 38)]),
112+
113+
lists:foreach(fun(Size) ->
114+
Iterations = 1000,
115+
{ok, Buf} = py_buffer:new(Size * Iterations),
116+
Data = binary:copy(<<0>>, Size),
117+
118+
%% Fill the buffer
119+
lists:foreach(fun(_) ->
120+
ok = py_buffer:write(Buf, Data)
121+
end, lists:seq(1, Iterations)),
122+
ok = py_buffer:close(Buf),
123+
124+
%% Measure Python read performance
125+
{ok, Result} = py:eval(Ctx, <<"bench_buffer_reads(buf, size, iterations)">>,
126+
#{<<"buf">> => Buf, <<"size">> => Size, <<"iterations">> => Iterations}),
127+
128+
ReadsPerSec = maps:get(<<"reads_per_sec">>, Result),
129+
MBPerSec = maps:get(<<"mb_per_sec">>, Result),
130+
131+
io:format("~8B | ~12w | ~12.2f~n",
132+
[Size, round(ReadsPerSec), MBPerSec])
133+
end, Sizes),
134+
ok.
135+
136+
run_buffer_vs_channel_bench() ->
137+
io:format("~n--- PyBuffer vs Channel Comparison ---~n"),
138+
io:format("Pattern: Erlang write/send -> Python read/receive~n"),
139+
io:format("Iterations: 1000~n~n"),
140+
141+
Ctx = py:context(1),
142+
ok = py:exec(Ctx, <<"
143+
from erlang.channel import Channel
144+
145+
def read_buffer_all(buf):
146+
'''Read entire buffer.'''
147+
return buf.read()
148+
149+
def recv_channel(ch_ref):
150+
'''Receive from channel.'''
151+
ch = Channel(ch_ref)
152+
return ch.try_receive()
153+
">>),
154+
155+
Sizes = [64, 256, 1024, 4096, 16384],
156+
157+
io:format("~8s | ~14s | ~14s | ~8s~n",
158+
["Size", "Buffer (ops/s)", "Channel (ops/s)", "Ratio"]),
159+
io:format("~s~n", [string:copies("-", 52)]),
160+
161+
lists:foreach(fun(Size) ->
162+
Data = binary:copy(<<0>>, Size),
163+
Iterations = 1000,
164+
165+
%% Benchmark PyBuffer: Erlang write -> Python read
166+
BufStart = erlang:monotonic_time(microsecond),
167+
lists:foreach(fun(_) ->
168+
{ok, Buf} = py_buffer:new(Size),
169+
ok = py_buffer:write(Buf, Data),
170+
ok = py_buffer:close(Buf),
171+
{ok, _} = py:eval(Ctx, <<"read_buffer_all(buf)">>, #{<<"buf">> => Buf})
172+
end, lists:seq(1, Iterations)),
173+
BufEnd = erlang:monotonic_time(microsecond),
174+
BufTime = (BufEnd - BufStart) / 1000000,
175+
BufOpsPerSec = Iterations / BufTime,
176+
177+
%% Benchmark Channel: Erlang send -> Python receive
178+
{ok, Ch} = py_channel:new(),
179+
ChanStart = erlang:monotonic_time(microsecond),
180+
lists:foreach(fun(_) ->
181+
ok = py_channel:send(Ch, Data),
182+
{ok, _} = py:eval(Ctx, <<"recv_channel(ch)">>, #{<<"ch">> => Ch})
183+
end, lists:seq(1, Iterations)),
184+
ChanEnd = erlang:monotonic_time(microsecond),
185+
ChanTime = (ChanEnd - ChanStart) / 1000000,
186+
ChanOpsPerSec = Iterations / ChanTime,
187+
py_channel:close(Ch),
188+
189+
Ratio = BufOpsPerSec / ChanOpsPerSec,
190+
io:format("~8B | ~14w | ~14w | ~.2fx~n",
191+
[Size, round(BufOpsPerSec), round(ChanOpsPerSec), Ratio])
192+
end, Sizes),
193+
ok.
194+
195+
run_streaming_bench() ->
196+
io:format("~n--- Streaming Comparison (chunked transfer) ---~n"),
197+
io:format("Total data: 1MB, varying chunk sizes~n~n"),
198+
199+
Ctx = py:context(1),
200+
ok = py:exec(Ctx, <<"
201+
from erlang.channel import Channel
202+
203+
def stream_read_buffer(buf):
204+
'''Stream read entire buffer.'''
205+
total = 0
206+
while True:
207+
chunk = buf.read(8192) # 8KB reads
208+
if not chunk:
209+
break
210+
total += len(chunk)
211+
return total
212+
213+
def stream_recv_channel(ch_ref, num_chunks):
214+
'''Stream receive from channel.'''
215+
ch = Channel(ch_ref)
216+
total = 0
217+
for _ in range(num_chunks):
218+
msg = ch.try_receive()
219+
if msg is None:
220+
break
221+
total += len(msg)
222+
return total
223+
">>),
224+
225+
ChunkSizes = [256, 1024, 4096, 16384, 65536],
226+
TotalBytes = 1048576, % 1MB
227+
228+
io:format("~10s | ~14s | ~14s | ~8s~n",
229+
["Chunk", "Buffer (MB/s)", "Channel (MB/s)", "Ratio"]),
230+
io:format("~s~n", [string:copies("-", 54)]),
231+
232+
lists:foreach(fun(ChunkSize) ->
233+
NumChunks = TotalBytes div ChunkSize,
234+
Chunk = binary:copy(<<0>>, ChunkSize),
235+
236+
%% Benchmark PyBuffer streaming (Erlang write -> Python read)
237+
{ok, Buf} = py_buffer:new(TotalBytes),
238+
BufStart = erlang:monotonic_time(microsecond),
239+
lists:foreach(fun(_) ->
240+
ok = py_buffer:write(Buf, Chunk)
241+
end, lists:seq(1, NumChunks)),
242+
ok = py_buffer:close(Buf),
243+
{ok, _} = py:eval(Ctx, <<"stream_read_buffer(buf)">>, #{<<"buf">> => Buf}),
244+
BufEnd = erlang:monotonic_time(microsecond),
245+
BufTime = (BufEnd - BufStart) / 1000000,
246+
BufMBPerSec = (TotalBytes / 1048576) / BufTime,
247+
248+
%% Benchmark Channel streaming (Erlang send -> Python receive)
249+
{ok, Ch} = py_channel:new(),
250+
ChanStart = erlang:monotonic_time(microsecond),
251+
lists:foreach(fun(_) ->
252+
ok = py_channel:send(Ch, Chunk)
253+
end, lists:seq(1, NumChunks)),
254+
{ok, _} = py:eval(Ctx, <<"stream_recv_channel(ch, num_chunks)">>,
255+
#{<<"ch">> => Ch, <<"num_chunks">> => NumChunks}),
256+
ChanEnd = erlang:monotonic_time(microsecond),
257+
ChanTime = (ChanEnd - ChanStart) / 1000000,
258+
ChanMBPerSec = (TotalBytes / 1048576) / ChanTime,
259+
py_channel:close(Ch),
260+
261+
Ratio = BufMBPerSec / ChanMBPerSec,
262+
io:format("~10B | ~14.2f | ~14.2f | ~.2fx~n",
263+
[ChunkSize, BufMBPerSec, ChanMBPerSec, Ratio])
264+
end, ChunkSizes),
265+
ok.

0 commit comments

Comments
 (0)