HTTP/2 protocol handler: tweak receive window sizes

We were using the default ones, provided by RFC7540. It appears they are way
too restrictive and conservative: when downloading something relatively big,
a stream keeps spending the whole session/its own 'recv' windows and thus
we have to constantly send WINDOW_UPDATE frames. This significantly slows
down our HTTP/2 implementation, making it orders of magnitude slower than
HTTP/1.1. To fix this:

- We send SETTINGS_INITIAL_WINDOW_SIZE in the first SETTINGS frame
  to inform our peer that per-stream WINDOW is bigger than 64Kb
- We increase the session's receive window size.

Task-number: QTBUG-63722
Change-Id: I31312fcfd5f0fc0aee6aaa5d3562cc7d1b931adc
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
bb10
Timur Pocheptsov 2017-10-12 15:02:09 +02:00
parent 65eed6d597
commit a520c179b8
5 changed files with 33 additions and 26 deletions

View File

@ -216,6 +216,8 @@ Frame default_SETTINGS_frame()
// MAX frame size (16 kb), disable/enable PUSH_PROMISE
builder.append(Settings::MAX_FRAME_SIZE_ID);
builder.append(quint32(maxFrameSize));
builder.append(Settings::INITIAL_WINDOW_SIZE_ID);
builder.append(initialStreamReceiveWindowSize);
builder.append(Settings::ENABLE_PUSH_ID);
builder.append(quint32(is_PUSH_PROMISE_enabled()));

View File

@ -129,10 +129,20 @@ enum Http2PredefinedParameters
maxConcurrentStreams = 100 // HTTP/2, 6.5.2
};
// It's int, it has internal linkage, it's ok to have it in headers -
// no ODR violation is possible.
// These are ints, const, they have internal linkage, it's ok to have them in
// headers - no ODR violation.
const quint32 lastValidStreamID((quint32(1) << 31) - 1); // HTTP/2, 5.1.1
// The default size of 64K is too small and limiting: if we use it, we end up
// sending WINDOW_UPDATE frames on a stream/session all the time, for each
// 2 DATE frames of size 16K (also default) we'll send a WINDOW_UPDATE frame
// for a given stream and have a download speed order of magnitude lower than
// our own HTTP/1.1 protocol handler. We choose a bigger window size (normally,
// HTTP/2 servers are not afraid to immediately set it to possible max anyway)
// and split this window size between our concurrent streams.
const qint32 initialSessionReceiveWindowSize = defaultSessionWindowSize * 10000;
const qint32 initialStreamReceiveWindowSize = initialSessionReceiveWindowSize / maxConcurrentStreams;
extern const Q_AUTOTEST_EXPORT char Http2clientPreface[clientPrefaceLength];
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request);

View File

@ -161,8 +161,6 @@ bool sum_will_overflow(qint32 windowSize, qint32 delta)
using namespace Http2;
const std::deque<quint32>::size_type QHttp2ProtocolHandler::maxRecycledStreams = 10000;
const qint32 QHttp2ProtocolHandler::sessionMaxRecvWindowSize;
const qint32 QHttp2ProtocolHandler::streamInitialRecvWindowSize;
const quint32 QHttp2ProtocolHandler::maxAcceptableTableSize;
QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *channel)
@ -374,12 +372,10 @@ bool QHttp2ProtocolHandler::sendClientPreface()
if (!frameWriter.write(*m_socket))
return false;
sessionRecvWindowSize = sessionMaxRecvWindowSize;
if (defaultSessionWindowSize < sessionMaxRecvWindowSize) {
const auto delta = sessionMaxRecvWindowSize - defaultSessionWindowSize;
if (!sendWINDOW_UPDATE(connectionStreamID, delta))
return false;
}
sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
const auto delta = Http2::initialSessionReceiveWindowSize - Http2::defaultSessionWindowSize;
if (!sendWINDOW_UPDATE(Http2::connectionStreamID, delta))
return false;
prefaceSent = true;
waitingForSettingsACK = true;
@ -549,20 +545,20 @@ void QHttp2ProtocolHandler::handleDATA()
if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) {
finishStream(stream);
deleteActiveStream(stream.streamID);
} else if (stream.recvWindow < streamInitialRecvWindowSize / 2) {
} else if (stream.recvWindow < Http2::initialStreamReceiveWindowSize / 2) {
QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection,
Q_ARG(quint32, stream.streamID),
Q_ARG(quint32, streamInitialRecvWindowSize - stream.recvWindow));
stream.recvWindow = streamInitialRecvWindowSize;
Q_ARG(quint32, Http2::initialStreamReceiveWindowSize - stream.recvWindow));
stream.recvWindow = Http2::initialStreamReceiveWindowSize;
}
}
}
if (sessionRecvWindowSize < sessionMaxRecvWindowSize / 2) {
if (sessionRecvWindowSize < Http2::initialSessionReceiveWindowSize / 2) {
QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection,
Q_ARG(quint32, connectionStreamID),
Q_ARG(quint32, sessionMaxRecvWindowSize - sessionRecvWindowSize));
sessionRecvWindowSize = sessionMaxRecvWindowSize;
Q_ARG(quint32, Http2::initialSessionReceiveWindowSize - sessionRecvWindowSize));
sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
}
}
@ -1203,7 +1199,7 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, b
const Stream newStream(message, newStreamID,
streamInitialSendWindowSize,
streamInitialRecvWindowSize);
Http2::initialStreamReceiveWindowSize);
if (!uploadDone) {
if (auto src = newStream.data()) {
@ -1398,7 +1394,8 @@ bool QHttp2ProtocolHandler::tryReserveStream(const Http2::Frame &pushPromiseFram
promise.reservedID = reservedID;
promise.pushHeader = requestHeader;
activeStreams.insert(reservedID, Stream(urlKey, reservedID, streamInitialRecvWindowSize));
activeStreams.insert(reservedID, Stream(urlKey, reservedID,
Http2::initialStreamReceiveWindowSize));
return true;
}
@ -1432,7 +1429,7 @@ void QHttp2ProtocolHandler::initReplyFromPushPromise(const HttpMessagePair &mess
// Let's pretent we're sending a request now:
Stream closedStream(message, promise.reservedID,
streamInitialSendWindowSize,
streamInitialRecvWindowSize);
Http2::initialStreamReceiveWindowSize);
closedStream.state = Stream::halfClosedLocal;
activeStreams.insert(promise.reservedID, closedStream);
promisedStream = &activeStreams[promise.reservedID];

View File

@ -176,13 +176,9 @@ private:
quint32 maxConcurrentStreams = Http2::maxConcurrentStreams;
// Control flow:
static const qint32 sessionMaxRecvWindowSize = Http2::defaultSessionWindowSize * 10;
// Signed integer, it can become negative (it's still a valid window size):
qint32 sessionRecvWindowSize = sessionMaxRecvWindowSize;
// We do not negotiate this window size
// We have to send WINDOW_UPDATE frames to our peer also.
static const qint32 streamInitialRecvWindowSize = Http2::defaultSessionWindowSize;
// Signed integer, it can become negative (it's still a valid window size):
qint32 sessionRecvWindowSize = Http2::initialSessionReceiveWindowSize;
// Updated by SETTINGS and WINDOW_UPDATE.
qint32 sessionSendWindowSize = Http2::defaultSessionWindowSize;

View File

@ -124,6 +124,7 @@ private:
const Http2Settings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}};
const Http2Settings tst_Http2::defaultClientSettings{{Http2::Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)},
{Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)},
{Http2::Settings::ENABLE_PUSH_ID, quint32(0)}};
namespace {
@ -274,7 +275,7 @@ void tst_Http2::flowControlClientSide()
ServerPtr srv(newServer(serverSettings));
const QByteArray respond(int(Http2::defaultSessionWindowSize * 50), 'x');
const QByteArray respond(int(Http2::initialStreamReceiveWindowSize * 5), 'x');
srv->setResponseBody(respond);
QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
@ -342,6 +343,7 @@ void tst_Http2::pushPromise()
const EnvVarGuard env("QT_HTTP2_ENABLE_PUSH_PROMISE", "1");
const Http2Settings clientSettings{{Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)},
{Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)},
{Settings::ENABLE_PUSH_ID, quint32(1)}};
ServerPtr srv(newServer(defaultServerSettings, clientSettings));