7 Commits
Author SHA1 Message Date
swaits 794d6dffb1 bump crate to 0.6.1 2023-02-10 23:02:46 -05:00
swaits c3d210cf16 refactor: switch to array-init
- Use array-init for the lowest level vector operations. Makes them nicer and less bug-prone, with zero performance impact.
- Remove unnecessary/unused ::new() functions on Vector and HeightVector.
- Update README with new dependency.
- Fix typo in README.

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-10 22:51:38 -05:00
swaits e79242118f bump crate to 0.6.0 2023-02-10 12:38:11 -07:00
swaits 0a62dc06b3 test sr.ht build manifest using package instead of cargo install
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-10 12:31:57 -07:00
swaits 79c90cbd4f test: get to 100% coverage, clean up cargo features
- eliminated `f64` feature because Cargo doesn’t understand mutually exclusive features. So, we default to `f64`, and leave the `f32` feature there if people want to change to that.
- integrated tarpaulin for coverage
- added tests to get to 100% coverage
- fixed NetworkCoordinate Div impl safety checks (now acts like Add and Sub)
- update README to explain cargo feature change
- cleanup build script with the new simpler features model

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-10 12:14:24 -07:00
swaits f829708e5a test: add examples to CI
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-09 20:25:51 -07:00
swaits 1ce84b63ec test: add more stuff to CI
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-09 20:17:24 -07:00
9 changed files with 125 additions and 58 deletions

No files matched your search

+25 -3
View File
@@ -1,26 +1,48 @@
image: archlinux image: archlinux
packages: packages:
- rustup - rustup
- cargo-tarpaulin
sources: sources:
- https://git.sr.ht/~swaits/vivaldi-nc - https://git.sr.ht/~swaits/vivaldi-nc
artifacts:
- vivaldi-nc/tarpaulin-report.html
tasks: tasks:
- setup: | - setup: |
rustup toolchain install nightly stable rustup toolchain install nightly stable
cd vivaldi-nc/ cd vivaldi-nc/
rustup run stable cargo fetch rustup run stable cargo fetch
- stable: | - format: |
rustup default stable rustup default stable
cd vivaldi-nc/ cd vivaldi-nc/
cargo fmt --verbose --check cargo fmt --verbose --check
- lint: |
rustup default stable
cd vivaldi-nc/
cargo clippy --verbose cargo clippy --verbose
- coverage: |
rustup default stable
cd vivaldi-nc/
cargo tarpaulin --verbose --all-features --out Html
- stable: |
rustup default stable
cd vivaldi-nc/
cargo build --verbose cargo build --verbose
cargo test --verbose cargo test --verbose --all-features
- nightly: | - nightly: |
rustup default nightly rustup default nightly
cd vivaldi-nc/ cd vivaldi-nc/
cargo clippy --verbose ||: cargo clippy --verbose ||:
cargo build --verbose ||: cargo build --verbose ||:
cargo test --verbose ||: cargo test --verbose --all-features ||:
- bench: |
rustup default stable
cd vivaldi-nc/
cargo bench --verbose --all-features
- examples: |
rustup default stable
cd vivaldi-nc/
cargo run --verbose --all-features --example planetlab
cargo run --verbose --all-features --example planetlab --release
- docs: | - docs: |
cd vivaldi-nc/ cd vivaldi-nc/
rustup run stable cargo doc --no-deps rustup run stable cargo doc --no-deps
+1
View File
@@ -1,3 +1,4 @@
/flamegraph.svg /flamegraph.svg
/target /target
Cargo.lock Cargo.lock
/tarpaulin-report.html
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vivaldi-nc" name = "vivaldi-nc"
version = "0.5.4" version = "0.6.1"
edition = "2021" edition = "2021"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
description = "Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with a clean and simple interface." description = "Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with a clean and simple interface."
@@ -16,9 +16,8 @@ exclude = [
] ]
[features] [features]
default = ["f32"] default = []
f32 = [] f32 = []
f64 = []
[profile.bench] [profile.bench]
debug = true debug = true
@@ -37,6 +36,7 @@ name = "million-updates"
harness = false harness = false
[dependencies] [dependencies]
array-init = "2.1.0"
num-traits = "0.2.15" num-traits = "0.2.15"
rand = "0.8.5" rand = "0.8.5"
serde = {version = "1.0.152", features = ["serde_derive"]} serde = {version = "1.0.152", features = ["serde_derive"]}
+11 -5
View File
@@ -112,18 +112,20 @@ usage examples.
### Cargo Features ### Cargo Features
By default, the internal data structures and operations all use `f32`. If you want to use `f64`, you can enable it as a cargo feature: By default, the internal data structures and operations all use `f64`, which is
slightly faster on modern architectures. If you want to use `f32` instead, you
can enable it as a cargo feature:
```toml ```toml
[dependencies] [dependencies]
vivaldi-nc = { version = "0.5.2", features = ["f64"] } vivaldi-nc = { version = "(version)", features = ["f32"] }
``` ```
### Examples ### Examples
The repository includes an example which loads a n-to-n latency sample from The repository includes an example which loads a 490 node N-to-N latency sample
PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean error from PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean
is reached. The output is a JSON array of elements which contain the NC's error is reached. The output is a JSON array of elements which contain the NC's
position, its height (or stem latency estimate), and its estimation of error position, its height (or stem latency estimate), and its estimation of error
(lower is better) that look like this: (lower is better) that look like this:
@@ -186,6 +188,10 @@ This crate depends on:
- [`serde_with`](https://crates.io/crates/serde_with): Because the inner - [`serde_with`](https://crates.io/crates/serde_with): Because the inner
`Vector<T,N>` uses a const generic length, we use this to help derive `Vector<T,N>` uses a const generic length, we use this to help derive
`Deserialize`. `Deserialize`.
- [`array-init`](https://crates.io/crates/array-init): Makes the array
operations for the internal `Vector` so much nicer. Once we have
`feature(array_zip)` stabilized, we could use that instead. Until then, this
works well with zero performance impact.
## Design Goals & Alternatives ## Design Goals & Alternatives
+13 -1
View File
@@ -2,6 +2,18 @@
// //
// for info on the format, see `NetLatency-Data/README.md` // for info on the format, see `NetLatency-Data/README.md`
//
// **** Features ****
//
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
#[cfg(feature = "f32")]
type FloatType = f32;
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
#[cfg(not(feature = "f32"))]
type FloatType = f64;
use std::{ use std::{
fs::File, fs::File,
io::{BufReader, Read}, io::{BufReader, Read},
@@ -85,7 +97,7 @@ fn main() {
nc[i_local].update(&nc_remote, rtt_measured); nc[i_local].update(&nc_remote, rtt_measured);
// complete, now let's sum up the error // complete, now let's sum up the error
error = nc.iter().map(|n| n.error()).sum::<f32>() / NUM_NODES as f32; error = nc.iter().map(|n| n.error()).sum::<FloatType>() / NUM_NODES as FloatType;
assert!(error.is_finite()); assert!(error.is_finite());
if error < 5.0 { if error < 5.0 {
break; break;
+43 -15
View File
@@ -58,7 +58,7 @@ use crate::vector::{self, Vector};
type FloatType = f32; type FloatType = f32;
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features /// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
#[cfg(feature = "f64")] #[cfg(not(feature = "f32"))]
type FloatType = f64; type FloatType = f64;
// //
@@ -92,11 +92,6 @@ pub struct HeightVector<const N: usize> {
// //
impl<const N: usize> HeightVector<N> { impl<const N: usize> HeightVector<N> {
/// A new height vector is a random unit vector
pub(crate) fn new() -> Self {
Self::random()
}
/// A new height vector is a random unit vector /// A new height vector is a random unit vector
pub(crate) fn random() -> Self { pub(crate) fn random() -> Self {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@@ -122,15 +117,16 @@ impl<const N: usize> HeightVector<N> {
/// by the inveerse of its length. /// by the inveerse of its length.
pub(crate) fn normalized(&self) -> Self { pub(crate) fn normalized(&self) -> Self {
let len = self.len(); let len = self.len();
if len < FloatType::EPSILON { // scale it by the inverse of its magnitude like any normal vector
// if we have a bad vector, generate a new random vector let ret = Self {
Self::new() position: self.position / len,
height: self.height / len,
};
if ret.is_valid() {
ret
} else { } else {
// otherwise scale it by the inverse of its magnitude like any normal vector // if we have a bad vector, generate a new random vector
Self { Self::random()
position: self.position / len,
height: self.height / len,
}
} }
} }
@@ -317,7 +313,7 @@ mod tests {
#[test] #[test]
fn test_new() { fn test_new() {
// new gives us a random unit length vec // new gives us a random unit length vec
let a = HeightVector::<3>::new(); let a = HeightVector::<3>::random();
assert_approx_eq!(a.len(), 1.0); assert_approx_eq!(a.len(), 1.0);
} }
@@ -377,4 +373,36 @@ mod tests {
let t = serde_json::to_string(&a); let t = serde_json::to_string(&a);
assert_eq!(t.as_ref().unwrap(), s); assert_eq!(t.as_ref().unwrap(), s);
} }
#[test]
fn test_sub_invalid() {
let valid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let mut invalid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
// force invalidity here - because `::from()` actually also catches this
invalid.height = 1.0 / 0.0;
let result = valid - invalid;
assert_approx_eq!(result.len(), 1.0);
}
#[test]
fn test_add_invalid() {
let valid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let mut invalid = HeightVector::<2>::from(([1.0 / 0.0, 2.0], 3.0));
// make sure `from()` caught and replaced the invalid vec
assert_approx_eq!(invalid.len(), 1.0);
// force invalidity here - because `::from()` actually also catches this
invalid.height = 1.0 / 0.0;
let result = valid + invalid;
assert_approx_eq!(result.len(), 1.0);
}
#[test]
fn test_zero_norm() {
let a = HeightVector::<2>::from(([0.0, 0.0], 0.0));
let b = a.normalized();
let len = b.len();
assert_approx_eq!(len, 1.0);
}
} }
-4
View File
@@ -108,10 +108,6 @@
mod height_vector; mod height_vector;
mod vector; mod vector;
// make sure we don't have conflicting features
#[cfg(all(feature = "f32", feature = "f64"))]
compile_error!("feature \"f32\" and feature \"f64\" cannot be enabled at the same time");
// publish our interface // publish our interface
pub mod network_coordinate; pub mod network_coordinate;
pub use network_coordinate::NetworkCoordinate; pub use network_coordinate::NetworkCoordinate;
+19 -5
View File
@@ -14,7 +14,7 @@ use crate::height_vector::HeightVector;
#[cfg(feature = "f32")] #[cfg(feature = "f32")]
type FloatType = f32; type FloatType = f32;
#[cfg(feature = "f64")] #[cfg(not(feature = "f32"))]
type FloatType = f64; type FloatType = f64;
// //
@@ -127,7 +127,7 @@ impl<const N: usize> NetworkCoordinate<N> {
#[cfg(feature = "f32")] #[cfg(feature = "f32")]
return Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0); return Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0);
#[cfg(feature = "f64")] #[cfg(not(feature = "f32"))]
return Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0); return Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0);
} }
@@ -195,14 +195,17 @@ impl<const N: usize> NetworkCoordinate<N> {
#[cfg(feature = "f32")] #[cfg(feature = "f32")]
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0; let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0;
#[cfg(feature = "f64")] #[cfg(not(feature = "f32"))]
let rtt_ms = rtt.as_secs_f64() * 1000.0; let rtt_ms = rtt.as_secs_f64() * 1000.0;
#[cfg(feature = "f64")] #[cfg(not(feature = "f32"))]
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0; let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0;
// rtt needs to be positive // rtt needs to be positive
if rtt_ms < 0.0 { if rtt_ms < 0.0 {
return self; // Note: `rtt` is guaranteed to be positive because `Duration` enforces it.
// If this panics, something changed where `Duration` now allows for negative
// values.
unreachable!();
} }
// Sample weight balances local and remote error. (1) // Sample weight balances local and remote error. (1)
@@ -233,6 +236,10 @@ impl<const N: usize> NetworkCoordinate<N> {
// error // error
if self.heightvec.is_invalid() { if self.heightvec.is_invalid() {
*self = Self::new(); *self = Self::new();
// We should never get here because the call to `normalized()` above should catch any
// invalid `heightvec`
unreachable!();
} }
// return reference to updated self // return reference to updated self
@@ -376,4 +383,11 @@ mod tests {
let estimate = a.estimated_rtt(&b); let estimate = a.estimated_rtt(&b);
assert_approx_eq!(estimate.as_secs_f32(), 0.080_099); assert_approx_eq!(estimate.as_secs_f32(), 0.080_099);
} }
#[test]
fn test_error_getter() {
let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}";
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
assert_approx_eq!(a.error(), 1.0);
}
} }
+10 -22
View File
@@ -3,6 +3,7 @@
//! This is the simplest possible implementation of a vector to support the needs of a //! This is the simplest possible implementation of a vector to support the needs of a
//! Vivaldi `HeightVector`. //! Vivaldi `HeightVector`.
use array_init::array_init;
use num_traits::Float; use num_traits::Float;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
@@ -37,11 +38,6 @@ impl<T, const N: usize> Vector<T, N>
where where
T: Float + Serialize + for<'d> Deserialize<'d>, T: Float + Serialize + for<'d> Deserialize<'d>,
{ {
/// Create a new Zero vector.
pub(crate) fn new() -> Self {
Self::default()
}
/// Compute magnitude of vector. /// Compute magnitude of vector.
pub(crate) fn len(&self) -> T { pub(crate) fn len(&self) -> T {
// use hypot() instead of sqrt() of sum of squares to avoid overflows // use hypot() instead of sqrt() of sum of squares to avoid overflows
@@ -90,11 +86,9 @@ where
/// Vector+Vector addition. /// Vector+Vector addition.
fn add(self, rhs: Self) -> Self::Output { fn add(self, rhs: Self) -> Self::Output {
let mut ret = [T::zero(); N]; //Self::Output::new(); Self::Output {
for i in 0..N { inner: array_init(|i| self[i] + rhs[i]),
ret[i] = self[i] + rhs[i];
} }
Self::Output::from(ret)
} }
} }
@@ -106,11 +100,9 @@ where
/// Vector-Vector subtraction. /// Vector-Vector subtraction.
fn sub(self, rhs: Self) -> Self::Output { fn sub(self, rhs: Self) -> Self::Output {
let mut ret = [T::zero(); N]; //Self::Output::new(); Self::Output {
for i in 0..N { inner: array_init(|i| self[i] - rhs[i]),
ret[i] = self[i] - rhs[i];
} }
Self::Output::from(ret)
} }
} }
@@ -122,11 +114,9 @@ where
/// Vector*scalar multiplication. /// Vector*scalar multiplication.
fn mul(self, rhs: T) -> Self::Output { fn mul(self, rhs: T) -> Self::Output {
let mut ret = Self::Output::new(); Self::Output {
for i in 0..N { inner: array_init(|i| self[i] * rhs),
ret.inner[i] = self.inner[i] * rhs;
} }
ret
} }
} }
@@ -138,11 +128,9 @@ where
/// Vector/scalar division. /// Vector/scalar division.
fn div(self, rhs: T) -> Self::Output { fn div(self, rhs: T) -> Self::Output {
let mut ret = Self::Output::new(); Self::Output {
for i in 0..N { inner: array_init(|i| self[i] / rhs),
ret.inner[i] = self.inner[i] / rhs;
} }
ret
} }
} }
@@ -235,7 +223,7 @@ mod tests {
#[test] #[test]
fn test_default_new() { fn test_default_new() {
let a = Vector::<f32, 3>::default(); let a = Vector::<f32, 3>::default();
let b = Vector::<f32, 3>::new(); let b = Vector::<f32, 3>::from([1.0; 3]) * 0.0;
let c = Vector::<f32, 3>::from([0.0; 3]); let c = Vector::<f32, 3>::from([0.0; 3]);
assert_eq!(a, b); assert_eq!(a, b);
assert_eq!(b, c); assert_eq!(b, c);