282 lines
8.7 KiB
Markdown
282 lines
8.7 KiB
Markdown
# Misurazione della Latenza
|
|
|
|
Questo documento descrive come il sistema misura la latenza di comunicazione tra il simulatore (client) e il radar (server), e come questi dati vengono utilizzati per l'analisi delle prestazioni.
|
|
|
|
## Panoramica
|
|
|
|
La latenza rappresenta il ritardo tra il momento in cui il server genera un pacchetto di dati e il momento in cui il client lo riceve. Una misurazione accurata della latenza è fondamentale per:
|
|
|
|
1. **Compensazione predittiva**: permettere al simulatore di anticipare la posizione futura dei target
|
|
2. **Analisi delle prestazioni**: correlare variazioni di latenza con errori di tracking
|
|
3. **Diagnostica**: identificare problemi di rete o sovraccarico del sistema
|
|
|
|
## Architettura del Sistema
|
|
|
|
### Componenti Principali
|
|
|
|
1. **ClockSynchronizer** (`target_simulator/utils/clock_synchronizer.py`)
|
|
- Sincronizza l'orologio del server con quello del client
|
|
- Gestisce wrap-around del contatore a 32-bit del server
|
|
- Calcola la latenza media
|
|
|
|
2. **DebugPayloadRouter** (`target_simulator/gui/payload_router.py`)
|
|
- Riceve i pacchetti dal server
|
|
- Alimenta il ClockSynchronizer con campioni di timing
|
|
- Calcola e memorizza le latenze individuali
|
|
|
|
3. **SimulationArchive** (`target_simulator/analysis/simulation_archive.py`)
|
|
- Salva i campioni di latenza con timestamp per analisi post-simulazione
|
|
|
|
## Come Funziona la Misurazione
|
|
|
|
### 1. Sincronizzazione degli Orologi
|
|
|
|
Il sistema deve correlare due orologi diversi:
|
|
- **Server clock**: un contatore a 32-bit che incrementa ad ogni aggiornamento del radar
|
|
- **Client clock**: `time.monotonic()` del sistema operativo locale
|
|
|
|
#### Problema del Wrap-Around
|
|
|
|
Il contatore del server è a 32-bit (max valore: 4,294,967,295). Quando raggiunge il massimo, "riparte da zero":
|
|
|
|
```
|
|
Server timetag: ... 4,294,967,293 → 4,294,967,294 → 4,294,967,295 → 0 → 1 → 2 ...
|
|
```
|
|
|
|
Il `ClockSynchronizer` rileva questi wrap e "srotola" il contatore:
|
|
|
|
```python
|
|
# Rileva wrap se il nuovo valore è molto più piccolo del precedente
|
|
diff = last_raw_timetag - raw_server_timetag
|
|
if diff > 2^31: # Metà del range massimo
|
|
wrap_count += 1
|
|
|
|
# Valore srotolato continuo
|
|
unwrapped_timetag = raw_server_timetag + wrap_count * 2^32
|
|
```
|
|
|
|
### 2. Modello di Regressione Lineare
|
|
|
|
Il sistema mantiene uno storico delle ultime 100 coppie di valori:
|
|
```
|
|
(server_timetag_srotolato, client_reception_time)
|
|
```
|
|
|
|
Usa `numpy.polyfit()` per trovare la relazione lineare ottimale:
|
|
|
|
```
|
|
client_time = m * server_ticks + b
|
|
```
|
|
|
|
Dove:
|
|
- **m** (slope): secondi del client per ogni tick del server (compensazione drift)
|
|
- **b** (intercept): tempo client quando il server era a zero (offset iniziale)
|
|
|
|
#### Esempio di Fit
|
|
|
|
```python
|
|
# Campioni storici (semplificati)
|
|
server_ticks = [1000000, 1001000, 1002000, 1003000]
|
|
client_times = [3255.000, 3256.000, 3257.000, 3258.000]
|
|
|
|
# Regressione lineare
|
|
m, b = np.polyfit(server_ticks, client_times, 1)
|
|
# Risultato: m ≈ 0.001, b ≈ 2255.0
|
|
|
|
# Interpretazione: ogni tick del server ≈ 1ms del client
|
|
```
|
|
|
|
### 3. Calcolo della Latenza per Ogni Pacchetto
|
|
|
|
Quando arriva un nuovo pacchetto:
|
|
|
|
```python
|
|
# 1. Nota il tempo di ricezione
|
|
reception_timestamp = time.monotonic() # es: 3255.123 secondi
|
|
|
|
# 2. Leggi il timetag dal pacchetto del server
|
|
server_timetag = payload.scenario.timetag # es: 1000121
|
|
|
|
# 3. Stima quando il pacchetto è stato GENERATO sul server
|
|
# (convertito in tempo client usando il modello)
|
|
est_generation_time = clock_sync.to_client_time(server_timetag)
|
|
# est_generation_time ≈ 3255.121 secondi
|
|
|
|
# 4. Calcola la latenza
|
|
latency = reception_timestamp - est_generation_time
|
|
# latency = 3255.123 - 3255.121 = 0.002 secondi = 2 ms
|
|
```
|
|
|
|
### 4. Latenza Media (per compensazione predittiva)
|
|
|
|
Il `ClockSynchronizer` calcola anche una **latenza media** su tutti i campioni storici:
|
|
|
|
```python
|
|
# Per ogni campione nello storico
|
|
for (server_tick, client_time) in history:
|
|
# Stima quando il pacchetto doveva essere generato (secondo il modello)
|
|
estimated_gen = m * server_tick + b
|
|
|
|
# La latenza di quel campione
|
|
latency_sample = client_time - estimated_gen
|
|
|
|
# Media delle latenze positive (filtra artifact)
|
|
average_latency = mean(latency_samples[latency_samples >= 0])
|
|
```
|
|
|
|
Questa latenza media viene:
|
|
- Visualizzata nell'interfaccia utente come "Avg. Latency"
|
|
- Salvata nei metadata dell'archivio come `estimated_latency_ms`
|
|
- Usata per calibrare il `prediction_offset_ms`
|
|
|
|
## Memorizzazione dei Dati
|
|
|
|
### Durante la Simulazione
|
|
|
|
Il `DebugPayloadRouter` mantiene un buffer circolare di campioni di latenza:
|
|
|
|
```python
|
|
# Buffer con capacità massima di 10000 campioni
|
|
_latency_samples = collections.deque(maxlen=10000)
|
|
|
|
# Ogni campione è una tupla (timestamp, latency_s)
|
|
_latency_samples.append((reception_timestamp, latency))
|
|
```
|
|
|
|
Capacità: 10000 campioni = sufficiente per ~2.7 ore a 1Hz.
|
|
|
|
### Nell'Archivio
|
|
|
|
Al termine della simulazione, tutti i campioni disponibili vengono salvati nel file JSON:
|
|
|
|
```json
|
|
{
|
|
"metadata": {
|
|
"estimated_latency_ms": 1.5,
|
|
"latency_summary": {
|
|
"mean_ms": 1.5,
|
|
"std_ms": 0.3,
|
|
"min_ms": 1.0,
|
|
"max_ms": 5.0,
|
|
"count": 1234
|
|
},
|
|
"latency_samples": [
|
|
[3255.123, 1.2],
|
|
[3255.456, 1.5],
|
|
[3255.789, 1.3],
|
|
...
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Formato: `[timestamp_monotonic, latency_ms]`
|
|
|
|
## Visualizzazione e Analisi
|
|
|
|
### Finestra di Analisi
|
|
|
|
La finestra di analisi (`AnalysisWindow`) mostra due grafici allineati temporalmente:
|
|
|
|
1. **Grafico superiore**: Errori di tracking (X, Y, Z) nel tempo
|
|
2. **Grafico inferiore**: Evoluzione della latenza nel tempo
|
|
|
|
Entrambi condividono la stessa scala temporale (timestamp monotonic), permettendo di:
|
|
- Identificare correlazioni tra picchi di latenza e aumenti di errore
|
|
- Verificare l'efficacia della compensazione predittiva
|
|
- Diagnosticare problemi di rete o sovraccarico
|
|
|
|
### Esempio di Analisi
|
|
|
|
```
|
|
Tempo (s) | Latenza (ms) | Errore X (ft)
|
|
-----------|--------------|---------------
|
|
3255.0 | 1.2 | 5.0
|
|
3255.5 | 1.3 | 5.5
|
|
3256.0 | 4.5 | 15.0 ← Picco latenza coincide con picco errore
|
|
3256.5 | 1.4 | 6.0
|
|
3257.0 | 1.2 | 5.2
|
|
```
|
|
|
|
## Considerazioni Tecniche
|
|
|
|
### Precisione della Misurazione
|
|
|
|
- **Risoluzione temporale**: dipende dalla frequenza di aggiornamento del radar
|
|
- **Accuratezza**: limitata dalla stabilità degli orologi e dal rumore di rete
|
|
- **Drift compensation**: il modello lineare compensa drift fino a ~100 ppm
|
|
|
|
### Filtri e Validazione
|
|
|
|
- **Latenze negative**: vengono scartate (artifact del modello durante transitori)
|
|
- **Outlier**: latenze estreme potrebbero indicare packet loss o riordino
|
|
- **Sample size**: servono almeno 10 campioni per un fit affidabile
|
|
|
|
### Limitazioni
|
|
|
|
1. **Latenza asimmetrica**: il sistema misura solo la latenza server→client
|
|
2. **Clock resolution**: limitata dalla risoluzione di `time.monotonic()` (~1ns su Linux, ~15.6ms su Windows vecchi)
|
|
3. **Packet reordering**: non gestito esplicitamente (assume arrivo in ordine)
|
|
|
|
## Best Practices per Sviluppatori
|
|
|
|
### Aggiungere Nuovi Campioni
|
|
|
|
```python
|
|
# Nel router o comunicatore
|
|
clock_sync.add_sample(
|
|
raw_server_timetag=packet.timetag,
|
|
client_reception_time=time.monotonic()
|
|
)
|
|
```
|
|
|
|
### Accedere alle Latenze
|
|
|
|
```python
|
|
# Latenza media
|
|
avg_latency_s = clock_sync.get_average_latency_s()
|
|
|
|
# Campioni recenti
|
|
samples = router.get_latency_samples(limit=100)
|
|
for timestamp, latency_s in samples:
|
|
print(f"t={timestamp:.3f}, latency={latency_s*1000:.1f}ms")
|
|
```
|
|
|
|
### Salvare nell'Archivio
|
|
|
|
```python
|
|
# Automatico in SimulationController._stop_or_finish_simulation()
|
|
samples = router.get_latency_samples(limit=None) # Tutti i campioni
|
|
samples_with_time = [
|
|
[round(ts, 3), round(lat * 1000.0, 3)]
|
|
for ts, lat in samples
|
|
]
|
|
extra_metadata["latency_samples"] = samples_with_time
|
|
```
|
|
|
|
## Riferimenti nel Codice
|
|
|
|
- `target_simulator/utils/clock_synchronizer.py`: Implementazione del sincronizzatore
|
|
- `target_simulator/gui/payload_router.py`: Linee 245-260, calcolo latenza
|
|
- `target_simulator/simulation/simulation_controller.py`: Linee 241-247, salvataggio archivio
|
|
- `target_simulator/gui/analysis_window.py`: Visualizzazione grafico latenze
|
|
|
|
## FAQ
|
|
|
|
**Q: Perché usiamo un modello lineare invece di una semplice differenza?**
|
|
A: Gli orologi del server e client hanno drift diversi. Il modello lineare compensa questa deriva nel tempo.
|
|
|
|
**Q: Cosa succede se la rete perde pacchetti?**
|
|
A: Il modello è robusto grazie ai 100 campioni storici. Pacchetti persi creano gap ma non invalidano il modello.
|
|
|
|
**Q: La latenza misurata include elaborazione lato client?**
|
|
A: Sì, include tempo di trasmissione + deserializzazione + accodamento. È la latenza "end-to-end" vista dall'applicazione.
|
|
|
|
**Q: Come posso aumentare la capacità del buffer?**
|
|
A: Modifica `maxlen` in `DebugPayloadRouter.__init__()`. Valore attuale: 10000 campioni.
|
|
|
|
---
|
|
|
|
*Documento creato: 2025-11-13*
|
|
*Ultima modifica: 2025-11-13*
|