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:
- Creare
CoordinateCacheclass inppi_adapter.py - Modificare
build_display_data()per usare cache - Test con 32 target a 20Hz
- 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:
- Prima fare profiling con
cProfileopy-spy - Se >10% tempo in lock wait → implementare
- 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:
- Implementare Double-Buffer Hub (MUST)
- Implementare Cache Coordinate (MUST)
- Virtualizzare Tabella Target (MUST)
- Considerare migrazione da Tkinter a Qt/GTK (MAJOR)
Riferimenti Codice
File Modificati (Fase 1 - Logging):
target_simulator/utils/logger.pyTkinterTextHandler: aggiunto buffering +flush_pending()_process_global_log_queue(): batching + polling adattivoGLOBAL_LOG_QUEUE_POLL_INTERVAL_MS: 100ms → 200msLOG_BATCH_SIZE: nuovo parametro (50)max_lines: nuovo parametro (1000)
File Modificati (Fase 2 - Tabella Target):
target_simulator/gui/simulation_controls.pyupdate_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: aggiungereCoordinateCache
File da Modificare (Fase 4, opzionale):
target_simulator/analysis/simulation_state_hub.py: double-buffering
Domande Aperte
- Qual è la latenza end-to-end attuale? (dal momento in cui server invia fino a visualizzazione GUI)
- Si osservano packet loss durante operazioni normali?
- Qual è il frame rate PPI attuale con 32 target? (possiamo misurarlo)
- Il sistema deve supportare replay/time-travel durante simulazione? (influenza architettura hub)
Prossimi Step Raccomandati:
- ✅ Test ottimizzazioni logging (questa PR)
- 🔲 Profiling con py-spy durante simulazione 32 target
- 🔲 Implementare cache coordinate se profiling mostra bottleneck
- 🔲 Decisione su double-buffer hub basata su dati, non intuizione