Skip to content

Commit 56a0851

Browse files
authored
Vastly improve perf of parsing bulk strings (#247)
1 parent b890b0b commit 56a0851

File tree

4 files changed

+61
-14
lines changed

4 files changed

+61
-14
lines changed

.formatter.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
inputs: [
33
"mix.exs",
44
"lib/**/*.ex",
5-
"test/**/*.exs"
5+
"test/**/*.exs",
6+
"benchmarks/**/*.exs",
67
],
78
import_deps: [:stream_data]
89
]

benchmarks/protocol.exs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Mix.install([
2+
{:redix, path: "."},
3+
{:benchee, "~> 1.1"},
4+
{:benchee_html, "~> 1.0"},
5+
{:benchee_markdown, "~> 0.3"},
6+
{:eredis, "~> 1.7"}
7+
])
8+
9+
defmodule Helpers do
10+
def parse_with_continuations([data | datas], cont \\ &Redix.Protocol.parse/1) do
11+
case cont.(data) do
12+
{:continuation, new_cont} -> parse_with_continuations(datas, new_cont)
13+
{:ok, value, rest} -> {:ok, value, rest}
14+
end
15+
end
16+
end
17+
18+
Benchee.run(
19+
%{
20+
"Parse a bulk string split into 1kb chunks" => fn %{chunks: datas} ->
21+
{:ok, _value, _rest} = Helpers.parse_with_continuations(datas)
22+
end
23+
},
24+
# Inputs are expressed in number of 1kb chunks
25+
inputs: %{
26+
"1 Kb" => 1,
27+
"1 Mb" => 1024,
28+
"70 Mb" => 70 * 1024
29+
},
30+
before_scenario: fn chunks_of_1kb ->
31+
chunks = for _ <- 1..chunks_of_1kb, do: :crypto.strong_rand_bytes(1024)
32+
total_size = chunks_of_1kb * 1024
33+
chunks = ["$#{total_size}\r\n" | chunks] ++ ["\r\n"]
34+
35+
%{chunks: chunks}
36+
end,
37+
save: [path: "redix-main.benchee", tag: "main"]
38+
)

lib/redix/protocol.ex

+16-13
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,7 @@ defmodule Redix.Protocol do
141141

142142
defp parse_integer_without_sign(<<digit, _::binary>> = bin) when digit in ?0..?9 do
143143
resolve_cont(parse_integer_digits(bin, 0), fn i, rest ->
144-
resolve_cont(until_crlf(rest), fn
145-
"", rest ->
146-
{:ok, i, rest}
147-
148-
<<char, _::binary>>, _rest ->
149-
raise ParseError, message: "expected CRLF, found: #{inspect(<<char>>)}"
150-
end)
144+
resolve_cont(crlf(rest), fn :no_value, rest -> {:ok, i, rest} end)
151145
end)
152146
end
153147

@@ -167,17 +161,19 @@ defmodule Redix.Protocol do
167161
{:ok, nil, rest}
168162

169163
size, rest ->
170-
parse_string_of_known_size(rest, size)
164+
parse_string_of_known_size(rest, _acc = [], _size_left = size)
171165
end)
172166
end
173167

174-
defp parse_string_of_known_size(data, size) do
168+
defp parse_string_of_known_size(data, acc, size_left) do
175169
case data do
176-
<<str::bytes-size(size), @crlf, rest::binary>> ->
177-
{:ok, str, rest}
170+
str when byte_size(str) < size_left ->
171+
{:continuation, &parse_string_of_known_size(&1, [acc | str], size_left - byte_size(str))}
178172

179-
_ ->
180-
{:continuation, &parse_string_of_known_size(data <> &1, size)}
173+
<<str::bytes-size(size_left), rest::binary>> ->
174+
resolve_cont(crlf(rest), fn :no_value, rest ->
175+
{:ok, IO.iodata_to_binary([acc | str]), rest}
176+
end)
181177
end
182178
end
183179

@@ -198,6 +194,13 @@ defmodule Redix.Protocol do
198194
defp until_crlf(<<?\r>>, acc), do: {:continuation, &until_crlf(<<?\r, &1::binary>>, acc)}
199195
defp until_crlf(<<byte, rest::binary>>, acc), do: until_crlf(rest, <<acc::binary, byte>>)
200196

197+
defp crlf(<<@crlf, rest::binary>>), do: {:ok, :no_value, rest}
198+
defp crlf(<<?\r>>), do: {:continuation, &crlf(<<?\r, &1::binary>>)}
199+
defp crlf(<<>>), do: {:continuation, &crlf/1}
200+
201+
defp crlf(<<byte, _::binary>>),
202+
do: raise(ParseError, message: "expected CRLF, found: #{inspect(<<byte>>)}")
203+
201204
defp take_elems(data, 0, acc) do
202205
{:ok, Enum.reverse(acc), data}
203206
end

test/redix/protocol_test.exs

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ defmodule Redix.ProtocolTest do
8686
assert parse_with_continuations(split_command) == {:ok, nil, ""}
8787
assert parse_with_continuations(split_command_with_rest) == {:ok, nil, "rest"}
8888
end
89+
90+
# No CRLF after bulk string contents
91+
assert_raise ParseError, "expected CRLF, found: \"n\"", fn ->
92+
parse("$3\r\nfoonocrlf")
93+
end
8994
end
9095

9196
property "arrays" do

0 commit comments

Comments
 (0)