diff --git a/.config/nextest.toml b/.config/nextest.toml index 83085586..559f39c6 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -15,6 +15,10 @@ fail-fast = false # Retry flaky tests (useful for proptests) retries = 0 +# Default-suite tests should stay under the 10-second per-test budget. Longer +# deterministic correctness tests belong behind the slow-tests feature. +slow-timeout = { period = "10s", terminate-after = 1 } + [profile.ci] # CI profile: optimized for GitHub Actions # Inherits from default profile @@ -29,8 +33,8 @@ fail-fast = false # Retry flaky tests once in CI (helps with proptests and timing-sensitive tests) retries = 1 -# Show slow tests (> 60s) in CI -slow-timeout = { period = "60s", terminate-after = 2 } +# Enforce the default-suite 10-second per-test budget in CI. +slow-timeout = { period = "10s", terminate-after = 1 } # JUnit report configuration [profile.ci.junit] diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index e5e94e80..0c1c7419 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -84,8 +84,6 @@ jobs: echo "::group::Running tests with nextest" # Generate JUnit XML for Codecov Test Analytics # JUnit path is configured in .config/nextest.toml [profile.ci.junit] - # Release mode is required: exact-predicate arithmetic makes 3D+ proptests - # exceed timeout limits in debug mode (>60s debug vs <1s release). # Avoid --all-features: it enables slow-tests and count-allocations which # are special-purpose features not suitable for general test runs. cargo nextest run --release --profile ci diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f84b226..3baec752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Flatten triangulation modules into focused APIs [#399](https://github.com/acgetchell/delaunay/pull/399) - Reconcile topology validation policy [#385](https://github.com/acgetchell/delaunay/pull/385) [#404](https://github.com/acgetchell/delaunay/pull/404) - Box Delaunay repair flip errors [#407](https://github.com/acgetchell/delaunay/pull/407) +- Run slow correctness cases through slow-tests [#412](https://github.com/acgetchell/delaunay/pull/412) ### Merged Pull Requests +- Run slow correctness cases through slow-tests [#412](https://github.com/acgetchell/delaunay/pull/412) +- Isolate strict insphere consistency control [#383](https://github.com/acgetchell/delaunay/pull/383) [#411](https://github.com/acgetchell/delaunay/pull/411) - Use typed errors in public examples [#365](https://github.com/acgetchell/delaunay/pull/365) [#410](https://github.com/acgetchell/delaunay/pull/410) - Prefer builder-based fallible examples [#214](https://github.com/acgetchell/delaunay/pull/214) [#409](https://github.com/acgetchell/delaunay/pull/409) - Box Delaunay repair flip errors [#407](https://github.com/acgetchell/delaunay/pull/407) @@ -37,7 +40,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support periodic flip parity for external cells [#391](https://github.com/acgetchell/delaunay/pull/391) - Refactor/387 tds mutation boundaries [#390](https://github.com/acgetchell/delaunay/pull/390) - Refresh release docs and benchmark guidance [#389](https://github.com/acgetchell/delaunay/pull/389) -- Isolate strict insphere consistency control [#383](https://github.com/acgetchell/delaunay/pull/383) ### Added @@ -119,8 +121,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update repair and construction mappings for the named boxed variant. - Cover clone and source behavior for wrapped linear-algebra errors. -- Isolate strict insphere consistency control [#383](https://github.com/acgetchell/delaunay/pull/383) - [`54a08b7`](https://github.com/acgetchell/delaunay/commit/54a08b7a9e773cfee4747ee8efc5ac59e73e064e) +- Isolate strict insphere consistency control [#383](https://github.com/acgetchell/delaunay/pull/383) [#411](https://github.com/acgetchell/delaunay/pull/411) + [`222e572`](https://github.com/acgetchell/delaunay/commit/222e572b4364007cdbf6d1fa6428ea7e670cfb56) - Document the strict insphere consistency environment knob as a process-wide once-per-process snapshot. @@ -129,6 +131,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 exercised without mutating global environment state. - Mark the production review checklist item complete. +- [**breaking**] Run slow correctness cases through slow-tests [#412](https://github.com/acgetchell/delaunay/pull/412) + [`1498153`](https://github.com/acgetchell/delaunay/commit/149815313530335a98f6951c63924e4774d509c1) + + - Define the slow-test bucket around deterministic correctness tests that exceed the default-suite budget. + - Move runnable high-dimensional properties out of ignored tests and into either the default suite or the slow-tests feature. + - Give just test-slow a release-mode nextest profile with a longer watchdog for intentional multi-minute regressions. + - Add a repository Semgrep guard against reintroducing slow ignored tests. + +#### Changed: Enforce explicit test buckets + +- Sort correctness tests into default and slow-tests buckets instead of relying on ignored tests. +- Move benchmark-style boundary and UUID iterator measurements into a Criterion benchmark target. +- Replace ignored flaky or known-failure cases with active assertions or slow-tests gating. +- Add a Semgrep guard against reintroducing ignored tests and align docs and helper recipes with the new taxonomy. +- Enforce 10-second default test budget [`772b2d0`](https://github.com/acgetchell/delaunay/commit/772b2d0298bf7cee4373602e05992f54ad1b010f) + + - Gate default-suite cases at or above the 10-second budget behind + slow-tests and remove obsolete high-dimensional periodic validation from + routine runs. + + - Move allocation hot-path contracts into a Criterion benchmark over + calibrated 2D-5D fixtures, leaving allocation_api as wiring smoke coverage. + + - Document the toroidal validation limits and add the bench-allocations + workflow. ### Documentation diff --git a/Cargo.toml b/Cargo.toml index 6c1860f5..9a8125c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,43 +84,48 @@ codegen-units = 1 debug = "line-tables-only" [[bench]] -name = "circumsphere_containment" -path = "benches/circumsphere_containment.rs" +name = "allocation_hot_paths" +path = "benches/allocation_hot_paths.rs" harness = false [[bench]] -name = "topology_guarantee_construction" -path = "benches/topology_guarantee_construction.rs" +name = "boundary_uuid_iter" +path = "benches/boundary_uuid_iter.rs" harness = false [[bench]] -name = "tds_clone" -path = "benches/tds_clone.rs" +name = "ci_performance_suite" +path = "benches/ci_performance_suite.rs" harness = false [[bench]] -name = "boundary_uuid_iter" -path = "benches/boundary_uuid_iter.rs" +name = "circumsphere_containment" +path = "benches/circumsphere_containment.rs" harness = false [[bench]] -name = "remove_vertex" -path = "benches/remove_vertex.rs" +name = "cold_path_predicates" +path = "benches/cold_path_predicates.rs" harness = false [[bench]] -name = "ci_performance_suite" -path = "benches/ci_performance_suite.rs" +name = "profiling_suite" +path = "benches/profiling_suite.rs" harness = false [[bench]] -name = "profiling_suite" -path = "benches/profiling_suite.rs" +name = "remove_vertex" +path = "benches/remove_vertex.rs" harness = false [[bench]] -name = "cold_path_predicates" -path = "benches/cold_path_predicates.rs" +name = "tds_clone" +path = "benches/tds_clone.rs" +harness = false + +[[bench]] +name = "topology_guarantee_construction" +path = "benches/topology_guarantee_construction.rs" harness = false [lints.rust] diff --git a/benches/README.md b/benches/README.md index 99f87338..abdf3d07 100644 --- a/benches/README.md +++ b/benches/README.md @@ -8,9 +8,10 @@ predicates fast across 2D-5D. | Benchmark | Purpose | Scale | Typical Runtime | Used By | |-----------|---------|-------|-----------------|---------| +| `allocation_hot_paths.rs` | Allocation contracts for public hot paths | Calibrated 2D-5D canary fixtures | ~1-2 min | Manual allocation checks | +| `boundary_uuid_iter.rs` | Focused boundary-facet and UUID iterator microbenchmarks | 3D small fixed schedules | <1 min | Manual micro-optimization checks | | `ci_performance_suite.rs` | Public workflow regression contract | Calibrated 2D-5D canaries | ~5-10 min | CI, baselines, `just perf-no-regressions` | | `circumsphere_containment.rs` | Compare circumsphere predicate methods | 2D-5D fixed, 3D random, edge cases | ~5 min | Predicate tuning, summaries | -| `boundary_uuid_iter.rs` | Focused boundary-facet and UUID iterator microbenchmarks | 3D small fixed schedules | <1 min | Manual micro-optimization checks | | `cold_path_predicates.rs` | Track hot/cold predicate paths | 2D-5D hot queries, near-boundary cases | ~2-5 min | Predicate optimization work | | `profiling_suite.rs` | Large-scale construction, memory, query, validation profiling | 2D/3D 10k, 4D 3k, 5D 1k | ~2-3 hr | Manual/monthly | | `remove_vertex.rs` | Vertex removal and rollback cost | 2D-5D fixed cases | ~1-5 min | Vertex removal | @@ -25,17 +26,19 @@ predicates fast across 2D-5D. | Quick local large-scale wall-clock guard | `just perf-large-scale-smoke` | | Fast local PR performance guard with temporary same-machine baseline | `just perf-no-regressions` | | Full CI benchmark suite only | `just bench-ci` | +| Allocation-contract microbenchmarks | `just bench-allocations` | | Persist/update the default local baseline artifact | `just perf-baseline` | | Generate a scratch baseline without replacing the default | `just perf-baseline-to [ref]` | | Persist/update the default local baseline from a release/ref | `just perf-baseline v0.7.5` | | Compare against an existing baseline | `just perf-compare ` | | Release performance summary | `just bench-perf-summary` | | Smoke-test benchmark harnesses | `just bench-smoke` | -| Vertex removal mutation baseline | `cargo bench --profile perf --bench remove_vertex -- --noplot` | -| Predicate comparison | `cargo bench --profile perf --bench circumsphere_containment -- --noplot` | +| Allocation hot-path contracts | `cargo bench --profile perf --bench allocation_hot_paths --features count-allocations -- --noplot` | | Boundary/UUID microbenchmarks | `cargo bench --profile perf --bench boundary_uuid_iter -- --noplot` | +| Predicate comparison | `cargo bench --profile perf --bench circumsphere_containment -- --noplot` | | Predicate cold-path work | `cargo bench --profile perf --bench cold_path_predicates -- --noplot` | | Large-scale scaling suite | `cargo bench --profile perf --bench profiling_suite -- --noplot` | +| Vertex removal mutation baseline | `cargo bench --profile perf --bench remove_vertex -- --noplot` | | One-dimension acceptance/profiling run | `just debug-large-scale-{2,3,4,5}d [n] [repair_every]` | | Deep profiling | `cargo bench --profile perf --bench profiling_suite --features count-allocations` | diff --git a/benches/allocation_hot_paths.rs b/benches/allocation_hot_paths.rs new file mode 100644 index 00000000..e7c4a637 --- /dev/null +++ b/benches/allocation_hot_paths.rs @@ -0,0 +1,472 @@ +#![forbid(unsafe_code)] + +//! Allocation-contract microbenchmarks for public hot paths. +//! +//! Run with: +//! +//! ```bash +//! cargo bench --profile perf --bench allocation_hot_paths --features count-allocations -- --noplot +//! ``` +//! +//! Without `count-allocations`, this target compiles and reports a no-op +//! placeholder so workspace benchmark compile checks remain feature-neutral. + +use criterion::{criterion_group, criterion_main}; + +#[cfg(feature = "count-allocations")] +#[path = "common/bench_utils.rs"] +mod bench_utils; + +#[cfg(feature = "count-allocations")] +mod allocation_contracts { + use allocation_counter::AllocationInfo; + use criterion::{BenchmarkGroup, BenchmarkId, Criterion, measurement::WallTime}; + use delaunay::prelude::algorithms::{LocateResult, locate_with_stats}; + use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulation, RetryPolicy, Vertex, vertex, + }; + use delaunay::prelude::generators::generate_random_points_seeded; + use delaunay::prelude::geometry::{ + AdaptiveKernel, Coordinate, FastKernel, Point, simplex_volume, + }; + use delaunay::prelude::query::measure_with_result; + use delaunay::prelude::tds::{SimplexKey, TdsError, VertexKey, facet_key_from_vertices}; + use std::{hint::black_box, num::NonZeroUsize, time::Duration}; + use thiserror::Error; + + use super::bench_utils::{bench_option, bench_result}; + + const CANARY_COUNT_2D: usize = 4_000; + const CANARY_COUNT_3D: usize = 750; + const CANARY_COUNT_4D: usize = 75; + const CANARY_COUNT_5D: usize = 25; + const CANARY_SEED_2D: u64 = 4_042; + const CANARY_SEED_3D: u64 = 873; + const CANARY_SEED_4D: u64 = 531; + const CANARY_SEED_5D: u64 = 816; + const SAMPLE_SIZE: usize = 32; + + type BenchTriangulation = DelaunayTriangulation, (), (), D>; + + #[derive(Debug, Error)] + enum AllocationBenchError { + #[error("{dimension}D fixture did not contain a simplex")] + MissingSimplex { dimension: usize }, + + #[error("{dimension}D fixture simplex has {actual} vertices, expected at least {required}")] + SimplexTooSmall { + dimension: usize, + required: usize, + actual: usize, + }, + + #[error("{dimension}D fixture vertex {vertex_key:?} was missing")] + MissingVertex { + dimension: usize, + vertex_key: VertexKey, + }, + + #[error("TDS lookup failed: {source}")] + Tds { + #[from] + source: TdsError, + }, + } + + struct DimensionFixture { + dt: BenchTriangulation, + simplex_key: SimplexKey, + facet_vertices: [VertexKey; D], + query: Point, + simplex_count: usize, + vertex_count: usize, + } + + fn retry_attempts(value: usize) -> NonZeroUsize { + let Some(attempts) = NonZeroUsize::new(value) else { + unreachable!("hard-coded retry attempt count must be non-zero"); + }; + attempts + } + + fn canary_vertices(count: usize, seed: u64) -> Vec> { + let points = bench_result( + generate_random_points_seeded::(count, (-100.0, 100.0), seed), + format!("failed to generate {D}D allocation benchmark points"), + ); + points.into_iter().map(|point| vertex!(point)).collect() + } + + fn first_simplex_key( + dt: &BenchTriangulation, + ) -> Result { + dt.tds() + .simplex_keys() + .next() + .ok_or(AllocationBenchError::MissingSimplex { dimension: D }) + } + + fn simplex_points( + dt: &BenchTriangulation, + simplex_key: SimplexKey, + ) -> Result>, AllocationBenchError> { + let tds = dt.tds(); + + tds.simplex_vertices(simplex_key)? + .iter() + .copied() + .map(|vertex_key| { + tds.vertex(vertex_key).map(|vertex| *vertex.point()).ok_or( + AllocationBenchError::MissingVertex { + dimension: D, + vertex_key, + }, + ) + }) + .collect() + } + + fn representative_simplex_key( + dt: &BenchTriangulation, + ) -> Result { + let mut best: Option<(SimplexKey, f64)> = None; + + for simplex_key in dt.tds().simplex_keys() { + let points = simplex_points(dt, simplex_key)?; + let Ok(volume) = simplex_volume(&points) else { + continue; + }; + let volume = volume.abs(); + if !volume.is_finite() || volume <= 0.0 { + continue; + } + + match best { + Some((_, best_volume)) if best_volume >= volume => {} + _ => best = Some((simplex_key, volume)), + } + } + + best.map_or_else(|| first_simplex_key(dt), |(simplex_key, _)| Ok(simplex_key)) + } + + fn first_facet_vertices( + dt: &BenchTriangulation, + simplex_key: SimplexKey, + ) -> Result<[VertexKey; D], AllocationBenchError> { + let vertices = dt.tds().simplex_vertices(simplex_key)?; + if vertices.len() < D { + return Err(AllocationBenchError::SimplexTooSmall { + dimension: D, + required: D, + actual: vertices.len(), + }); + } + + let mut facet_vertices = [vertices[0]; D]; + facet_vertices.copy_from_slice(&vertices[..D]); + Ok(facet_vertices) + } + + fn simplex_barycenter( + dt: &BenchTriangulation, + simplex_key: SimplexKey, + ) -> Result, AllocationBenchError> { + let points = simplex_points(dt, simplex_key)?; + let mut coords = [0.0_f64; D]; + for point in &points { + for (coord, vertex_coord) in coords.iter_mut().zip(point.coords()) { + *coord += *vertex_coord; + } + } + + let vertex_count = bench_result( + u32::try_from(points.len()), + "simplex vertex count should fit in u32", + ); + let inv_vertex_count = 1.0 / f64::from(vertex_count); + for coord in &mut coords { + *coord *= inv_vertex_count; + } + + Ok(Point::new(coords)) + } + + fn prepare_fixture(count: usize, seed: u64) -> DimensionFixture { + let vertices = canary_vertices::(count, seed); + let attempts = retry_attempts(6); + let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts, + base_seed: Some(seed), + }); + let dt = bench_result( + BenchTriangulation::::new_with_options(&vertices, options), + format!("failed to build {D}D allocation benchmark triangulation"), + ); + let simplex_key = + bench_result(representative_simplex_key(&dt), "missing benchmark simplex"); + let facet_vertices = bench_result( + first_facet_vertices(&dt, simplex_key), + "failed to prepare benchmark facet vertices", + ); + let query = bench_result( + simplex_barycenter(&dt, simplex_key), + "failed to prepare benchmark locate query", + ); + let simplex_count = dt.tds().simplices().count(); + let vertex_count = dt.tds().vertices().count(); + + DimensionFixture { + dt, + simplex_key, + facet_vertices, + query, + simplex_count, + vertex_count, + } + } + + fn assert_zero_allocations(info: &AllocationInfo, operation: &str) { + assert_eq!( + info.count_total, 0, + "{operation} should not allocate; allocation info: {info:?}" + ); + assert_eq!( + info.bytes_total, 0, + "{operation} should allocate zero bytes; allocation info: {info:?}" + ); + assert_eq!( + info.count_current, 0, + "{operation} should not retain allocations; allocation info: {info:?}" + ); + assert_eq!( + info.bytes_current, 0, + "{operation} should retain zero bytes; allocation info: {info:?}" + ); + } + + fn assert_allocation_budget(info: &AllocationInfo, operation: &str, max_allocations: u64) { + assert!( + info.count_total <= max_allocations, + "{operation} exceeded allocation budget {max_allocations}; allocation info: {info:?}" + ); + assert_eq!( + info.count_current, 0, + "{operation} should not retain allocations; allocation info: {info:?}" + ); + assert_eq!( + info.bytes_current, 0, + "{operation} should retain zero bytes; allocation info: {info:?}" + ); + } + + const fn locate_fast_path_allocation_budget() -> u64 { + match D { + 2 | 3 => 1, + 4 => 2_000, + 5 => 4_000, + _ => 10_000, + } + } + + fn bench_public_iterators( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &DimensionFixture, + ) { + let tds = fixture.dt.tds(); + let tri = fixture.dt.as_triangulation(); + let simplex_count = fixture.simplex_count; + let vertex_count = fixture.vertex_count; + + group.bench_function( + BenchmarkId::new(format!("zero_alloc/public_iterators_{D}d"), vertex_count), + |b| { + b.iter(|| { + let (counts, info) = measure_with_result(|| { + black_box(( + tds.simplices().count(), + tds.vertices().count(), + tds.simplex_keys().count(), + tds.vertex_keys().count(), + tri.simplices().count(), + tri.vertices().count(), + fixture.dt.simplices().count(), + fixture.dt.vertices().count(), + )) + }); + + assert_eq!( + counts, + ( + simplex_count, + vertex_count, + simplex_count, + vertex_count, + simplex_count, + vertex_count, + simplex_count, + vertex_count, + ) + ); + assert_zero_allocations( + &info, + "TDS and public simplices()/vertices() iterators", + ); + }); + }, + ); + } + + fn bench_tds_simplex_vertices( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &DimensionFixture, + ) { + let tds = fixture.dt.tds(); + let simplex_key = fixture.simplex_key; + + group.bench_function( + BenchmarkId::new( + format!("zero_alloc/tds_simplex_vertices_{D}d"), + fixture.vertex_count, + ), + |b| { + b.iter(|| { + let (vertex_count, info) = measure_with_result(|| { + tds.simplex_vertices(simplex_key).map(|keys| keys.len()) + }); + assert_eq!( + bench_result(vertex_count, "Tds::simplex_vertices should succeed"), + D + 1 + ); + assert_zero_allocations(&info, "Tds::simplex_vertices"); + }); + }, + ); + } + + fn bench_simplex_vertex_uuid_iter( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &DimensionFixture, + ) { + let tds = fixture.dt.tds(); + let simplex = bench_option( + tds.simplex(fixture.simplex_key), + format!("{D}D benchmark simplex should exist"), + ); + + group.bench_function( + BenchmarkId::new( + format!("zero_alloc/simplex_vertex_uuid_iter_{D}d"), + fixture.vertex_count, + ), + |b| { + b.iter(|| { + let (uuid_count, info) = measure_with_result(|| { + simplex + .vertex_uuid_iter(tds) + .try_fold(0usize, |count, uuid| uuid.map(|_| count + 1)) + }); + assert_eq!( + bench_result(uuid_count, "Simplex::vertex_uuid_iter should succeed"), + D + 1 + ); + assert_zero_allocations(&info, "Simplex::vertex_uuid_iter"); + }); + }, + ); + } + + fn bench_facet_key_from_vertices( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &DimensionFixture, + ) { + group.bench_function( + BenchmarkId::new( + format!("zero_alloc/facet_key_from_vertices_{D}d"), + fixture.vertex_count, + ), + |b| { + b.iter(|| { + let (facet_key, info) = measure_with_result(|| { + black_box(facet_key_from_vertices(&fixture.facet_vertices)) + }); + assert_ne!(facet_key, 0); + assert_zero_allocations(&info, "facet_key_from_vertices"); + }); + }, + ); + } + + fn bench_locate_with_hint_fast_path( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &DimensionFixture, + ) { + let kernel = FastKernel::::new(); + let simplex_key = fixture.simplex_key; + + group.bench_function( + BenchmarkId::new( + format!("bounded_alloc/locate_with_hint_fast_kernel_{D}d"), + fixture.vertex_count, + ), + |b| { + b.iter(|| { + let (locate_result, info) = measure_with_result(|| { + locate_with_stats(fixture.dt.tds(), &kernel, &fixture.query, Some(simplex_key)) + }); + let (location, stats) = + bench_result(locate_result, "hinted locate_with_stats should succeed"); + + assert!(matches!(location, LocateResult::InsideSimplex(found) if found == simplex_key)); + assert!(stats.used_hint); + assert!(!stats.fell_back_to_scan()); + assert_allocation_budget( + &info, + "hinted locate_with_stats fast path", + locate_fast_path_allocation_budget::(), + ); + }); + }, + ); + } + + fn bench_dimension( + group: &mut BenchmarkGroup<'_, WallTime>, + count: usize, + seed: u64, + ) { + let fixture = prepare_fixture::(count, seed); + + bench_public_iterators(group, &fixture); + bench_tds_simplex_vertices(group, &fixture); + bench_simplex_vertex_uuid_iter(group, &fixture); + bench_facet_key_from_vertices(group, &fixture); + bench_locate_with_hint_fast_path(group, &fixture); + } + + pub fn bench_allocation_hot_paths(c: &mut Criterion) { + let mut group = c.benchmark_group("allocation_hot_paths"); + group.sample_size(SAMPLE_SIZE); + group.warm_up_time(Duration::from_secs(1)); + group.measurement_time(Duration::from_secs(2)); + + bench_dimension::<2>(&mut group, CANARY_COUNT_2D, CANARY_SEED_2D); + bench_dimension::<3>(&mut group, CANARY_COUNT_3D, CANARY_SEED_3D); + bench_dimension::<4>(&mut group, CANARY_COUNT_4D, CANARY_SEED_4D); + bench_dimension::<5>(&mut group, CANARY_COUNT_5D, CANARY_SEED_5D); + group.finish(); + } +} + +#[cfg(feature = "count-allocations")] +use allocation_contracts::bench_allocation_hot_paths; + +#[cfg(not(feature = "count-allocations"))] +fn bench_allocation_hot_paths(c: &mut criterion::Criterion) { + let mut group = c.benchmark_group("allocation_hot_paths"); + group.bench_function("count_allocations_feature_disabled", |b| b.iter(|| ())); + group.finish(); +} + +criterion_group!(benches, bench_allocation_hot_paths); +criterion_main!(benches); diff --git a/docs/code_organization.md b/docs/code_organization.md index 85f3cd4f..56cc8c80 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -349,10 +349,13 @@ cargo test --test circumsphere_debug_tools --features diagnostics -- --nocapture **Note**: Memory allocation profiling is available through the `count-allocations` feature: ```bash -# Run allocation profiling tests (just command) +# Verify allocation measurement wiring (just command) just test-allocation -# Run benchmarks with allocation counting (direct cargo for specific bench) +# Run hot-path allocation contracts (just command) +just bench-allocations + +# Run broader profiling benchmarks with allocation counting (direct cargo) cargo bench --profile perf --bench profiling_suite --features count-allocations ``` @@ -515,9 +518,10 @@ than through a `delaunay::delaunay` or `delaunay::triangulation` facade. 3D/4D construction plus hull queries, topology editing, diagnostics, conversion ergonomics, numerical robustness, and Delaunay repair - **`benches/`** - Performance benchmarks with automated baseline management (2D-5D coverage) and memory allocation tracking - (see: [benches/profiling_suite.rs](../benches/README.md#profiling-suite)) + (see: [benches/profiling_suite.rs](../benches/README.md#profiling-suite) and + [benches/allocation_hot_paths.rs](../benches/README.md)) - **`tests/`** - Integration tests including basic TDS validation (creation, neighbor assignment, boundary analysis), - debugging utilities, regression testing, allocation profiling tools + debugging utilities, regression testing, allocation-measurement smoke coverage (see: [tests/allocation_api.rs](../tests/README.md#allocation_apirs)), and robust predicates validation - **`docs/`** - User and contributor documentation, including architecture/reference guides, `docs/dev/` workflow rules for agents, archived design notes, and templates @@ -566,7 +570,9 @@ The project includes optional memory profiling capabilities: - **Memory Benchmarks**: Dedicated benchmarks for RSS and allocation scaling analysis (`profiling_suite.rs`) - comprehensive profiling suite with calibrated large-scale 2D-5D point counts. **Recommended for manual profiling runs** rather than CI due to long execution time. Use `PROFILING_DEV_MODE=1` for faster auxiliary diagnostics. -- **Integration Testing**: `allocation_api.rs` provides utilities for testing memory usage in various scenarios +- **Allocation Contracts**: `allocation_hot_paths.rs` keeps zero-allocation and bounded-allocation hot-path checks over + calibrated 2D-5D triangulations in Criterion benchmarks, while `allocation_api.rs` only smoke-tests that allocation + measurement is wired correctly. - **CI Integration**: Automated profiling benchmarks with detailed allocation reports #### Performance-oriented infrastructure @@ -611,7 +617,8 @@ just test-release # All tests in release mode just test-slow # Run correctness tests over the 10s default-suite budget just test-slow-release # Compatibility alias for just test-slow just test-diagnostics # Run diagnostics tools with output -just test-allocation # Run allocation profiling tests +just test-allocation # Verify allocation measurement wiring +just bench-allocations # Run allocation-contract microbenchmarks ``` **Quality and Linting:** diff --git a/docs/limitations.md b/docs/limitations.md index bb8da521..19c71a82 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -68,12 +68,18 @@ Toroidal support has two modes: - `.toroidal([..])` canonicalizes vertices into the fundamental domain, then builds the Euclidean Delaunay triangulation of the wrapped point set. It does - not identify opposite boundary facets. + not identify opposite boundary facets. The 2D and 3D canonical toroidal + construction paths are part of routine release coverage. - `.toroidal_periodic([..])` uses the 3^D image-point method to construct a true periodic quotient with rewired neighbor pointers. This path is strongest - in 2D. Compact 3D smoke coverage is active, while full 3D validation and - 4D/5D periodic validation are ignored slow tests because image expansion grows - quickly with dimension. + in 2D, where periodic triangulation and validation remain in routine release + coverage. Compact 3D periodic construction and lifted-predicate smoke coverage + are active. Full 3D periodic PL-manifold validation is tested as a known + limitation: construction succeeds, but the quotient can still report an + unclosed boundary ridge instead of a closed 3-torus. 4D/5D periodic validation + is outside routine test coverage because image expansion grows quickly with + dimension and high-dimensional quotient selection is not yet robust enough for + release validation. Spherical and hyperbolic topologies are public metadata and behavior-model scaffolds today. They do not yet provide full construction or validation diff --git a/docs/production_review_remediation_checklist.md b/docs/production_review_remediation_checklist.md index 705c92e6..d79819f6 100644 --- a/docs/production_review_remediation_checklist.md +++ b/docs/production_review_remediation_checklist.md @@ -126,11 +126,12 @@ Treat partial items as still open until their acceptance notes are satisfied. - [x] **30. Add rollback identity regression for facet caches.** Convex hull facet-cache tests now prove a failed insertion rollback preserves the cached `(identity, generation)` provenance key. -- [x] **31. Add allocation-bounded hot-path tests.** - `tests/allocation_api.rs` now asserts zero allocations for TDS/public +- [x] **31. Add allocation-bounded hot-path benchmarks.** + `benches/allocation_hot_paths.rs` now asserts zero allocations over calibrated 2D-5D triangulations for TDS/public `simplices()`/`vertices()` iterator paths, `Tds::simplex_vertices`, and `facet_key_from_vertices`, plus an explicit allocation budget for the hinted - locate fast path under `--features count-allocations`. + locate fast path under `--features count-allocations`. `tests/allocation_api.rs` + remains a narrow wiring smoke test for the allocation measurement API. - [x] **32. Benchmark `Tds::clone` cost versus triangulation size.** `benches/tds_clone.rs` now measures full `Tds::clone()` snapshot cost across deterministic 2D-5D triangulations before any rollback redesign. diff --git a/justfile b/justfile index 24963851..1061dc79 100644 --- a/justfile +++ b/justfile @@ -129,6 +129,10 @@ bench: bench-ci: cargo bench --profile perf --bench ci_performance_suite +# Allocation-contract microbenchmarks for public hot paths. +bench-allocations: + cargo bench --profile perf --bench allocation_hot_paths --features count-allocations -- --noplot + # Compile benchmarks without running them. Manifest lints enforce the warning # policy without using RUSTFLAGS that fragment Cargo artifact caches. bench-compile: @@ -236,16 +240,16 @@ coverage-ci: _ensure-cargo-llvm-cov cargo llvm-cov {{ _coverage_base_args }} --cobertura --output-path coverage/cobertura.xml -- --skip prop_ debug-large-scale-2d n="36000" repair_every="1": _ensure-nextest - DELAUNAY_BULK_PROGRESS_EVERY=2000 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_2D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile ci --features slow-tests --test large_scale_debug debug_large_scale_2d -- --exact --nocapture + DELAUNAY_BULK_PROGRESS_EVERY=2000 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_2D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile slow --features slow-tests --test large_scale_debug debug_large_scale_2d -- --exact --nocapture debug-large-scale-3d n="7500" repair_every="1": _ensure-nextest - DELAUNAY_BULK_PROGRESS_EVERY=500 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_3D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile ci --features slow-tests --test large_scale_debug debug_large_scale_3d -- --exact --nocapture + DELAUNAY_BULK_PROGRESS_EVERY=500 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_3D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile slow --features slow-tests --test large_scale_debug debug_large_scale_3d -- --exact --nocapture debug-large-scale-4d n="900" repair_every="1": _ensure-nextest - DELAUNAY_BULK_PROGRESS_EVERY=100 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_4D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile ci --features slow-tests --test large_scale_debug debug_large_scale_4d -- --exact --nocapture + DELAUNAY_BULK_PROGRESS_EVERY=100 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_4D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile slow --features slow-tests --test large_scale_debug debug_large_scale_4d -- --exact --nocapture debug-large-scale-5d n="140" repair_every="1": _ensure-nextest - DELAUNAY_BULK_PROGRESS_EVERY=20 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_5D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile ci --features slow-tests --test large_scale_debug debug_large_scale_5d -- --exact --nocapture + DELAUNAY_BULK_PROGRESS_EVERY=20 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_5D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo nextest run --release --profile slow --features slow-tests --test large_scale_debug debug_large_scale_5d -- --exact --nocapture # Default recipe shows available commands default: @@ -421,6 +425,7 @@ perf-help: @echo " just perf-compare # Compare against a specific dev-mode baseline" @echo " just bench # Full benchmark suite with perf profile" @echo " just bench-ci # CI benchmark suite with perf profile" + @echo " just bench-allocations # Allocation-contract microbenchmarks" @echo " just perf-no-regressions # Fast pre-PR 2D-5D regression guard" @echo " just bench-smoke # Smoke-test benchmark harnesses" @echo "" @@ -471,7 +476,7 @@ perf-large-scale-smoke max_secs="60": _ensure-nextest DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS="$max_secs" \ "$n_env=$n_points" \ DELAUNAY_LARGE_DEBUG_REPAIR_EVERY=1 \ - cargo nextest run --release --profile ci --features slow-tests --test large_scale_debug "$test_name" -- --exact --nocapture; then + cargo nextest run --release --profile slow --features slow-tests --test large_scale_debug "$test_name" -- --exact --nocapture; then echo "āœ… ${dimension} completed within the ${max_secs}s test-runtime cap" else local code=$? @@ -943,9 +948,7 @@ test-allocation: _ensure-nextest test-diagnostics: _ensure-nextest cargo nextest run --profile ci --test circumsphere_debug_tools --features diagnostics -- --nocapture -# test-integration: runs all integration tests (includes proptests) in release mode. -# Release mode is required because exact-predicate arithmetic in debug mode makes -# 3D+ proptests exceed CI timeout limits (>60s debug vs <1s release). +# test-integration: runs all default integration tests under the 10s per-test budget. test-integration: _ensure-nextest cargo nextest run --release --profile ci --tests diff --git a/tests/README.md b/tests/README.md index de1ae9e5..4bb00f7e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -92,7 +92,7 @@ Property-based tests for Tds (Triangulation Data Structure) combinatorial/topolo - **Vertex Count Consistency**: Vertex key count matches reported vertex count - **Dimension Consistency**: Reported dimension matches actual structure -**Dimensions Tested:** 2D-5D +**Dimensions Tested:** 2D-5D; 4D/5D full TDS property variants over the 10-second budget run through `just test-slow`. **Run with:** `cargo test --test proptest_tds` or included in `just test` @@ -106,7 +106,7 @@ Property-based tests focused on coherent orientation invariants in the TDS layer - **Tamper detection**: simplex-order tampering is detected as `OrientationViolation` - **Incremental coherence**: orientation remains coherent after each successful insertion -**Dimensions Tested:** 2D-5D (5D slow variants run through `just test-slow`) +**Dimensions Tested:** 2D-5D; 4D/5D full orientation property variants over the 10-second budget run through `just test-slow`. **Run with:** `cargo test --test proptest_orientation` or included in `just test` @@ -133,7 +133,7 @@ Property-based tests for Triangulation layer invariants (generic geometric layer **Note:** Tests use `DelaunayTriangulation` for construction (most convenient way to obtain valid triangulations). The properties tested are generic Triangulation-layer concerns applicable to any triangulation with a kernel. -**Dimensions Tested:** 2D-5D +**Dimensions Tested:** 2D-5D; 4D/5D quality and facet-topology variants over the 10-second budget run through `just test-slow`. **Run with:** `cargo test --test proptest_triangulation` or included in `just test` @@ -162,7 +162,7 @@ Property-based tests for `DelaunayTriangulation` invariants (all Delaunay-specif - Inverse edge/triangle queues for 4D/5D repair - See `src/core/algorithms/flips.rs` for implementation -**Slow variants:** 5D duplicate-coordinate and insertion-order robustness properties are gated by `slow-tests`. +**Slow variants:** 4D/5D empty-circumsphere, duplicate-coordinate, duplicate-cloud, and insertion-order robustness properties are gated by `slow-tests`. **Dimensions Tested:** 2D-5D; variants over the 10-second budget run through `just test-slow`. @@ -179,6 +179,8 @@ Property-based tests for Simplex data structure verifying simplex-level invarian - **Facet Completeness**: All facets properly defined and accessible - **Vertex References**: All vertex keys are valid and consistent +**Dimensions Tested:** 2D-5D; 4D/5D full simplex property variants over the 10-second budget run through `just test-slow`. + **Run with:** `cargo test --release --test proptest_simplex` #### [`proptest_convex_hull.rs`](./proptest_convex_hull.rs) @@ -192,6 +194,8 @@ Property-based tests for convex hull computation verifying hull properties and i - **Boundary Subset Property**: Hull is a subset of triangulation boundary - **Dimension Consistency**: Hull dimension matches point set dimension +**Dimensions Tested:** 2D-5D; convex-hull property variants over the 10-second budget run through `just test-slow`. + **Run with:** `cargo test --release --test proptest_convex_hull` #### [`proptest_facet.rs`](./proptest_facet.rs) @@ -205,6 +209,8 @@ Property-based tests for Facet operations verifying facet adjacency and orientat - **Orientation Alternation**: Adjacent simplices have opposite facet orientations - **Facet Key Validity**: All facet identifiers are valid and retrievable +**Dimensions Tested:** 2D-5D; 4D/5D full facet property variants over the 10-second budget run through `just test-slow`. + **Run with:** `cargo test --release --test proptest_facet` #### [`proptest_geometry.rs`](./proptest_geometry.rs) @@ -250,6 +256,8 @@ Property-based tests for serialization and deserialization verifying data preser - **Simplex Data Integrity**: Simplex-associated data is preserved - **Cross-dimensional Serialization**: Works correctly for all supported dimensions +**Dimensions Tested:** 2D-5D; serialization property variants over the 10-second budget run through `just test-slow`. + **Run with:** `cargo test --release --test proptest_serialization` **Property Testing Notes:** @@ -426,18 +434,19 @@ Tests error handling for coordinate conversion operations, particularly focusing #### [`allocation_api.rs`](./allocation_api.rs) -Memory allocation profiling and testing utilities for tracking memory usage patterns during triangulation operations. +Smoke test for the optional allocation measurement API. -**Monitoring Areas:** +**Coverage:** -- Point and vertex creation allocations -- Triangulation data structure memory usage -- Complex workflow allocation patterns -- Memory efficiency validation +- `count-allocations` feature wiring +- `measure_with_result` returns the measured value +- allocation counters record an intentional allocation **Run with:** `just test-allocation` -**Note:** This uses the `count-allocations` feature flag automatically. +**Note:** Hot-path allocation budgets live in +[`benches/allocation_hot_paths.rs`](../benches/README.md), +not in the default test suite. ## Running Tests @@ -460,7 +469,7 @@ cargo test --test # Examples just test-diagnostics # circumsphere_debug_tools cargo test --test delaunay_incremental_insertion # specific integration test -just test-allocation # allocation profiling +just test-allocation # allocation measurement wiring ``` ### Performance Considerations @@ -548,7 +557,7 @@ Before releases, run the full integration test suite: # Complete test validation just test-release -# Include allocation testing +# Verify allocation measurement wiring just test-allocation # Comprehensive pre-release checks diff --git a/tests/allocation_api.rs b/tests/allocation_api.rs index 2ce80ae9..47b391f3 100644 --- a/tests/allocation_api.rs +++ b/tests/allocation_api.rs @@ -1,281 +1,50 @@ -//! Allocation-bounded tests for performance-sensitive public hot paths. +//! Smoke tests for allocation measurement support. //! //! These tests run only with `--features count-allocations` because the feature -//! installs the allocation-counting global allocator. +//! installs the allocation-counting global allocator. Hot-path allocation +//! budgets live in `benches/allocation_hot_paths.rs`. #![cfg(feature = "count-allocations")] -use allocation_counter::AllocationInfo; -use delaunay::prelude::algorithms::{LocateError, LocateResult, locate_with_stats}; -use delaunay::prelude::construction::{ - DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, -}; -use delaunay::prelude::geometry::{Coordinate, FastKernel, Point}; -use delaunay::prelude::tds::{ - SimplexKey, SimplexValidationError, TdsError, VertexKey, facet_key_from_vertices, - measure_with_result, -}; -use std::hint::black_box; -use thiserror::Error; +use delaunay::prelude::query::measure_with_result; -const SIMPLEX_5D_DIMENSION: usize = 5; -const SIMPLEX_VERTEX_COUNT: usize = SIMPLEX_5D_DIMENSION + 1; -const SIMPLEX_SIMPLEX_COUNT: usize = 1; -const LOCATE_FAST_PATH_ALLOCATION_BUDGET: u64 = 1; - -type TestTriangulation2D = DelaunayTriangulation, (), (), 2>; -type TestTriangulation5D = DelaunayTriangulation, (), (), SIMPLEX_5D_DIMENSION>; - -/// Typed failure modes for allocation-bounded public API checks. -#[derive(Debug, Error)] -enum AllocationTestError { - #[error("triangulation construction failed: {source}")] - Construction { - #[from] - source: DelaunayTriangulationConstructionError, - }, - - #[error("TDS lookup failed: {source}")] - Tds { - #[from] - source: TdsError, - }, - - #[error("simplex validation failed: {source}")] - Simplex { - #[from] - source: SimplexValidationError, - }, - - #[error("point location failed: {source}")] - Locate { - #[from] - source: LocateError, - }, - - #[error("deterministic simplex fixture did not contain a simplex")] - MissingSimplex, - - #[error("fixture simplex has {actual} vertices, expected at least {required}")] - SimplexTooSmall { required: usize, actual: usize }, -} - -/// Build a minimal 2D simplex for the hinted locate fast-path allocation check. -fn deterministic_2d_simplex() -> Result { - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - Ok(DelaunayTriangulation::with_kernel( - &FastKernel::new(), - &vertices, - )?) -} - -/// Build a 5D unit simplex fixture for stack-sized topology hot-path checks. -fn deterministic_5d_simplex() -> Result { - let vertices = [ - vertex!([0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 0.0, 1.0]), - ]; - - Ok(DelaunayTriangulation::with_kernel( - &FastKernel::new(), - &vertices, - )?) -} - -/// Return the only simplex in the deterministic 5D simplex fixture. -fn only_5d_simplex_key(dt: &TestTriangulation5D) -> Result { - dt.tds() - .simplex_keys() - .next() - .ok_or(AllocationTestError::MissingSimplex) -} - -/// Extract one facet from the 5D simplex without allocating a temporary buffer. -fn first_facet_vertices( - dt: &TestTriangulation5D, -) -> Result<[VertexKey; SIMPLEX_5D_DIMENSION], AllocationTestError> { - let simplex_key = only_5d_simplex_key(dt)?; - let simplex = dt - .tds() - .simplex(simplex_key) - .ok_or(AllocationTestError::MissingSimplex)?; - let vertices = simplex.vertices(); - if vertices.len() < SIMPLEX_5D_DIMENSION { - return Err(AllocationTestError::SimplexTooSmall { - required: SIMPLEX_5D_DIMENSION, - actual: vertices.len(), - }); - } - - let mut facet_vertices = [vertices[0]; SIMPLEX_5D_DIMENSION]; - facet_vertices.copy_from_slice(&vertices[..SIMPLEX_5D_DIMENSION]); - Ok(facet_vertices) -} +#[test] +fn measure_with_result_reports_zero_for_stack_only_work() { + let (result, alloc_info) = measure_with_result(|| { + let values = [1_u8, 2, 3, 4]; + values.iter().copied().sum::() + }); -/// Assert an operation performed no allocations and retained no allocator state. -fn assert_zero_allocations(info: &AllocationInfo, operation: &str) { + assert_eq!(result, 10); assert_eq!( - info.count_total, 0, - "{operation} should not allocate; allocation info: {info:?}" + alloc_info.count_total, 0, + "stack-only work should not allocate: {alloc_info:?}" ); assert_eq!( - info.bytes_total, 0, - "{operation} should allocate zero bytes; allocation info: {info:?}" - ); - assert_eq!( - info.count_current, 0, - "{operation} should not retain allocations; allocation info: {info:?}" - ); - assert_eq!( - info.bytes_current, 0, - "{operation} should retain zero bytes; allocation info: {info:?}" - ); -} - -/// Assert an operation stayed within a known allocation-count budget. -fn assert_allocation_budget(info: &AllocationInfo, operation: &str, max_allocations: u64) { - assert!( - info.count_total <= max_allocations, - "{operation} exceeded allocation budget {max_allocations}; allocation info: {info:?}" + alloc_info.bytes_total, 0, + "stack-only work should allocate zero bytes: {alloc_info:?}" ); assert_eq!( - info.count_current, 0, - "{operation} should not retain allocations; allocation info: {info:?}" + alloc_info.count_current, 0, + "stack-only work should not retain allocations: {alloc_info:?}" ); assert_eq!( - info.bytes_current, 0, - "{operation} should retain zero bytes; allocation info: {info:?}" + alloc_info.bytes_current, 0, + "stack-only work should retain zero bytes: {alloc_info:?}" ); } #[test] -fn tds_and_public_iterators_are_zero_allocation() -> Result<(), AllocationTestError> { - let dt = deterministic_5d_simplex()?; - let tds = dt.tds(); - let tri = dt.as_triangulation(); - - let (counts, info) = measure_with_result(|| { - black_box(( - tds.simplices().count(), - tds.vertices().count(), - tds.simplex_keys().count(), - tds.vertex_keys().count(), - tri.simplices().count(), - tri.vertices().count(), - dt.simplices().count(), - dt.vertices().count(), - )) - }); +fn measure_with_result_returns_value_and_allocation_info() { + let (result, alloc_info) = measure_with_result(|| vec![0_u8; 1024]); - assert_eq!( - counts, - ( - SIMPLEX_SIMPLEX_COUNT, - SIMPLEX_VERTEX_COUNT, - SIMPLEX_SIMPLEX_COUNT, - SIMPLEX_VERTEX_COUNT, - SIMPLEX_SIMPLEX_COUNT, - SIMPLEX_VERTEX_COUNT, - SIMPLEX_SIMPLEX_COUNT, - SIMPLEX_VERTEX_COUNT, - ) + assert_eq!(result.len(), 1024); + assert!( + alloc_info.count_total > 0, + "allocation measurement should record the vector allocation: {alloc_info:?}" ); - assert_zero_allocations(&info, "TDS and public simplices()/vertices() iterators"); - Ok(()) -} - -#[test] -fn tds_simplex_vertices_is_zero_allocation() -> Result<(), AllocationTestError> { - let dt = deterministic_5d_simplex()?; - let simplex_key = only_5d_simplex_key(&dt)?; - - let (vertex_count, info) = measure_with_result(|| { - dt.tds() - .simplex_vertices(simplex_key) - .map(|keys| keys.len()) - }); - assert_eq!(vertex_count?, SIMPLEX_VERTEX_COUNT); - assert_zero_allocations(&info, "Tds::simplex_vertices"); - Ok(()) -} - -#[test] -fn simplex_vertex_uuid_iter_is_zero_allocation() -> Result<(), AllocationTestError> { - let dt = deterministic_5d_simplex()?; - let tds = dt.tds(); - let simplex_key = only_5d_simplex_key(&dt)?; - let simplex = tds - .simplex(simplex_key) - .ok_or(AllocationTestError::MissingSimplex)?; - let expected_uuids = simplex.vertex_uuids(tds)?; - - let (result, info) = measure_with_result(|| { - let iter = simplex.vertex_uuid_iter(tds); - let exact_size_len = iter.len(); - let mut matched = 0usize; - - for (index, uuid) in iter.enumerate() { - let uuid = uuid?; - if expected_uuids - .get(index) - .is_some_and(|expected| *expected == uuid) - { - matched += 1; - } - } - - Ok::<_, SimplexValidationError>((exact_size_len, matched)) - }); - let (exact_size_len, matched) = result?; - - assert_eq!(exact_size_len, SIMPLEX_VERTEX_COUNT); - assert_eq!(matched, expected_uuids.len()); - assert_zero_allocations(&info, "Simplex::vertex_uuid_iter"); - Ok(()) -} - -#[test] -fn facet_key_from_vertices_is_zero_allocation() -> Result<(), AllocationTestError> { - let facet_vertices = first_facet_vertices(&deterministic_5d_simplex()?)?; - - let (facet_key, info) = - measure_with_result(|| black_box(facet_key_from_vertices(&facet_vertices))); - assert_ne!(facet_key, 0); - assert_zero_allocations(&info, "facet_key_from_vertices"); - Ok(()) -} - -#[test] -fn locate_with_hint_fast_path_stays_allocation_bounded() -> Result<(), AllocationTestError> { - let dt = deterministic_2d_simplex()?; - let simplex_key = dt - .tds() - .simplex_keys() - .next() - .ok_or(AllocationTestError::MissingSimplex)?; - let kernel = FastKernel::::new(); - let query = Point::new([0.25, 0.25]); - - let (locate_result, info) = - measure_with_result(|| locate_with_stats(dt.tds(), &kernel, &query, Some(simplex_key))); - let (location, stats) = locate_result?; - - assert!(matches!(location, LocateResult::InsideSimplex(found) if found == simplex_key)); - assert!(stats.used_hint); - assert!(!stats.fell_back_to_scan()); - assert_allocation_budget( - &info, - "hinted locate_with_stats fast path", - LOCATE_FAST_PATH_ALLOCATION_BUDGET, + assert!( + alloc_info.bytes_total >= 1024, + "allocation measurement should record at least the vector payload bytes: {alloc_info:?}" ); - Ok(()) } diff --git a/tests/proptest_convex_hull.rs b/tests/proptest_convex_hull.rs index 9ab22a92..79ab48ee 100644 --- a/tests/proptest_convex_hull.rs +++ b/tests/proptest_convex_hull.rs @@ -339,5 +339,5 @@ test_minimal_simplex_hull!(4); test_minimal_simplex_hull!(5); test_convex_hull_properties!(2, 4, 10); test_convex_hull_properties!(3, 5, 12); -test_convex_hull_properties!(4, 6, 14); +test_convex_hull_properties!(4, 6, 14, #[cfg(feature = "slow-tests")]); test_convex_hull_properties!(5, 7, 16, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_delaunay_triangulation.rs b/tests/proptest_delaunay_triangulation.rs index bce32229..cd67fd5d 100644 --- a/tests/proptest_delaunay_triangulation.rs +++ b/tests/proptest_delaunay_triangulation.rs @@ -900,7 +900,7 @@ macro_rules! gen_duplicate_coords_test { gen_duplicate_coords_test!(2, 3, 10); gen_duplicate_coords_test!(3, 4, 12); -gen_duplicate_coords_test!(4, 5, 14); +gen_duplicate_coords_test!(4, 5, 14, #[cfg(feature = "slow-tests")]); gen_duplicate_coords_test!(5, 6, 16, #[cfg(feature = "slow-tests")]); /// Allow runtime tuning for the empty-circumsphere property in higher dimensions. @@ -995,8 +995,8 @@ proptest! { // 2D–5D coverage (keep ranges small to bound runtime) test_empty_circumsphere!(2, 6, 10); test_empty_circumsphere!(3, 6, 10); -test_empty_circumsphere!(4, 6, 12); -test_empty_circumsphere!(5, 7, 12); +test_empty_circumsphere!(4, 6, 12, #[cfg(feature = "slow-tests")]); +test_empty_circumsphere!(5, 7, 12, #[cfg(feature = "slow-tests")]); // ============================================================================= // FAST HIGH-DIMENSIONAL CI SMOKE TESTS @@ -1880,7 +1880,7 @@ macro_rules! gen_insertion_order_robustness_high_dim { }; } -gen_insertion_order_robustness_high_dim!(4, 6, 12); +gen_insertion_order_robustness_high_dim!(4, 6, 12, #[cfg(feature = "slow-tests")]); gen_insertion_order_robustness_high_dim!(5, 7, 12, #[cfg(feature = "slow-tests")]); // ============================================================================= @@ -2007,5 +2007,5 @@ macro_rules! gen_duplicate_cloud_test { gen_duplicate_cloud_test!(2, 2); gen_duplicate_cloud_test!(3, 3); -gen_duplicate_cloud_test!(4, 4); +gen_duplicate_cloud_test!(4, 4, #[cfg(feature = "slow-tests")]); gen_duplicate_cloud_test!(5, 5, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_euler_characteristic.rs b/tests/proptest_euler_characteristic.rs index 7b31acd1..5a0fd355 100644 --- a/tests/proptest_euler_characteristic.rs +++ b/tests/proptest_euler_characteristic.rs @@ -212,5 +212,5 @@ fn test_seeded_random_generator_euler_consistent() { // - Match patterns in other proptest files test_euler_properties!(2, 4, 15); test_euler_properties!(3, 5, 20); -test_euler_properties!(4, 6, 25); +test_euler_properties!(4, 6, 25, #[cfg(feature = "slow-tests")]); test_euler_properties!(5, 7, 16, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_facet.rs b/tests/proptest_facet.rs index a8d2f180..7bf5cd63 100644 --- a/tests/proptest_facet.rs +++ b/tests/proptest_facet.rs @@ -174,7 +174,7 @@ macro_rules! test_facet_properties { // Parameters: dimension, min_vertices, max_vertices, expected_facet_vertices (D) test_facet_properties!(2, 4, 10, 2); test_facet_properties!(3, 5, 12, 3); -test_facet_properties!(4, 6, 14, 4); +test_facet_properties!(4, 6, 14, 4, #[cfg(feature = "slow-tests")]); test_facet_properties!(5, 7, 16, 5, #[cfg(feature = "slow-tests")]); // Additional invariant: facet multiplicity (each facet should belong to 1 or 2 simplices) @@ -226,5 +226,5 @@ macro_rules! test_facet_multiplicity { test_facet_multiplicity!(2, 4, 10); test_facet_multiplicity!(3, 5, 12); -test_facet_multiplicity!(4, 6, 14); +test_facet_multiplicity!(4, 6, 14, #[cfg(feature = "slow-tests")]); test_facet_multiplicity!(5, 7, 16, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_orientation.rs b/tests/proptest_orientation.rs index 2237a2f8..abbb5b34 100644 --- a/tests/proptest_orientation.rs +++ b/tests/proptest_orientation.rs @@ -138,7 +138,12 @@ macro_rules! gen_orientation_incremental_props { gen_orientation_construction_and_tamper_props!(2, 4, 10); gen_orientation_construction_and_tamper_props!(3, 5, 12); -gen_orientation_construction_and_tamper_props!(4, 6, 14); +gen_orientation_construction_and_tamper_props!( + 4, + 6, + 14, + #[cfg(feature = "slow-tests")] +); gen_orientation_construction_and_tamper_props!( 5, 7, @@ -148,5 +153,5 @@ gen_orientation_construction_and_tamper_props!( gen_orientation_incremental_props!(2, 4, 10); gen_orientation_incremental_props!(3, 5, 12); -gen_orientation_incremental_props!(4, 6, 14); +gen_orientation_incremental_props!(4, 6, 14, #[cfg(feature = "slow-tests")]); gen_orientation_incremental_props!(5, 7, 16, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_serialization.rs b/tests/proptest_serialization.rs index d177df4b..1d703543 100644 --- a/tests/proptest_serialization.rs +++ b/tests/proptest_serialization.rs @@ -238,5 +238,5 @@ macro_rules! test_serialization_properties { // Parameters: dimension, min_vertices, max_vertices test_serialization_properties!(2, 4, 10); test_serialization_properties!(3, 5, 12); -test_serialization_properties!(4, 6, 14); +test_serialization_properties!(4, 6, 14, #[cfg(feature = "slow-tests")]); test_serialization_properties!(5, 7, 16, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_simplex.rs b/tests/proptest_simplex.rs index 419778f4..bfa8a43c 100644 --- a/tests/proptest_simplex.rs +++ b/tests/proptest_simplex.rs @@ -143,5 +143,5 @@ macro_rules! test_simplex_properties { // Parameters: dimension, min_vertices, max_vertices, expected_vertices (D+1), max_neighbors (D+1) test_simplex_properties!(2, 4, 10, 3, 3); test_simplex_properties!(3, 5, 12, 4, 4); -test_simplex_properties!(4, 6, 14, 5, 5); +test_simplex_properties!(4, 6, 14, 5, 5, #[cfg(feature = "slow-tests")]); test_simplex_properties!(5, 7, 16, 6, 6, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_tds.rs b/tests/proptest_tds.rs index 78435b9f..c448f949 100644 --- a/tests/proptest_tds.rs +++ b/tests/proptest_tds.rs @@ -327,7 +327,7 @@ macro_rules! gen_simplex_vertex_count { gen_tds_validity!(2); gen_tds_validity!(3); -gen_tds_validity!(4); +gen_tds_validity!(4, #[cfg(feature = "slow-tests")]); gen_tds_validity!(5, #[cfg(feature = "slow-tests")]); // ============================================================================= @@ -338,7 +338,7 @@ gen_neighbor_symmetry!(2); gen_neighbor_symmetry!(3); -gen_neighbor_symmetry!(4); +gen_neighbor_symmetry!(4, #[cfg(feature = "slow-tests")]); gen_neighbor_symmetry!(5, #[cfg(feature = "slow-tests")]); @@ -350,7 +350,7 @@ gen_neighbor_index_semantics!(2); gen_neighbor_index_semantics!(3); -gen_neighbor_index_semantics!(4); +gen_neighbor_index_semantics!(4, #[cfg(feature = "slow-tests")]); gen_neighbor_index_semantics!(5, #[cfg(feature = "slow-tests")]); @@ -362,7 +362,7 @@ gen_simplex_vertices_exist_in_tds!(2); gen_simplex_vertices_exist_in_tds!(3); -gen_simplex_vertices_exist_in_tds!(4); +gen_simplex_vertices_exist_in_tds!(4, #[cfg(feature = "slow-tests")]); gen_simplex_vertices_exist_in_tds!(5, #[cfg(feature = "slow-tests")]); @@ -374,7 +374,7 @@ gen_no_duplicate_simplices!(2); gen_no_duplicate_simplices!(3); -gen_no_duplicate_simplices!(4); +gen_no_duplicate_simplices!(4, #[cfg(feature = "slow-tests")]); gen_no_duplicate_simplices!(5, #[cfg(feature = "slow-tests")]); @@ -386,7 +386,7 @@ gen_dimension_consistency!(2, 3); gen_dimension_consistency!(3, 4); -gen_dimension_consistency!(4, 5); +gen_dimension_consistency!(4, 5, #[cfg(feature = "slow-tests")]); gen_dimension_consistency!(5, 6, #[cfg(feature = "slow-tests")]); @@ -398,7 +398,7 @@ gen_vertex_count_consistency!(2); gen_vertex_count_consistency!(3); -gen_vertex_count_consistency!(4); +gen_vertex_count_consistency!(4, #[cfg(feature = "slow-tests")]); gen_vertex_count_consistency!(5, #[cfg(feature = "slow-tests")]); @@ -410,7 +410,7 @@ gen_simplex_vertex_count!(2, 3); gen_simplex_vertex_count!(3, 4); -gen_simplex_vertex_count!(4, 5); +gen_simplex_vertex_count!(4, 5, #[cfg(feature = "slow-tests")]); gen_simplex_vertex_count!(5, 6, #[cfg(feature = "slow-tests")]); @@ -447,7 +447,7 @@ gen_is_connected!(2); gen_is_connected!(3); -gen_is_connected!(4); +gen_is_connected!(4, #[cfg(feature = "slow-tests")]); gen_is_connected!(5, #[cfg(feature = "slow-tests")]); diff --git a/tests/proptest_triangulation.rs b/tests/proptest_triangulation.rs index 6046eb43..78a605cc 100644 --- a/tests/proptest_triangulation.rs +++ b/tests/proptest_triangulation.rs @@ -660,7 +660,7 @@ test_simplex_quality_properties!(5, 6); // Parameters: dimension, min_vertices, max_vertices test_quality_properties!(2, 4, 10); test_quality_properties!(3, 5, 12); -test_quality_properties!(4, 6, 14); +test_quality_properties!(4, 6, 14, #[cfg(feature = "slow-tests")]); test_quality_properties!(5, 7, 16, #[cfg(feature = "slow-tests")]); // ============================================================================= @@ -779,7 +779,7 @@ macro_rules! test_facet_topology_invariant { // Parameters: dimension, min_vertices, max_vertices test_facet_topology_invariant!(2, 4, 10); test_facet_topology_invariant!(3, 5, 12); -test_facet_topology_invariant!(4, 6, 14); +test_facet_topology_invariant!(4, 6, 14, #[cfg(feature = "slow-tests")]); test_facet_topology_invariant!(5, 7, 16, #[cfg(feature = "slow-tests")]); // ============================================================================= diff --git a/tests/triangulation_builder.rs b/tests/triangulation_builder.rs index 02271101..034f6536 100644 --- a/tests/triangulation_builder.rs +++ b/tests/triangulation_builder.rs @@ -17,11 +17,12 @@ use delaunay::prelude::construction::{ use delaunay::prelude::geometry::{Coordinate, Point, RobustKernel}; use delaunay::prelude::insertion::InsertionErrorSourceKind; use delaunay::prelude::repair::DelaunayRepairError; -use delaunay::prelude::tds::{ - InvariantError, InvariantErrorSummaryDetail, TriangulationValidationErrorKind, -}; +#[cfg(feature = "slow-tests")] +use delaunay::prelude::tds::InvariantError; +use delaunay::prelude::tds::{InvariantErrorSummaryDetail, TriangulationValidationErrorKind}; use delaunay::prelude::topology::spaces::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; use delaunay::prelude::topology::validation::{count_simplices, euler_characteristic}; +#[cfg(feature = "slow-tests")] use delaunay::prelude::triangulation::TriangulationValidationError; use delaunay::prelude::validation::ValidationPolicy; @@ -498,6 +499,7 @@ fn test_builder_periodic_topology_level4_smoke_3d() { } } #[test] +#[cfg(feature = "slow-tests")] fn test_builder_toroidal_periodic_validate_levels_1_to_4_3d_known_limitation() { let dt = build_toroidal_periodic_triangulation::<3>(); @@ -512,19 +514,6 @@ fn test_builder_toroidal_periodic_validate_levels_1_to_4_3d_known_limitation() { } } -gen_toroidal_periodic_validation_test!( - 4, - levels_1_to_3, - false, - #[cfg(feature = "slow-tests")] -); -gen_toroidal_periodic_validation_test!( - 5, - levels_1_to_3, - false, - #[cfg(feature = "slow-tests")] -); - /// Explicit 7-vertex torus (Heawood triangulation) with `GlobalTopology::Toroidal` /// is rejected until explicit non-Euclidean construction has Level 4 validation. ///