Skip to content

http2: client throughput capped at 64 KiB per round trip (connection window cannot be raised) #1997

Closed

Description

@banglcb

A client built on package:http2 is much slower than dart:io's
HttpClient (HTTP/1.1) over the same path once there is latency. Measured
with a 4 MiB response from a Go net/http server, through a local proxy that
adds 100 ms of round-trip time:

Client Time
dart:io HttpClient (HTTP/1.1) ~350 ms
package:http2, default settings 7.1 s
package:http2, streamWindowSize: 16 MiB 7.1 s

Three causes, in order of weight:

  1. The connection receive window is fixed at 65535 bytes. HTTP/2 has no
    setting for it; it grows only by a WINDOW_UPDATE on stream 0, and the
    package never sends one beyond replenishing what was consumed. So at most
    64 KiB is in flight per connection per round trip, whatever
    streamWindowSize is.
  2. A raised streamWindowSize only counts from the peer's SETTINGS ACK.
    Receive windows of new streams use the acknowledged settings, so a
    response that starts within the first round trip is capped at 64 KiB for
    that round trip, and a frame larger than 65535 bytes arriving before the
    ACK fails flow control.
  3. One WINDOW_UPDATE per DATA frame, for both the stream and the
    connection (the TODO in IncomingWindowHandler.dataProcessed), plus
    16 KiB frames: about 800 small frames and writes per 4 MiB, roughly
    60 ms of CPU per 4 MiB in the measurement above.

Proposal (a PR is ready and follows this issue):

  • Settings.connectionWindowSize, granted by one WINDOW_UPDATE on stream 0
    after the initial SETTINGS;
  • Settings.maxFrameSize, sent as SETTINGS_MAX_FRAME_SIZE;
  • the initial streamWindowSize and maxFrameSize in force for this end's
    receive side from the moment they are sent (no stream exists yet, and the
    peer reads the SETTINGS before any stream);
  • above the default 65535-byte window, one WINDOW_UPDATE per half window
    processed instead of per frame; default-sized windows unchanged;
  • defaults unchanged.

With streamWindowSize: 6 MiB, connectionWindowSize: 16 MiB,
maxFrameSize: 1 MiB the same transfer takes ~340 ms (~140 ms without the
added delay, against ~155 ms for HTTP/1.1).

A related latent issue, for the maintainers' judgement: when this end's own
SETTINGS_INITIAL_WINDOW_SIZE is acknowledged, SettingsHandler emits
onInitialWindowSizeChange, which StreamHandler applies to the send
windows of open streams (RFC 9113 section 6.9.2 reserves that for the
peer's setting). It is unreachable today because settings are sent only
once, at connection setup, and with the proposal above the acknowledged
value is already in place (difference zero); it would matter if settings
could be changed later.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions