use crate::types::response::ProcessingStats;
use ifc_lite_geometry::{GeometryDiagnostics, RectFastSummary};
pub const PIPELINE_DIAGNOSTICS_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PipelinePhaseTimings {
pub entity_scan_ms: u64,
pub lookup_ms: u64,
pub preprocess_ms: u64,
pub parse_ms: u64,
pub geometry_ms: u64,
pub total_ms: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PipelineDiagnostics {
pub schema_version: u32,
pub batches: u64,
pub element_count: u64,
pub mesh_count: u64,
pub triangle_count: u64,
pub backstop_count: u64,
pub total_csg_failures: u64,
pub products_with_failures: u64,
pub hosts_with_openings: u64,
pub silent_no_ops: u64,
pub rect_fast: RectFastSummary,
pub point_cache_hits: u64,
pub point_cache_misses: u64,
pub faceted_brep_ms: u64,
pub phase_ms: PipelinePhaseTimings,
}
impl Default for PipelineDiagnostics {
fn default() -> Self {
Self {
schema_version: PIPELINE_DIAGNOSTICS_SCHEMA_VERSION,
batches: 0,
element_count: 0,
mesh_count: 0,
triangle_count: 0,
backstop_count: 0,
total_csg_failures: 0,
products_with_failures: 0,
hosts_with_openings: 0,
silent_no_ops: 0,
rect_fast: RectFastSummary::default(),
point_cache_hits: 0,
point_cache_misses: 0,
faceted_brep_ms: 0,
phase_ms: PipelinePhaseTimings::default(),
}
}
}
impl PipelineDiagnostics {
#[allow(clippy::too_many_arguments)]
pub fn record_batch(
&mut self,
element_count: u64,
mesh_count: u64,
triangle_count: u64,
backstop_count: u64,
point_cache_hits: u64,
point_cache_misses: u64,
faceted_brep_ms: u64,
geometry_ms: u64,
diag: &GeometryDiagnostics,
) {
self.batches += 1;
self.element_count += element_count;
self.mesh_count += mesh_count;
self.triangle_count += triangle_count;
self.backstop_count += backstop_count;
self.point_cache_hits += point_cache_hits;
self.point_cache_misses += point_cache_misses;
self.faceted_brep_ms += faceted_brep_ms;
self.total_csg_failures += diag.total_csg_failures;
self.products_with_failures += diag.products_with_failures;
self.hosts_with_openings += diag.hosts_with_openings;
self.silent_no_ops += diag.silent_no_ops;
self.rect_fast.fired += diag.rect_fast.fired;
self.rect_fast.openings_cut += diag.rect_fast.openings_cut;
self.rect_fast.defer_host_not_box += diag.rect_fast.defer_host_not_box;
self.rect_fast.defer_not_through += diag.rect_fast.defer_not_through;
self.rect_fast.defer_off_face += diag.rect_fast.defer_off_face;
self.rect_fast.defer_near_edge += diag.rect_fast.defer_near_edge;
self.rect_fast.defer_no_openings += diag.rect_fast.defer_no_openings;
self.phase_ms.geometry_ms += geometry_ms;
self.phase_ms.total_ms += geometry_ms;
}
pub fn from_processing_stats(stats: &ProcessingStats, element_count: u64) -> Self {
let (hosts_with_openings, silent_no_ops, rect_fast) = match &stats.geometry_diagnostics {
Some(d) => (d.hosts_with_openings, d.silent_no_ops, d.rect_fast),
None => (0, 0, RectFastSummary::default()),
};
Self {
schema_version: PIPELINE_DIAGNOSTICS_SCHEMA_VERSION,
batches: 1,
element_count,
mesh_count: stats.total_meshes as u64,
triangle_count: stats.total_triangles as u64,
backstop_count: stats.degenerate_triangles_dropped,
total_csg_failures: stats.total_csg_failures,
products_with_failures: stats.products_with_failures,
hosts_with_openings,
silent_no_ops,
rect_fast,
point_cache_hits: stats.point_cache_hits,
point_cache_misses: stats.point_cache_misses,
faceted_brep_ms: stats.faceted_brep_time_ms,
phase_ms: PipelinePhaseTimings {
entity_scan_ms: stats.entity_scan_time_ms,
lookup_ms: stats.lookup_time_ms,
preprocess_ms: stats.preprocess_time_ms,
parse_ms: stats.parse_time_ms,
geometry_ms: stats.geometry_time_ms,
total_ms: stats.total_time_ms,
},
}
}
pub fn is_empty(&self) -> bool {
self.batches == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_camelcase_keys_matching_the_ts_contract() {
let mut d = PipelineDiagnostics::default();
d.record_batch(10, 12, 3000, 2, 40, 8, 5, 42, &GeometryDiagnostics::default());
let v = serde_json::to_value(&d).expect("serializes");
for key in [
"schemaVersion",
"batches",
"elementCount",
"meshCount",
"triangleCount",
"backstopCount",
"totalCsgFailures",
"productsWithFailures",
"hostsWithOpenings",
"silentNoOps",
"rectFast",
"pointCacheHits",
"pointCacheMisses",
"facetedBrepMs",
"phaseMs",
] {
assert!(v.get(key).is_some(), "missing top-level key {key}");
}
for key in [
"entityScanMs",
"lookupMs",
"preprocessMs",
"parseMs",
"geometryMs",
"totalMs",
] {
assert!(v["phaseMs"].get(key).is_some(), "missing phaseMs key {key}");
}
assert!(v["rectFast"].get("deferHostNotBox").is_some());
assert_eq!(v["schemaVersion"], PIPELINE_DIAGNOSTICS_SCHEMA_VERSION);
}
#[test]
fn record_batch_accumulates_across_batches() {
let mut d = PipelineDiagnostics::default();
assert!(d.is_empty());
let diag = GeometryDiagnostics {
total_csg_failures: 2,
hosts_with_openings: 3,
..Default::default()
};
d.record_batch(10, 12, 3000, 1, 30, 4, 3, 40, &diag);
d.record_batch(5, 6, 1500, 0, 12, 1, 2, 10, &GeometryDiagnostics::default());
assert!(!d.is_empty());
assert_eq!(d.batches, 2);
assert_eq!(d.element_count, 15);
assert_eq!(d.mesh_count, 18);
assert_eq!(d.triangle_count, 4500);
assert_eq!(d.backstop_count, 1);
assert_eq!(d.total_csg_failures, 2);
assert_eq!(d.hosts_with_openings, 3);
assert_eq!(d.point_cache_hits, 42);
assert_eq!(d.point_cache_misses, 5);
assert_eq!(d.faceted_brep_ms, 5);
assert_eq!(d.phase_ms.geometry_ms, 50);
}
#[test]
fn from_processing_stats_maps_all_phase_timers() {
let stats = ProcessingStats {
total_meshes: 7,
total_triangles: 99,
parse_time_ms: 11,
entity_scan_time_ms: 5,
lookup_time_ms: 2,
preprocess_time_ms: 3,
geometry_time_ms: 40,
total_time_ms: 60,
degenerate_triangles_dropped: 4,
total_csg_failures: 1,
products_with_failures: 1,
point_cache_hits: 128,
point_cache_misses: 32,
faceted_brep_time_ms: 9,
..Default::default()
};
let d = PipelineDiagnostics::from_processing_stats(&stats, 20);
assert_eq!(d.batches, 1);
assert_eq!(d.element_count, 20);
assert_eq!(d.mesh_count, 7);
assert_eq!(d.backstop_count, 4);
assert_eq!(d.point_cache_hits, 128);
assert_eq!(d.point_cache_misses, 32);
assert_eq!(d.faceted_brep_ms, 9);
assert_eq!(d.phase_ms.parse_ms, 11);
assert_eq!(d.phase_ms.entity_scan_ms, 5);
assert_eq!(d.phase_ms.lookup_ms, 2);
assert_eq!(d.phase_ms.preprocess_ms, 3);
assert_eq!(d.phase_ms.geometry_ms, 40);
assert_eq!(d.phase_ms.total_ms, 60);
}
}