Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cabc5898e |
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
cache: false
|
cache: false
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
cache: false
|
cache: false
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
sudo apt-get install --no-install-recommends musl-tools
|
sudo apt-get install --no-install-recommends musl-tools
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
cache: false
|
cache: false
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install stable Rust toolchain
|
- name: Install stable Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
sudo apt-get install --no-install-recommends musl-tools
|
sudo apt-get install --no-install-recommends musl-tools
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
cache-key: ${{ matrix.task }}-${{ matrix.target }}
|
cache-key: ${{ matrix.task }}-${{ matrix.target }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
cache-key: clippy-sarif
|
cache-key: clippy-sarif
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-proto
|
## irtt-proto
|
||||||
|
|
||||||
### 0.5.1
|
|
||||||
|
|
||||||
#### Added
|
|
||||||
|
|
||||||
- Added a crate-specific README and runnable API doctests to make the published crate's protocol types and codecs easier to discover and use.
|
|
||||||
|
|
||||||
### 0.5.0
|
### 0.5.0
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -34,18 +28,6 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-client
|
## irtt-client
|
||||||
|
|
||||||
### 0.5.3
|
|
||||||
|
|
||||||
#### Added
|
|
||||||
|
|
||||||
- `SocketConfig` can bind UDP sockets to a network interface and set a packet mark on Linux, Android, and Fuchsia, or select a routing FIB on FreeBSD.
|
|
||||||
|
|
||||||
### 0.5.2
|
|
||||||
|
|
||||||
#### Fixed
|
|
||||||
|
|
||||||
- Kernel transmit-timestamp correlation is now invalidated when a probe send fails, preventing a later timestamp from being matched to the wrong probe.
|
|
||||||
|
|
||||||
### 0.5.1
|
### 0.5.1
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -74,12 +56,6 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-server
|
## irtt-server
|
||||||
|
|
||||||
### 0.5.3
|
|
||||||
|
|
||||||
#### Added
|
|
||||||
|
|
||||||
- Added a crate-specific README and usage examples for single- and multi-listener servers, plus runnable API doctests.
|
|
||||||
|
|
||||||
### 0.5.2
|
### 0.5.2
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
@@ -119,12 +95,6 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-stats
|
## irtt-stats
|
||||||
|
|
||||||
### 0.5.1
|
|
||||||
|
|
||||||
#### Added
|
|
||||||
|
|
||||||
- Added a crate-specific README, a finite-versus-continuous statistics example, and runnable API doctests for the published crate.
|
|
||||||
|
|
||||||
### 0.5.0
|
### 0.5.0
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
@@ -138,12 +108,6 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-rs
|
## irtt-rs
|
||||||
|
|
||||||
### 0.7.1
|
|
||||||
|
|
||||||
#### Fixed
|
|
||||||
|
|
||||||
- `IRTT_SERVER_NO_DSCP` now accepts ordinary boolean environment values such as `1` and `0`; falsey values leave DSCP marking enabled, while truthy values disable it. The `--no-dscp` command-line flag remains a flag without a value.
|
|
||||||
|
|
||||||
### 0.7.0
|
### 0.7.0
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|||||||
Generated
+45
-52
@@ -213,9 +213,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -223,9 +223,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.7"
|
version = "4.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -235,14 +235,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.7"
|
version = "4.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -403,7 +403,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -414,7 +414,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -710,12 +710,12 @@ dependencies = [
|
|||||||
"indoc",
|
"indoc",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-client"
|
name = "irtt-client"
|
||||||
version = "0.5.3"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
"irtt-server",
|
"irtt-server",
|
||||||
@@ -738,7 +738,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-proto"
|
name = "irtt-proto"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"md-5",
|
"md-5",
|
||||||
@@ -749,7 +749,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-rs"
|
name = "irtt-rs"
|
||||||
version = "0.7.1"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@@ -764,7 +764,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-server"
|
name = "irtt-server"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
@@ -776,10 +776,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-stats"
|
name = "irtt-stats"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"irtt-client",
|
"irtt-client",
|
||||||
"measurement-stats",
|
|
||||||
"proptest",
|
"proptest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -806,9 +805,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -888,9 +887,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
version = "0.18.4"
|
version = "0.18.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
|
checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
@@ -915,12 +914,6 @@ dependencies = [
|
|||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "measurement-stats"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0edbfa45a060f44b15568637075ecebbe258c22f86ce6f2b5d58d7a64cbdb689"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -950,9 +943,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.3"
|
version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -1125,9 +1118,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.9.1"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"
|
checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
@@ -1135,9 +1128,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.9.1"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"
|
checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
@@ -1145,9 +1138,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.9.1"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"
|
checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
@@ -1158,9 +1151,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.9.1"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"
|
checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
]
|
]
|
||||||
@@ -1576,7 +1569,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1635,9 +1628,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.0"
|
version = "1.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1712,9 +1705,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.5"
|
version = "3.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1847,7 +1840,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1893,7 +1886,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2002,9 +1995,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -2015,9 +2008,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -2025,22 +2018,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 2.0.119",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.128"
|
version = "0.2.127"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-rs"
|
name = "irtt-rs"
|
||||||
version = "0.7.1"
|
version = "0.7.0"
|
||||||
description = "IRTT-compatible client, TUI, and server applications"
|
description = "IRTT-compatible client, TUI, and server applications"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -47,7 +47,7 @@ server = ["dep:irtt-server", "dep:tokio"]
|
|||||||
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
|
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.6.7", features = ["derive", "env"] }
|
clap = { version = "4.6.6", features = ["derive", "env"] }
|
||||||
crossterm = { version = "0.29.0", optional = true }
|
crossterm = { version = "0.29.0", optional = true }
|
||||||
ctrlc = { version = "3.5.2", features = ["termination"] }
|
ctrlc = { version = "3.5.2", features = ["termination"] }
|
||||||
irtt-client = { workspace = true, optional = true }
|
irtt-client = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-client"
|
name = "irtt-client"
|
||||||
version = "0.5.3"
|
version = "0.5.1"
|
||||||
description = "IRTT-compatible Rust client library"
|
description = "IRTT-compatible Rust client library"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -242,31 +242,6 @@ impl AsyncClientTestHooks {
|
|||||||
/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
|
/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
|
||||||
/// methods are polled by the caller and require a current Tokio runtime with I/O
|
/// methods are polled by the caller and require a current Tokio runtime with I/O
|
||||||
/// and time enabled.
|
/// and time enabled.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Use this low-level adapter when the application already owns Tokio and wants
|
|
||||||
/// to drive the session lifecycle itself. This example type-checks without
|
|
||||||
/// contacting a server.
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use irtt_client::{AsyncClient, ClientConfig};
|
|
||||||
///
|
|
||||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let mut client = AsyncClient::connect(ClientConfig::default()).await?;
|
|
||||||
/// let outcome = client.open().await?;
|
|
||||||
/// println!("opened: {outcome:?}");
|
|
||||||
/// let sent = client.send_probe().await?;
|
|
||||||
/// println!("sent: {sent:?}");
|
|
||||||
/// let received = client.recv().await?;
|
|
||||||
/// println!("received: {received:?}");
|
|
||||||
/// let timed_out = client.poll_timeouts()?;
|
|
||||||
/// println!("timeouts: {timed_out:?}");
|
|
||||||
/// let closed = client.close().await?;
|
|
||||||
/// println!("closed: {closed:?}");
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AsyncClient {
|
pub struct AsyncClient {
|
||||||
socket: tokio::net::UdpSocket,
|
socket: tokio::net::UdpSocket,
|
||||||
|
|||||||
@@ -103,31 +103,6 @@ use crate::{
|
|||||||
/// session, send probes, receive replies, poll timeouts, and close. Callers
|
/// session, send probes, receive replies, poll timeouts, and close. Callers
|
||||||
/// that do not need to own this loop can use the unified managed API in
|
/// that do not need to own this loop can use the unified managed API in
|
||||||
/// [`crate::managed`] when the `tokio` feature is enabled.
|
/// [`crate::managed`] when the `tokio` feature is enabled.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// `Client` is the runtime-free adapter. The caller drives receives and timeout
|
|
||||||
/// polling between sends; see the standalone example for a complete loop.
|
|
||||||
/// This example type-checks without contacting a server:
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use irtt_client::{Client, ClientConfig};
|
|
||||||
///
|
|
||||||
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let mut client = Client::connect(ClientConfig::default())?;
|
|
||||||
/// let outcome = client.open()?;
|
|
||||||
/// println!("opened: {outcome:?}");
|
|
||||||
/// let sent = client.send_probe()?;
|
|
||||||
/// println!("sent: {sent:?}");
|
|
||||||
/// let received = client.recv_once()?;
|
|
||||||
/// println!("received: {received:?}");
|
|
||||||
/// let timed_out = client.poll_timeouts()?;
|
|
||||||
/// println!("timeouts: {timed_out:?}");
|
|
||||||
/// let closed = client.close()?;
|
|
||||||
/// println!("closed: {closed:?}");
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
runtime: SessionMachine,
|
runtime: SessionMachine,
|
||||||
|
|||||||
@@ -46,31 +46,6 @@ pub(crate) const DEFAULT_MAX_PENDING: usize = 4096;
|
|||||||
/// request and the local client behavior used to drive the UDP socket. Values
|
/// request and the local client behavior used to drive the UDP socket. Values
|
||||||
/// that are negotiated by the server are available after opening the session
|
/// that are negotiated by the server are available after opening the session
|
||||||
/// through [`NegotiatedParams`](crate::NegotiatedParams).
|
/// through [`NegotiatedParams`](crate::NegotiatedParams).
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Configure a finite run with larger probes and accept documented server
|
|
||||||
/// restrictions. Use `duration: None` for continuous mode, or
|
|
||||||
/// [`RunMode::NoTest`] for negotiation without probes.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// use irtt_client::{ClientConfig, NegotiationPolicy};
|
|
||||||
///
|
|
||||||
/// let config = ClientConfig {
|
|
||||||
/// server_addr: "example.net:2112".into(),
|
|
||||||
/// duration: Some(Duration::from_secs(10)),
|
|
||||||
/// interval: Duration::from_millis(250),
|
|
||||||
/// length: 1200,
|
|
||||||
/// negotiation_policy: NegotiationPolicy::Loose,
|
|
||||||
/// ..ClientConfig::default()
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// assert_eq!(config.duration, Some(Duration::from_secs(10)));
|
|
||||||
/// assert_eq!(config.interval, Duration::from_millis(250));
|
|
||||||
/// assert_eq!(config.length, 1200);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ClientConfig {
|
pub struct ClientConfig {
|
||||||
/// Remote server name or address.
|
/// Remote server name or address.
|
||||||
@@ -201,29 +176,6 @@ impl Default for ClientConfig {
|
|||||||
/// These settings affect how the client binds, resolves, and receives from the
|
/// These settings affect how the client binds, resolves, and receives from the
|
||||||
/// socket. They do not change the IRTT protocol parameters negotiated with the
|
/// socket. They do not change the IRTT protocol parameters negotiated with the
|
||||||
/// server.
|
/// server.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Bind to a chosen local endpoint and set the outgoing IPv4 TTL:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::net::{Ipv4Addr, SocketAddr};
|
|
||||||
///
|
|
||||||
/// use irtt_client::{ClientConfig, SocketConfig};
|
|
||||||
///
|
|
||||||
/// let config = ClientConfig {
|
|
||||||
/// socket_config: SocketConfig {
|
|
||||||
/// bind_addr: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))),
|
|
||||||
/// ttl: Some(32),
|
|
||||||
/// ipv4_only: true,
|
|
||||||
/// ..SocketConfig::default()
|
|
||||||
/// },
|
|
||||||
/// ..ClientConfig::default()
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// assert_eq!(config.socket_config.ttl, Some(32));
|
|
||||||
/// assert!(config.socket_config.ipv4_only);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct SocketConfig {
|
pub struct SocketConfig {
|
||||||
/// Local address to bind before connecting the UDP socket.
|
/// Local address to bind before connecting the UDP socket.
|
||||||
@@ -231,15 +183,6 @@ pub struct SocketConfig {
|
|||||||
/// `None` binds to an unspecified address with an ephemeral port matching
|
/// `None` binds to an unspecified address with an ephemeral port matching
|
||||||
/// the selected remote address family.
|
/// the selected remote address family.
|
||||||
pub bind_addr: Option<SocketAddr>,
|
pub bind_addr: Option<SocketAddr>,
|
||||||
/// Network interface to bind this socket to (`SO_BINDTODEVICE`).
|
|
||||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
|
||||||
pub bind_to_device: Option<String>,
|
|
||||||
/// Firewall mark applied to outgoing packets (`SO_MARK`).
|
|
||||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
|
||||||
pub mark: Option<u32>,
|
|
||||||
/// FreeBSD FIB used for routing this socket (`SO_SETFIB`).
|
|
||||||
#[cfg(target_os = "freebsd")]
|
|
||||||
pub fib: Option<u32>,
|
|
||||||
/// Optional IPv4 TTL or IPv6 hop limit applied to sent packets.
|
/// Optional IPv4 TTL or IPv6 hop limit applied to sent packets.
|
||||||
///
|
///
|
||||||
/// `None` leaves the platform default unchanged. Values must fit in the
|
/// `None` leaves the platform default unchanged. Values must fit in the
|
||||||
|
|||||||
@@ -260,19 +260,6 @@ pub struct RttSample {
|
|||||||
/// processing time or comparing wall-clock timestamps from different hosts. It
|
/// processing time or comparing wall-clock timestamps from different hosts. It
|
||||||
/// should not be used to represent raw elapsed wall-clock or monotonic time,
|
/// should not be used to represent raw elapsed wall-clock or monotonic time,
|
||||||
/// which is non-negative and represented by [`Duration`].
|
/// which is non-negative and represented by [`Duration`].
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Signed timing values retain a negative result instead of clamping clock
|
|
||||||
/// skew or server processing adjustments to zero:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_client::SignedDuration;
|
|
||||||
///
|
|
||||||
/// let adjusted_rtt = SignedDuration::from_nanos(-250_000);
|
|
||||||
/// assert!(adjusted_rtt.is_negative());
|
|
||||||
/// assert_eq!(adjusted_rtt.as_micros(), -250);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct SignedDuration {
|
pub struct SignedDuration {
|
||||||
ns: i128,
|
ns: i128,
|
||||||
|
|||||||
@@ -44,42 +44,6 @@ const POST_DEADLINE_RECEIVE_BUDGET: usize = TARGET_WORK_BUDGET;
|
|||||||
const MAX_BROADCAST_CHANNEL_CAPACITY: usize = usize::MAX >> 1;
|
const MAX_BROADCAST_CHANNEL_CAPACITY: usize = usize::MAX >> 1;
|
||||||
|
|
||||||
/// Entry point for constructing a unified Tokio managed task.
|
/// Entry point for constructing a unified Tokio managed task.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Construct the task and its separate control handle, subscribe before
|
|
||||||
/// starting the task, then run it under the caller's Tokio runtime. A reachable
|
|
||||||
/// server is required at runtime, so this example only type-checks.
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use irtt_client::{ClientConfig, ClientEvent};
|
|
||||||
/// use irtt_client::managed::{
|
|
||||||
/// ManagedClient, ManagedClientConfig, ManagedEvent, ManagedTargetConfig,
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let config = ManagedClientConfig {
|
|
||||||
/// client: ClientConfig {
|
|
||||||
/// duration: Some(std::time::Duration::from_secs(10)),
|
|
||||||
/// ..ClientConfig::default()
|
|
||||||
/// },
|
|
||||||
/// ..ManagedClientConfig::default()
|
|
||||||
/// };
|
|
||||||
/// let targets = vec![ManagedTargetConfig::new("edge", "127.0.0.1:2112")];
|
|
||||||
/// let (task, handle) = ManagedClient::task(config, targets)?;
|
|
||||||
/// let mut events = handle.subscribe()?;
|
|
||||||
/// let driver = tokio::spawn(task);
|
|
||||||
///
|
|
||||||
/// while let Ok(event) = events.recv().await {
|
|
||||||
/// if let ManagedEvent::Client { event: ClientEvent::EchoReply { .. }, .. } = event {
|
|
||||||
/// println!("received a probe reply");
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// let outcome = driver.await?;
|
|
||||||
/// println!("run ended: {:?}", outcome.end_reason);
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ManagedClient;
|
pub struct ManagedClient;
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ use crate::{
|
|||||||
socket_options::apply_ttl_to_socket,
|
socket_options::apply_ttl_to_socket,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "linux"
|
|
||||||
))]
|
|
||||||
use crate::socket_options::apply_routing_options;
|
|
||||||
|
|
||||||
pub(crate) fn validate_open_timeouts(timeouts: &[Duration]) -> Result<(), ClientError> {
|
pub(crate) fn validate_open_timeouts(timeouts: &[Duration]) -> Result<(), ClientError> {
|
||||||
if timeouts.is_empty() {
|
if timeouts.is_empty() {
|
||||||
return Err(ClientError::NoOpenTimeouts);
|
return Err(ClientError::NoOpenTimeouts);
|
||||||
@@ -123,13 +115,6 @@ fn create_connected_udp_socket(
|
|||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
) -> Result<UdpSocket, ClientError> {
|
) -> Result<UdpSocket, ClientError> {
|
||||||
let socket = create_prebind_udp_socket(config, remote)?;
|
let socket = create_prebind_udp_socket(config, remote)?;
|
||||||
#[cfg(any(
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "linux"
|
|
||||||
))]
|
|
||||||
apply_routing_options(&socket, config, remote)?;
|
|
||||||
let bind_addr = config.bind_addr.unwrap_or_else(|| {
|
let bind_addr = config.bind_addr.unwrap_or_else(|| {
|
||||||
if remote.is_ipv4() {
|
if remote.is_ipv4() {
|
||||||
SocketAddr::from(([0, 0, 0, 0], 0))
|
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||||
|
|||||||
@@ -5,64 +5,11 @@ use std::{
|
|||||||
|
|
||||||
use socket2::SockRef;
|
use socket2::SockRef;
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "linux"
|
|
||||||
))]
|
|
||||||
use socket2::Socket;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{MAX_DSCP_CODEPOINT, MAX_TTL},
|
config::{MAX_DSCP_CODEPOINT, MAX_TTL},
|
||||||
error::ClientError,
|
error::ClientError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "linux"
|
|
||||||
))]
|
|
||||||
pub(crate) fn apply_routing_options(
|
|
||||||
socket: &Socket,
|
|
||||||
config: &crate::SocketConfig,
|
|
||||||
remote: SocketAddr,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
|
||||||
{
|
|
||||||
if let Some(device) = &config.bind_to_device {
|
|
||||||
socket
|
|
||||||
.bind_device(Some(device.as_bytes()))
|
|
||||||
.map_err(|source| ClientError::SocketOption {
|
|
||||||
operation: "bind socket to device",
|
|
||||||
remote,
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
if let Some(mark) = config.mark {
|
|
||||||
socket
|
|
||||||
.set_mark(mark)
|
|
||||||
.map_err(|source| ClientError::SocketOption {
|
|
||||||
operation: "set firewall mark",
|
|
||||||
remote,
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "freebsd")]
|
|
||||||
if let Some(fib) = config.fib {
|
|
||||||
socket
|
|
||||||
.set_fib(fib)
|
|
||||||
.map_err(|source| ClientError::SocketOption {
|
|
||||||
operation: "set FIB",
|
|
||||||
remote,
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a public DSCP codepoint (`0..=`[`MAX_DSCP_CODEPOINT`]) into the
|
/// Converts a public DSCP codepoint (`0..=`[`MAX_DSCP_CODEPOINT`]) into the
|
||||||
/// raw IP TOS / Traffic Class byte it occupies the upper six bits of.
|
/// raw IP TOS / Traffic Class byte it occupies the upper six bits of.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-proto"
|
name = "irtt-proto"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
description = "IRTT protocol encoding and decoding primitives"
|
description = "IRTT protocol encoding and decoding primitives"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -60,50 +60,6 @@ pub struct TimestampFields {
|
|||||||
pub send_mono: Option<i64>,
|
pub send_mono: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes an ECHO reply.
|
|
||||||
///
|
|
||||||
/// `params` supplies the negotiated layout and length, exactly as on the
|
|
||||||
/// request side, and every optional field the layout requires must be present
|
|
||||||
/// in `reply`: a `Some` where the layout has no field, or a `None` where it has
|
|
||||||
/// one, is an error. `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC`
|
|
||||||
/// and signs the finished datagram.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`ProtoError::MissingFlag`] or [`ProtoError::UnexpectedFlag`] when
|
|
||||||
/// the reply does not carry exactly the packet-type flags this encoder
|
|
||||||
/// requires, [`ProtoError::MissingField`] or [`ProtoError::UnexpectedField`]
|
|
||||||
/// when `reply`'s optional fields do not match the negotiated layout, and
|
|
||||||
/// [`ProtoError::PayloadTooLarge`] when the payload does not fit the negotiated
|
|
||||||
/// length.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{
|
|
||||||
/// decode_echo_reply, encode_echo_reply, EchoReply, Params, ReceivedStats,
|
|
||||||
/// TimestampFields, FLAG_REPLY,
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// // `received_stats` negotiates a receive counter; four payload bytes.
|
|
||||||
/// let params = Params {
|
|
||||||
/// length: 24,
|
|
||||||
/// received_stats: ReceivedStats::Count,
|
|
||||||
/// ..Params::default()
|
|
||||||
/// };
|
|
||||||
/// let reply = EchoReply {
|
|
||||||
/// flags: FLAG_REPLY,
|
|
||||||
/// token: 0x1234_5678_9abc_def0,
|
|
||||||
/// sequence: 7,
|
|
||||||
/// recv_count: Some(3),
|
|
||||||
/// recv_window: None,
|
|
||||||
/// timestamps: TimestampFields::default(),
|
|
||||||
/// payload: vec![1, 2, 3, 4],
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// let packet = encode_echo_reply(&reply, ¶ms, None).unwrap();
|
|
||||||
/// assert_eq!(decode_echo_reply(&packet, ¶ms, None).unwrap(), reply);
|
|
||||||
/// ```
|
|
||||||
pub fn encode_echo_reply(
|
pub fn encode_echo_reply(
|
||||||
reply: &EchoReply,
|
reply: &EchoReply,
|
||||||
params: &Params,
|
params: &Params,
|
||||||
|
|||||||
@@ -68,23 +68,6 @@ pub fn verify_hmac(key: &[u8], packet: &[u8], hmac_offset: usize) -> Result<()>
|
|||||||
/// [`ProtoError::InvalidHmacOffset`] when the field is truncated, and
|
/// [`ProtoError::InvalidHmacOffset`] when the field is truncated, and
|
||||||
/// [`ProtoError::BadHmac`] when the MAC does not match. The packet is never
|
/// [`ProtoError::BadHmac`] when the MAC does not match. The packet is never
|
||||||
/// modified.
|
/// modified.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{encode_request, verify_packet_hmac, Params, RequestToEncode};
|
|
||||||
///
|
|
||||||
/// // Synthetic example key — use a real shared secret in production.
|
|
||||||
/// let key = b"example key";
|
|
||||||
/// let params = Params::with_protocol_defaults();
|
|
||||||
/// let packet = encode_request(
|
|
||||||
/// RequestToEncode::Open { params: ¶ms, no_test: false },
|
|
||||||
/// Some(key),
|
|
||||||
/// ).unwrap();
|
|
||||||
///
|
|
||||||
/// verify_packet_hmac(key, &packet).unwrap();
|
|
||||||
/// assert!(verify_packet_hmac(b"wrong key", &packet).is_err());
|
|
||||||
/// ```
|
|
||||||
pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
|
pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
|
||||||
let envelope = envelope::decode_structural(packet)?;
|
let envelope = envelope::decode_structural(packet)?;
|
||||||
if !envelope.hmac_present {
|
if !envelope.hmac_present {
|
||||||
|
|||||||
@@ -135,20 +135,6 @@ pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
|
|||||||
/// A ceiling on what a negotiated length may legitimately *be* — an MTU, a
|
/// A ceiling on what a negotiated length may legitimately *be* — an MTU, a
|
||||||
/// resource bound, a maximum packet size — is deliberately not here. That is
|
/// resource bound, a maximum packet size — is deliberately not here. That is
|
||||||
/// server and runtime policy, and it belongs where the negotiation happens.
|
/// server and runtime policy, and it belongs where the negotiation happens.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{echo_packet_len, Params};
|
|
||||||
///
|
|
||||||
/// // The default layout's mandatory field block is 16 bytes (header, token,
|
|
||||||
/// // sequence), so a smaller requested length is floored up to it.
|
|
||||||
/// assert_eq!(echo_packet_len(false, &Params::default()).unwrap(), 16);
|
|
||||||
/// assert_eq!(
|
|
||||||
/// echo_packet_len(false, &Params { length: 40, ..Params::default() }).unwrap(),
|
|
||||||
/// 40,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
|
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
|
||||||
let header_len = echo_header_len(hmac, params);
|
let header_len = echo_header_len(hmac, params);
|
||||||
let requested = if params.length <= 0 {
|
let requested = if params.length <= 0 {
|
||||||
|
|||||||
@@ -42,39 +42,6 @@
|
|||||||
//! which reply kind to expect, and the two need genuinely different semantic
|
//! which reply kind to expect, and the two need genuinely different semantic
|
||||||
//! context.
|
//! context.
|
||||||
//!
|
//!
|
||||||
//! # Example
|
|
||||||
//!
|
|
||||||
//! The normal path is directional: a sender encodes with [`encode_request`], a
|
|
||||||
//! receiver classifies with [`decode_request`], and authentication is a
|
|
||||||
//! separate step once the applicable key is known.
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use irtt_proto::{
|
|
||||||
//! decode_request, encode_request, verify_packet_hmac, DecodedRequestKind, Params,
|
|
||||||
//! RequestToEncode,
|
|
||||||
//! };
|
|
||||||
//!
|
|
||||||
//! // Sender side: encode an authenticated open request.
|
|
||||||
//! let params = Params::with_protocol_defaults();
|
|
||||||
//! let key = b"example key";
|
|
||||||
//! let packet = encode_request(
|
|
||||||
//! RequestToEncode::Open { params: ¶ms, no_test: false },
|
|
||||||
//! Some(key),
|
|
||||||
//! ).unwrap();
|
|
||||||
//!
|
|
||||||
//! // Receiver side: classify structurally, before any key is known.
|
|
||||||
//! let request = decode_request(&packet).unwrap();
|
|
||||||
//! assert!(request.hmac_present);
|
|
||||||
//! let DecodedRequestKind::Open { no_test, params: encoded } = request.kind else {
|
|
||||||
//! panic!("expected an open request");
|
|
||||||
//! };
|
|
||||||
//! assert!(!no_test);
|
|
||||||
//! assert_eq!(Params::decode(encoded).unwrap(), params);
|
|
||||||
//!
|
|
||||||
//! // Authentication is a separate step, once the applicable key is known.
|
|
||||||
//! verify_packet_hmac(key, &packet).unwrap();
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
|
|||||||
@@ -13,34 +13,6 @@ pub struct OpenReply {
|
|||||||
pub params: Params,
|
pub params: Params,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes an open reply.
|
|
||||||
///
|
|
||||||
/// `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC` and signs the
|
|
||||||
/// finished datagram, while `None` produces an unauthenticated reply and clears
|
|
||||||
/// any caller-supplied `FLAG_HMAC`. A zero token is rejected unless the reply
|
|
||||||
/// also carries `FLAG_CLOSE` (a rejected open).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`ProtoError::MissingFlag`] when the reply lacks `FLAG_OPEN` or
|
|
||||||
/// `FLAG_REPLY`, and [`ProtoError::ZeroToken`] for a zero token without
|
|
||||||
/// `FLAG_CLOSE`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{
|
|
||||||
/// decode_open_reply, encode_open_reply, OpenReply, Params, FLAG_OPEN, FLAG_REPLY,
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// let reply = OpenReply {
|
|
||||||
/// flags: FLAG_OPEN | FLAG_REPLY,
|
|
||||||
/// token: 0x1234_5678_9abc_def0,
|
|
||||||
/// params: Params::with_protocol_defaults(),
|
|
||||||
/// };
|
|
||||||
/// let packet = encode_open_reply(&reply, None).unwrap();
|
|
||||||
/// assert_eq!(decode_open_reply(&packet, None).unwrap(), reply);
|
|
||||||
/// ```
|
|
||||||
pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||||
let params = reply.params.encode();
|
let params = reply.params.encode();
|
||||||
let mut out = envelope::begin_checked(
|
let mut out = envelope::begin_checked(
|
||||||
@@ -58,18 +30,6 @@ pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<V
|
|||||||
envelope::finish(out, hmac_key)
|
envelope::finish(out, hmac_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes an open reply.
|
|
||||||
///
|
|
||||||
/// Requires `FLAG_OPEN` and `FLAG_REPLY`, and — when `hmac_key` is supplied —
|
|
||||||
/// verifies the authentication field. The decoded [`Params`] are validated:
|
|
||||||
/// malformed or incompatible parameter values are rejected here.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`ProtoError::MissingFlag`], [`ProtoError::HmacPresenceMismatch`],
|
|
||||||
/// [`ProtoError::BadHmac`], [`ProtoError::PacketTooShort`],
|
|
||||||
/// [`ProtoError::ZeroToken`], or a parameter decode error. See
|
|
||||||
/// [`encode_open_reply`] for a round-trip example.
|
|
||||||
pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenReply> {
|
pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenReply> {
|
||||||
let envelope = envelope::decode(
|
let envelope = envelope::decode(
|
||||||
packet,
|
packet,
|
||||||
|
|||||||
@@ -16,29 +16,6 @@ pub const MAX_SERVER_FILL_BYTES: usize = 32;
|
|||||||
/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
|
/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
|
||||||
/// request with an empty parameter payload means. It is not a set of sensible
|
/// request with an empty parameter payload means. It is not a set of sensible
|
||||||
/// client settings; a client builds its request from its own configuration.
|
/// client settings; a client builds its request from its own configuration.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Construct a `Params`, encode it, and decode it back:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
|
|
||||||
///
|
|
||||||
/// let params = Params {
|
|
||||||
/// protocol_version: 1,
|
|
||||||
/// duration_ns: 3_000_000_000,
|
|
||||||
/// interval_ns: 1_000_000_000,
|
|
||||||
/// length: 1472,
|
|
||||||
/// received_stats: ReceivedStats::Both,
|
|
||||||
/// stamp_at: StampAt::Both,
|
|
||||||
/// clock: Clock::Both,
|
|
||||||
/// server_fill: Some(ServerFill { value: "rand".to_owned() }),
|
|
||||||
/// ..Params::default()
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// let encoded = params.encode();
|
|
||||||
/// assert_eq!(Params::decode(&encoded).unwrap(), params);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
pub protocol_version: i64,
|
pub protocol_version: i64,
|
||||||
@@ -111,20 +88,6 @@ impl Params {
|
|||||||
/// Presence means the tag appeared at least once. Repeated known tags keep
|
/// Presence means the tag appeared at least once. Repeated known tags keep
|
||||||
/// last-value-wins, and unknown tags remain ignored and untracked.
|
/// last-value-wins, and unknown tags remain ignored and untracked.
|
||||||
///
|
///
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::Params;
|
|
||||||
///
|
|
||||||
/// // Wire bytes: tag 2 (duration) carrying an explicit zero. Interval
|
|
||||||
/// // (tag 3) is absent, so both fields decode to zero but only one is
|
|
||||||
/// // reported present.
|
|
||||||
/// let decoded = Params::decode_with_presence(&[0x02, 0x00]).unwrap();
|
|
||||||
/// assert_eq!(decoded.params.duration_ns, 0);
|
|
||||||
/// assert!(decoded.presence.duration_ns);
|
|
||||||
/// assert!(!decoded.presence.interval_ns);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`decode`]: Params::decode
|
/// [`decode`]: Params::decode
|
||||||
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
|
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
|
||||||
let mut params = Self::default();
|
let mut params = Self::default();
|
||||||
|
|||||||
@@ -88,26 +88,6 @@ pub enum DecodedRequestKind<'a> {
|
|||||||
/// the negotiated length is positive and wider than `usize`. A *negative*
|
/// the negotiated length is positive and wider than `usize`. A *negative*
|
||||||
/// negotiated length is not an error: see [`echo_packet_len`], which floors it
|
/// negotiated length is not an error: see [`echo_packet_len`], which floors it
|
||||||
/// at the mandatory field block.
|
/// at the mandatory field block.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{encode_request, Params, RequestToEncode};
|
|
||||||
///
|
|
||||||
/// // An unauthenticated ECHO probe. `params` supplies the negotiated length,
|
|
||||||
/// // which reserves four payload bytes beyond the 16-byte echo header.
|
|
||||||
/// let params = Params { length: 20, ..Params::default() };
|
|
||||||
/// let packet = encode_request(
|
|
||||||
/// RequestToEncode::Echo {
|
|
||||||
/// token: 0x1234_5678_9abc_def0,
|
|
||||||
/// sequence: 7,
|
|
||||||
/// params: ¶ms,
|
|
||||||
/// payload: &[1, 2, 3, 4],
|
|
||||||
/// },
|
|
||||||
/// None,
|
|
||||||
/// ).unwrap();
|
|
||||||
/// assert_eq!(packet.len(), 20);
|
|
||||||
/// ```
|
|
||||||
pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||||
match request {
|
match request {
|
||||||
RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
|
RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
|
||||||
@@ -158,21 +138,6 @@ pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) ->
|
|||||||
/// Returns [`ProtoError::PacketTooShort`], [`ProtoError::BadMagic`],
|
/// Returns [`ProtoError::PacketTooShort`], [`ProtoError::BadMagic`],
|
||||||
/// [`ProtoError::ReservedFlags`], or [`ProtoError::UnexpectedFlag`] for
|
/// [`ProtoError::ReservedFlags`], or [`ProtoError::UnexpectedFlag`] for
|
||||||
/// `FLAG_REPLY`.
|
/// `FLAG_REPLY`.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_proto::{decode_request, encode_request, DecodedRequestKind, RequestToEncode};
|
|
||||||
///
|
|
||||||
/// let packet = encode_request(
|
|
||||||
/// RequestToEncode::Close { token: 0x1234_5678_9abc_def0 },
|
|
||||||
/// None,
|
|
||||||
/// ).unwrap();
|
|
||||||
///
|
|
||||||
/// let request = decode_request(&packet).unwrap();
|
|
||||||
/// assert!(!request.hmac_present);
|
|
||||||
/// assert_eq!(request.kind, DecodedRequestKind::Close { token: 0x1234_5678_9abc_def0 });
|
|
||||||
/// ```
|
|
||||||
pub fn decode_request(packet: &[u8]) -> Result<DecodedRequest<'_>> {
|
pub fn decode_request(packet: &[u8]) -> Result<DecodedRequest<'_>> {
|
||||||
let envelope = envelope::decode_structural(packet)?;
|
let envelope = envelope::decode_structural(packet)?;
|
||||||
if has(envelope.flags, FLAG_REPLY) {
|
if has(envelope.flags, FLAG_REPLY) {
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
//! Variable-length integer encoding for IRTT wire values.
|
|
||||||
//!
|
|
||||||
//! Tags use an unsigned base-128 varint ([`encode_uvarint`]), and signed
|
|
||||||
//! integers (such as parameter values) use the same encoding after a zigzag
|
|
||||||
//! transform ([`encode_varint`]), which keeps small negative numbers compact.
|
|
||||||
//!
|
|
||||||
//! # Example
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use irtt_proto::varint::{decode_varint, encode_varint};
|
|
||||||
//!
|
|
||||||
//! let mut out = Vec::new();
|
|
||||||
//! encode_varint(1_000_000_000, &mut out);
|
|
||||||
//! assert_eq!(decode_varint(&out).unwrap(), (1_000_000_000, out.len()));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use crate::{ProtoError, Result};
|
use crate::{ProtoError, Result};
|
||||||
|
|
||||||
pub fn encode_uvarint(mut value: u64, out: &mut Vec<u8>) {
|
pub fn encode_uvarint(mut value: u64, out: &mut Vec<u8>) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-server"
|
name = "irtt-server"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
description = "IRTT-compatible Rust server library"
|
description = "IRTT-compatible Rust server library"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -98,28 +98,6 @@ pub enum TimestampAllowance {
|
|||||||
/// [`max_packet_length`](Self::max_packet_length) like every other reply, and
|
/// [`max_packet_length`](Self::max_packet_length) like every other reply, and
|
||||||
/// `none` is zero-filled here rather than left as residue. An operator
|
/// `none` is zero-filled here rather than left as residue. An operator
|
||||||
/// restriction can be added if a real use case turns up.
|
/// restriction can be added if a real use case turns up.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Configure a modest per-listener session budget and restrict timestamp
|
|
||||||
/// placement while leaving DSCP marking disabled:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// use irtt_server::{ServerConfig, TimestampAllowance};
|
|
||||||
///
|
|
||||||
/// let config = ServerConfig::default()
|
|
||||||
/// .with_max_sessions(128)
|
|
||||||
/// .with_idle_timeout(Duration::from_secs(30))
|
|
||||||
/// .with_timestamp_allowance(TimestampAllowance::Single)
|
|
||||||
/// .with_dscp_allowed(false);
|
|
||||||
///
|
|
||||||
/// assert_eq!(config.max_sessions(), 128);
|
|
||||||
/// assert_eq!(config.idle_timeout(), Duration::from_secs(30));
|
|
||||||
/// assert_eq!(config.timestamp_allowance(), TimestampAllowance::Single);
|
|
||||||
/// assert!(!config.dscp_allowed());
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
hmac_key: Option<Vec<u8>>,
|
hmac_key: Option<Vec<u8>>,
|
||||||
|
|||||||
@@ -47,32 +47,6 @@ use crate::{
|
|||||||
/// a transport abstraction, and there is no blocking or alternate-runtime
|
/// a transport abstraction, and there is no blocking or alternate-runtime
|
||||||
/// counterpart.
|
/// counterpart.
|
||||||
///
|
///
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// The core can be driven directly with a normally encoded open request. It
|
|
||||||
/// returns a reply to send and records the new session without performing I/O:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::net::{Ipv4Addr, SocketAddr};
|
|
||||||
///
|
|
||||||
/// use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
|
|
||||||
/// use irtt_server::{ServerConfig, ServerCore};
|
|
||||||
///
|
|
||||||
/// let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 40_000));
|
|
||||||
/// let mut core = ServerCore::new(ServerConfig::default());
|
|
||||||
/// let params = Params::with_protocol_defaults();
|
|
||||||
/// let request = encode_request(
|
|
||||||
/// RequestToEncode::Open { params: ¶ms, no_test: false },
|
|
||||||
/// None,
|
|
||||||
/// )
|
|
||||||
/// .unwrap();
|
|
||||||
///
|
|
||||||
/// let reply = core.handle_datagram(peer, &request).unwrap().unwrap();
|
|
||||||
/// let open = decode_open_reply(reply.bytes(), None).unwrap();
|
|
||||||
/// assert_ne!(open.token, 0);
|
|
||||||
/// assert_eq!(core.session_count(), 1);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Scope
|
/// # Scope
|
||||||
///
|
///
|
||||||
/// The core implements open handling, session creation and negotiation,
|
/// The core implements open handling, session creation and negotiation,
|
||||||
|
|||||||
@@ -53,28 +53,6 @@ const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(1);
|
|||||||
/// [`ServerRuntimeError::WildcardSourceSelectionUnsupported`] — rather than
|
/// [`ServerRuntimeError::WildcardSourceSelectionUnsupported`] — rather than
|
||||||
/// served by a listener whose replies a client on a second address would
|
/// served by a listener whose replies a client on a second address would
|
||||||
/// silently discard. Explicit-address listeners are unaffected everywhere.
|
/// silently discard. Explicit-address listeners are unaffected everywhere.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Bind one listener, inspect the resolved endpoint, and let the caller decide
|
|
||||||
/// when serving ends. This example type-checks without opening a socket:
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use std::{net::{Ipv4Addr, SocketAddr}, time::Duration};
|
|
||||||
///
|
|
||||||
/// use irtt_server::{Server, ServerConfig};
|
|
||||||
///
|
|
||||||
/// # async fn serve() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let mut server = Server::bind(
|
|
||||||
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
|
|
||||||
/// ServerConfig::default(),
|
|
||||||
/// )
|
|
||||||
/// .await?;
|
|
||||||
/// println!("listening on {}", server.local_addr()?);
|
|
||||||
/// server.run(tokio::time::sleep(Duration::from_secs(60))).await?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
|
|||||||
@@ -57,29 +57,6 @@ use crate::{Server, ServerConfig, ServerRuntimeError};
|
|||||||
/// whole set: a service configured for IPv4 and IPv6 must not quietly continue
|
/// whole set: a service configured for IPv4 and IPv6 must not quietly continue
|
||||||
/// as IPv4 only. Every listener task is joined before `run` returns, so no task
|
/// as IPv4 only. Every listener task is joined before `run` returns, so no task
|
||||||
/// outlives it.
|
/// outlives it.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Bind independent listeners, inspect their resolved addresses, then serve
|
|
||||||
/// them under one caller-owned shutdown future. This example type-checks
|
|
||||||
/// without binding sockets:
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use std::{net::{Ipv4Addr, SocketAddr}, time::Duration};
|
|
||||||
///
|
|
||||||
/// use irtt_server::{ServerConfig, ServerSet};
|
|
||||||
///
|
|
||||||
/// # async fn serve() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let addrs = [
|
|
||||||
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
|
|
||||||
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
|
|
||||||
/// ];
|
|
||||||
/// let set = ServerSet::bind(addrs, ServerConfig::default()).await?;
|
|
||||||
/// println!("listening on {:?}", set.local_addrs());
|
|
||||||
/// set.run(tokio::time::sleep(Duration::from_secs(60))).await?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ServerSet {
|
pub struct ServerSet {
|
||||||
servers: Vec<Server>,
|
servers: Vec<Server>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-stats"
|
name = "irtt-stats"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
description = "Statistics aggregation for irtt-client events"
|
description = "Statistics aggregation for irtt-client events"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -12,7 +12,6 @@ keywords = ["irtt", "networking", "latency", "statistics"]
|
|||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
measurement-stats = "0.1.0"
|
|
||||||
irtt-client.workspace = true
|
irtt-client.workspace = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ This crate does not open sessions or drive sockets. Feed it the `ClientEvent`
|
|||||||
stream produced by an `irtt-client` `Client`, `AsyncClient`, or managed
|
stream produced by an `irtt-client` `Client`, `AsyncClient`, or managed
|
||||||
session via `StatsCollector::process`, and read back a `Snapshot`.
|
session via `StatsCollector::process`, and read back a `Snapshot`.
|
||||||
|
|
||||||
Generic timing accumulation and count-window storage come from
|
|
||||||
`measurement-stats`. `TimeStats` remains available from `irtt_stats` as a
|
|
||||||
re-export. Event interpretation, IPDV pairing, loss, and snapshot reconstruction
|
|
||||||
remain here.
|
|
||||||
|
|
||||||
A `StatsCollector` has one sequence/IPDV namespace and one packet count, both
|
A `StatsCollector` has one sequence/IPDV namespace and one packet count, both
|
||||||
scoped to a single target. A managed session covering multiple targets needs
|
scoped to a single target. A managed session covering multiple targets needs
|
||||||
one collector per target — as the `irtt-rs` CLI does — since a target's
|
one collector per target — as the `irtt-rs` CLI does — since a target's
|
||||||
@@ -31,15 +26,6 @@ bounded running statistics instead (no exact median) plus a bounded
|
|||||||
unbounded sessions. Rolling-window snapshots are always running-only and
|
unbounded sessions. Rolling-window snapshots are always running-only and
|
||||||
report no medians, whichever `StatsConfig` produced them.
|
report no medians, whichever `StatsConfig` produced them.
|
||||||
|
|
||||||
An enabled time-based rolling window has no event-count cap, including in
|
|
||||||
continuous mode. It expires only on new events, anchored at their timestamps;
|
|
||||||
reading a snapshot does not advance time. Use count-based rolling storage for a
|
|
||||||
hard bound on retained events. The memory estimate excludes time-window storage.
|
|
||||||
|
|
||||||
Exact snapshots sort a temporary copy of each metric's retained samples, adding
|
|
||||||
O(n) scratch memory and O(n log n) computation for that metric. The retention
|
|
||||||
estimate is not peak snapshot or allocator accounting.
|
|
||||||
|
|
||||||
See `examples/` in the repository for a runnable comparison of both modes.
|
See `examples/` in the repository for a runnable comparison of both modes.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use measurement_stats::TimeAccumulator;
|
|
||||||
|
|
||||||
use crate::ipdv::{IpdvSample, IpdvTracker};
|
use crate::ipdv::{IpdvSample, IpdvTracker};
|
||||||
use crate::loss::loss_stats;
|
use crate::loss::loss_stats;
|
||||||
use crate::normalization::{ReplySample, StatsEvent};
|
use crate::normalization::{ReplySample, StatsEvent};
|
||||||
|
use crate::time_stats::TimeMetric;
|
||||||
use crate::{
|
use crate::{
|
||||||
EventCounts, EventStatsUpdate, IpdvPairUpdate, IpdvStats, LateReplyMode, OneWayDelayStats,
|
EventCounts, EventStatsUpdate, IpdvPairUpdate, IpdvStats, LateReplyMode, OneWayDelayStats,
|
||||||
PacketCounts, RttStats, SampleMode, ServerProcessingStats, Snapshot,
|
PacketCounts, RttStats, SampleMode, ServerProcessingStats, Snapshot,
|
||||||
@@ -16,17 +15,17 @@ pub(crate) const CONTINUOUS_SEQUENCE_LIMIT: usize = 4096;
|
|||||||
pub(crate) struct CoreStats {
|
pub(crate) struct CoreStats {
|
||||||
events: EventCounts,
|
events: EventCounts,
|
||||||
packets: PacketCounts,
|
packets: PacketCounts,
|
||||||
send_call: TimeAccumulator,
|
send_call: TimeMetric,
|
||||||
timer_error: TimeAccumulator,
|
timer_error: TimeMetric,
|
||||||
rtt_primary: TimeAccumulator,
|
rtt_primary: TimeMetric,
|
||||||
rtt_raw: TimeAccumulator,
|
rtt_raw: TimeMetric,
|
||||||
rtt_adjusted: TimeAccumulator,
|
rtt_adjusted: TimeMetric,
|
||||||
ipdv_round_trip: TimeAccumulator,
|
ipdv_round_trip: TimeMetric,
|
||||||
ipdv_send: TimeAccumulator,
|
ipdv_send: TimeMetric,
|
||||||
ipdv_receive: TimeAccumulator,
|
ipdv_receive: TimeMetric,
|
||||||
send_delay: TimeAccumulator,
|
send_delay: TimeMetric,
|
||||||
receive_delay: TimeAccumulator,
|
receive_delay: TimeMetric,
|
||||||
server_processing: TimeAccumulator,
|
server_processing: TimeMetric,
|
||||||
ipdv_tracker: IpdvTracker,
|
ipdv_tracker: IpdvTracker,
|
||||||
late_replies: LateReplyMode,
|
late_replies: LateReplyMode,
|
||||||
}
|
}
|
||||||
@@ -43,25 +42,20 @@ impl CoreStats {
|
|||||||
Some(CONTINUOUS_SEQUENCE_LIMIT)
|
Some(CONTINUOUS_SEQUENCE_LIMIT)
|
||||||
};
|
};
|
||||||
|
|
||||||
let metric = || match sample_mode {
|
|
||||||
SampleMode::Exact => TimeAccumulator::exact_unbounded(),
|
|
||||||
SampleMode::RunningOnly => TimeAccumulator::running(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
events: EventCounts::default(),
|
events: EventCounts::default(),
|
||||||
packets: PacketCounts::default(),
|
packets: PacketCounts::default(),
|
||||||
send_call: metric(),
|
send_call: TimeMetric::new(retain_exact),
|
||||||
timer_error: metric(),
|
timer_error: TimeMetric::new(retain_exact),
|
||||||
rtt_primary: metric(),
|
rtt_primary: TimeMetric::new(retain_exact),
|
||||||
rtt_raw: metric(),
|
rtt_raw: TimeMetric::new(retain_exact),
|
||||||
rtt_adjusted: metric(),
|
rtt_adjusted: TimeMetric::new(retain_exact),
|
||||||
ipdv_round_trip: metric(),
|
ipdv_round_trip: TimeMetric::new(retain_exact),
|
||||||
ipdv_send: metric(),
|
ipdv_send: TimeMetric::new(retain_exact),
|
||||||
ipdv_receive: metric(),
|
ipdv_receive: TimeMetric::new(retain_exact),
|
||||||
send_delay: metric(),
|
send_delay: TimeMetric::new(retain_exact),
|
||||||
receive_delay: metric(),
|
receive_delay: TimeMetric::new(retain_exact),
|
||||||
server_processing: metric(),
|
server_processing: TimeMetric::new(retain_exact),
|
||||||
ipdv_tracker: IpdvTracker::new(sequence_limit),
|
ipdv_tracker: IpdvTracker::new(sequence_limit),
|
||||||
late_replies,
|
late_replies,
|
||||||
}
|
}
|
||||||
@@ -233,8 +227,8 @@ impl CoreStats {
|
|||||||
events: self.events,
|
events: self.events,
|
||||||
packets,
|
packets,
|
||||||
loss: loss_stats(packets),
|
loss: loss_stats(packets),
|
||||||
send_call: self.send_call.snapshot(),
|
send_call: self.send_call.stats(),
|
||||||
timer_error: self.timer_error.snapshot(),
|
timer_error: self.timer_error.stats(),
|
||||||
rtt: self.rtt_stats(),
|
rtt: self.rtt_stats(),
|
||||||
ipdv: self.ipdv_stats(),
|
ipdv: self.ipdv_stats(),
|
||||||
one_way_delay: self.one_way_delay_stats(),
|
one_way_delay: self.one_way_delay_stats(),
|
||||||
@@ -244,30 +238,30 @@ impl CoreStats {
|
|||||||
|
|
||||||
fn rtt_stats(&self) -> RttStats {
|
fn rtt_stats(&self) -> RttStats {
|
||||||
RttStats {
|
RttStats {
|
||||||
primary: self.rtt_primary.snapshot(),
|
primary: self.rtt_primary.stats(),
|
||||||
raw: self.rtt_raw.snapshot(),
|
raw: self.rtt_raw.stats(),
|
||||||
adjusted: self.rtt_adjusted.snapshot(),
|
adjusted: self.rtt_adjusted.stats(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ipdv_stats(&self) -> IpdvStats {
|
fn ipdv_stats(&self) -> IpdvStats {
|
||||||
IpdvStats {
|
IpdvStats {
|
||||||
round_trip: self.ipdv_round_trip.snapshot(),
|
round_trip: self.ipdv_round_trip.stats(),
|
||||||
send: self.ipdv_send.snapshot(),
|
send: self.ipdv_send.stats(),
|
||||||
receive: self.ipdv_receive.snapshot(),
|
receive: self.ipdv_receive.stats(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn one_way_delay_stats(&self) -> OneWayDelayStats {
|
fn one_way_delay_stats(&self) -> OneWayDelayStats {
|
||||||
OneWayDelayStats {
|
OneWayDelayStats {
|
||||||
send_delay: self.send_delay.snapshot(),
|
send_delay: self.send_delay.stats(),
|
||||||
receive_delay: self.receive_delay.snapshot(),
|
receive_delay: self.receive_delay.stats(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn server_processing_stats(&self) -> ServerProcessingStats {
|
fn server_processing_stats(&self) -> ServerProcessingStats {
|
||||||
ServerProcessingStats {
|
ServerProcessingStats {
|
||||||
processing: self.server_processing.snapshot(),
|
processing: self.server_processing.stats(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,8 @@
|
|||||||
//! [`StatsConfig::continuous`] ([`SampleMode::RunningOnly`]) keeps only
|
//! [`StatsConfig::continuous`] ([`SampleMode::RunningOnly`]) keeps only
|
||||||
//! running statistics — no exact median — and bounds its adjacent-sequence
|
//! running statistics — no exact median — and bounds its adjacent-sequence
|
||||||
//! IPDV tracking, so memory stays bounded for a long-running or unbounded
|
//! IPDV tracking, so memory stays bounded for a long-running or unbounded
|
||||||
//! session. An enabled time-based rolling window has no item cap and is not
|
//! session. Rolling snapshots always use running statistics regardless of
|
||||||
//! hard-bounded; it expires only when new events arrive. Rolling snapshots
|
//! this setting.
|
||||||
//! always use running statistics regardless of this setting.
|
|
||||||
//!
|
//!
|
||||||
//! See `examples/` in the repository for a runnable comparison of both modes.
|
//! See `examples/` in the repository for a runnable comparison of both modes.
|
||||||
|
|
||||||
@@ -35,13 +34,13 @@ mod loss;
|
|||||||
mod normalization;
|
mod normalization;
|
||||||
mod retention;
|
mod retention;
|
||||||
mod rolling;
|
mod rolling;
|
||||||
|
mod time_stats;
|
||||||
|
|
||||||
use core::CoreStats;
|
use core::CoreStats;
|
||||||
pub use loss::LossStats;
|
pub use loss::LossStats;
|
||||||
#[doc(inline)]
|
|
||||||
pub use measurement_stats::TimeStats;
|
|
||||||
use normalization::normalize_event;
|
use normalization::normalize_event;
|
||||||
use rolling::RollingEvents;
|
use rolling::RollingEvents;
|
||||||
|
pub use time_stats::TimeStats;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
/// Configuration for statistics collection.
|
/// Configuration for statistics collection.
|
||||||
@@ -55,10 +54,6 @@ pub struct StatsConfig {
|
|||||||
/// and one unique reply event.
|
/// and one unique reply event.
|
||||||
pub rolling_count: Option<usize>,
|
pub rolling_count: Option<usize>,
|
||||||
/// Time span of recent normalized events retained for time-based rolling snapshots.
|
/// Time span of recent normalized events retained for time-based rolling snapshots.
|
||||||
///
|
|
||||||
/// This window has no item cap, even in continuous mode. Its memory depends
|
|
||||||
/// on event volume inside the interval. Expiry occurs on insertion, anchored
|
|
||||||
/// at the latest event timestamp, not when a snapshot is read.
|
|
||||||
pub rolling_time: Option<Duration>,
|
pub rolling_time: Option<Duration>,
|
||||||
/// Whether matched late replies contribute measurements.
|
/// Whether matched late replies contribute measurements.
|
||||||
pub late_replies: LateReplyMode,
|
pub late_replies: LateReplyMode,
|
||||||
@@ -71,44 +66,6 @@ impl StatsConfig {
|
|||||||
/// them reports a median in the cumulative snapshot once it has samples,
|
/// them reports a median in the cumulative snapshot once it has samples,
|
||||||
/// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent
|
/// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent
|
||||||
/// replies can still complete IPDV pairs.
|
/// replies can still complete IPDV pairs.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_stats::{StatsCollector, StatsConfig};
|
|
||||||
///
|
|
||||||
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
/// # use std::time::{Duration, Instant, SystemTime};
|
|
||||||
/// # use irtt_client::{ClientEvent, ClientTimestamp};
|
|
||||||
/// #
|
|
||||||
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
|
|
||||||
/// # let sent_at = ClientTimestamp {
|
|
||||||
/// # mono: Instant::now() + Duration::from_millis(at_ms),
|
|
||||||
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
|
|
||||||
/// # };
|
|
||||||
/// # ClientEvent::EchoSent {
|
|
||||||
/// # seq,
|
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
|
||||||
/// # scheduled_at: sent_at.mono,
|
|
||||||
/// # sent_at,
|
|
||||||
/// # bytes: 64,
|
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
|
||||||
/// # timer_error: Duration::from_micros(2),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// let mut collector = StatsCollector::new(StatsConfig::finite());
|
|
||||||
/// for (seq, send_call_us) in [10_u64, 30, 20].into_iter().enumerate() {
|
|
||||||
/// collector.process(&sent(seq as u32, seq as u64 * 10, send_call_us));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let snapshot = collector.snapshot();
|
|
||||||
/// // Finite mode retains exact samples, so the send-call metric reports an
|
|
||||||
/// // exact median once it has samples: the middle of the sorted
|
|
||||||
/// // 10, 20, 30 µs values, not their mean.
|
|
||||||
/// assert_eq!(snapshot.send_call.count, 3);
|
|
||||||
/// assert_eq!(snapshot.send_call.median_ns, Some(20_000.0));
|
|
||||||
/// ```
|
|
||||||
pub fn finite() -> Self {
|
pub fn finite() -> Self {
|
||||||
Self {
|
Self {
|
||||||
samples: SampleMode::Exact,
|
samples: SampleMode::Exact,
|
||||||
@@ -122,46 +79,7 @@ impl StatsConfig {
|
|||||||
///
|
///
|
||||||
/// Continuous mode uses running statistics, retains no exact samples so no
|
/// Continuous mode uses running statistics, retains no exact samples so no
|
||||||
/// timing metric reports a median, and bounds adjacent-sequence IPDV
|
/// timing metric reports a median, and bounds adjacent-sequence IPDV
|
||||||
/// tracking for long-running sessions. An explicitly enabled `rolling_time`
|
/// tracking for long-running sessions.
|
||||||
/// window is not hard-bounded; use `rolling_count` for bounded event storage.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_stats::{StatsCollector, StatsConfig};
|
|
||||||
///
|
|
||||||
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
/// # use std::time::{Duration, Instant, SystemTime};
|
|
||||||
/// # use irtt_client::{ClientEvent, ClientTimestamp};
|
|
||||||
/// #
|
|
||||||
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
|
|
||||||
/// # let sent_at = ClientTimestamp {
|
|
||||||
/// # mono: Instant::now() + Duration::from_millis(at_ms),
|
|
||||||
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
|
|
||||||
/// # };
|
|
||||||
/// # ClientEvent::EchoSent {
|
|
||||||
/// # seq,
|
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
|
||||||
/// # scheduled_at: sent_at.mono,
|
|
||||||
/// # sent_at,
|
|
||||||
/// # bytes: 64,
|
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
|
||||||
/// # timer_error: Duration::from_micros(2),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// let mut collector = StatsCollector::new(StatsConfig::continuous());
|
|
||||||
/// for seq in 0..3 {
|
|
||||||
/// collector.process(&sent(seq, u64::from(seq) * 10, 10));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let snapshot = collector.snapshot();
|
|
||||||
/// // Continuous mode still tracks running statistics — count, mean, min,
|
|
||||||
/// // and max — but retains no exact samples, so it reports no median.
|
|
||||||
/// assert_eq!(snapshot.send_call.count, 3);
|
|
||||||
/// assert!(snapshot.send_call.mean_ns > 0.0);
|
|
||||||
/// assert_eq!(snapshot.send_call.median_ns, None);
|
|
||||||
/// ```
|
|
||||||
pub fn continuous() -> Self {
|
pub fn continuous() -> Self {
|
||||||
Self {
|
Self {
|
||||||
samples: SampleMode::RunningOnly,
|
samples: SampleMode::RunningOnly,
|
||||||
@@ -217,34 +135,6 @@ impl StatsConfig {
|
|||||||
/// The result is deterministic and computed with saturating arithmetic, so
|
/// The result is deterministic and computed with saturating arithmetic, so
|
||||||
/// an enormous `probe_count` saturates at [`u64::MAX`] rather than
|
/// an enormous `probe_count` saturates at [`u64::MAX`] rather than
|
||||||
/// wrapping or panicking.
|
/// wrapping or panicking.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_stats::StatsConfig;
|
|
||||||
///
|
|
||||||
/// // Finite retention grows with the probe count...
|
|
||||||
/// let finite = StatsConfig::finite();
|
|
||||||
/// assert!(
|
|
||||||
/// finite.estimated_retained_bytes(1_000_000)
|
|
||||||
/// > finite.estimated_retained_bytes(1_000)
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // ...while continuous retention is bounded, so a far longer run is no
|
|
||||||
/// // more expensive.
|
|
||||||
/// let continuous = StatsConfig::continuous();
|
|
||||||
/// assert_eq!(
|
|
||||||
/// continuous.estimated_retained_bytes(1_000_000),
|
|
||||||
/// continuous.estimated_retained_bytes(10_000_000),
|
|
||||||
/// );
|
|
||||||
/// assert!(
|
|
||||||
/// continuous.estimated_retained_bytes(1_000_000)
|
|
||||||
/// < finite.estimated_retained_bytes(1_000_000)
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Enormous probe counts saturate rather than wrapping or panicking.
|
|
||||||
/// assert_eq!(finite.estimated_retained_bytes(u64::MAX), u64::MAX);
|
|
||||||
/// ```
|
|
||||||
pub fn estimated_retained_bytes(&self, probe_count: u64) -> u64 {
|
pub fn estimated_retained_bytes(&self, probe_count: u64) -> u64 {
|
||||||
retention::estimated_retained_bytes(self, probe_count)
|
retention::estimated_retained_bytes(self, probe_count)
|
||||||
}
|
}
|
||||||
@@ -302,60 +192,6 @@ pub enum SampleMode {
|
|||||||
///
|
///
|
||||||
/// A collector maintains cumulative statistics and, when configured, rolling
|
/// A collector maintains cumulative statistics and, when configured, rolling
|
||||||
/// windows. Rolling snapshots are recomputed from retained normalized events.
|
/// windows. Rolling snapshots are recomputed from retained normalized events.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Feed a short stream of `irtt-client` events to a finite-mode collector and
|
|
||||||
/// read back a cumulative snapshot:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_stats::{StatsCollector, StatsConfig};
|
|
||||||
///
|
|
||||||
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
/// # use std::time::{Duration, Instant, SystemTime};
|
|
||||||
/// # use irtt_client::{ClientEvent, ClientTimestamp, RttSample, SignedDuration};
|
|
||||||
/// #
|
|
||||||
/// # /// A synthetic reply; a real session delivers these as `ClientEvent`s.
|
|
||||||
/// # fn reply(seq: u32, rtt_ms: u64) -> ClientEvent {
|
|
||||||
/// # let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112);
|
|
||||||
/// # let sent_at = ClientTimestamp {
|
|
||||||
/// # mono: Instant::now() + Duration::from_millis(u64::from(seq) * 10),
|
|
||||||
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(u64::from(seq) * 10),
|
|
||||||
/// # };
|
|
||||||
/// # let raw = Duration::from_millis(rtt_ms);
|
|
||||||
/// # ClientEvent::EchoReply {
|
|
||||||
/// # seq,
|
|
||||||
/// # remote,
|
|
||||||
/// # sent_at,
|
|
||||||
/// # received_at: ClientTimestamp {
|
|
||||||
/// # mono: sent_at.mono + raw,
|
|
||||||
/// # wall: sent_at.wall + raw,
|
|
||||||
/// # },
|
|
||||||
/// # rtt: RttSample {
|
|
||||||
/// # raw,
|
|
||||||
/// # adjusted: None,
|
|
||||||
/// # effective: SignedDuration::from_duration(raw),
|
|
||||||
/// # },
|
|
||||||
/// # server_timing: None,
|
|
||||||
/// # one_way: None,
|
|
||||||
/// # received_stats: None,
|
|
||||||
/// # bytes: 64,
|
|
||||||
/// # packet_meta: irtt_client::PacketMeta::default(),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// let mut collector = StatsCollector::new(StatsConfig::finite());
|
|
||||||
/// for (seq, rtt_ms) in [10_u64, 30, 20].into_iter().enumerate() {
|
|
||||||
/// collector.process(&reply(seq as u32, rtt_ms));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let snapshot = collector.snapshot();
|
|
||||||
/// assert_eq!(snapshot.events.echo_replies, 3);
|
|
||||||
/// assert_eq!(snapshot.rtt.primary.count, 3);
|
|
||||||
/// // Finite mode retains exact samples, so the round-trip metric reports an
|
|
||||||
/// // exact median: the middle of the sorted 10, 20, 30 ms samples.
|
|
||||||
/// assert_eq!(snapshot.rtt.primary.median_ns, Some(20_000_000.0));
|
|
||||||
/// ```
|
|
||||||
pub struct StatsCollector {
|
pub struct StatsCollector {
|
||||||
cumulative: CoreStats,
|
cumulative: CoreStats,
|
||||||
rolling: RollingEvents,
|
rolling: RollingEvents,
|
||||||
@@ -400,53 +236,6 @@ impl StatsCollector {
|
|||||||
/// running statistics only, whatever [`StatsConfig::samples`] is set to,
|
/// running statistics only, whatever [`StatsConfig::samples`] is set to,
|
||||||
/// so its timing metrics report no median. A rolling window is a bounded
|
/// so its timing metrics report no median. A rolling window is a bounded
|
||||||
/// recent view rather than the run's retained history.
|
/// recent view rather than the run's retained history.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use irtt_stats::{StatsCollector, StatsConfig};
|
|
||||||
///
|
|
||||||
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
/// # use std::time::{Duration, Instant, SystemTime};
|
|
||||||
/// # use irtt_client::{ClientEvent, ClientTimestamp};
|
|
||||||
/// #
|
|
||||||
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
|
|
||||||
/// # let sent_at = ClientTimestamp {
|
|
||||||
/// # mono: Instant::now() + Duration::from_millis(at_ms),
|
|
||||||
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
|
|
||||||
/// # };
|
|
||||||
/// # ClientEvent::EchoSent {
|
|
||||||
/// # seq,
|
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
|
||||||
/// # scheduled_at: sent_at.mono,
|
|
||||||
/// # sent_at,
|
|
||||||
/// # bytes: 64,
|
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
|
||||||
/// # timer_error: Duration::from_micros(2),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// let mut collector = StatsCollector::new(StatsConfig {
|
|
||||||
/// rolling_count: Some(2),
|
|
||||||
/// ..StatsConfig::finite()
|
|
||||||
/// });
|
|
||||||
/// collector.process(&sent(0, 0, 10));
|
|
||||||
/// collector.process(&sent(1, 10, 10));
|
|
||||||
/// collector.process(&sent(2, 20, 10));
|
|
||||||
///
|
|
||||||
/// // The window keeps only the last two normalized events...
|
|
||||||
/// let rolling = collector.rolling_count().unwrap();
|
|
||||||
/// assert_eq!(rolling.packets.packets_sent, 2);
|
|
||||||
/// // ...and is recomputed with running statistics, so it reports no median
|
|
||||||
/// // even though the collector retains exact samples for its cumulative
|
|
||||||
/// // snapshot.
|
|
||||||
/// assert_eq!(rolling.send_call.median_ns, None);
|
|
||||||
///
|
|
||||||
/// // The cumulative snapshot still sees every event.
|
|
||||||
/// let cumulative = collector.snapshot();
|
|
||||||
/// assert_eq!(cumulative.packets.packets_sent, 3);
|
|
||||||
/// assert_eq!(cumulative.send_call.median_ns, Some(10_000.0));
|
|
||||||
/// ```
|
|
||||||
pub fn rolling_count(&self) -> Option<Snapshot> {
|
pub fn rolling_count(&self) -> Option<Snapshot> {
|
||||||
self.rolling.count_snapshot()
|
self.rolling.count_snapshot()
|
||||||
}
|
}
|
||||||
@@ -455,47 +244,6 @@ impl StatsCollector {
|
|||||||
///
|
///
|
||||||
/// Like [`StatsCollector::rolling_count`], this is recomputed with running
|
/// Like [`StatsCollector::rolling_count`], this is recomputed with running
|
||||||
/// statistics only and reports no medians.
|
/// statistics only and reports no medians.
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// use irtt_stats::{StatsCollector, StatsConfig};
|
|
||||||
///
|
|
||||||
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
||||||
/// # use std::time::Instant;
|
|
||||||
/// # use irtt_client::{ClientEvent, ClientTimestamp};
|
|
||||||
/// #
|
|
||||||
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
|
|
||||||
/// # let sent_at = ClientTimestamp {
|
|
||||||
/// # mono: Instant::now() + Duration::from_millis(at_ms),
|
|
||||||
/// # wall: std::time::SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
|
|
||||||
/// # };
|
|
||||||
/// # ClientEvent::EchoSent {
|
|
||||||
/// # seq,
|
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
|
||||||
/// # scheduled_at: sent_at.mono,
|
|
||||||
/// # sent_at,
|
|
||||||
/// # bytes: 64,
|
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
|
||||||
/// # timer_error: Duration::from_micros(2),
|
|
||||||
/// # }
|
|
||||||
/// # }
|
|
||||||
/// #
|
|
||||||
/// let mut collector = StatsCollector::new(StatsConfig {
|
|
||||||
/// rolling_time: Some(Duration::from_millis(15)),
|
|
||||||
/// ..StatsConfig::finite()
|
|
||||||
/// });
|
|
||||||
/// collector.process(&sent(0, 0, 10));
|
|
||||||
/// collector.process(&sent(1, 10, 10));
|
|
||||||
/// collector.process(&sent(2, 30, 10));
|
|
||||||
///
|
|
||||||
/// // A 15 ms window anchored at the latest event (30 ms) retains only the
|
|
||||||
/// // events from the last 15 ms — here just the final send.
|
|
||||||
/// let rolling = collector.rolling_time().unwrap();
|
|
||||||
/// assert_eq!(rolling.packets.packets_sent, 1);
|
|
||||||
/// ```
|
|
||||||
pub fn rolling_time(&self) -> Option<Snapshot> {
|
pub fn rolling_time(&self) -> Option<Snapshot> {
|
||||||
self.rolling.time_snapshot()
|
self.rolling.time_snapshot()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
use std::{collections::VecDeque, time::Duration};
|
use std::{collections::VecDeque, time::Duration};
|
||||||
|
|
||||||
use measurement_stats::CountWindow;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::CoreStats, normalization::StatsEvent, LateReplyMode, SampleMode, Snapshot, StatsConfig,
|
core::CoreStats, normalization::StatsEvent, LateReplyMode, SampleMode, Snapshot, StatsConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct RollingEvents {
|
pub(crate) struct RollingEvents {
|
||||||
|
count_limit: Option<usize>,
|
||||||
time_limit: Option<Duration>,
|
time_limit: Option<Duration>,
|
||||||
count_events: Option<CountWindow<StatsEvent>>,
|
count_events: Option<VecDeque<StatsEvent>>,
|
||||||
time_events: Option<VecDeque<StatsEvent>>,
|
time_events: Option<VecDeque<StatsEvent>>,
|
||||||
late_replies: LateReplyMode,
|
late_replies: LateReplyMode,
|
||||||
}
|
}
|
||||||
@@ -17,16 +16,20 @@ pub(crate) struct RollingEvents {
|
|||||||
impl RollingEvents {
|
impl RollingEvents {
|
||||||
pub(crate) fn new(config: StatsConfig) -> Self {
|
pub(crate) fn new(config: StatsConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
count_limit: config.rolling_count,
|
||||||
time_limit: config.rolling_time,
|
time_limit: config.rolling_time,
|
||||||
count_events: config.rolling_count.map(CountWindow::new),
|
count_events: config.rolling_count.map(|_| VecDeque::new()),
|
||||||
time_events: config.rolling_time.map(|_| VecDeque::new()),
|
time_events: config.rolling_time.map(|_| VecDeque::new()),
|
||||||
late_replies: config.late_replies,
|
late_replies: config.late_replies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn push(&mut self, event: StatsEvent) {
|
pub(crate) fn push(&mut self, event: StatsEvent) {
|
||||||
if let Some(window) = self.count_events.as_mut() {
|
if let (Some(limit), Some(window)) = (self.count_limit, self.count_events.as_mut()) {
|
||||||
window.push(event.clone());
|
window.push_back(event.clone());
|
||||||
|
while window.len() > limit {
|
||||||
|
window.pop_front();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(duration), Some(window)) = (self.time_limit, self.time_events.as_mut()) {
|
if let (Some(duration), Some(window)) = (self.time_limit, self.time_events.as_mut()) {
|
||||||
@@ -43,22 +46,19 @@ impl RollingEvents {
|
|||||||
pub(crate) fn count_snapshot(&self) -> Option<Snapshot> {
|
pub(crate) fn count_snapshot(&self) -> Option<Snapshot> {
|
||||||
self.count_events
|
self.count_events
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|events| snapshot_window(events.iter(), self.late_replies))
|
.map(|events| snapshot_window(events, self.late_replies))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn time_snapshot(&self) -> Option<Snapshot> {
|
pub(crate) fn time_snapshot(&self) -> Option<Snapshot> {
|
||||||
self.time_events
|
self.time_events
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|events| snapshot_window(events.iter(), self.late_replies))
|
.map(|events| snapshot_window(events, self.late_replies))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recompute a window snapshot under the same normalized semantics as the
|
/// Recompute a window snapshot under the same normalized semantics as the
|
||||||
/// cumulative collector, including its late-reply measurement policy.
|
/// cumulative collector, including its late-reply measurement policy.
|
||||||
fn snapshot_window<'a>(
|
fn snapshot_window(events: &VecDeque<StatsEvent>, late_replies: LateReplyMode) -> Snapshot {
|
||||||
events: impl Iterator<Item = &'a StatsEvent>,
|
|
||||||
late_replies: LateReplyMode,
|
|
||||||
) -> Snapshot {
|
|
||||||
let mut core = CoreStats::new(SampleMode::RunningOnly, late_replies);
|
let mut core = CoreStats::new(SampleMode::RunningOnly, late_replies);
|
||||||
for event in events {
|
for event in events {
|
||||||
core.apply(event.clone());
|
core.apply(event.clone());
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
/// Summary statistics for timing values.
|
||||||
|
///
|
||||||
|
/// All fields use nanoseconds except `variance_ns2`, which is nanoseconds
|
||||||
|
/// squared. Values may represent signed timing quantities such as scheduling
|
||||||
|
/// error or one-way delay.
|
||||||
|
pub struct TimeStats {
|
||||||
|
/// Number of samples included in this summary.
|
||||||
|
pub count: u64,
|
||||||
|
/// Sum of all samples, in nanoseconds.
|
||||||
|
pub total_ns: i128,
|
||||||
|
/// Smallest sample, in nanoseconds.
|
||||||
|
pub min_ns: Option<i128>,
|
||||||
|
/// Largest sample, in nanoseconds.
|
||||||
|
pub max_ns: Option<i128>,
|
||||||
|
/// Arithmetic mean, in nanoseconds.
|
||||||
|
pub mean_ns: f64,
|
||||||
|
/// Median, in nanoseconds, when exact samples were retained.
|
||||||
|
///
|
||||||
|
/// This is `None` when the collector was not configured to retain exact
|
||||||
|
/// samples, and also when it was but this metric received none.
|
||||||
|
pub median_ns: Option<f64>,
|
||||||
|
/// Sample variance, in nanoseconds squared.
|
||||||
|
pub variance_ns2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeStats {
|
||||||
|
/// Returns the sample standard deviation, in nanoseconds.
|
||||||
|
pub fn stddev_ns(&self) -> f64 {
|
||||||
|
self.variance_ns2.sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this summary contains no samples.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.count == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) struct TimeMetric {
|
||||||
|
running: RunningStats,
|
||||||
|
samples: Option<Vec<i128>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeMetric {
|
||||||
|
pub(crate) fn new(retain_samples: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
running: RunningStats::default(),
|
||||||
|
samples: retain_samples.then(Vec::new),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_ns(&mut self, value: i128) {
|
||||||
|
self.running.push(value);
|
||||||
|
if let Some(samples) = self.samples.as_mut() {
|
||||||
|
samples.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stats(&self) -> TimeStats {
|
||||||
|
self.running
|
||||||
|
.stats(self.samples.as_ref().and_then(|samples| median_ns(samples)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
struct RunningStats {
|
||||||
|
count: u64,
|
||||||
|
total_ns: i128,
|
||||||
|
min_ns: Option<i128>,
|
||||||
|
max_ns: Option<i128>,
|
||||||
|
mean_ns: f64,
|
||||||
|
m2_ns2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunningStats {
|
||||||
|
fn push(&mut self, value: i128) {
|
||||||
|
self.count += 1;
|
||||||
|
self.total_ns = self.total_ns.saturating_add(value);
|
||||||
|
self.min_ns = Some(self.min_ns.map_or(value, |min| min.min(value)));
|
||||||
|
self.max_ns = Some(self.max_ns.map_or(value, |max| max.max(value)));
|
||||||
|
let x = value as f64;
|
||||||
|
let delta = x - self.mean_ns;
|
||||||
|
self.mean_ns += delta / self.count as f64;
|
||||||
|
let delta2 = x - self.mean_ns;
|
||||||
|
self.m2_ns2 += delta * delta2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stats(&self, median_ns: Option<f64>) -> TimeStats {
|
||||||
|
TimeStats {
|
||||||
|
count: self.count,
|
||||||
|
total_ns: self.total_ns,
|
||||||
|
min_ns: self.min_ns,
|
||||||
|
max_ns: self.max_ns,
|
||||||
|
mean_ns: if self.count == 0 { 0.0 } else { self.mean_ns },
|
||||||
|
median_ns,
|
||||||
|
variance_ns2: sample_variance(self.count, self.m2_ns2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_variance(count: u64, m2: f64) -> f64 {
|
||||||
|
if count < 2 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
m2 / (count - 1) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn median_ns(samples: &[i128]) -> Option<f64> {
|
||||||
|
if samples.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut sorted = samples.to_vec();
|
||||||
|
sorted.sort_unstable();
|
||||||
|
let mid = sorted.len() / 2;
|
||||||
|
Some(if sorted.len() % 2 == 1 {
|
||||||
|
sorted[mid] as f64
|
||||||
|
} else {
|
||||||
|
(sorted[mid - 1] as f64 + sorted[mid] as f64) / 2.0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn running_duration_stats_use_sample_variance() {
|
||||||
|
let mut metric = TimeMetric::new(false);
|
||||||
|
metric.push_ns(1);
|
||||||
|
metric.push_ns(2);
|
||||||
|
metric.push_ns(3);
|
||||||
|
let stats = metric.stats();
|
||||||
|
assert_eq!(stats.count, 3);
|
||||||
|
assert_eq!(stats.total_ns, 6);
|
||||||
|
assert_eq!(stats.min_ns, Some(1));
|
||||||
|
assert_eq!(stats.max_ns, Some(3));
|
||||||
|
assert_eq!(stats.mean_ns, 2.0);
|
||||||
|
assert_eq!(stats.variance_ns2, 1.0);
|
||||||
|
assert_eq!(stats.stddev_ns(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_median_handles_odd_and_even_samples() {
|
||||||
|
assert_eq!(median_ns(&[3, 1, 2]), Some(2.0));
|
||||||
|
assert_eq!(median_ns(&[4, 1, 2, 3]), Some(2.5));
|
||||||
|
assert_eq!(median_ns(&[-5, 1, 3]), Some(1.0));
|
||||||
|
assert_eq!(median_ns(&[-5, 1, 3, 7]), Some(2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_sample_stddev_is_zero() {
|
||||||
|
let mut metric = TimeMetric::new(false);
|
||||||
|
metric.push_ns(42);
|
||||||
|
let stats = metric.stats();
|
||||||
|
assert_eq!(stats.variance_ns2, 0.0);
|
||||||
|
assert_eq!(stats.stddev_ns(), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,26 @@ use irtt_stats::{
|
|||||||
EventStatsUpdate, IpdvPairUpdate, LateReplyMode, SampleMode, StatsCollector, StatsConfig,
|
EventStatsUpdate, IpdvPairUpdate, LateReplyMode, SampleMode, StatsCollector, StatsConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_mode_reports_a_send_call_median() {
|
||||||
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
|
for (seq, send_call_us) in [10_u64, 30, 20].into_iter().enumerate() {
|
||||||
|
collector.process(&sent_with_timings(
|
||||||
|
seq as u32,
|
||||||
|
ts(seq as u64 * 10),
|
||||||
|
send_call_us,
|
||||||
|
2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = collector.snapshot();
|
||||||
|
assert_eq!(snapshot.send_call.count, 3);
|
||||||
|
assert_eq!(snapshot.send_call.min_ns, Some(10_000));
|
||||||
|
assert_eq!(snapshot.send_call.max_ns, Some(30_000));
|
||||||
|
// Sorted 10, 20, 30 us: the median is the middle sample, not the mean.
|
||||||
|
assert_eq!(snapshot.send_call.median_ns, Some(20_000.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exact_mode_reports_a_timer_error_median() {
|
fn exact_mode_reports_a_timer_error_median() {
|
||||||
let mut collector = StatsCollector::new(StatsConfig::finite());
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
|
|||||||
Generated
+6
-6
@@ -31,9 +31,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.5"
|
version = "1.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
|
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -97,9 +97,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.12"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
@@ -285,9 +285,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.5"
|
version = "3.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
Reference in new issue
Block a user