Files
rfcp/RFCP-Phase-2.5.1-Performance-AppClose.md
2026-02-01 14:26:17 +02:00

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:

  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:

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 survive
  • killAllBackendProcesses() uses execSync but 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:

  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):

:: 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