From 3bd70f7910c1b4b642bc9b1bd44829b7f2e23769 Mon Sep 17 00:00:00 2001 From: rUv Date: Sun, 17 May 2026 19:29:07 -0400 Subject: [PATCH] =?UTF-8?q?fix(sensing):=20adaptive=5Fclassifier=20sorts?= =?UTF-8?q?=20with=20unwrap=5For(Equal)=20=E2=80=94=20NaN=20panic=20(close?= =?UTF-8?q?s=20#611)=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported by @bannned-bit. v2/crates/wifi-densepose-sensing-server/src/ adaptive_classifier.rs:94 did: sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); f64::partial_cmp returns None on NaN, so `.unwrap()` panics. CSI data from real ESP32 hardware can produce NaN (silent DSP div-by-zero, empty buffer, etc.), and this code path runs on every frame in the classify() hot path — a single NaN frame kills the entire sensing server process. Fix swaps for unwrap_or(Ordering::Equal), matching the pattern the same file already uses at lines 149-150 and 155 (those sites were already NaN-safe; this site was an oversight). Scoped audit: greped the v2/ tree for `partial_cmp(b).unwrap()`. The other 3 hits are in #[cfg(test)] blocks (spectrogram.rs:269, depth.rs:234, connectivity.rs:477) where panic-on-NaN is acceptable because test inputs are controlled. Only adaptive_classifier.rs:94 was a production-path crash. Severity: critical per reporter — runtime panic on real-world data. Patch: 1-line behavioural change + comment. --- CHANGELOG.md | 7 +++++++ .../src/adaptive_classifier.rs | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd2a304..a2db4593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- **`adaptive_classifier.rs:94` no longer panics on NaN feature values** (closes #611). + `sorted.sort_by(|a, b| a.partial_cmp(b).unwrap())` returned `None` and panicked + whenever a single `NaN` reached the classifier from real ESP32 hardware (silent + DSP div-by-zero, empty buffer). One bad frame killed the entire sensing-server + process. Swapped for `unwrap_or(Ordering::Equal)`, matching the pattern the + same file already used at lines 149-150 and 155. Per-frame hot path; this was + a real production crash vector. - **`ui/utils/pose-renderer.js` no longer divides by zero** when two render frames land in the same `performance.now()` tick (issue #519 Bug 2). `deltaTime` is now `Math.max(currentTime - lastFrameTime, 1)` before the `1000 / deltaTime` division, capping displayed FPS at 1000 — far above any real render rate, but finite so the EMA `averageFps = averageFps * 0.9 + fps * 0.1` no longer poisons itself to `Infinity` on a single zero-dt tick. ### Removed diff --git a/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs b/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs index b89cb58c..cc652f43 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs @@ -91,7 +91,11 @@ fn subcarrier_stats(amps: &[f64]) -> (f64, f64, f64, f64, f64, f64, f64, f64) { // IQR (inter-quartile range). let mut sorted = amps.to_vec(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + // partial_cmp returns None on NaN — fall back to Equal so a single NaN + // frame from real ESP32 hardware (silent DSP div-by-zero, empty buffer) + // can't panic the whole sensing server (#611). The same file already + // uses unwrap_or(Equal) at lines 149-150 and 155; this was an oversight. + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); let q1 = sorted[sorted.len() / 4]; let q3 = sorted[3 * sorted.len() / 4]; let iqr = q3 - q1;