Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(SendMessage): always use available send space #1835

Closed
wants to merge 1 commit into from

Conversation

mxinden
Copy link
Collaborator

@mxinden mxinden commented Apr 16, 2024

Always use up send space on QUIC layer to ensure receiving ConnectionEvent::SendStreamWritable event when more send space is available.

See #1819 for details. This commit implements option 2.

Fixes #1819.


Not particularly happy with the fix. Though, as of today, I am considering this to be the least intrusive option. Will ponder a bit more before marking this as ready-for-review. Input welcome anyways.

Always use up send space on QUIC layer to ensure receiving
`ConnectionEvent::SendStreamWritable` event when more send space is available.

See mozilla#1819 for details. This commit
implements option 2.

Fixes mozilla#1819.
Comment on lines +171 to +174
if buf.is_empty() {
qerror!([self], "zero-length send_data");
return Err(Error::InvalidInput);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in neqo_transport::SendStream::send_internal which is called by this function anyways. Thus just moving the return Err up the stack, which allows the else if available <= 2 to depend on buf not to be empty.

if buf.is_empty() {
qerror!([self], "zero-length send on stream");
return Err(Error::InvalidInput);
}
.

Copy link

Benchmark results

Performance differences relative to 3e53709.

  • drain a timer quickly time: [434.43 ns 440.28 ns 446.05 ns]
    change: [-0.6361% +0.5817% +1.9138%] (p = 0.37 > 0.05)
    No change in performance detected.

  • coalesce_acked_from_zero 1+1 entries
    time: [196.93 ns 197.45 ns 197.99 ns]
    change: [-0.5925% -0.2667% +0.0599%] (p = 0.12 > 0.05)
    No change in performance detected.

  • coalesce_acked_from_zero 3+1 entries
    time: [240.84 ns 241.48 ns 242.18 ns]
    change: [-0.4095% +0.1031% +0.6494%] (p = 0.71 > 0.05)
    No change in performance detected.

  • coalesce_acked_from_zero 10+1 entries
    time: [239.13 ns 239.78 ns 240.59 ns]
    change: [-1.3030% -0.4717% +0.1387%] (p = 0.22 > 0.05)
    No change in performance detected.

  • coalesce_acked_from_zero 1000+1 entries
    time: [216.83 ns 217.01 ns 217.22 ns]
    change: [-0.5875% +0.0378% +0.6067%] (p = 0.91 > 0.05)
    No change in performance detected.

  • RxStreamOrderer::inbound_frame()
    time: [118.50 ms 118.60 ms 118.72 ms]
    change: [-0.6484% -0.4215% -0.2274%] (p = 0.00 < 0.05)
    Change within noise threshold.

  • transfer/Run multiple transfers with varying seeds
    time: [120.68 ms 120.93 ms 121.19 ms]
    thrpt: [33.005 MiB/s 33.077 MiB/s 33.146 MiB/s]
    change:
    time: [-1.3269% -1.0257% -0.7034%] (p = 0.00 < 0.05)
    thrpt: [+0.7084% +1.0364% +1.3448%]
    Change within noise threshold.

  • transfer/Run multiple transfers with the same seed
    time: [121.43 ms 121.66 ms 121.90 ms]
    thrpt: [32.815 MiB/s 32.880 MiB/s 32.940 MiB/s]
    change:
    time: [-0.7134% -0.4569% -0.1872%] (p = 0.00 < 0.05)
    thrpt: [+0.1876% +0.4590% +0.7185%]
    Change within noise threshold.

  • 1-conn/1-100mb-resp (aka. Download)/client
    time: [1.0767 s 1.0835 s 1.0913 s]
    thrpt: [91.631 MiB/s 92.294 MiB/s 92.879 MiB/s]
    change:
    time: [-2.9181% -1.5420% -0.2541%] (p = 0.04 < 0.05)
    thrpt: [+0.2547% +1.5661% +3.0059%]
    Change within noise threshold.

  • 1-conn/10_000-parallel-1b-resp (aka. RPS)/client
    time: [422.36 ms 424.63 ms 426.91 ms]
    thrpt: [23.424 Kelem/s 23.550 Kelem/s 23.676 Kelem/s]
    change:
    time: [-1.5623% -0.8706% -0.2014%] (p = 0.02 < 0.05)
    thrpt: [+0.2018% +0.8783% +1.5871%]
    Change within noise threshold.

  • 1-conn/1-1b-resp (aka. HPS)/client
    time: [50.202 ms 50.529 ms 50.856 ms]
    thrpt: [19.663 elem/s 19.791 elem/s 19.920 elem/s]
    change:
    time: [-1.4199% -0.5432% +0.3622%] (p = 0.25 > 0.05)
    thrpt: [-0.3609% +0.5462% +1.4403%]
    No change in performance detected.

Client/server transfer results

Transfer of 134217728 bytes over loopback.

Client Server CC Pacing Mean [ms] Min [ms] Max [ms] Relative
msquic msquic 658.4 ± 238.6 402.7 976.7 1.00
neqo msquic reno on 931.6 ± 208.9 735.0 1264.1 1.00
neqo msquic reno 939.1 ± 253.7 762.6 1511.9 1.00
neqo msquic cubic on 968.5 ± 213.9 760.1 1423.2 1.00
neqo msquic cubic 984.5 ± 295.5 770.9 1485.8 1.00
msquic neqo reno on 4326.8 ± 142.4 4093.6 4551.4 1.00
msquic neqo reno 4427.2 ± 367.1 4077.9 5182.2 1.00
msquic neqo cubic on 4460.7 ± 315.4 4172.1 5239.8 1.00
msquic neqo cubic 4406.1 ± 194.8 4138.4 4774.7 1.00
neqo neqo reno on 3713.5 ± 274.7 3440.2 4341.9 1.00
neqo neqo reno 3767.7 ± 210.6 3379.5 4068.4 1.00
neqo neqo cubic on 4319.0 ± 227.8 4042.1 4765.3 1.00
neqo neqo cubic 4508.6 ± 277.5 4161.6 5075.9 1.00

⬇️ Download logs

Comment on lines +188 to +198
// Rare case, where available send space at most fits the data frame
// header (min 2 bytes), but no data. Don't return `Ok(0)`, but
// instead use up `available` anyways, with a small but non-zero
// data frame, buffering the remainder, thereby signaling to the
// QUIC layer that more is needed, and thus ensure to receive
// [`neqo_transport::ConnectionEvent::SendStreamWritable`] when more
// send space is available.
const MIN_DATA: usize = 1; // arbitrary small value, larger zero
return self
.send_data_atomic(conn, &buf[..MIN_DATA])
.map(|()| MIN_DATA);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see a test showing that this is OK. I'm fairly sure that it isn't.

You aren't sending a DATA frame header for this. That header is always at least 2 bytes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinthomson the DATA frame header is sent as part of the call to self.send_data_atomic:

fn send_data_atomic(&mut self, conn: &mut Connection, buf: &[u8]) -> Res<()> {
let data_frame = HFrame::Data {
len: buf.len() as u64,
};
let mut enc = Encoder::default();
data_frame.encode(&mut enc);
self.stream.buffer(enc.as_ref());
self.stream.buffer(buf);
_ = self.stream.send_buffer(conn)?;
Ok(())
}

Not saying that this doesn't need a test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so that will result in us sending three bytes to the stream, in a case where we only have space for 2 or less. How does that ever work?

Copy link
Collaborator Author

@mxinden mxinden Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a case where we only have space for 2 or less

We will use up the available space and buffer the remaining. E.g. say that we have 2 bytes available, we send the header (2bytes) and buffer 1 byte.

Once more space is available, we get an event (because we previously exhausted the send space) and can thus first empty the 1byte from the buffer and then send more goodput.

Does that make sense @martinthomson?

(Not particularly happy with this hack. Still thinking about alternatives.)

Edit: alternative #1838

mxinden added a commit to mxinden/neqo that referenced this pull request Apr 17, 2024
Previously `SendMessage::send_data` could stall, if less than the minimum
message size is available to be sent. See
mozilla#1819 for details.

This commit implements solution (3) proposed in
mozilla#1819.

This commit introduces `SendStream::set_writable_event_low_watermark` which is
then used in `SendMessage::send_data` to signal to `SendStream` the minimum
required send space (low watermark) for the next send. Once reached,
`SendStream` emits a `SendStreamWritable` eventually triggering another
`SendMessage::send_data`.

Alternative to mozilla#1835. Compared to
mozilla#1835, this fix does not utilize the
`SendMessage` buffer, thus does not introduce an indirection to the send path.
In addition, under the assumption that available send space is increased in
larger batches, this fix does not send tiny data frames (2 byte header, 1 byte
goodput). Downside, compared to mozilla#1835, is
that it requires both changes in `neqo-transport` and `neqo-http3`.

Secondarily, this fixes mozilla#1821 as well.
mxinden added a commit to mxinden/neqo that referenced this pull request Apr 17, 2024
Previously `SendMessage::send_data` could stall, if less than the minimum
message size is available to be sent. See
mozilla#1819 for details.

This commit implements solution (3) proposed in
mozilla#1819.

This commit introduces `SendStream::set_writable_event_low_watermark` which is
then used in `SendMessage::send_data` to signal to `SendStream` the minimum
required send space (low watermark) for the next send. Once reached,
`SendStream` emits a `SendStreamWritable` eventually triggering another
`SendMessage::send_data`.

Alternative to mozilla#1835. Compared to
mozilla#1835, this fix does not utilize the
`SendMessage` buffer, thus does not introduce an indirection to the send path.
In addition, under the assumption that available send space is increased in
larger batches, this fix does not send tiny data frames (2 byte header, 1 byte
goodput). Downside, compared to mozilla#1835, is
that it requires both changes in `neqo-transport` and `neqo-http3`.

Secondarily, this fixes mozilla#1821 as well.
@mxinden
Copy link
Collaborator Author

mxinden commented Apr 22, 2024

Closing here in favor of #1838.

@mxinden mxinden closed this Apr 22, 2024
github-merge-queue bot pushed a commit that referenced this pull request May 7, 2024
…1838)

* fix(SendMessage): use SendStream::set_writable_event_low_watermark

Previously `SendMessage::send_data` could stall, if less than the minimum
message size is available to be sent. See
#1819 for details.

This commit implements solution (3) proposed in
#1819.

This commit introduces `SendStream::set_writable_event_low_watermark` which is
then used in `SendMessage::send_data` to signal to `SendStream` the minimum
required send space (low watermark) for the next send. Once reached,
`SendStream` emits a `SendStreamWritable` eventually triggering another
`SendMessage::send_data`.

Alternative to #1835. Compared to
#1835, this fix does not utilize the
`SendMessage` buffer, thus does not introduce an indirection to the send path.
In addition, under the assumption that available send space is increased in
larger batches, this fix does not send tiny data frames (2 byte header, 1 byte
goodput). Downside, compared to #1835, is
that it requires both changes in `neqo-transport` and `neqo-http3`.

Secondarily, this fixes #1821 as well.

* Move const

* Add documentation

* Add SendStream test

* Fix intra doc links

* Add neqo-http3 test

* Replace goodput with payload

* Re-trigger benchmarks

Let's see whether the "Download" benchmark is consistent.

* Rename emit_writable_event to maybe_emit_writable_event

* Replace expect with unwrap

* Use NonZeroUsize::get

* Replace expect with unwrap

* %s/Actually sending/Sending

* Typo

* Have update() return available amount

* Document setting once would suffice

* Reduce verbosity

* fix: drop RefCell mutable borrow early
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

http3::SendMessage::send_data can stall
2 participants