diff options
| author | tv <tv@krebsco.de> | 2026-11-03 04:53:08 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-03 04:53:08 +0100 |
| commit | 9ee8bae8cd740fc038ac6ca14eae4fad458cda31 (patch) | |
| tree | 702725259298525c95cffc886fa59e58118c1cf4 /src/swarm/topology | |
| parent | 5581992d5603322d7f02bd76625334be3f8a2b18 (diff) | |
add writer tree benchmark
Diffstat (limited to 'src/swarm/topology')
| -rw-r--r-- | src/swarm/topology/tests.rs | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/src/swarm/topology/tests.rs b/src/swarm/topology/tests.rs index d3a3e38..807ea63 100644 --- a/src/swarm/topology/tests.rs +++ b/src/swarm/topology/tests.rs @@ -311,3 +311,151 @@ fn hub_degree_is_bounded_in_large_swarms() { hub_degree_is_bounded(n, stable_count); } } + +/// What passing one change on costs over the graph the member list gives, by the +/// number of messages, while some links don't work (`down`: a share of them, chosen by +/// their members' keys): as now (every node that gets it tells all its neighbours but +/// the one it came from, over the links that work), and along the writer's tree. +/// +/// The tree: every member's distance from the writer over the member list's graph (as +/// every node computes it alike), and each member's parent the neighbour it reaches +/// (as it tells its neighbours, `swarm::reach`) that comes first by distance, then pair +/// score, of those before it (by distance, then pair key: no cycles). Whoever has a +/// change of the writer's passes it on to its children in the writer's tree, however +/// it got it. A member with no parent it reaches waits for an announcement from a +/// neighbour that has it (a pull), then passes it on to its own children. +/// +/// Reported per change: messages along the tree, pulls, the share that got it along the +/// tree alone, and how many announcement rounds the last one waited. Arrival in +/// breadth-first order: no delays, no losses. A benchmark: `cargo benchmarks`. +#[test] +#[ignore = "a benchmark: cargo benchmarks"] +fn bench_writer_trees() { + use std::collections::{HashMap, HashSet}; + // Along the tree from `from`, to all below it: the messages counted in `total`. + fn along<'a>( + from: &'a str, + children: &HashMap<&'a str, Vec<&'a str>>, + has: &mut HashSet<&'a str>, + total: &mut usize, + ) { + let mut queue = VecDeque::from([from]); + has.insert(from); + while let Some(m) = queue.pop_front() { + for &child in children.get(m).map_or(&[][..], Vec::as_slice) { + *total += 1; + if has.insert(child) { + queue.push_back(child); + } + } + } + } + println!(); + let k = 12; + for n in [13, 50, 100, 250, 1000, 2000] { + let ms = members(n); + let g = graph(&ms, k); + let key: HashMap<&str, u64> = ms.iter().map(|m| (m.as_str(), pair_key(m))).collect(); + let degree: f64 = g.values().map(BTreeSet::len).sum::<usize>() as f64 / n as f64; + let sources: Vec<&str> = ms + .iter() + .step_by((n / 10).max(1)) + .map(String::as_str) + .collect(); + for down_percent in [0, 5, 20] { + let works = + |a: &str, b: &str| g[a].contains(b) && mix(key[a] ^ key[b]) % 100 >= down_percent; + let (mut now_total, mut tree_total, mut pulls_total) = (0usize, 0usize, 0usize); + let (mut eager_total, mut reached_total) = (0usize, 0usize); + let (mut depth_max, mut children_max, mut rounds_max) = (0usize, 0usize, 0usize); + for &source in &sources { + // As now, over the links that work. + let mut seen: HashSet<&str> = HashSet::from([source]); + let mut queue: VecDeque<(&str, Option<&str>)> = VecDeque::from([(source, None)]); + while let Some((m, from)) = queue.pop_front() { + for next in &g[m] { + if Some(next.as_str()) == from || !works(m, next) { + continue; + } + now_total += 1; + if seen.insert(next.as_str()) { + queue.push_back((next.as_str(), Some(m))); + } + } + } + // Distances over the member list's graph. + let mut depth: HashMap<&str, usize> = HashMap::from([(source, 0)]); + let mut order = vec![source]; + let mut at = 0; + while at < order.len() { + let m = order[at]; + at += 1; + for next in &g[m] { + if !depth.contains_key(next.as_str()) { + depth.insert(next.as_str(), depth[m] + 1); + order.push(next.as_str()); + } + } + } + depth_max = depth_max.max(depth.values().copied().max().unwrap_or(0)); + // Each member's parent: one it reaches, before it. + let rank = |m: &str| (depth[m], key[m]); + let mut children: HashMap<&str, Vec<&str>> = HashMap::new(); + for &m in &order[1..] { + let parent = g[m] + .iter() + .map(String::as_str) + .filter(|p| rank(p) < rank(m) && works(m, p)) + .min_by_key(|p| (depth[p], pair_score(p, m))); + if let Some(parent) = parent { + children.entry(parent).or_default().push(m); + } + } + children_max = children_max.max(children.values().map(Vec::len).max().unwrap_or(0)); + // Along the tree from whoever has it; then pulls where announcements + // reach, round by round. + let mut has: HashSet<&str> = HashSet::new(); + along(source, &children, &mut has, &mut tree_total); + eager_total += has.len(); + let mut rounds = 0; + loop { + let pulling: Vec<&str> = ms + .iter() + .map(String::as_str) + .filter(|m| { + !has.contains(m) + && g[*m] + .iter() + .any(|x| has.contains(x.as_str()) && works(m, x)) + }) + .collect(); + if pulling.is_empty() { + break; + } + rounds += 1; + for m in pulling { + if !has.contains(m) { + pulls_total += 1; + along(m, &children, &mut has, &mut tree_total); + } + } + } + rounds_max = rounds_max.max(rounds); + reached_total += has.len(); + } + let changes = sources.len() as f64; + let share = |count: usize| 100.0 * count as f64 / changes / n as f64; + println!( + "{n:>5} members, {degree:.1} neighbours, {down_percent:>2}% of links down: per change \ + {:>6.0} messages now; along the writer's tree {:>5.0}, and {:>4.0} pulls \ + ({:.1}% along the tree alone, {:.1}% in all, the last after {rounds_max} \ + announcement rounds; {depth_max} hops, {children_max} children at most)", + now_total as f64 / changes, + tree_total as f64 / changes, + pulls_total as f64 / changes, + share(eager_total), + share(reached_total), + ); + } + } +} |
