S1005403_RisCC/doc/performance_optimizations.md

10 KiB

Performance Optimizations - Target Simulator

Data: 13 novembre 2025
Obiettivo: Disaccoppiare GUI da comunicazione/simulazione per eliminare overhead e disallineamenti


Colli di Bottiglia Identificati

1. Sistema di Logging (RISOLTO)

Problema:

  • Polling sincrono ogni 100ms
  • Scrittura widget Tkinter ad ogni singolo log (3 operazioni: NORMAL → insert → DISABLED)
  • Nessun batching dei messaggi
  • Scroll automatico ad ogni log

Soluzione Implementata:

  • Batching intelligente: buffer interno che accumula log e li scrive in un'unica operazione
  • Polling adattivo:
    • 200ms con attività recente
    • 400ms con attività moderata
    • 1000ms quando idle (>10s senza log)
  • Limite righe: max 1000 righe nel widget (previene memory bloat)
  • Auto-scroll intelligente: scroll automatico solo se utente era in fondo (non disturba se sta leggendo in alto)
  • Batch size limit: max 50 log processati per ciclo (previene freeze GUI durante log flooding)

Guadagno Atteso: 60-80% riduzione overhead logging durante operazioni normali


2. GUI Refresh Loop

Problema:

  • _gui_refresh_loop() eseguito ogni 40ms (25 FPS)
  • build_display_data() esegue trasformazioni trigonometriche per ogni target, ogni frame
  • Aggiorna tabella target distruggendo/ricreando widget
  • Redraw completo canvas Matplotlib

Stato: DA IMPLEMENTARE

Soluzioni Proposte (priorità decrescente):

A. Cache Trasformazioni Coordinate (PRIORITÀ ALTA)

class CoordinateCache:
    def __init__(self):
        self._cache = {}
        self._transform_matrix = None
        self._ownship_key = None
    
    def get_transformed_position(self, target_id, position, ownship_state):
        # Invalida cache solo se ownship cambia
        cache_key = (ownship_state['position'], ownship_state['heading'])
        if cache_key != self._ownship_key:
            self._transform_matrix = compute_matrix(ownship_state)
            self._cache.clear()
            self._ownship_key = cache_key
        
        # Usa matrice pre-calcolata
        if target_id not in self._cache:
            self._cache[target_id] = apply_matrix(position, self._transform_matrix)
        return self._cache[target_id]

Guadagno Atteso: 70-90% riduzione tempo in build_display_data()

B. Virtualizzazione Tabella Target (PRIORITÀ MEDIA)

Aggiornare solo righe modificate invece di ricreare tutto:

def update_targets_table(self, targets):
    existing = {item.id for item in self.tree.get_children()}
    current = {t.target_id for t in targets}
    
    # Rimuovi solo target spariti
    for item_id in (existing - current):
        self.tree.delete(item_id)
    
    # Aggiorna o inserisci solo modificati
    for target in targets:
        if target.target_id in existing:
            self.tree.item(target.target_id, values=...)  # UPDATE
        else:
            self.tree.insert(..., iid=target.target_id)   # INSERT

Guadagno Atteso: 50-70% riduzione overhead aggiornamento UI

C. Frame Rate Adattivo (PRIORITÀ BASSA)

def _gui_refresh_loop(self):
    start = time.perf_counter()
    # ... operazioni GUI ...
    elapsed_ms = (time.perf_counter() - start) * 1000
    
    # Adatta frame rate al carico
    if elapsed_ms > 30:
        next_delay = 80  # 12 FPS sotto carico
    else:
        next_delay = 40  # 25 FPS normale

3. SimulationStateHub Lock Contention

Problema:

  • Singolo threading.Lock() per tutte le operazioni
  • GUI, SimulationEngine, e thread UDP competono per lo stesso lock
  • Letture GUI possono bloccare scritture critiche (ricezione UDP)

Stato: DOCUMENTATO, DA IMPLEMENTARE SE NECESSARIO

Soluzione Proposta: Double-Buffering (implementare solo se profiling mostra contention)

class SimulationStateHub:
    def __init__(self):
        self._write_buffer = {}  # Thread sim/network scrivono qui
        self._read_buffer = {}   # GUI legge da qui
        self._swap_lock = threading.Lock()  # Lock minimale solo per swap
    
    def swap_buffers(self):
        """Chiamato ogni 40ms da GUI thread prima di leggere"""
        with self._swap_lock:  # Lock brevissimo (<1ms)
            self._read_buffer = dict(self._write_buffer)  # Shallow copy
    
    def add_real_state(self, ...):
        # NESSUN LOCK - scrittura diretta non thread-safe ma veloce
        self._write_buffer[target_id] = state
    
    def get_target_history_for_gui(self, ...):
        # NESSUN LOCK - lettura da buffer dedicato
        return self._read_buffer.get(target_id)

Trade-off:

  • Zero contention tra GUI e comunicazione
  • Latenza visiva max 40ms (trascurabile)
  • Complessità maggiore
  • Memoria raddoppiata per dati target (trascurabile con 32 target)

Quando Implementare: Solo se il profiling mostra >10% del tempo speso in attesa del lock


Metriche di Successo

Prima delle Ottimizzazioni (baseline)

  • Misurare tempo medio in _gui_refresh_loop
  • Misurare packet loss rate durante logging intenso
  • Misurare tempo in build_display_data()
  • Contare lock acquisizioni/sec su SimulationStateHub._lock

Dopo Ottimizzazioni Logging (COMPLETATE)

  • Verificare tempo processing log < 2ms per batch
  • Zero packet loss con DEBUG logging attivo
  • CPU usage logging ridotto del 60%+

Dopo Ottimizzazioni GUI (DA IMPLEMENTARE)

  • build_display_data() ridotto a <5ms (da ~15-20ms)
  • Frame time GUI < 20ms (50+ FPS possibili)
  • Nessun lag visibile con 32 target attivi

Piano di Implementazione

Fase 1: Logging ( COMPLETATA)

  • Batching Tkinter handler
  • Polling adattivo
  • Limite righe widget
  • Auto-scroll intelligente

Fase 2: Virtualizzazione Tabella ( COMPLETATA)

Effort: 2 ore
Impact: Alto
Risk: Basso
Status: IMPLEMENTATA

Modifiche:

  • Refactor SimulationControls.update_targets_table() con diff-based approach
  • Nuovo metodo helper _calculate_geo_position() per codice più pulito
  • Usa iid=str(target.target_id) per lookup O(1) invece di O(n)
  • Solo operazioni necessarie: remove solo spariti, update in-place esistenti, insert solo nuovi

Risultati Attesi:

  • 50-70% riduzione widget operations
  • Con 32 target a 25 FPS: risparmio di 5-15 secondi/minuto
  • Zero flickering visibile (aggiornamenti più smooth)

Test:

python tools/test_table_virtualization.py

Fase 3: Cache Coordinate (RACCOMANDATO NEXT)

Effort: 2-3 ore
Impact: Alto
Risk: Basso

Step:

  1. Creare CoordinateCache class in ppi_adapter.py
  2. Modificare build_display_data() per usare cache
  3. Test con 32 target a 20Hz
  4. Benchmark: prima/dopo

Fase 4: Double-Buffer Hub (SOLO SE NECESSARIO)

Effort: 1-2 giorni
Impact: Alto (se c'è contention)
Risk: Alto (thread safety delicato)

Step:

  1. Prima fare profiling con cProfile o py-spy
  2. Se >10% tempo in lock wait → implementare
  3. Altrimenti SKIP (ottimizzazione prematura)

Strumenti di Profiling Raccomandati

1. cProfile (built-in Python)

$env:PYTHONPATH='C:\src\____GitProjects\target_simulator'
python -m cProfile -o profile.stats -m target_simulator

# Analisi
python -c "import pstats; p = pstats.Stats('profile.stats'); p.sort_stats('cumulative').print_stats(30)"

2. py-spy (sampling profiler, non invasivo)

pip install py-spy
py-spy record -o profile.svg --native -- python -m target_simulator
# Apri profile.svg in browser per flame graph

3. Logging Performance Metrics

Aggiungere in MainView._gui_refresh_loop():

import time
start = time.perf_counter()
# ... operazioni ...
elapsed = (time.perf_counter() - start) * 1000
if elapsed > 20:
    self.logger.warning(f"GUI refresh slow: {elapsed:.1f}ms")

Configurazione per Testing

Test 1: Logging sotto stress

# In qualsiasi parte del codice, temporaneo
import logging
logger = logging.getLogger("stress_test")
for i in range(1000):
    logger.debug(f"Test message {i}")

Atteso: Nessun freeze GUI, tutti i log visibili entro 1-2 secondi

Test 2: 32 Target simultanei

Creare scenario con 32 target, ciascuno con trajectory diversa.
Atteso: Frame time GUI < 30ms, nessun packet loss

Test 3: Protocollo JSON vs Legacy

Testare entrambi i protocolli con stesso scenario.
Atteso: Prestazioni simili (il collo di bottiglia non è il protocollo ma la GUI)


Note Tecniche

Limiti Attuali Accettabili (32 target)

  • Lock contention su SimulationStateHub: OK (<5% tempo)
  • Trasformazioni coordinate senza cache: OK ma migliorabile
  • Tkinter TreeView rebuild: OK ma migliorabile

Quando Scalare oltre 32 target

Se in futuro servono 50-100+ target:

  1. Implementare Double-Buffer Hub (MUST)
  2. Implementare Cache Coordinate (MUST)
  3. Virtualizzare Tabella Target (MUST)
  4. Considerare migrazione da Tkinter a Qt/GTK (MAJOR)

Riferimenti Codice

File Modificati (Fase 1 - Logging):

  • target_simulator/utils/logger.py
    • TkinterTextHandler: aggiunto buffering + flush_pending()
    • _process_global_log_queue(): batching + polling adattivo
    • GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS: 100ms → 200ms
    • LOG_BATCH_SIZE: nuovo parametro (50)
    • max_lines: nuovo parametro (1000)

File Modificati (Fase 2 - Tabella Target):

  • target_simulator/gui/simulation_controls.py
    • update_targets_table(): diff-based approach invece di delete+insert tutto
    • _calculate_geo_position(): helper method per calcolo lat/lon
    • Usa iid=str(target.target_id) per fast lookup

File da Modificare (Fase 3, next):

  • target_simulator/gui/ppi_adapter.py: aggiungere CoordinateCache

File da Modificare (Fase 4, opzionale):

  • target_simulator/analysis/simulation_state_hub.py: double-buffering

Domande Aperte

  1. Qual è la latenza end-to-end attuale? (dal momento in cui server invia fino a visualizzazione GUI)
  2. Si osservano packet loss durante operazioni normali?
  3. Qual è il frame rate PPI attuale con 32 target? (possiamo misurarlo)
  4. Il sistema deve supportare replay/time-travel durante simulazione? (influenza architettura hub)

Prossimi Step Raccomandati:

  1. Test ottimizzazioni logging (questa PR)
  2. 🔲 Profiling con py-spy durante simulazione 32 target
  3. 🔲 Implementare cache coordinate se profiling mostra bottleneck
  4. 🔲 Decisione su double-buffer hub basata su dati, non intuizione