Skip to content

Commit 044035c

Browse files
frankmcsherryclaude
andcommitted
interactive: a triangles benchmark, by binary joins and by a delta query
`triangles_bench` counts triangles in a random graph by binary joins, which arrange the two-paths, or by a delta query of half joins, which arranges only the edges. It reports the time and memory to load the graph and the time per tick as edges are replaced, and counts heap allocations to report the heap's peak. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2ff0abe commit 044035c

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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

Comments
 (0)