578 lines
16 KiB
Markdown
578 lines
16 KiB
Markdown
# 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
|