Skip to content

Commit 41036fc

Browse files
larseggertmxinden
andauthored
feat: More ECN and DSCP stats (#2505)
* feat: More ECN stats Inspired by @martinduke's [IETF presentation on ECN](https://datatracker.ietf.org/meeting/122/materials/slides-122-tsvwg-udp-ecn-00), add some more stats around what packet types we send and receive with different ECN codepoints. This will require additional glue code to ship these stats back as telemetry. * fmt * Improve coverage? * Record ECN mark transitions * Tweak output * Output tweaks * Add DSCP stats * Update neqo-transport/src/stats.rs Co-authored-by: Max Inden <[email protected]> Signed-off-by: Lars Eggert <[email protected]> * Fix rebase * Update neqo-transport/src/stats.rs Co-authored-by: Max Inden <[email protected]> Signed-off-by: Lars Eggert <[email protected]> * Suggestions from @mxinden * Fixes and simplifications * Update neqo-transport/src/connection/mod.rs Co-authored-by: Max Inden <[email protected]> Signed-off-by: Lars Eggert <[email protected]> * Fix * Update neqo-transport/src/connection/mod.rs Co-authored-by: Max Inden <[email protected]> Signed-off-by: Lars Eggert <[email protected]> * Fixes * Fix DSCP tracking and add some tests --------- Signed-off-by: Lars Eggert <[email protected]> Co-authored-by: Max Inden <[email protected]>
1 parent 054b7cb commit 41036fc

File tree

10 files changed

+315
-49
lines changed

10 files changed

+315
-49
lines changed

neqo-common/src/tos.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
use std::fmt::Debug;
88

99
use enum_map::Enum;
10-
use strum::FromRepr;
10+
use strum::{EnumIter, FromRepr};
1111

1212
/// ECN (Explicit Congestion Notification) codepoints mapped to the
1313
/// lower 2 bits of the TOS field.
1414
/// <https://www.iana.org/assignments/dscp-registry/dscp-registry.xhtml>
15-
#[derive(Copy, Clone, PartialEq, Eq, Enum, Default, Debug, FromRepr)]
15+
#[derive(Copy, Clone, PartialEq, Eq, Enum, Default, Debug, FromRepr, EnumIter)]
1616
#[repr(u8)]
1717
pub enum IpTosEcn {
1818
#[default]

neqo-transport/src/connection/mod.rs

+31-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::{
2020

2121
use neqo_common::{
2222
event::Provider as EventProvider, hex, hex_snip_middle, hrtime, qdebug, qerror, qinfo,
23-
qlog::NeqoQlog, qtrace, qwarn, Datagram, Decoder, Encoder, IpTos, Role,
23+
qlog::NeqoQlog, qtrace, qwarn, Datagram, Decoder, Encoder, IpTos, IpTosEcn, Role,
2424
};
2525
use neqo_crypto::{
2626
agent::CertificateInfo, Agent, AntiReplay, AuthenticationStatus, Cipher, Client, Group,
@@ -1528,20 +1528,32 @@ impl Connection {
15281528
}
15291529

15301530
/// After a Initial, Handshake, `ZeroRtt`, or Short packet is successfully processed.
1531+
#[expect(clippy::too_many_arguments, reason = "Yes, but they're needed.")]
15311532
fn postprocess_packet(
15321533
&mut self,
15331534
path: &PathRef,
15341535
tos: IpTos,
15351536
remote: SocketAddr,
15361537
packet: &PublicPacket,
1538+
packet_number: PacketNumber,
15371539
migrate: bool,
15381540
now: Instant,
15391541
) {
1542+
let ecn_mark = IpTosEcn::from(tos);
1543+
let mut stats = self.stats.borrow_mut();
1544+
stats.ecn_rx[packet.packet_type()] += ecn_mark;
1545+
if let Some(last_ecn_mark) = stats.ecn_last_mark.filter(|&last_ecn_mark| {
1546+
last_ecn_mark != ecn_mark && stats.ecn_rx_transition[last_ecn_mark][ecn_mark].is_none()
1547+
}) {
1548+
stats.ecn_rx_transition[last_ecn_mark][ecn_mark] =
1549+
Some((packet.packet_type(), packet_number));
1550+
}
1551+
1552+
stats.ecn_last_mark = Some(ecn_mark);
1553+
drop(stats);
15401554
let space = PacketNumberSpace::from(packet.packet_type());
15411555
if let Some(space) = self.acks.get_mut(space) {
1542-
let space_ecn_marks = space.ecn_marks();
1543-
*space_ecn_marks += tos.into();
1544-
self.stats.borrow_mut().ecn_rx = *space_ecn_marks;
1556+
*space.ecn_marks() += ecn_mark;
15451557
} else {
15461558
qtrace!("Not tracking ECN for dropped packet number space");
15471559
}
@@ -1627,6 +1639,7 @@ impl Connection {
16271639
break;
16281640
}
16291641
};
1642+
self.stats.borrow_mut().dscp_rx[tos.into()] += 1;
16301643
match self.preprocess_packet(&packet, path, dcid.as_ref(), now)? {
16311644
PreprocessResult::Continue => (),
16321645
PreprocessResult::Next => break,
@@ -1639,6 +1652,7 @@ impl Connection {
16391652
match packet.decrypt(&mut self.crypto.states, now + pto) {
16401653
Ok(payload) => {
16411654
// OK, we have a valid packet.
1655+
let pn = payload.pn();
16421656
self.idle_timeout.on_packet_received(now);
16431657
self.log_packet(
16441658
packet::MetaData::new_in(path, tos, packet_len, &payload),
@@ -1657,14 +1671,14 @@ impl Connection {
16571671

16581672
let space = PacketNumberSpace::from(payload.packet_type());
16591673
if let Some(space) = self.acks.get_mut(space) {
1660-
if space.is_duplicate(payload.pn()) {
1661-
qdebug!("Duplicate packet {space}-{}", payload.pn());
1674+
if space.is_duplicate(pn) {
1675+
qdebug!("Duplicate packet {space}-{}", pn);
16621676
self.stats.borrow_mut().dups_rx += 1;
16631677
} else {
16641678
match self.process_packet(path, &payload, now) {
16651679
Ok(migrate) => {
16661680
self.postprocess_packet(
1667-
path, tos, remote, &packet, migrate, now,
1681+
path, tos, remote, &packet, pn, migrate, now,
16681682
);
16691683
}
16701684
Err(e) => {
@@ -2493,14 +2507,17 @@ impl Connection {
24932507
continue;
24942508
}
24952509

2510+
// If we don't have a TOS for this UDP datagram yet (i.e. `tos` is `None`), get it,
2511+
// adding a `RecoveryToken::EcnEct0` to `tokens` in case of loss.
2512+
let tos = packet_tos.get_or_insert_with(|| path.borrow().tos(&mut tokens));
24962513
self.log_packet(
24972514
packet::MetaData::new_out(
24982515
path,
24992516
pt,
25002517
pn,
25012518
builder.len() + aead_expansion,
25022519
&builder.as_ref()[payload_start..],
2503-
*packet_tos.get_or_insert(path.borrow().tos(&mut tokens)),
2520+
*tos,
25042521
),
25052522
now,
25062523
);
@@ -2540,6 +2557,12 @@ impl Connection {
25402557
self.loss_recovery.on_packet_sent(path, sent, now);
25412558
}
25422559

2560+
// Track which packet types are sent with which ECN codepoints. For
2561+
// coalesced packets, this increases the counts for each packet type
2562+
// contained in the coalesced packet. This is per Section 13.4.1 of
2563+
// RFC 9000.
2564+
self.stats.borrow_mut().ecn_tx[pt] += IpTosEcn::from(*tos);
2565+
25432566
if space == PacketNumberSpace::Handshake
25442567
&& self.role == Role::Server
25452568
&& self.state == State::Confirmed

neqo-transport/src/connection/tests/ecn.rs

+64-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use std::time::Duration;
88

99
use neqo_common::{Datagram, IpTos, IpTosEcn};
10+
use strum::IntoEnumIterator as _;
1011
use test_fixture::{
1112
assertions::{assert_v4_path, assert_v6_path},
1213
fixture_init, now, DEFAULT_ADDR_V4,
@@ -20,6 +21,7 @@ use crate::{
2021
send_with_modifier_and_receive, DEFAULT_RTT,
2122
},
2223
ecn,
24+
packet::PacketType,
2325
path::MAX_PATH_PROBES,
2426
ConnectionId, ConnectionParameters, StreamType,
2527
};
@@ -141,6 +143,44 @@ fn migration_delay_to_ecn_blackhole() {
141143
}
142144
}
143145

146+
#[test]
147+
fn debug() {
148+
let stats = crate::Stats::default();
149+
assert_eq!(
150+
format!("{stats:?}"),
151+
"stats for\u{0020}
152+
rx: 0 drop 0 dup 0 saved 0
153+
tx: 0 lost 0 lateack 0 ptoack 0
154+
pmtud: 0 sent 0 acked 0 lost 0 change 0 iface_mtu 0 pmtu
155+
resumed: false
156+
frames rx:
157+
crypto 0 done 0 token 0 close 0
158+
ack 0 (max 0) ping 0 padding 0
159+
stream 0 reset 0 stop 0
160+
max: stream 0 data 0 stream_data 0
161+
blocked: stream 0 data 0 stream_data 0
162+
datagram 0
163+
ncid 0 rcid 0 pchallenge 0 presponse 0
164+
ack_frequency 0
165+
frames tx:
166+
crypto 0 done 0 token 0 close 0
167+
ack 0 (max 0) ping 0 padding 0
168+
stream 0 reset 0 stop 0
169+
max: stream 0 data 0 stream_data 0
170+
blocked: stream 0 data 0 stream_data 0
171+
datagram 0
172+
ncid 0 rcid 0 pchallenge 0 presponse 0
173+
ack_frequency 0
174+
ecn:
175+
tx:
176+
acked:
177+
rx:
178+
path validation outcomes: ValidationCount({Capable: 0, NotCapable(BlackHole): 0, NotCapable(Bleaching): 0, NotCapable(ReceivedUnsentECT1): 0})
179+
mark transitions:
180+
dscp: \n"
181+
);
182+
}
183+
144184
#[test]
145185
fn stats() {
146186
let now = now();
@@ -166,14 +206,33 @@ fn stats() {
166206
}
167207
}
168208

169-
for codepoint in [IpTosEcn::Ect1, IpTosEcn::Ce] {
170-
assert_eq!(stats.ecn_tx[codepoint], 0);
171-
assert_eq!(stats.ecn_rx[codepoint], 0);
209+
for packet_type in PacketType::iter() {
210+
for codepoint in [IpTosEcn::Ect1, IpTosEcn::Ce] {
211+
assert_eq!(stats.ecn_tx[packet_type][codepoint], 0);
212+
assert_eq!(stats.ecn_tx_acked[packet_type][codepoint], 0);
213+
assert_eq!(stats.ecn_rx[packet_type][codepoint], 0);
214+
}
172215
}
173216
}
174217

175-
assert!(client.stats().ecn_tx[IpTosEcn::Ect0] <= server.stats().ecn_rx[IpTosEcn::Ect0]);
176-
assert!(server.stats().ecn_tx[IpTosEcn::Ect0] <= client.stats().ecn_rx[IpTosEcn::Ect0]);
218+
for packet_type in PacketType::iter() {
219+
assert!(
220+
client.stats().ecn_tx_acked[packet_type][IpTosEcn::Ect0]
221+
<= server.stats().ecn_rx[packet_type][IpTosEcn::Ect0]
222+
);
223+
assert!(
224+
server.stats().ecn_tx_acked[packet_type][IpTosEcn::Ect0]
225+
<= client.stats().ecn_rx[packet_type][IpTosEcn::Ect0]
226+
);
227+
assert_eq!(
228+
client.stats().ecn_tx[packet_type][IpTosEcn::Ect0],
229+
server.stats().ecn_rx[packet_type][IpTosEcn::Ect0]
230+
);
231+
assert_eq!(
232+
server.stats().ecn_tx[packet_type][IpTosEcn::Ect0],
233+
client.stats().ecn_rx[packet_type][IpTosEcn::Ect0]
234+
);
235+
}
177236
}
178237

179238
#[test]

neqo-transport/src/connection/tests/vn.rs

+23-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use std::time::Duration;
88

9-
use neqo_common::{event::Provider as _, Decoder, Encoder};
9+
use neqo_common::{event::Provider as _, Decoder, Encoder, IpTosDscp};
1010
use test_fixture::{assertions, datagram, now};
1111

1212
use super::{
@@ -17,12 +17,20 @@ use super::{
1717
use crate::{
1818
packet::PACKET_BIT_LONG,
1919
tparams::{TransportParameter, TransportParameterId::*},
20-
ConnectionParameters, Error, Version, MIN_INITIAL_PACKET_SIZE,
20+
ConnectionParameters, Error, Stats, Version, MIN_INITIAL_PACKET_SIZE,
2121
};
2222

2323
// The expected PTO duration after the first Initial is sent.
2424
const INITIAL_PTO: Duration = Duration::from_millis(300);
2525

26+
/// # Panics
27+
///
28+
/// When the count of received packets doesn't match the count of received packets with the
29+
/// (default) DSCP.
30+
pub fn assert_dscp(stats: &Stats) {
31+
assert_eq!(stats.dscp_rx[IpTosDscp::Cs0], stats.packets_rx);
32+
}
33+
2634
#[test]
2735
fn unknown_version() {
2836
let mut client = default_client();
@@ -33,6 +41,7 @@ fn unknown_version() {
3341
unknown_version_packet.resize(MIN_INITIAL_PACKET_SIZE, 0x0);
3442
drop(client.process(Some(datagram(unknown_version_packet)), now()));
3543
assert_eq!(1, client.stats().dropped_rx);
44+
assert_dscp(&client.stats());
3645
}
3746

3847
#[test]
@@ -48,6 +57,7 @@ fn server_receive_unknown_first_packet() {
4857
);
4958

5059
assert_eq!(1, server.stats().dropped_rx);
60+
assert_dscp(&server.stats());
5161
}
5262

5363
fn create_vn(initial_pkt: &[u8], versions: &[u32]) -> Vec<u8> {
@@ -89,6 +99,7 @@ fn version_negotiation_current_version() {
8999
assert_eq!(delay, INITIAL_PTO);
90100
assert_eq!(*client.state(), State::WaitInitial);
91101
assert_eq!(1, client.stats().dropped_rx);
102+
assert_dscp(&client.stats());
92103
}
93104

94105
#[test]
@@ -110,6 +121,7 @@ fn version_negotiation_version0() {
110121
assert_eq!(delay, INITIAL_PTO);
111122
assert_eq!(*client.state(), State::WaitInitial);
112123
assert_eq!(1, client.stats().dropped_rx);
124+
assert_dscp(&client.stats());
113125
}
114126

115127
#[test]
@@ -132,6 +144,7 @@ fn version_negotiation_only_reserved() {
132144
}
133145
_ => panic!("Invalid client state"),
134146
}
147+
assert_dscp(&client.stats());
135148
}
136149

137150
#[test]
@@ -153,6 +166,7 @@ fn version_negotiation_corrupted() {
153166
assert_eq!(delay, INITIAL_PTO);
154167
assert_eq!(*client.state(), State::WaitInitial);
155168
assert_eq!(1, client.stats().dropped_rx);
169+
assert_dscp(&client.stats());
156170
}
157171

158172
#[test]
@@ -174,6 +188,7 @@ fn version_negotiation_empty() {
174188
assert_eq!(delay, INITIAL_PTO);
175189
assert_eq!(*client.state(), State::WaitInitial);
176190
assert_eq!(1, client.stats().dropped_rx);
191+
assert_dscp(&client.stats());
177192
}
178193

179194
#[test]
@@ -195,6 +210,7 @@ fn version_negotiation_not_supported() {
195210
}
196211
_ => panic!("Invalid client state"),
197212
}
213+
assert_dscp(&client.stats());
198214
}
199215

200216
#[test]
@@ -217,6 +233,7 @@ fn version_negotiation_bad_cid() {
217233
assert_eq!(delay, INITIAL_PTO);
218234
assert_eq!(*client.state(), State::WaitInitial);
219235
assert_eq!(1, client.stats().dropped_rx);
236+
assert_dscp(&client.stats());
220237
}
221238

222239
#[test]
@@ -227,6 +244,8 @@ fn compatible_upgrade() {
227244
connect(&mut client, &mut server);
228245
assert_eq!(client.version(), Version::Version2);
229246
assert_eq!(server.version(), Version::Version2);
247+
assert_dscp(&client.stats());
248+
assert_dscp(&server.stats());
230249
}
231250

232251
/// When the first packet from the client is gigantic, the server might generate acknowledgment
@@ -264,6 +283,8 @@ fn compatible_upgrade_large_initial() {
264283
// Only handshake padding is "dropped".
265284
assert_eq!(client.stats().dropped_rx, 1);
266285
assert!(matches!(server.stats().dropped_rx, 2 | 3));
286+
assert_dscp(&client.stats());
287+
assert_dscp(&server.stats());
267288
}
268289

269290
/// A server that supports versions 1 and 2 might prefer version 1 and that's OK.

0 commit comments

Comments
 (0)