Skip to content

Commit 0ba0a23

Browse files
committed
Add basic benchmark
1 parent 26699b1 commit 0ba0a23

5 files changed

Lines changed: 212 additions & 5 deletions

File tree

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Used by "mix format"
22
[
3-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
3+
inputs: ["{mix,.formatter}.exs", "{bench,config,lib,test}/**/*.{ex,exs}"]
44
]

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://github.com/benjreinhart/server_sent_events/actions/workflows/ci.yml/badge.svg)](https://github.com/benjreinhart/server_sent_events/actions/workflows/ci.yml)
44
[![License](https://img.shields.io/hexpm/l/server_sent_events.svg)](https://github.com/benjreinhart/server_sent_events/blob/main/LICENSE.md)
5-
[![Version](https://img.shields.io/hexpm/v/server_sent_events.svg)](https://hex.pm/packages/server_sent_events)
5+
[![Version](https://img.shields.io/hexpm/v/server_sent_events.svg)](https://hexdocs.pm/server_sent_events/readme.html)
66

77
Lightweight, ultra-fast Server Sent Event parser for Elixir.
88

@@ -11,9 +11,9 @@ This module fully conforms to the official [Server Sent Events specification](ht
1111
## Usage
1212

1313
```elixir
14-
{events, rest} = ServerSentEvents.parse("event: event\ndata: {\"complete\":true}\n\n")
14+
{events, buffer} = ServerSentEvents.parse("event: event\ndata: {\"complete\":true}\n\n")
1515
IO.inspect(events) # [%{event: "event", data: "{\"complete\":true}\n"}]
16-
IO.inspect(rest) # ""
16+
IO.inspect(buffer) # ""
1717
```
1818

1919
Parsing a chunk containing zero or more events followed by an incomplete event returns the incomplete data.
@@ -98,3 +98,13 @@ def deps do
9898
]
9999
end
100100
```
101+
102+
## Benchmarking
103+
104+
Run the local benchmark with:
105+
106+
```sh
107+
mix bench
108+
```
109+
110+
The benchmark exercises both large complete payloads and large payloads that end with an incomplete trailing event, and reports execution time and memory usage.

bench/parse_bench.exs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
defmodule ServerSentEvents.Bench do
2+
@target_sizes [
3+
{"1 MB", 1_048_576},
4+
{"4 MB", 4_194_304}
5+
]
6+
7+
@data_lines_per_event 8
8+
@data_chunk String.duplicate("abcdefghijklmnopqrstuvwxyz0123456789", 16)
9+
@incomplete_trim_bytes 97
10+
11+
def run do
12+
inputs = inputs()
13+
14+
print_inputs(inputs)
15+
16+
Benchee.run(
17+
%{
18+
"complete large payload" => &parse_complete!/1,
19+
"incomplete large payload" => &parse_incomplete!/1
20+
},
21+
inputs: inputs,
22+
memory_time: 2,
23+
reduction_time: 0,
24+
time: 5,
25+
warmup: 2
26+
)
27+
end
28+
29+
defp inputs do
30+
Map.new(@target_sizes, fn {label, target_bytes} ->
31+
{label, build_input(target_bytes)}
32+
end)
33+
end
34+
35+
defp build_input(target_bytes) do
36+
events = build_events(target_bytes)
37+
complete_payload = IO.iodata_to_binary(events)
38+
complete_event_count = length(events)
39+
40+
if complete_event_count < 2 do
41+
raise "expected at least two events for benchmark input generation"
42+
end
43+
44+
last_event = List.last(events)
45+
incomplete_last_event_size = byte_size(last_event) - @incomplete_trim_bytes
46+
47+
if incomplete_last_event_size <= 0 do
48+
raise "trailing event trim removed the entire event"
49+
end
50+
51+
prefix_size = byte_size(complete_payload) - byte_size(last_event)
52+
53+
incomplete_payload =
54+
binary_part(complete_payload, 0, prefix_size) <>
55+
binary_part(last_event, 0, incomplete_last_event_size)
56+
57+
%{
58+
complete_event_count: complete_event_count,
59+
complete_payload: complete_payload,
60+
incomplete_complete_event_count: complete_event_count - 1,
61+
incomplete_payload: incomplete_payload,
62+
incomplete_rest_bytes: incomplete_last_event_size
63+
}
64+
end
65+
66+
defp build_events(target_bytes) do
67+
1
68+
|> Stream.iterate(&(&1 + 1))
69+
|> Enum.reduce_while({[], 0}, fn id, {events, total_bytes} ->
70+
event = build_event(id)
71+
next_total_bytes = total_bytes + byte_size(event)
72+
73+
cond do
74+
next_total_bytes < target_bytes ->
75+
{:cont, {[event | events], next_total_bytes}}
76+
77+
events == [] ->
78+
{:cont, {[event | events], next_total_bytes}}
79+
80+
true ->
81+
{:halt, Enum.reverse([event | events])}
82+
end
83+
end)
84+
end
85+
86+
defp build_event(id) do
87+
[
88+
"id: ",
89+
Integer.to_string(id),
90+
"\n",
91+
"event: completion.delta\n",
92+
build_data_lines(id),
93+
"\n"
94+
]
95+
|> IO.iodata_to_binary()
96+
end
97+
98+
defp build_data_lines(id) do
99+
for part <- 1..@data_lines_per_event do
100+
[
101+
"data: {\"id\":",
102+
Integer.to_string(id),
103+
",\"part\":",
104+
Integer.to_string(part),
105+
",\"content\":\"",
106+
@data_chunk,
107+
"\"}\n"
108+
]
109+
end
110+
end
111+
112+
defp parse_complete!(input) do
113+
case ServerSentEvents.parse(input.complete_payload) do
114+
{events, rest} ->
115+
if rest == "" and length(events) == input.complete_event_count do
116+
events
117+
else
118+
raise """
119+
unexpected result for complete payload:
120+
events=#{length(events)} expected=#{input.complete_event_count}
121+
rest_bytes=#{byte_size(rest)}
122+
"""
123+
end
124+
end
125+
end
126+
127+
defp parse_incomplete!(input) do
128+
case ServerSentEvents.parse(input.incomplete_payload) do
129+
{events, rest} ->
130+
if length(events) == input.incomplete_complete_event_count and
131+
byte_size(rest) == input.incomplete_rest_bytes do
132+
{events, rest}
133+
else
134+
raise """
135+
unexpected result for incomplete payload:
136+
events=#{length(events)} expected=#{input.incomplete_complete_event_count}
137+
rest_bytes=#{byte_size(rest)} expected_rest_bytes=#{input.incomplete_rest_bytes}
138+
"""
139+
end
140+
end
141+
end
142+
143+
defp print_inputs(inputs) do
144+
IO.puts("Benchmarking ServerSentEvents.parse/1")
145+
146+
Enum.each(inputs, fn {label, input} ->
147+
IO.puts([
148+
" ",
149+
label,
150+
": complete=",
151+
format_bytes(byte_size(input.complete_payload)),
152+
" (",
153+
Integer.to_string(input.complete_event_count),
154+
" events), incomplete=",
155+
format_bytes(byte_size(input.incomplete_payload)),
156+
" (",
157+
Integer.to_string(input.incomplete_complete_event_count),
158+
" complete + ",
159+
format_bytes(input.incomplete_rest_bytes),
160+
" buffered)"
161+
])
162+
end)
163+
164+
IO.puts("")
165+
end
166+
167+
defp format_bytes(bytes) when bytes >= 1_048_576 do
168+
:io_lib.format("~.2f MB", [bytes / 1_048_576]) |> IO.iodata_to_binary()
169+
end
170+
171+
defp format_bytes(bytes) when bytes >= 1024 do
172+
:io_lib.format("~.2f KB", [bytes / 1024]) |> IO.iodata_to_binary()
173+
end
174+
175+
defp format_bytes(bytes) do
176+
"#{bytes} B"
177+
end
178+
end
179+
180+
ServerSentEvents.Bench.run()

mix.exs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ defmodule ServerSentEvents.MixProject do
1919
homepage_url: @github_repo_url,
2020
package: package(),
2121
deps: deps(),
22-
docs: docs()
22+
docs: docs(),
23+
aliases: aliases()
2324
]
2425
end
2526

@@ -29,6 +30,12 @@ defmodule ServerSentEvents.MixProject do
2930
]
3031
end
3132

33+
def cli do
34+
[
35+
preferred_envs: [bench: :dev]
36+
]
37+
end
38+
3239
def description do
3340
@description
3441
end
@@ -45,10 +52,17 @@ defmodule ServerSentEvents.MixProject do
4552

4653
defp deps do
4754
[
55+
{:benchee, "~> 1.3", only: :dev, runtime: false},
4856
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
4957
]
5058
end
5159

60+
defp aliases do
61+
[
62+
bench: "run bench/parse_bench.exs"
63+
]
64+
end
65+
5266
defp docs do
5367
[
5468
main: "readme",

mix.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
%{
2+
"benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"},
3+
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
24
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
35
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
46
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
57
"makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"},
68
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
79
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
10+
"statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"},
811
}

0 commit comments

Comments
 (0)