aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-01-02 13:29:56 +0100
committerLoïc Hoguin <[email protected]>2020-01-02 13:29:56 +0100
commit592029070dea7c1f7b85d465e250ef6842e1a46b (patch)
treea52ad6276da55a693d4d0a42b2bad8606400ac49
parent3a7232b019f975a594a696eace46abcbfeec5b2e (diff)
downloadcowboy-592029070dea7c1f7b85d465e250ef6842e1a46b.tar.gz
cowboy-592029070dea7c1f7b85d465e250ef6842e1a46b.tar.bz2
cowboy-592029070dea7c1f7b85d465e250ef6842e1a46b.zip
Reduce number of Transport:send/2 calls for HTTP/2
When sending a complete response it is far more efficient to send the headers and the body in one Transport:send/2 call instead of two or more, at least for small responses. This is the HTTP/2 counterpart to what was done for HTTP/1.1 many years ago in bfab8d4b22d858e7cffa97d04210a62fae56681c. In HTTP/2's case however the implementation is a little more difficult due to flow control. On the other hand the optimization will apply not only for headers/body but also for the body of multiple separate responses, which may need to be sent all at the same time when we receive a WINDOW_UPDATE frame. When a body is sent using sendfile however a separate call is still made.
-rw-r--r--src/cowboy_http2.erl112
1 files changed, 75 insertions, 37 deletions
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index aa2dfbc..c8a4c7a 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -345,7 +345,7 @@ frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
%% We may need to send an alarm for each of the streams sending data.
lists:foldl(
fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end,
- send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData),
+ send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData, []),
SendData);
{error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} ->
reset_stream(State#state{http2_machine=HTTP2Machine},
@@ -623,11 +623,11 @@ commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) ->
commands(State, StreamID, Tail);
%% Send a response body chunk.
commands(State0, StreamID, [{data, IsFin, Data}|Tail]) ->
- State = maybe_send_data(State0, StreamID, IsFin, Data),
+ State = maybe_send_data(State0, StreamID, IsFin, Data, []),
commands(State, StreamID, Tail);
%% Send trailers.
commands(State0, StreamID, [{trailers, Trailers}|Tail]) ->
- State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}),
+ State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}, []),
commands(State, StreamID, Tail);
%% Send a push promise.
%%
@@ -728,7 +728,7 @@ update_window(State=#state{socket=Socket, transport=Transport,
%% Send the response, trailers or data.
-send_response(State0, StreamID, StatusCode, Headers, Body) ->
+send_response(State0=#state{http2_machine=HTTP2Machine0}, StreamID, StatusCode, Headers, Body) ->
Size = case Body of
{sendfile, _, Bytes, _} -> Bytes;
_ -> iolist_size(Body)
@@ -738,8 +738,14 @@ send_response(State0, StreamID, StatusCode, Headers, Body) ->
State = send_headers(State0, StreamID, fin, StatusCode, Headers),
maybe_terminate_stream(State, StreamID, fin);
_ ->
- State = send_headers(State0, StreamID, nofin, StatusCode, Headers),
- maybe_send_data(State, StreamID, fin, Body)
+ %% @todo Add a test for HEAD to make sure we don't send the body when
+ %% returning {response...} from a stream handler (or {headers...} then {data...}).
+ {ok, _IsFin, HeaderBlock, HTTP2Machine}
+ = cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, nofin,
+ #{status => cow_http:status_to_integer(StatusCode)},
+ headers_to_list(Headers)),
+ maybe_send_data(State0#state{http2_machine=HTTP2Machine}, StreamID, fin, Body,
+ [cow_http2:headers(StreamID, nofin, HeaderBlock)])
end.
send_headers(State=#state{socket=Socket, transport=Transport,
@@ -758,17 +764,24 @@ headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) ->
headers_to_list(Headers) ->
maps:to_list(Headers).
-maybe_send_data(State0=#state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0) ->
+maybe_send_data(State0=#state{socket=Socket, transport=Transport,
+ http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0, Prefix) ->
Data = case is_tuple(Data0) of
false -> {data, Data0};
true -> Data0
end,
case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of
{ok, HTTP2Machine} ->
+ %% If we have prefix data (like a HEADERS frame) we need to send it
+ %% even if we do not send any DATA frames.
+ case Prefix of
+ [] -> ok;
+ _ -> Transport:send(Socket, Prefix)
+ end,
maybe_send_data_alarm(State0#state{http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID);
{send, SendData, HTTP2Machine} ->
State = #state{http2_status=Status, streams=Streams}
- = send_data(State0#state{http2_machine=HTTP2Machine}, SendData),
+ = send_data(State0#state{http2_machine=HTTP2Machine}, SendData, Prefix),
%% Terminate the connection if we are closing and all streams have completed.
if
Status =:= closing, Streams =:= #{} ->
@@ -778,39 +791,64 @@ maybe_send_data(State0=#state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Dat
end
end.
-send_data(State, []) ->
- State;
-send_data(State0, [{StreamID, IsFin, SendData}|Tail]) ->
- State = send_data(State0, StreamID, IsFin, SendData),
- send_data(State, Tail).
-
-send_data(State0, StreamID, IsFin, [Data]) ->
- State = send_data_frame(State0, StreamID, IsFin, Data),
- maybe_terminate_stream(State, StreamID, IsFin);
-send_data(State0, StreamID, IsFin, [Data|Tail]) ->
- State = send_data_frame(State0, StreamID, nofin, Data),
- send_data(State, StreamID, IsFin, Tail).
-
-send_data_frame(State=#state{socket=Socket, transport=Transport},
- StreamID, IsFin, {data, Data}) ->
- Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)),
- State;
-send_data_frame(State=#state{socket=Socket, transport=Transport, opts=Opts},
- StreamID, IsFin, {sendfile, Offset, Bytes, Path}) ->
- Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)),
- %% When sendfile is disabled we explicitly use the fallback.
- _ = case maps:get(sendfile, Opts, true) of
- true -> Transport:sendfile(Socket, Path, Offset, Bytes);
- false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+send_data(State0=#state{socket=Socket, transport=Transport, opts=Opts}, SendData, Prefix) ->
+ {Acc, State} = prepare_data(State0, SendData, [], Prefix),
+ _ = [case Data of
+ {sendfile, Offset, Bytes, Path} ->
+ %% When sendfile is disabled we explicitly use the fallback.
+ _ = case maps:get(sendfile, Opts, true) of
+ true -> Transport:sendfile(Socket, Path, Offset, Bytes);
+ false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+ end;
+ _ ->
+ Transport:send(Socket, Data)
+ end || Data <- Acc],
+ State.
+
+prepare_data(State, [], Acc, []) ->
+ {lists:reverse(Acc), State};
+prepare_data(State, [], Acc, Buffer) ->
+ {lists:reverse([lists:reverse(Buffer)|Acc]), State};
+prepare_data(State0, [{StreamID, IsFin, SendData}|Tail], Acc0, Buffer0) ->
+ {Acc, Buffer, State} = prepare_data(State0, StreamID, IsFin, SendData, Acc0, Buffer0),
+ prepare_data(State, Tail, Acc, Buffer).
+
+prepare_data(State0, StreamID, IsFin, [], Acc, Buffer) ->
+ State = maybe_terminate_stream(State0, StreamID, IsFin),
+ {Acc, Buffer, State};
+prepare_data(State0, StreamID, IsFin, [FrameData|Tail], Acc, Buffer) ->
+ FrameIsFin = case Tail of
+ [] -> IsFin;
+ _ -> nofin
end,
- State;
+ case prepare_data_frame(State0, StreamID, FrameIsFin, FrameData) of
+ {{MoreData, Sendfile}, State} when is_tuple(Sendfile) ->
+ case Buffer of
+ [] ->
+ prepare_data(State, StreamID, IsFin, Tail,
+ [Sendfile, MoreData|Acc], []);
+ _ ->
+ prepare_data(State, StreamID, IsFin, Tail,
+ [Sendfile, lists:reverse([MoreData|Buffer])|Acc], [])
+ end;
+ {MoreData, State} ->
+ prepare_data(State, StreamID, IsFin, Tail,
+ Acc, [MoreData|Buffer])
+ end.
+
+prepare_data_frame(State, StreamID, IsFin, {data, Data}) ->
+ {cow_http2:data(StreamID, IsFin, Data),
+ State};
+prepare_data_frame(State, StreamID, IsFin, Sendfile={sendfile, _, Bytes, _}) ->
+ {{cow_http2:data_header(StreamID, IsFin, Bytes), Sendfile},
+ State};
%% The stream is terminated in cow_http2_machine:prepare_trailers.
-send_data_frame(State=#state{socket=Socket, transport=Transport,
- http2_machine=HTTP2Machine0}, StreamID, nofin, {trailers, Trailers}) ->
+prepare_data_frame(State=#state{http2_machine=HTTP2Machine0},
+ StreamID, nofin, {trailers, Trailers}) ->
{ok, HeaderBlock, HTTP2Machine}
= cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers),
- Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)),
- State#state{http2_machine=HTTP2Machine}.
+ {cow_http2:headers(StreamID, fin, HeaderBlock),
+ State#state{http2_machine=HTTP2Machine}}.
%% After we have sent or queued data we may need to set or clear an alarm.
%% We do this by comparing the HTTP2Machine buffer state before/after for