|
| 1 | +//! Triangles in a random graph, by binary joins or by a delta query of half joins. The binary |
| 2 | +//! joins arrange the two-paths; the delta query arranges only the edges, by source, by |
| 3 | +//! destination, and by themselves. Reports the time and memory to load the graph, and then the |
| 4 | +//! time per tick as edges are replaced. |
| 5 | +//! |
| 6 | +//! ```text |
| 7 | +//! cargo run --release --example triangles_bench -- [join|delta] [edges] [nodes] [churn] [ticks] [workers] |
| 8 | +//! ``` |
| 9 | +
|
| 10 | +use std::alloc::{GlobalAlloc, Layout, System}; |
| 11 | +use std::sync::atomic::{AtomicUsize, Ordering}; |
| 12 | +use std::time::Instant; |
| 13 | + |
| 14 | +/// Counts live heap bytes and their peak, to tell the heap's size from the process's. |
| 15 | +struct Counting; |
| 16 | +static LIVE: AtomicUsize = AtomicUsize::new(0); |
| 17 | +static PEAK: AtomicUsize = AtomicUsize::new(0); |
| 18 | +unsafe impl GlobalAlloc for Counting { |
| 19 | + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { |
| 20 | + let live = LIVE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); |
| 21 | + PEAK.fetch_max(live, Ordering::Relaxed); |
| 22 | + unsafe { System.alloc(layout) } |
| 23 | + } |
| 24 | + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { |
| 25 | + LIVE.fetch_sub(layout.size(), Ordering::Relaxed); |
| 26 | + unsafe { System.dealloc(ptr, layout) } |
| 27 | + } |
| 28 | +} |
| 29 | +#[global_allocator] |
| 30 | +static ALLOC: Counting = Counting; |
| 31 | + |
| 32 | +fn heap() -> String { |
| 33 | + format!("heap {:.1} MiB (peak {:.1} MiB)", LIVE.load(Ordering::Relaxed) as f64 / 1048576.0, PEAK.load(Ordering::Relaxed) as f64 / 1048576.0) |
| 34 | +} |
| 35 | + |
| 36 | +use interactive::server::{RenderBackend, Server}; |
| 37 | +use interactive::{lower, parse}; |
| 38 | + |
| 39 | +const EDGES: &str = r#" |
| 40 | + let e = input 0 : ((int, int) ; ()) | key(((x, y), ()) -> (x, y)); |
| 41 | + let r = e | key((x, y) -> (y, x)); |
| 42 | + let s = e | key((x, y) -> ((x, y), ())); |
| 43 | +"#; |
| 44 | + |
| 45 | +/// Each triangle `(a, b, c)` has edges `(a, b)`, `(b, c)`, and `(a, c)`. |
| 46 | +const JOIN: &str = r#" |
| 47 | + let paths = r | join(e, (b, a, c) -> ((a, c), b)); |
| 48 | + let tri = paths | join(s, ((a, c), b, ()) -> ((a, b, c), ())); |
| 49 | + export "count" = tri | key((_, _) -> ((), ())) | count; |
| 50 | +"#; |
| 51 | + |
| 52 | +/// One path of half joins per edge of the triangle, each meeting the earlier edges' changes at or |
| 53 | +/// before its own time and the later edges' strictly before. |
| 54 | +const DELTA: &str = r#" |
| 55 | + let d1 = r | lift | half_join(e, 1, (b, a, c) -> ((a, c), b)) | half_join(s, 1, ((a, c), b, ()) -> ((a, b, c), ())); |
| 56 | + let d2 = e | lift | half_join(r, 0, (b, c, a) -> ((a, c), b)) | half_join(s, 1, ((a, c), b, ()) -> ((a, b, c), ())); |
| 57 | + let d3 = e | lift | half_join(e, 0, (a, c, b) -> ((b, c), a)) | half_join(s, 0, ((b, c), a, ()) -> ((a, b, c), ())); |
| 58 | + let tri = (d1 + d2 + d3) | delay((_, (_, t)) -> t) | key((abc, (v, _)) -> (abc, v)); |
| 59 | + export "count" = tri | key((_, _) -> ((), ())) | count; |
| 60 | +"#; |
| 61 | + |
| 62 | +/// This process's memory in MiB: on macOS its `footprint`, elsewhere its RSS. |
| 63 | +fn memory_mib() -> Option<f64> { |
| 64 | + let out = std::process::Command::new("footprint").args(["-p", &std::process::id().to_string()]).output(); |
| 65 | + if let Ok(out) = out { |
| 66 | + let text = String::from_utf8_lossy(&out.stdout).into_owned(); |
| 67 | + let mut words = text.split("Footprint:").nth(1)?.split_whitespace(); |
| 68 | + let n: f64 = words.next()?.parse().ok()?; |
| 69 | + return match words.next()? { |
| 70 | + "B" => Some(n / (1024.0 * 1024.0)), |
| 71 | + "KB" => Some(n / 1024.0), |
| 72 | + "MB" => Some(n), |
| 73 | + "GB" => Some(n * 1024.0), |
| 74 | + _ => None, |
| 75 | + }; |
| 76 | + } |
| 77 | + let status = std::fs::read_to_string("/proc/self/status").ok()?; |
| 78 | + let kib: f64 = status.lines().find(|l| l.starts_with("VmRSS:"))?.split_whitespace().nth(1)?.parse().ok()?; |
| 79 | + Some(kib / 1024.0) |
| 80 | +} |
| 81 | + |
| 82 | +fn mem() -> String { |
| 83 | + memory_mib().map_or_else(|| "n/a".to_string(), |m| format!("{m:>7.1} MiB")) |
| 84 | +} |
| 85 | + |
| 86 | +fn main() { |
| 87 | + let mut args = std::env::args().skip(1); |
| 88 | + let method = args.next().unwrap_or("delta".into()); |
| 89 | + let nums: Vec<u64> = args.map(|a| a.parse().expect("integer argument")).collect(); |
| 90 | + let edges = nums.first().copied().unwrap_or(1_000_000); |
| 91 | + let nodes = nums.get(1).copied().unwrap_or(100_000); |
| 92 | + let churn = nums.get(2).copied().unwrap_or(1_000); |
| 93 | + let ticks = nums.get(3).copied().unwrap_or(20); |
| 94 | + let workers = nums.get(4).copied().unwrap_or(1) as usize; |
| 95 | + let query = match method.as_str() { |
| 96 | + "join" => JOIN, |
| 97 | + "delta" => DELTA, |
| 98 | + other => panic!("unknown method {other:?}: join or delta"), |
| 99 | + }; |
| 100 | + let source = format!("{EDGES}{query}"); |
| 101 | + |
| 102 | + timely::execute(timely::Config::process(workers), move |worker| { |
| 103 | + let index = worker.index(); |
| 104 | + let mut server = Server::with_backend(RenderBackend::Corgi); |
| 105 | + let mut program = lower::lower_tree(parse::pipe::parse(&source)); |
| 106 | + program.optimize(); |
| 107 | + server.install(worker, "p", &program).unwrap(); |
| 108 | + server.load(worker, "p", 0, &format!("random:nodes={nodes},edges={edges},churn={churn}")).unwrap(); |
| 109 | + let loaded = Instant::now(); |
| 110 | + server.tick(worker); |
| 111 | + let count = |server: &mut Server, worker: &mut timely::worker::Worker| -> i64 { |
| 112 | + server.snapshot(worker, "count").unwrap().iter().map(|(_, v, d)| match v { interactive::ir::Value::Int(n) => n * d, _ => 0 }).sum() |
| 113 | + }; |
| 114 | + // A snapshot is a dataflow, so every worker takes part; worker 0 receives the rows. |
| 115 | + let elapsed = loaded.elapsed(); |
| 116 | + let n = count(&mut server, worker); |
| 117 | + if index == 0 { |
| 118 | + println!("{method}: {edges} edges over {nodes} nodes, {workers} workers"); |
| 119 | + println!("load: {:>9.1} ms mem {} {} count {n}", elapsed.as_secs_f64() * 1e3, mem(), heap()); |
| 120 | + } |
| 121 | + let churned = Instant::now(); |
| 122 | + for _ in 0..ticks { server.tick(worker); } |
| 123 | + let elapsed = churned.elapsed(); |
| 124 | + let n = count(&mut server, worker); |
| 125 | + if index == 0 { |
| 126 | + println!( |
| 127 | + "churn: {:>9.2} ms/tick over {ticks} ticks ({churn} edges replaced per tick) mem {} count {n}", |
| 128 | + elapsed.as_secs_f64() * 1e3 / ticks as f64, mem(), |
| 129 | + ); |
| 130 | + } |
| 131 | + }) |
| 132 | + .unwrap(); |
| 133 | +} |
0 commit comments