From 1547d14ff1bf5666331e4c64d558b6a4f221b780 Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Tue, 15 Sep 2026 22:53:26 -0500 Subject: [PATCH] PERF-2 P0-b: CG iterations per Poisson solve in the profile (ms per V-cycle) and sub-timers inside the patch regeneration (outline, hull + offset, ring projection, Winslow sweeps, respace, warm bookkeeping, mesh finalisation) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL --- .../specialized/rtx-cfd/src/mesh/patch_gen.rs | 38 +++++++++ .../solvers/incompressible/embedded/mod.rs | 12 +-- .../incompressible/embedded/projection.rs | 10 ++- .../rtx-fsi/tests/fsi2_harness/overset.rs | 83 +++++++++++-------- 4 files changed, 100 insertions(+), 43 deletions(-) diff --git a/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs b/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs index c0b3873..c4a9a0f 100644 --- a/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs +++ b/crates/specialized/rtx-cfd/src/mesh/patch_gen.rs @@ -721,6 +721,23 @@ pub fn cylinder_flag_patch_deformed_tip( /// [`cylinder_flag_patch_deformed_from`] with the flat tip. #[allow(clippy::too_many_arguments)] +thread_local! { + /// PERF-2 P0 (`docs/perf2_campaign.md`): wall time [ns] of the + /// regeneration's stages on this thread — outline, hull + offset, ring + /// projection, Winslow sweeps, respace, warm bookkeeping, mesh + /// finalisation — and the number of builds (slot 7). + static REGEN_NS: std::cell::RefCell<[u64; 8]> = const { std::cell::RefCell::new([0; 8]) }; +} + +fn regen_charge(slot: usize, start: std::time::Instant) { + REGEN_NS.with(|r| r.borrow_mut()[slot] += start.elapsed().as_nanos() as u64); +} + +/// The regeneration's stage times so far on this thread (see `REGEN_NS`). +pub fn regen_profile() -> [u64; 8] { + REGEN_NS.with(|r| *r.borrow()) +} + pub fn cylinder_flag_patch_deformed_from_tip( prev: Option<&PatchMesh>, centre: [f64; 2], @@ -736,9 +753,11 @@ pub fn cylinder_flag_patch_deformed_from_tip( winslow_sweeps: usize, tip_corner: f64, ) -> CfdResult<(PatchMesh, (usize, f64))> { + let t0 = std::time::Instant::now(); let (inner, tip) = cylinder_flag_outline_deformed_tip( centre, r, t, edges, x_tip_ref, fillet, 3, 16, h, 1.15, tip_corner, ); + regen_charge(0, t0); // With a flat tip the outline's corners would put 90° corners on the // hull, whose normal offset folds the rays at the tip; the tip disc // (already a hull source) covers the tip, so the inner points ahead of @@ -1087,8 +1106,11 @@ fn o_grid_from_outline( ) -> CfdResult<(PatchMesh, (usize, f64))> { inner.reverse(); // clockwise let ns = inner.len(); + let t0 = std::time::Instant::now(); let hull = convex_hull(hull_src); let outer_poly = offset_convex_polygon(&hull, offset, 24); + regen_charge(1, t0); + let t0 = std::time::Instant::now(); // Initial outer ring: the inner point pushed along its outward normal // (the ring is clockwise, so the outward normal is the LEFT-hand normal // of the direction of travel) and projected onto the hull offset — @@ -1109,6 +1131,7 @@ fn o_grid_from_outline( ) }) .collect(); + regen_charge(2, t0); let mut inner_closed = inner.clone(); inner_closed.push(inner[0]); // Transfinite start, or the previous mesh's interior with the new @@ -1153,15 +1176,23 @@ fn o_grid_from_outline( let mut last = f64::INFINITY; let mut done = 0; for k in 0..winslow_sweeps { + let t0 = std::time::Instant::now(); let before = x.clone(); + regen_charge(5, t0); + let t0 = std::time::Instant::now(); winslow_smooth(&mut x, ns, 0.0, 1, Some(&outer_poly)); + regen_charge(3, t0); + let t0 = std::time::Instant::now(); respace_rays(&mut x, ns, &eta); + regen_charge(4, t0); + let t0 = std::time::Instant::now(); last = x .iter() .zip(&before) .flat_map(|(r, b)| r.iter().zip(b)) .map(|(p, q)| ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt()) .fold(0.0, f64::max); + regen_charge(5, t0); done = k + 1; if last < 1e-10 * h { break; @@ -1169,10 +1200,15 @@ fn o_grid_from_outline( } (done, last) } else { + let t0 = std::time::Instant::now(); let report = winslow_smooth(&mut x, ns, 1e-10 * h, winslow_sweeps, Some(&outer_poly)); + regen_charge(3, t0); + let t0 = std::time::Instant::now(); respace_rays(&mut x, ns, &eta); + regen_charge(4, t0); report }; + let t0 = std::time::Instant::now(); let mut xs = Vec::with_capacity((nn + 1) * (ns + 1)); let mut ys = Vec::with_capacity(xs.capacity()); for row in &x { @@ -1182,5 +1218,7 @@ fn o_grid_from_outline( } } let mesh = PatchMesh::from_nodes(ns, nn, xs, ys, Some([0.0, 0.0]))?; + regen_charge(6, t0); + REGEN_NS.with(|r| r.borrow_mut()[7] += 1); Ok((mesh, report)) } diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/mod.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/mod.rs index 0ecefec..63b581d 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/mod.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/mod.rs @@ -168,9 +168,9 @@ pub struct EmbeddedPisoSolver { /// (measured: at 1e-2 the first corrector's rounds never settled below /// a 1e-3 relative change — 20/20 rounds every step). inner_stop_factor: f64, - /// PERF-2 P0: Poisson `(setup ns, iterate ns, calls)` summed over - /// every multigrid-PCG solve (`docs/perf2_campaign.md`). - poisson_profile: std::cell::Cell<(u64, u64, u64)>, + /// PERF-2 P0: Poisson `(setup ns, iterate ns, calls, CG iterations)` + /// summed over every multigrid-PCG solve (`docs/perf2_campaign.md`). + poisson_profile: std::cell::Cell<(u64, u64, u64, u64)>, moving: bool, /// Mask hysteresis band in multiples of the min cell size (0 = off). mask_hysteresis: f64, @@ -197,7 +197,7 @@ impl EmbeddedPisoSolver { fringe: None, fringe_correction: Vec::new(), inner_stop_factor: 1e-2, - poisson_profile: std::cell::Cell::new((0, 0, 0)), + poisson_profile: std::cell::Cell::new((0, 0, 0, 0)), moving: false, mask_hysteresis: 0.0, time: 0.0, @@ -294,8 +294,8 @@ impl EmbeddedPisoSolver { } /// Relative part of the pressure solve's inner stop (see the field). - /// The Poisson solves' `(setup ns, iterate ns, calls)` so far. - pub fn poisson_profile(&self) -> (u64, u64, u64) { + /// The Poisson solves' `(setup ns, iterate ns, calls, CG iterations)` so far. + pub fn poisson_profile(&self) -> (u64, u64, u64, u64) { self.poisson_profile.get() } diff --git a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/projection.rs b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/projection.rs index 967d7f8..77914fc 100644 --- a/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/projection.rs +++ b/crates/specialized/rtx-cfd/src/solvers/incompressible/embedded/projection.rs @@ -304,9 +304,13 @@ impl EmbeddedPisoSolver { inner_stop, anchor_cell, ); - let (s0, i0, c0) = self.poisson_profile.get(); - self.poisson_profile - .set((s0 + solution.setup_ns, i0 + solution.iterate_ns, c0 + 1)); + let (s0, i0, c0, k0) = self.poisson_profile.get(); + self.poisson_profile.set(( + s0 + solution.setup_ns, + i0 + solution.iterate_ns, + c0 + 1, + k0 + solution.iterations as u64, + )); // Unconverged: fall back to the SOR sweeps for this projection // rather than apply a correction that did not reach the stop. multigrid_converged = solution.converged; diff --git a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset.rs b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset.rs index c4a0b91..21eeec5 100644 --- a/crates/specialized/rtx-fsi/tests/fsi2_harness/overset.rs +++ b/crates/specialized/rtx-fsi/tests/fsi2_harness/overset.rs @@ -1164,43 +1164,58 @@ impl OversetFluid { /// setup / iterate split. pub fn profile_line(&self) -> Option { let t = self.solver.timers()?; - let (ps, pi, pc) = self.solver.background().poisson_profile(); + let (ps, pi, pc, pk) = self.solver.background().poisson_profile(); + let rg = rtx_cfd::mesh::patch_gen::regen_profile(); let s = |ns: u64| ns as f64 * 1e-9; let adv = s(t.advance_ns).max(1e-300); let pct = |ns: u64| 100.0 * s(ns) / adv; - Some(format!( - " advance split over {} steps ({} rounds), {:.0} s: overlap build {:.0} s ({:.1}%), predictor bg {:.0} s ({:.1}%), predictor patch {:.0} s ({:.1}%), bg Poisson {:.0} s ({:.1}%) [setup {:.0} s, iterate {:.0} s, {} solves], patch BiCGSTAB {:.0} s ({:.1}%), round exchange {:.0} s ({:.1}%), apply {:.0} s ({:.1}%), end exchange {:.0} s ({:.1}%), other {:.0} s", - t.steps, - t.rounds, - adv, - s(t.overlap_build_ns), - pct(t.overlap_build_ns), - s(t.predictor_bg_ns), - pct(t.predictor_bg_ns), - s(t.predictor_patch_ns), - pct(t.predictor_patch_ns), - s(t.bg_solve_ns), - pct(t.bg_solve_ns), - s(ps), - s(pi), - pc, - s(t.patch_solve_ns), - pct(t.patch_solve_ns), - s(t.round_exchange_ns), - pct(t.round_exchange_ns), - s(t.apply_ns), - pct(t.apply_ns), - s(t.end_exchange_ns), - pct(t.end_exchange_ns), - adv - s(t.overlap_build_ns - + t.predictor_bg_ns - + t.predictor_patch_ns - + t.bg_solve_ns - + t.patch_solve_ns - + t.round_exchange_ns - + t.apply_ns - + t.end_exchange_ns), - )) + Some( + format!( + " advance split over {} steps ({} rounds), {:.0} s: overlap build {:.0} s ({:.1}%), predictor bg {:.0} s ({:.1}%), predictor patch {:.0} s ({:.1}%), bg Poisson {:.0} s ({:.1}%) [setup {:.0} s, iterate {:.0} s, {} solves, {} CG iterations, {:.2} ms per V-cycle], patch BiCGSTAB {:.0} s ({:.1}%), round exchange {:.0} s ({:.1}%), apply {:.0} s ({:.1}%), end exchange {:.0} s ({:.1}%), other {:.0} s", + t.steps, + t.rounds, + adv, + s(t.overlap_build_ns), + pct(t.overlap_build_ns), + s(t.predictor_bg_ns), + pct(t.predictor_bg_ns), + s(t.predictor_patch_ns), + pct(t.predictor_patch_ns), + s(t.bg_solve_ns), + pct(t.bg_solve_ns), + s(ps), + s(pi), + pc, + pk, + 1e3 * s(pi) / (pk + pc).max(1) as f64, + s(t.patch_solve_ns), + pct(t.patch_solve_ns), + s(t.round_exchange_ns), + pct(t.round_exchange_ns), + s(t.apply_ns), + pct(t.apply_ns), + s(t.end_exchange_ns), + pct(t.end_exchange_ns), + adv - s(t.overlap_build_ns + + t.predictor_bg_ns + + t.predictor_patch_ns + + t.bg_solve_ns + + t.patch_solve_ns + + t.round_exchange_ns + + t.apply_ns + + t.end_exchange_ns), + ) + &format!( + "\n regeneration split ({} builds): outline {:.0} s, hull + offset {:.0} s, ring projection {:.0} s, Winslow sweeps {:.0} s, respace {:.0} s, warm bookkeeping {:.0} s, mesh finalisation {:.0} s", + rg[7], + s(rg[0]), + s(rg[1]), + s(rg[2]), + s(rg[3]), + s(rg[4]), + s(rg[5]), + s(rg[6]) + ), + ) } pub fn time(&self) -> f64 {