16 KiB
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:
# 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():
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:
- max_walls=100: Only consider 100 closest walls (was 351-458)
- max_los_checks=10: Only verify LOS for 10 shortest paths (was 50)
- 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:
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
# 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 survivekillAllBackendProcesses()usesexecSyncbut maybe fails silently- Electron might quit before kill completes
AGGRESSIVE FIX — Multiple kill strategies:
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:
- 4 kill strategies on Windows (taskkill, PID tree, PowerShell, WMIC)
isQuittingflag prevents multiple shutdown attemptsevent.preventDefault()on close to allow async shutdown- Graceful shutdown first via API, then force kill
- 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):
:: 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
# Run Detailed preset
# Expected: < 90 seconds (was 292s)
# Expected: < 100ms/point (was 329ms)
Test 2: App Close
# 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
# 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