aboutsummaryrefslogtreecommitdiffstats
path: root/test/ws_perf_SUITE.erl
diff options
context:
space:
mode:
Diffstat (limited to 'test/ws_perf_SUITE.erl')
-rw-r--r--test/ws_perf_SUITE.erl308
1 files changed, 308 insertions, 0 deletions
diff --git a/test/ws_perf_SUITE.erl b/test/ws_perf_SUITE.erl
new file mode 100644
index 0000000..ff88554
--- /dev/null
+++ b/test/ws_perf_SUITE.erl
@@ -0,0 +1,308 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(ws_perf_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+-import(cowboy_test, [gun_open/2]).
+-import(cowboy_test, [gun_down/1]).
+
+%% ct.
+
+all() ->
+ [{group, binary}, {group, ascii}, {group, mixed}, {group, japanese}].
+
+groups() ->
+ CommonGroups = cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel),
+ SubGroups = [G || G = {GN, _, _} <- CommonGroups,
+ GN =:= http orelse GN =:= h2c orelse GN =:= http_compress orelse GN =:= h2c_compress],
+ [
+ {binary, [], SubGroups},
+ {ascii, [], SubGroups},
+ {mixed, [], SubGroups},
+ {japanese, [], SubGroups}
+ ].
+
+init_per_suite(Config) ->
+ %% Optionally enable `perf` for the current node.
+% spawn(fun() -> ct:pal(os:cmd("perf record -g -F 9999 -o /tmp/ws_perf.data -p " ++ os:getpid() ++ " -- sleep 60")) end),
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(Name, Config) when Name =:= http; Name =:= http_compress ->
+ init_info(Name, Config),
+ cowboy_test:init_common_groups(Name, Config, ?MODULE);
+init_per_group(Name, Config) when Name =:= h2c; Name =:= h2c_compress ->
+ init_info(Name, Config),
+ {Flavor, Opts} = case Name of
+ h2c -> {vanilla, #{}};
+ h2c_compress -> {compress, #{stream_handlers => [cowboy_compress_h, cowboy_stream_h]}}
+ end,
+ Config1 = cowboy_test:init_http(Name, Opts#{
+ connection_window_margin_size => 64*1024,
+ enable_connect_protocol => true,
+ env => #{dispatch => init_dispatch(Config)},
+ max_frame_size_sent => 64*1024,
+ max_frame_size_received => 16384 * 1024 - 1,
+ max_received_frame_rate => {10_000_000, 1},
+ stream_window_data_threshold => 1024,
+ stream_window_margin_size => 64*1024
+ }, [{flavor, Flavor}|Config]),
+ lists:keyreplace(protocol, 1, Config1, {protocol, http2});
+init_per_group(ascii, Config) ->
+ init_text_data("ascii.txt", Config);
+init_per_group(mixed, Config) ->
+ init_text_data("grok_segond.txt", Config);
+init_per_group(japanese, Config) ->
+ init_text_data("japanese.txt", Config);
+init_per_group(binary, Config) ->
+ [{frame_type, binary}|Config].
+
+init_info(Name, Config) ->
+ DataInfo = case config(frame_type, Config) of
+ text -> config(text_data_filename, Config);
+ binary -> binary
+ end,
+ ConnInfo = case Name of
+ http -> "cleartext HTTP/1.1";
+ http_compress -> "cleartext HTTP/1.1 with compression";
+ h2c -> "cleartext HTTP/2";
+ h2c_compress -> "cleartext HTTP/2 with compression"
+ end,
+ ct:pal("Websocket over ~s (~s)", [ConnInfo, DataInfo]).
+
+init_text_data(Filename, Config) ->
+ {ok, Text} = file:read_file(filename:join(config(data_dir, Config), Filename)),
+ [
+ {frame_type, text},
+ {text_data, Text},
+ {text_data_filename, Filename}
+ |Config].
+
+end_per_group(Name, _Config) ->
+ cowboy_test:stop_group(Name).
+
+%% Dispatch configuration.
+
+init_dispatch(_Config) ->
+ cowboy_router:compile([
+ {"localhost", [
+ {"/ws_echo", ws_echo, []},
+ {"/ws_ignore", ws_ignore, []}
+ ]}
+ ]).
+
+%% Support functions for testing using Gun.
+
+do_gun_open_ws(Path, Config) ->
+ ConnPid = gun_open(Config, #{
+ http2_opts => #{
+ connection_window_margin_size => 64*1024,
+ max_frame_size_sent => 64*1024,
+ max_frame_size_received => 16384 * 1024 - 1,
+ notify_settings_changed => true,
+ stream_window_data_threshold => 1024,
+ stream_window_margin_size => 64*1024
+ },
+ tcp_opts => [{nodelay, true}],
+ ws_opts => #{compress => config(flavor, Config) =:= compress}
+ }),
+ case config(protocol, Config) of
+ http -> ok;
+ http2 ->
+ {notify, settings_changed, #{enable_connect_protocol := true}}
+ = gun:await(ConnPid, undefined) %% @todo Maybe have a gun:await/1?
+ end,
+ StreamRef = gun:ws_upgrade(ConnPid, Path),
+ receive
+ {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
+ {ok, ConnPid, StreamRef};
+ {gun_response, ConnPid, _, _, Status, Headers} ->
+ exit({ws_upgrade_failed, Status, Headers});
+ {gun_error, ConnPid, StreamRef, Reason} ->
+ exit({ws_upgrade_failed, Reason})
+ after 1000 ->
+ error(timeout)
+ end.
+
+receive_ws(ConnPid, StreamRef) ->
+ receive
+ {gun_ws, ConnPid, StreamRef, Frame} ->
+ {ok, Frame}
+ after 30000 ->
+ {error, timeout}
+ end.
+
+%% Tests.
+
+echo_1_00064KiB(Config) ->
+ doc("Send and receive a 64KiB frame."),
+ do_echo(Config, echo_1, 1, 64 * 1024).
+
+echo_1_00256KiB(Config) ->
+ doc("Send and receive a 256KiB frame."),
+ do_echo(Config, echo_1, 1, 256 * 1024).
+
+echo_1_01024KiB(Config) ->
+ doc("Send and receive a 1024KiB frame."),
+ do_echo(Config, echo_1, 1, 1024 * 1024).
+
+echo_1_04096KiB(Config) ->
+ doc("Send and receive a 4096KiB frame."),
+ do_echo(Config, echo_1, 1, 4096 * 1024).
+
+%% Minus one because frames can only get so big.
+echo_1_16384KiB(Config) ->
+ doc("Send and receive a 16384KiB - 1 frame."),
+ do_echo(Config, echo_1, 1, 16384 * 1024 - 1).
+
+echo_N_00000B(Config) ->
+ doc("Send and receive a 0B frame 1000 times."),
+ do_echo(Config, echo_N, 1000, 0).
+
+echo_N_00256B(Config) ->
+ doc("Send and receive a 256B frame 1000 times."),
+ do_echo(Config, echo_N, 1000, 256).
+
+echo_N_01024B(Config) ->
+ doc("Send and receive a 1024B frame 1000 times."),
+ do_echo(Config, echo_N, 1000, 1024).
+
+echo_N_04096B(Config) ->
+ doc("Send and receive a 4096B frame 1000 times."),
+ do_echo(Config, echo_N, 1000, 4096).
+
+echo_N_16384B(Config) ->
+ doc("Send and receive a 16384B frame 1000 times."),
+ do_echo(Config, echo_N, 1000, 16384).
+
+%echo_N_16384B_10K(Config) ->
+% doc("Send and receive a 16384B frame 10000 times."),
+% do_echo(Config, echo_N, 10000, 16384).
+
+do_echo(Config, What, Num, FrameSize) ->
+ {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_echo", Config),
+ FrameType = config(frame_type, Config),
+ FrameData = case FrameType of
+ text -> do_text_data(Config, FrameSize);
+ binary -> rand:bytes(FrameSize)
+ end,
+ %% Heat up the processes before doing the real run.
+% do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData),
+ {Time, _} = timer:tc(?MODULE, do_echo_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]),
+ do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]),
+ gun:ws_send(ConnPid, StreamRef, close),
+ {ok, close} = receive_ws(ConnPid, StreamRef),
+ gun_down(ConnPid).
+
+do_echo_loop(_, _, 0, _, _) ->
+ ok;
+do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData) ->
+ gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}),
+ {ok, {FrameType, FrameData}} = receive_ws(ConnPid, StreamRef),
+ do_echo_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData).
+
+send_1_00064KiB(Config) ->
+ doc("Send a 64KiB frame."),
+ do_send(Config, send_1, 1, 64 * 1024).
+
+send_1_00256KiB(Config) ->
+ doc("Send a 256KiB frame."),
+ do_send(Config, send_1, 1, 256 * 1024).
+
+send_1_01024KiB(Config) ->
+ doc("Send a 1024KiB frame."),
+ do_send(Config, send_1, 1, 1024 * 1024).
+
+send_1_04096KiB(Config) ->
+ doc("Send a 4096KiB frame."),
+ do_send(Config, send_1, 1, 4096 * 1024).
+
+%% Minus one because frames can only get so big.
+send_1_16384KiB(Config) ->
+ doc("Send a 16384KiB - 1 frame."),
+ do_send(Config, send_1, 1, 16384 * 1024 - 1).
+
+send_N_00000B(Config) ->
+ doc("Send a 0B frame 10000 times."),
+ do_send(Config, send_N, 10000, 0).
+
+send_N_00256B(Config) ->
+ doc("Send a 256B frame 10000 times."),
+ do_send(Config, send_N, 10000, 256).
+
+send_N_01024B(Config) ->
+ doc("Send a 1024B frame 10000 times."),
+ do_send(Config, send_N, 10000, 1024).
+
+send_N_04096B(Config) ->
+ doc("Send a 4096B frame 10000 times."),
+ do_send(Config, send_N, 10000, 4096).
+
+send_N_16384B(Config) ->
+ doc("Send a 16384B frame 10000 times."),
+ do_send(Config, send_N, 10000, 16384).
+
+%send_N_16384B_10K(Config) ->
+% doc("Send and receive a 16384B frame 10000 times."),
+% do_send(Config, send_N, 10000, 16384).
+
+do_send(Config, What, Num, FrameSize) ->
+ {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_ignore", Config),
+ FrameType = config(frame_type, Config),
+ FrameData = case FrameType of
+ text -> do_text_data(Config, FrameSize);
+ binary -> rand:bytes(FrameSize)
+ end,
+ %% Heat up the processes before doing the real run.
+% do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData),
+ {Time, _} = timer:tc(?MODULE, do_send_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]),
+ do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]),
+ gun:ws_send(ConnPid, StreamRef, close),
+ {ok, close} = receive_ws(ConnPid, StreamRef),
+ gun_down(ConnPid).
+
+do_send_loop(ConnPid, StreamRef, 0, _, _) ->
+ gun:ws_send(ConnPid, StreamRef, {text, <<"CHECK">>}),
+ {ok, {text, <<"CHECK">>}} = receive_ws(ConnPid, StreamRef),
+ ok;
+do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData) ->
+ gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}),
+ do_send_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData).
+
+%% Internal.
+
+do_text_data(Config, FrameSize) ->
+ do_text_data1(config(text_data, Config), FrameSize).
+
+do_text_data1(LargeText, FrameSize) when byte_size(LargeText) >= FrameSize ->
+ binary:part(LargeText, 0, FrameSize);
+do_text_data1(LargeText, FrameSize) ->
+ do_text_data1(<<LargeText/binary, LargeText/binary>>, FrameSize).
+
+do_format_size(Size) when Size < 1024 ->
+ integer_to_list(Size) ++ "B";
+do_format_size(Size) when Size < (1024*1024) ->
+ integer_to_list(Size div 1024) ++ "KiB";
+do_format_size(Size) ->
+ integer_to_list(Size div (1024*1024)) ++ "MiB".
+
+do_log(Str, Args) ->
+ ct:log(Str, Args),
+ io:format(ct_default_gl, Str ++ "~n", Args).