//! What social cards cost: each stage of drawing one and the PNG it makes, //! a cached card against a new one, and how many a second the cores draw //! together. //! //! cargo run --release -p knotless-serve --example cards -- [--out ] //! //! With `--out`, the cards are written there as PNGs, to look at. use knotless_serve::card::{self, Card}; use std::time::{Duration, Instant}; /// Runs `f` until a second has passed, at least ten times; the median and /// the slowest one in a hundred. fn time(mut f: impl FnMut() -> T) -> (Duration, Duration) { let mut runs = Vec::new(); let start = Instant::now(); while runs.len() < 10 || start.elapsed() < Duration::from_secs(1) { let t = Instant::now(); std::hint::black_box(f()); runs.push(t.elapsed()); } runs.sort(); (runs[runs.len() / 2], runs[runs.len() * 99 / 100]) } fn ms(d: Duration) -> String { format!("{:.3}", d.as_secs_f64() * 1000.0) } fn cards() -> Vec<(&'static str, Card)> { vec![ ("home", Card::Home), ( "repo", Card::Repo { owner: "alice.bsky.social".into(), name: "knotless".into(), description: Some( "Git repositories stored natively on atproto: no git server, nothing to install on the PDS." .into(), ), pulls: 3, issues: 12, }, ), ( "issue", Card::Item { owner: "alice.bsky.social".into(), name: "knotless".into(), pull: false, state: "open".into(), title: "Cloning a repository with ten thousand refs takes a minute on a slow PDS" .into(), author: "bob.example.com".into(), comments: 4, }, ), ( "pull", Card::Item { owner: "alice.bsky.social".into(), name: "knotless".into(), pull: true, state: "merged".into(), title: "Draw social cards in process".into(), author: "carol.test".into(), comments: 1, }, ), ( "emoji", Card::Item { owner: "alice.bsky.social".into(), name: "knotless".into(), pull: false, state: "closed".into(), title: "Titles with emoji 🎉 draw in color: 👩🏽‍💻 🇯🇵 ❤️ 🦀 not as boxes".into(), author: "dana.test".into(), comments: 2, }, ), ] } fn main() { let args: Vec = std::env::args().collect(); let out = args .iter() .position(|a| a == "--out") .map(|i| std::path::PathBuf::from(&args[i + 1])); if let Some(dir) = &out { std::fs::create_dir_all(dir).unwrap(); } // The fonts load on first use; that is not what is measured. card::render(&Card::Home.svg()); println!( "{:<6} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}", "card", "svg ms", "hash ms", "draw ms", "png ms", "all ms", "KB" ); for (name, card) in cards() { let svg = card.svg(); let (svg_ms, _) = time(|| card.svg()); let (hash_ms, _) = time(|| card::version(&svg)); let (draw_ms, _) = time(|| card::raster(&svg)); let pixmap = card::raster(&svg); let (png_ms, _) = time(|| card::encode(&pixmap)); let (all_ms, _) = time(|| card::render(&svg)); let png = card::render(&svg); println!( "{name:<6} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8.1}", ms(svg_ms), ms(hash_ms), ms(draw_ms), ms(png_ms), ms(all_ms), png.len() as f64 / 1024.0 ); if let Some(dir) = &out { std::fs::write(dir.join(format!("{name}.png")), png).unwrap(); } } // A whole card as a request for it pays, new and cached; and the cores // drawing new ones together. let rt = tokio::runtime::Runtime::new().unwrap(); let (_, issue) = &cards()[2]; let svg = issue.svg(); let (new, new99) = time(|| card::render(&svg)); let cache = card::Cache::new(1 << 16); let version = card::version(&svg); rt.block_on(cache.png(&version, svg.clone())); let (hit, hit99) = time(|| { rt.block_on(async { let svg = issue.svg(); let version = card::version(&svg); cache.png(&version, svg).await }) }); println!( "\nan issue card, new: {} ms (p99 {}); cached, with its SVG and hash: {} ms (p99 {})", ms(new), ms(new99), ms(hit), ms(hit99) ); let n = 256; let start = Instant::now(); rt.block_on(futures::future::join_all((0..n).map(|i| { let card = Card::Item { owner: "alice.bsky.social".into(), name: "knotless".into(), pull: i % 2 == 0, state: "open".into(), title: format!("Issue number {i}, one of many drawn at once"), author: "bob.example.com".into(), comments: i, }; let svg = card.svg(); let cache = &cache; async move { cache.png(&card::version(&svg), svg).await } }))); let took = start.elapsed(); let cores = std::thread::available_parallelism().map_or(1, |n| n.get()); println!( "{n} new cards at once on {cores} cores: {} ms, {:.0} cards a second", took.as_millis(), n as f64 / took.as_secs_f64() ); }