forked from 1b-agent/opencadstudio-upstream
433 lines
15 KiB
Rust
433 lines
15 KiB
Rust
//! OpenCADStudio COMMANDS benchmark suite (`harness = false`).
|
|
//! Currently: EXPLODE (geometry pipeline + scene apply).
|
|
|
|
#[path = "support/harness.rs"]
|
|
mod harness;
|
|
|
|
use std::hint::black_box;
|
|
use std::time::Instant;
|
|
|
|
use codec::entities::{
|
|
Circle, Dimension, DimensionLinear, Insert, Line, LwPolyline, LwVertex, MLine, Point,
|
|
Polyline2D, Polyline3D, Ray, Solid, Text, Vertex2D, Vertex3DPolyline,
|
|
};
|
|
use codec::tables::BlockRecord;
|
|
use codec::types::{Vector2, Vector3};
|
|
use codec::{CadDocument, EntityType, Handle};
|
|
|
|
use harness::BenchmarkRunner;
|
|
use OpenCADStudio::modules::draw::modify::explode::{apply_explode_replacements, explode_batch};
|
|
use OpenCADStudio::modules::draw::modify::flatten;
|
|
use OpenCADStudio::scene::Scene;
|
|
|
|
const BLOCK_NAME: &str = "BENCH_BLOCK";
|
|
const VERTS_PER_POLYLINE: usize = 50;
|
|
const MEMBERS_PER_BLOCK: usize = 20;
|
|
|
|
/// (lwpolylines, inserts, dimensions, mlines) — full = 2,500 compounds.
|
|
fn counts(quick: bool) -> (usize, usize, usize, usize) {
|
|
if quick {
|
|
(1_000, 100, 100, 50)
|
|
} else {
|
|
(2_000, 200, 200, 100)
|
|
}
|
|
}
|
|
|
|
fn fixture_entities(quick: bool) -> usize {
|
|
let (a, b, c, d) = counts(quick);
|
|
a + b + c + d
|
|
}
|
|
|
|
/// Shared 20-member block (10 lines + 10 three-vertex polylines) registered
|
|
/// in `block_records` with `entity_handles` set — what explode_from_document
|
|
/// needs to materialize inserts.
|
|
fn add_bench_block(doc: &mut CadDocument) {
|
|
let mut member_handles = Vec::with_capacity(MEMBERS_PER_BLOCK);
|
|
for k in 0..MEMBERS_PER_BLOCK {
|
|
if k % 2 == 0 {
|
|
let mut line = Line::new();
|
|
let x = (k / 2) as f64 * 3.0;
|
|
line.start = Vector3::new(x, 0.0, 0.0);
|
|
line.end = Vector3::new(x, 4.0, 0.0);
|
|
member_handles.push(doc.add_entity(EntityType::Line(line)).unwrap());
|
|
} else {
|
|
let mut pl = LwPolyline::new();
|
|
pl.vertices = vec![
|
|
LwVertex::new(Vector2::new(0.0, (k as f64) * 0.5)),
|
|
LwVertex::new(Vector2::new(2.0, (k as f64) * 0.5)),
|
|
LwVertex::new(Vector2::new(2.0, (k as f64) * 0.5 + 2.0)),
|
|
];
|
|
member_handles.push(doc.add_entity(EntityType::LwPolyline(pl)).unwrap());
|
|
}
|
|
}
|
|
let mut block = BlockRecord::new(BLOCK_NAME);
|
|
block.handle = doc.allocate_handle();
|
|
block.entity_handles = member_handles;
|
|
doc.block_records.add(block).unwrap();
|
|
}
|
|
|
|
/// Populate the document with the compound fixture; returns the handles of
|
|
/// the fixture entities (block members are NOT included).
|
|
fn push_fixture_entities(doc: &mut CadDocument, quick: bool) -> Vec<Handle> {
|
|
let (n_pl, n_ins, n_dim, n_ml) = counts(quick);
|
|
let mut handles = Vec::with_capacity(fixture_entities(quick));
|
|
|
|
for i in 0..n_pl {
|
|
let base = i as f64 * 0.01;
|
|
let mut pl = LwPolyline::new();
|
|
pl.vertices = (0..VERTS_PER_POLYLINE)
|
|
.map(|k| {
|
|
let mut v =
|
|
LwVertex::new(Vector2::new(base + k as f64 * 2.0, (k % 5) as f64 * 0.75));
|
|
if k % 2 == 1 {
|
|
v.bulge = 0.5;
|
|
}
|
|
v
|
|
})
|
|
.collect();
|
|
handles.push(doc.add_entity(EntityType::LwPolyline(pl)).unwrap());
|
|
}
|
|
|
|
for i in 0..n_ins {
|
|
let pos = Vector3::new((i % 50) as f64 * 5.0, (i / 50) as f64 * 5.0, 0.0);
|
|
let ins = Insert::new(BLOCK_NAME, pos);
|
|
handles.push(doc.add_entity(EntityType::Insert(ins)).unwrap());
|
|
}
|
|
|
|
for i in 0..n_dim {
|
|
let x = (i % 100) as f64 * 12.0;
|
|
let y = (i / 100) as f64 * 12.0;
|
|
let mut d = DimensionLinear::new(Vector3::new(x, y, 0.0), Vector3::new(x + 10.0, y, 0.0));
|
|
d.definition_point = Vector3::new(x + 5.0, y + 3.0, 0.0);
|
|
d.base.text_middle_point = Vector3::new(x + 5.0, y + 4.0, 0.0);
|
|
handles.push(
|
|
doc.add_entity(EntityType::Dimension(Dimension::Linear(d)))
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
for i in 0..n_ml {
|
|
let x = (i % 100) as f64 * 8.0;
|
|
let y = (i / 100) as f64 * 8.0;
|
|
let pts = [
|
|
Vector3::new(x, y, 0.0),
|
|
Vector3::new(x + 4.0, y, 0.0),
|
|
Vector3::new(x + 4.0, y + 4.0, 0.0),
|
|
Vector3::new(x + 8.0, y + 4.0, 0.0),
|
|
Vector3::new(x + 8.0, y + 8.0, 0.0),
|
|
Vector3::new(x + 12.0, y + 8.0, 0.0),
|
|
];
|
|
handles.push(
|
|
doc.add_entity(EntityType::MLine(MLine::from_points(&pts)))
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
handles
|
|
}
|
|
|
|
fn median(times: &[f64]) -> f64 {
|
|
let mut sorted = times.to_vec();
|
|
sorted.sort_by(|a, b| a.total_cmp(b));
|
|
let n = sorted.len();
|
|
if n % 2 == 0 {
|
|
(sorted[n / 2 - 1] + sorted[n / 2]) * 0.5
|
|
} else {
|
|
sorted[n / 2]
|
|
}
|
|
}
|
|
|
|
/// EXPLODE geometry: time `explode_batch` over the whole fixture.
|
|
/// Document build + `items` slice construction are untimed; `black_box` on
|
|
/// the result keeps the compiler from eliding the work.
|
|
fn bench_explode_geometry(runner: &mut BenchmarkRunner) {
|
|
let name = "explode_geometry";
|
|
if !runner.should_run(name) {
|
|
return;
|
|
}
|
|
let quick = runner.quick_mode;
|
|
let sample_count = if quick { 3 } else { 10 };
|
|
let entity_count = fixture_entities(quick);
|
|
let mut times = Vec::with_capacity(sample_count);
|
|
|
|
for _ in 0..sample_count {
|
|
let mut doc = CadDocument::new();
|
|
add_bench_block(&mut doc);
|
|
let handles = push_fixture_entities(&mut doc, quick);
|
|
let items: Vec<(Handle, &EntityType)> = handles
|
|
.iter()
|
|
.filter_map(|&h| doc.get_entity(h).map(|e| (h, e)))
|
|
.collect();
|
|
assert_eq!(items.len(), entity_count);
|
|
|
|
let t0 = Instant::now();
|
|
let pieces = black_box(explode_batch(&items, &doc));
|
|
times.push(t0.elapsed().as_micros() as f64);
|
|
assert_eq!(pieces.len(), entity_count);
|
|
assert!(
|
|
pieces.iter().any(|p| !p.is_empty()),
|
|
"fixture must produce non-empty pieces — block-record/explosion path broken"
|
|
);
|
|
}
|
|
|
|
let med = median(×);
|
|
let throughput = entity_count as f64 / (med / 1_000_000.0);
|
|
runner.record(
|
|
name,
|
|
"EXPLODE geometry pipeline: explode_batch over the compound fixture",
|
|
"us",
|
|
times,
|
|
Some((throughput, "entities/s")),
|
|
None,
|
|
);
|
|
}
|
|
|
|
/// EXPLODE apply: fresh Scene per sample (untimed), replacements computed
|
|
/// untimed, `apply_explode_replacements` timed.
|
|
fn bench_explode_scene_apply(runner: &mut BenchmarkRunner) {
|
|
let name = "explode_scene_apply";
|
|
if !runner.should_run(name) {
|
|
return;
|
|
}
|
|
let quick = runner.quick_mode;
|
|
let sample_count = if quick { 3 } else { 10 };
|
|
let entity_count = fixture_entities(quick);
|
|
let mut times = Vec::with_capacity(sample_count);
|
|
let mut total_pieces = 0usize;
|
|
|
|
for _ in 0..sample_count {
|
|
let mut scene = Scene::new();
|
|
add_bench_block(&mut scene.document);
|
|
let handles = push_fixture_entities(&mut scene.document, quick);
|
|
|
|
let (replacements, pieces_count) = {
|
|
let items: Vec<(Handle, &EntityType)> = handles
|
|
.iter()
|
|
.filter_map(|&h| scene.document.get_entity(h).map(|e| (h, e)))
|
|
.collect();
|
|
let per_entity = explode_batch(&items, &scene.document);
|
|
let reps: Vec<(Handle, Vec<EntityType>)> = handles
|
|
.into_iter()
|
|
.zip(per_entity)
|
|
.filter(|(_, p)| !p.is_empty())
|
|
.collect();
|
|
let count = reps.iter().map(|(_, p)| p.len()).sum();
|
|
(reps, count)
|
|
}; // `items` borrow of scene.document ends here
|
|
|
|
let t0 = Instant::now();
|
|
black_box(apply_explode_replacements(&mut scene, replacements));
|
|
times.push(t0.elapsed().as_micros() as f64);
|
|
assert!(
|
|
pieces_count > entity_count,
|
|
"fixture pieces ({pieces_count}) must far exceed input count ({entity_count}) — a broken explosion path returns empty lists"
|
|
);
|
|
if total_pieces == 0 {
|
|
total_pieces = pieces_count; // deterministic — capture once
|
|
}
|
|
}
|
|
|
|
let med = median(×);
|
|
let throughput = total_pieces as f64 / (med / 1_000_000.0);
|
|
runner.record(
|
|
name,
|
|
"EXPLODE apply: apply_explode_replacements (erase sources + add pieces)",
|
|
"us",
|
|
times,
|
|
Some((throughput, "pieces/s")),
|
|
None,
|
|
);
|
|
}
|
|
|
|
/// (lines, circles, polyline3d, polyline2d, texts, points, solids, rays, flat
|
|
/// lines) — full = 2,500 entities, 2,300 of which move.
|
|
fn flatten_counts(quick: bool) -> [usize; 9] {
|
|
if quick {
|
|
[120, 60, 80, 60, 60, 40, 40, 20, 20] // 500 total, 460 move
|
|
} else {
|
|
[600, 300, 400, 300, 300, 200, 200, 100, 100] // 2,500 total, 2,300 move
|
|
}
|
|
}
|
|
|
|
fn flatten_fixture_entities(quick: bool) -> usize {
|
|
flatten_counts(quick).iter().sum()
|
|
}
|
|
|
|
fn flatten_moved_expected(quick: bool) -> usize {
|
|
flatten_fixture_entities(quick) - if quick { 40 } else { 200 }
|
|
}
|
|
|
|
/// Mixed-Z fixture: everything non-flat except the trailing ray + flat-line
|
|
/// groups, which FLATTEN must leave untouched (None).
|
|
fn push_flatten_fixture(doc: &mut CadDocument, quick: bool) -> Vec<Handle> {
|
|
let [n_line, n_circle, n_pl3d, n_pl2d, n_text, n_point, n_solid, n_ray, n_flat] =
|
|
flatten_counts(quick);
|
|
let mut handles = Vec::with_capacity(flatten_fixture_entities(quick));
|
|
|
|
for i in 0..n_line {
|
|
let mut line = Line::new();
|
|
let x = i as f64;
|
|
line.start = Vector3::new(x, 0.0, 1.0 + (i % 5) as f64);
|
|
line.end = Vector3::new(x + 1.0, 1.0, 2.0);
|
|
line.thickness = 1.0;
|
|
handles.push(doc.add_entity(EntityType::Line(line)).unwrap());
|
|
}
|
|
for i in 0..n_circle {
|
|
let mut c =
|
|
Circle::from_center_radius(Vector3::new(i as f64, 0.0, 1.0 + (i % 3) as f64), 2.0);
|
|
if i % 2 == 1 {
|
|
c.normal = Vector3::new(0.0, 0.6, 0.8); // tilted → ellipse
|
|
}
|
|
handles.push(doc.add_entity(EntityType::Circle(c)).unwrap());
|
|
}
|
|
for i in 0..n_pl3d {
|
|
let mut pl = Polyline3D::new();
|
|
pl.elevation = (i % 4) as f64;
|
|
for k in 0..6 {
|
|
pl.vertices.push(Vertex3DPolyline::new(Vector3::new(
|
|
i as f64 + k as f64,
|
|
k as f64,
|
|
1.0 + (k % 3) as f64,
|
|
)));
|
|
}
|
|
handles.push(doc.add_entity(EntityType::Polyline3D(pl)).unwrap());
|
|
}
|
|
for i in 0..n_pl2d {
|
|
let mut pl = Polyline2D::new();
|
|
pl.elevation = 3.0 + (i % 5) as f64;
|
|
for k in 0..6 {
|
|
pl.vertices
|
|
.push(Vertex2D::new(Vector3::new(k as f64, i as f64, 0.0)));
|
|
}
|
|
handles.push(doc.add_entity(EntityType::Polyline2D(pl)).unwrap());
|
|
}
|
|
for i in 0..n_text {
|
|
let mut t = Text::new();
|
|
t.insertion_point = Vector3::new(i as f64, 0.0, 1.5);
|
|
handles.push(doc.add_entity(EntityType::Text(t)).unwrap());
|
|
}
|
|
for i in 0..n_point {
|
|
let mut p = Point::new();
|
|
p.location = Vector3::new(i as f64, 1.0, 0.75);
|
|
handles.push(doc.add_entity(EntityType::Point(p)).unwrap());
|
|
}
|
|
for i in 0..n_solid {
|
|
let x = i as f64;
|
|
let s = Solid::new(
|
|
Vector3::new(x, 0.0, 1.0),
|
|
Vector3::new(x + 1.0, 0.0, 2.0),
|
|
Vector3::new(x + 1.0, 1.0, 3.0),
|
|
Vector3::new(x, 1.0, 4.0),
|
|
);
|
|
handles.push(doc.add_entity(EntityType::Solid(s)).unwrap());
|
|
}
|
|
for i in 0..n_ray {
|
|
let r = Ray::new(
|
|
Vector3::new(i as f64, 0.0, 5.0),
|
|
Vector3::new(0.0, 0.0, 1.0),
|
|
);
|
|
handles.push(doc.add_entity(EntityType::Ray(r)).unwrap());
|
|
}
|
|
for i in 0..n_flat {
|
|
let mut line = Line::new();
|
|
let x = i as f64;
|
|
line.start = Vector3::new(x, 0.0, 0.0);
|
|
line.end = Vector3::new(x + 1.0, 1.0, 0.0);
|
|
handles.push(doc.add_entity(EntityType::Line(line)).unwrap());
|
|
}
|
|
handles
|
|
}
|
|
|
|
/// FLATTEN plan: collect candidates + compute replacements (fresh doc/sample).
|
|
fn bench_flatten_plan(runner: &mut BenchmarkRunner) {
|
|
let name = "flatten_plan";
|
|
if !runner.should_run(name) {
|
|
return;
|
|
}
|
|
let quick = runner.quick_mode;
|
|
let sample_count = if quick { 3 } else { 10 };
|
|
let entity_count = flatten_fixture_entities(quick);
|
|
let moved_expected = flatten_moved_expected(quick);
|
|
let mut times = Vec::with_capacity(sample_count);
|
|
|
|
for _ in 0..sample_count {
|
|
let mut scene = Scene::new();
|
|
push_flatten_fixture(&mut scene.document, quick);
|
|
|
|
let t0 = Instant::now();
|
|
let candidates = black_box(flatten::collect_flatten_handles(&scene));
|
|
let updates = black_box(flatten::plan_flatten(&scene, &candidates));
|
|
times.push(t0.elapsed().as_micros() as f64);
|
|
|
|
assert_eq!(candidates.len(), entity_count);
|
|
assert_eq!(updates.len(), moved_expected);
|
|
}
|
|
|
|
let med = median(×);
|
|
let throughput = entity_count as f64 / (med / 1_000_000.0);
|
|
runner.record(
|
|
name,
|
|
"FLATTEN plan: collect_flatten_handles + plan_flatten over the mixed-Z fixture",
|
|
"us",
|
|
times,
|
|
Some((throughput, "entities/s")),
|
|
None,
|
|
);
|
|
}
|
|
|
|
/// FLATTEN apply: fresh Scene per sample, plan computed untimed, the publish
|
|
/// loop timed.
|
|
fn bench_flatten_scene_apply(runner: &mut BenchmarkRunner) {
|
|
let name = "flatten_scene_apply";
|
|
if !runner.should_run(name) {
|
|
return;
|
|
}
|
|
let quick = runner.quick_mode;
|
|
let sample_count = if quick { 3 } else { 10 };
|
|
let moved_expected = flatten_moved_expected(quick);
|
|
let mut times = Vec::with_capacity(sample_count);
|
|
|
|
for _ in 0..sample_count {
|
|
let mut scene = Scene::new();
|
|
let handles = push_flatten_fixture(&mut scene.document, quick);
|
|
let updates = flatten::plan_flatten(&scene, &handles);
|
|
assert_eq!(updates.len(), moved_expected);
|
|
|
|
let t0 = Instant::now();
|
|
let moved = black_box(flatten::apply_flatten_updates(&mut scene, updates));
|
|
times.push(t0.elapsed().as_micros() as f64);
|
|
assert_eq!(moved, moved_expected);
|
|
}
|
|
|
|
let med = median(×);
|
|
let throughput = moved_expected as f64 / (med / 1_000_000.0);
|
|
runner.record(
|
|
name,
|
|
"FLATTEN apply: apply_flatten_updates (per-entity update_entity publish)",
|
|
"us",
|
|
times,
|
|
Some((throughput, "entities/s")),
|
|
None,
|
|
);
|
|
}
|
|
|
|
fn main() {
|
|
println!("\nInitializing OpenCADStudio Command Benchmarks...");
|
|
let mut runner = BenchmarkRunner::new(
|
|
format!("{:^120}", "OPENCADSTUDIO COMMAND BENCHMARK REPORT"),
|
|
"cad_command_metrics.json",
|
|
);
|
|
if runner.quick_mode {
|
|
println!("Mode: QUICK (halved fixture, 3 samples per metric)");
|
|
} else {
|
|
println!("Mode: FULL (2,500 compounds, 10 samples per metric)");
|
|
}
|
|
|
|
bench_explode_geometry(&mut runner);
|
|
bench_explode_scene_apply(&mut runner);
|
|
bench_flatten_plan(&mut runner);
|
|
bench_flatten_scene_apply(&mut runner);
|
|
|
|
runner.finish();
|
|
}
|