facett-grid/examples/grid_bench.rs
//! CPU frame/interaction benchmark (no window or GPU):
//! `cargo run --release -p facett-grid --example grid_bench -- 1000000`
//! For process peak memory, run the built executable under the platform's memory
//! profiler (e.g. `/usr/bin/time -l target/release/examples/grid_bench 1000000` on macOS).
//! Frame timings include layout, painting and tessellation, excluding GPU submission.
use std::{hint::black_box, sync::Arc, time::Instant};
use arrow_array::{Float64Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use facett_core::Facet;
use facett_grid::{Grid, Msg};
fn percentiles(mut samples: Vec<f64>) -> serde_json::Value {
samples.sort_by(f64::total_cmp);
serde_json::json!({
"p50_ms": samples[samples.len() / 2],
"p95_ms": samples[(samples.len() * 95 / 100).min(samples.len() - 1)],
"max_ms": samples.last(),
})
}
fn frame(ctx: &egui::Context, grid: &mut Grid, scroll: bool) -> (f64, usize) {
let start = Instant::now();
let mut input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(1000.0, 700.0),
)),
..Default::default()
};
if scroll {
input.events = vec![
egui::Event::PointerMoved(egui::pos2(400.0, 300.0)),
egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -48.0),
modifiers: egui::Modifiers::NONE,
phase: egui::TouchPhase::Move,
},
];
}
let output = ctx.run_ui(input, |ui| {
egui::CentralPanel::default().show(ui, |ui| grid.ui(ui));
});
let primitives = ctx.tessellate(output.shapes, output.pixels_per_point);
let vertices = primitives
.iter()
.map(|p| match &p.primitive {
egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
_ => 0,
})
.sum();
black_box(&primitives);
(start.elapsed().as_secs_f64() * 1000.0, vertices)
}
fn main() {
let n = std::env::args()
.nth(1)
.map(|s| s.parse::<usize>().expect("row count"))
.unwrap_or(100_000)
.max(1);
let batch = RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
Field::new("region", DataType::Utf8, false),
Field::new("description", DataType::Utf8, false),
])),
vec![
Arc::new(Int64Array::from_iter_values(0..n as i64)),
Arc::new(Float64Array::from_iter_values(
(0..n).map(|i| ((i * 37) % 10000) as f64 / 10.0),
)),
Arc::new(StringArray::from_iter_values(
(0..n).map(|i| ["north", "south", "east", "west"][i % 4]),
)),
Arc::new(StringArray::from_iter_values(
(0..n).map(|i| format!("Order {i}: goods and services")),
)),
],
)
.unwrap();
let source_bytes = batch.get_array_memory_size();
let start = Instant::now();
let mut grid = Grid::from_record_batch("benchmark", &batch).with_totals(true);
let ingest_ms = start.elapsed().as_secs_f64() * 1000.0;
let ctx = egui::Context::default();
let (first_frame_ms, _) = frame(&ctx, &mut grid, false);
for _ in 0..10 {
frame(&ctx, &mut grid, false);
}
let mut vertices = 0;
let scrolling: Vec<_> = (0..120)
.map(|_| {
let (ms, count) = frame(&ctx, &mut grid, true);
vertices = count;
ms
})
.collect();
assert!(vertices > 0);
let start = Instant::now();
grid.click_header(1, false);
let sort_ms = start.elapsed().as_secs_f64() * 1000.0;
let start = Instant::now();
grid.update(Msg::SetFilter("north".into()));
let filter_submit_ms = start.elapsed().as_secs_f64() * 1000.0;
let mut searching = Vec::new();
while grid.filter_pending() {
searching.push(frame(&ctx, &mut grid, false).0);
}
let filter_complete_ms = start.elapsed().as_secs_f64() * 1000.0;
searching.push(frame(&ctx, &mut grid, false).0); // includes the new filtered sums
assert_eq!(grid.visible_rows(), n.div_ceil(4));
println!(
"{}",
serde_json::json!({
"rows": n, "columns": 4, "totals": true,
"source_array_bytes": source_bytes,
"ingest_ms": ingest_ms, "first_frame_ms": first_frame_ms,
"scroll_cpu": percentiles(scrolling), "last_frame_vertices": vertices,
"sort_ms": sort_ms, "filter_submit_ms": filter_submit_ms,
"filter_complete_ms": filter_complete_ms, "search_cpu": percentiles(searching),
"method": "Headless egui layout + paint + tessellation; no GPU or vsync. source_array_bytes is source buffers, not process memory."
})
);
}