# RFCP Phase 2.5.1: Performance Tuning + App Close Fix **Date:** February 1, 2025 **Type:** Performance + Bug Fix **Priority:** HIGH **Depends on:** Phase 2.5.0 --- ## 🎯 Summary Phase 2.5.0 vectorization працює, але bottleneck залишився: - `line_intersects_polygons_batch()` має внутрішній loop по polygons - 351-458 walls перевіряються для кожної точки - Результат: 329ms/point (майже без змін) Також: App close ДОСІ не працює — хрестик не закриває backend. --- ## 📊 Current Performance Analysis ``` [DOMINANT_PATH_VEC] Point #1: buildings=50, walls=351 [DOMINANT_PATH_VEC] Point #2: buildings=50, walls=439 [DOMINANT_PATH_VEC] Point #3: buildings=50, walls=458 Result: 329ms/point — NOT improved! ``` **Root cause:** LOS check loop inside vectorized function: ```python # geometry_vectorized.py line ~85 for i, length in enumerate(polygon_lengths): # ← THIS LOOP # Check each polygon... ``` --- ## 🔧 Fix 2.5.1a: Limit Walls for Reflection Check **File:** `backend/app/services/geometry_vectorized.py` **In `find_best_reflection_path_vectorized()`:** ```python def find_best_reflection_path_vectorized( tx: np.ndarray, rx: np.ndarray, building_walls_start: np.ndarray, building_walls_end: np.ndarray, wall_to_building: np.ndarray, obstacle_polygons_x: np.ndarray, obstacle_polygons_y: np.ndarray, obstacle_lengths: np.ndarray, max_candidates: int = 50, max_walls: int = 100, # ← ADD THIS max_los_checks: int = 10 # ← ADD THIS ) -> Tuple[Optional[np.ndarray], float, float]: """ Find best single-reflection path using vectorized operations. OPTIMIZATION: Limit walls and LOS checks for speed. - max_walls: Only consider closest N walls for reflection - max_los_checks: Only verify LOS for top N shortest paths """ num_walls = len(building_walls_start) if num_walls == 0: return None, np.inf, 0.0 # === OPTIMIZATION 1: Limit walls by distance to path midpoint === if num_walls > max_walls: # Calculate midpoint of TX-RX path midpoint = (tx + rx) / 2 # Calculate wall midpoints wall_midpoints = (building_walls_start + building_walls_end) / 2 # Distance from each wall to path midpoint wall_distances = np.linalg.norm(wall_midpoints - midpoint, axis=1) # Take closest walls closest_indices = np.argpartition(wall_distances, max_walls)[:max_walls] building_walls_start = building_walls_start[closest_indices] building_walls_end = building_walls_end[closest_indices] wall_to_building = wall_to_building[closest_indices] # Log reduction (first 3 points only) # print(f"[WALLS] Reduced {num_walls} → {max_walls} walls") # Step 1: Calculate all reflection points at once refl_points, valid = calculate_reflection_points_batch( tx, rx, building_walls_start, building_walls_end ) if not np.any(valid): return None, np.inf, 0.0 # Step 2: Calculate path lengths for valid reflections valid_indices = np.where(valid)[0] valid_refl = refl_points[valid] tx_to_refl = np.linalg.norm(valid_refl - tx, axis=1) refl_to_rx = np.linalg.norm(rx - valid_refl, axis=1) path_lengths = tx_to_refl + refl_to_rx # === OPTIMIZATION 2: Sort by path length, check LOS only for top N === # Sort indices by path length (shortest first) sorted_order = np.argsort(path_lengths) # Limit to max_los_checks check_count = min(len(sorted_order), max_los_checks) # Step 3: Check LOS only for top candidates best_idx = -1 best_length = np.inf for i in range(check_count): idx = sorted_order[i] refl_pt = valid_refl[idx] length = path_lengths[idx] # Skip if already longer than best found if length >= best_length: continue # Check TX → reflection LOS intersects1, _ = line_intersects_polygons_batch( tx, refl_pt, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths ) if np.any(intersects1): continue # Check reflection → RX LOS intersects2, _ = line_intersects_polygons_batch( refl_pt, rx, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths ) if np.any(intersects2): continue # Valid path found! best_idx = idx best_length = length break # ← EARLY EXIT: First valid shortest path is best if best_idx < 0: return None, np.inf, 0.0 best_point = valid_refl[best_idx] # Reflection loss calculation direct_dist = np.linalg.norm(rx - tx) path_ratio = best_length / max(direct_dist, 1.0) reflection_loss = 3.0 + 7.0 * min(1.0, (path_ratio - 1.0) * 2) return best_point, best_length, reflection_loss ``` **Key optimizations:** 1. **max_walls=100**: Only consider 100 closest walls (was 351-458) 2. **max_los_checks=10**: Only verify LOS for 10 shortest paths (was 50) 3. **Early exit**: Stop when first valid path found (sorted by length) **Expected speedup:** 5-10x for reflection calculation --- ## 🔧 Fix 2.5.1b: Simplify LOS Check for Obstacles **File:** `backend/app/services/geometry_vectorized.py` **Optimize `line_intersects_polygons_batch()` — limit polygons:** ```python def line_intersects_polygons_batch( p1: np.ndarray, p2: np.ndarray, polygons_x: np.ndarray, polygons_y: np.ndarray, polygon_lengths: np.ndarray, max_polygons: int = 30 # ← ADD LIMIT ) -> Tuple[np.ndarray, np.ndarray]: """ Check if line p1→p2 intersects multiple polygons. OPTIMIZATION: Only check nearest polygons to the line. """ num_polygons = len(polygon_lengths) if num_polygons == 0: return np.array([], dtype=bool), np.array([]) # === OPTIMIZATION: Filter to nearby polygons first === if num_polygons > max_polygons: # Quick filter: bounding box check line_min_x = min(p1[0], p2[0]) - 50 # 50m buffer line_max_x = max(p1[0], p2[0]) + 50 line_min_y = min(p1[1], p2[1]) - 50 line_max_y = max(p1[1], p2[1]) + 50 # Calculate polygon centroids quickly # This is approximate but fast idx = 0 nearby_mask = np.zeros(num_polygons, dtype=bool) for i, length in enumerate(polygon_lengths): if length < 3: idx += length continue # Polygon centroid (approximate: first vertex) px = polygons_x[idx] py = polygons_y[idx] # Check if centroid is near the line bounding box if (line_min_x <= px <= line_max_x and line_min_y <= py <= line_max_y): nearby_mask[i] = True idx += length # If still too many, take first max_polygons that are nearby nearby_indices = np.where(nearby_mask)[0] if len(nearby_indices) > max_polygons: nearby_indices = nearby_indices[:max_polygons] nearby_mask = np.zeros(num_polygons, dtype=bool) nearby_mask[nearby_indices] = True else: nearby_mask = np.ones(num_polygons, dtype=bool) # Now check only nearby polygons intersects = np.zeros(num_polygons, dtype=bool) min_t = np.ones(num_polygons) * np.inf idx = 0 for i, length in enumerate(polygon_lengths): if length < 3 or not nearby_mask[i]: idx += length continue # Get polygon vertices px = polygons_x[idx:idx + length] py = polygons_y[idx:idx + length] # Create edge segments starts = np.stack([px, py], axis=1) ends = np.stack([np.roll(px, -1), np.roll(py, -1)], axis=1) # Check intersections edge_intersects, t_vals = line_segments_intersect_batch(p1, p2, starts, ends) if np.any(edge_intersects): intersects[i] = True min_t[i] = np.min(t_vals[edge_intersects]) idx += length line_length = np.linalg.norm(p2 - p1) min_distances = min_t * line_length return intersects, min_distances ``` --- ## 🔧 Fix 2.5.1c: Update Constants in dominant_path_service.py **File:** `backend/app/services/dominant_path_service.py` ```python # At top of file, update constants: MAX_BUILDINGS_FOR_LINE = 30 # was 50 MAX_BUILDINGS_FOR_REFLECTION = 20 # was 30 MAX_DISTANCE_FROM_PATH = 200 # was 300m # In find_dominant_paths_vectorized(), add parameters: result = find_best_reflection_path_vectorized( tx_local, rx_local, walls_start, walls_end, wall_to_bldg, poly_x, poly_y, poly_lengths, max_candidates=30, # was 50 max_walls=100, # NEW: limit walls max_los_checks=10 # NEW: limit LOS checks ) ``` --- ## 🔴 Fix 2.5.1d: App Close — AGGRESSIVE FIX **The X button STILL doesn't close the app!** **File:** `desktop/main.js` **Problem analysis:** - `killBackend()` is called but processes survive - `killAllBackendProcesses()` uses `execSync` but maybe fails silently - Electron might quit before kill completes **AGGRESSIVE FIX — Multiple kill strategies:** ```javascript const { execSync, spawn } = require('child_process'); const path = require('path'); let backendProcess = null; let isQuitting = false; // Track all spawned PIDs let knownPids = new Set(); function killAllRfcpProcesses() { console.log('[KILL] === Starting aggressive kill ==='); if (process.platform === 'win32') { // Strategy 1: Kill by image name (most reliable) try { console.log('[KILL] Strategy 1: taskkill /F /IM'); execSync('taskkill /F /IM rfcp-server.exe', { stdio: 'pipe', timeout: 5000, windowsHide: true }); console.log('[KILL] Strategy 1: SUCCESS'); } catch (e) { console.log('[KILL] Strategy 1: No processes or already killed'); } // Strategy 2: Kill by tree if we have PID if (backendProcess && backendProcess.pid) { try { console.log(`[KILL] Strategy 2: taskkill /F /T /PID ${backendProcess.pid}`); execSync(`taskkill /F /T /PID ${backendProcess.pid}`, { stdio: 'pipe', timeout: 5000, windowsHide: true }); console.log('[KILL] Strategy 2: SUCCESS'); } catch (e) { console.log('[KILL] Strategy 2: PID not found'); } } // Strategy 3: PowerShell kill (backup) try { console.log('[KILL] Strategy 3: PowerShell Stop-Process'); execSync('powershell -Command "Get-Process rfcp-server -ErrorAction SilentlyContinue | Stop-Process -Force"', { stdio: 'pipe', timeout: 5000, windowsHide: true }); console.log('[KILL] Strategy 3: SUCCESS'); } catch (e) { console.log('[KILL] Strategy 3: PowerShell failed or no processes'); } // Strategy 4: WMIC kill (legacy but works) try { console.log('[KILL] Strategy 4: WMIC process delete'); execSync('wmic process where "name=\'rfcp-server.exe\'" delete', { stdio: 'pipe', timeout: 5000, windowsHide: true }); console.log('[KILL] Strategy 4: SUCCESS'); } catch (e) { console.log('[KILL] Strategy 4: WMIC failed'); } } else { // Unix try { execSync('pkill -9 -f rfcp-server', { stdio: 'pipe', timeout: 5000 }); } catch (e) { // Ignore } } console.log('[KILL] === Kill sequence complete ==='); } // Call backend shutdown endpoint before killing async function gracefulShutdown() { console.log('[SHUTDOWN] Starting graceful shutdown...'); // Step 1: Try graceful API shutdown try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 2000); await fetch('http://127.0.0.1:8888/api/system/shutdown', { method: 'POST', signal: controller.signal }); clearTimeout(timeout); console.log('[SHUTDOWN] Backend acknowledged shutdown'); // Wait for backend to cleanup await new Promise(r => setTimeout(r, 1000)); } catch (e) { console.log('[SHUTDOWN] Backend did not respond:', e.message); } // Step 2: Force kill everything killAllRfcpProcesses(); // Step 3: Wait and verify await new Promise(r => setTimeout(r, 500)); console.log('[SHUTDOWN] Shutdown complete'); } // === EVENT HANDLERS === // When window X is clicked mainWindow.on('close', async (event) => { console.log('[EVENT] Window close clicked'); if (!isQuitting) { event.preventDefault(); // Prevent immediate close isQuitting = true; await gracefulShutdown(); // Now actually quit app.quit(); } }); // When all windows closed app.on('window-all-closed', () => { console.log('[EVENT] All windows closed'); killAllRfcpProcesses(); // Extra safety if (process.platform !== 'darwin') { app.quit(); } }); // Before quit app.on('before-quit', async (event) => { console.log('[EVENT] Before quit'); if (!isQuitting) { event.preventDefault(); isQuitting = true; await gracefulShutdown(); app.quit(); } }); // Will quit (sync only!) app.on('will-quit', () => { console.log('[EVENT] Will quit - final sync kill'); killAllRfcpProcesses(); }); // Process exit process.on('exit', () => { console.log('[EVENT] Process exit'); killAllRfcpProcesses(); }); // Handle Ctrl+C in dev mode process.on('SIGINT', () => { console.log('[SIGNAL] SIGINT'); killAllRfcpProcesses(); process.exit(0); }); process.on('SIGTERM', () => { console.log('[SIGNAL] SIGTERM'); killAllRfcpProcesses(); process.exit(0); }); ``` **Key changes:** 1. **4 kill strategies** on Windows (taskkill, PID tree, PowerShell, WMIC) 2. **`isQuitting` flag** prevents multiple shutdown attempts 3. **`event.preventDefault()`** on close to allow async shutdown 4. **Graceful shutdown first** via API, then force kill 5. **Every event handler** calls killAllRfcpProcesses() --- ## 🔧 Fix 2.5.1e: Update Test Scripts for Win11 **File:** All .bat scripts **Replace `wmic` with PowerShell (wmic deprecated in Win11):** ```batch :: OLD (wmic - deprecated): for /f "tokens=2 delims==" %%a in ('wmic OS get FreePhysicalMemory /value') do ... :: NEW (PowerShell): for /f %%a in ('powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a FREE_RAM=%%a/1024 :: OLD (CPU): for /f "tokens=2 delims==" %%a in ('wmic cpu get LoadPercentage /value') do ... :: NEW (PowerShell): for /f %%a in ('powershell -Command "(Get-CimInstance Win32_Processor).LoadPercentage"') do set CPU=%%a ``` --- ## 📁 Files to Modify | File | Changes | |------|---------| | `backend/app/services/geometry_vectorized.py` | Add max_walls, max_los_checks limits; optimize LOS check | | `backend/app/services/dominant_path_service.py` | Update constants, pass new parameters | | `desktop/main.js` | Aggressive multi-strategy kill; async shutdown with prevention | | `installer/*.bat` | Replace wmic with PowerShell commands | --- ## 🧪 Testing ### Test 1: Performance ```bash # Run Detailed preset # Expected: < 90 seconds (was 292s) # Expected: < 100ms/point (was 329ms) ``` ### Test 2: App Close ```bash # Start RFCP.exe # Click X # Check Task Manager — should be 0 rfcp-server.exe # Check console for [KILL] logs showing all strategies ``` ### Test 3: Accuracy Spot Check ```bash # Run Standard preset, note coverage shape # Run Detailed preset, compare coverage shape # Should be similar (within visual inspection) ``` --- ## ✅ Success Criteria - [ ] Detailed preset completes in < 90 seconds - [ ] ms/point < 100ms (was 329ms) - [ ] App close works on first X click - [ ] No rfcp-server.exe in Task Manager after close - [ ] Test scripts work on Windows 11 (no wmic errors) --- ## 📈 Expected Performance | Metric | Before 2.5.1 | After 2.5.1 | |--------|--------------|-------------| | Walls checked | 351-458 | 100 max | | LOS checks | 50 | 10 max | | ms/point | 329ms | ~50-80ms | | Detailed time | 292s | ~60-90s | | App close | ❌ broken | ✅ works | --- ## 🔜 After 2.5.1 If performance is acceptable: - Phase 2.6: Fun facts loading screen - Phase 2.6: Export to GeoJSON/KML - Phase 2.7: Multi-site interference visualization