Просмотр исходного кода

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.
Loïc Hoguin 5 лет назад
Родитель
Сommit
592029070d
1 измененных файлов с 75 добавлено и 37 удалено
  1. 75 37
      src/cowboy_http2.erl

+ 75 - 37
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