[fix](trx-frontend-http): deduplicate AIS history snapshot by MMSI

AIS vessels transmit every 2-30 s; without deduplication the 24-hour ring
buffer can hold tens of thousands of entries, making the /decode/history
response huge and causing O(n^2) DOM thrashing on the client side.

- Add AIS_HISTORY_MAX = 10 000 to cap the ring buffer memory footprint.
- snapshot_ais_history() now returns the latest message per MMSI (one entry
  per vessel), sorted ascending by ts_ms so the client replays in order.

This matches APRS history behaviour: APRS stations transmit infrequently so
their history is naturally compact; AIS history is now equally compact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-10 20:17:01 +01:00
parent e1fe1980ea
commit 46c0f8d0bb
@@ -9,7 +9,7 @@
//! - Subsequent binary messages: raw Opus packets (RX)
//! - Browser sends binary messages: raw Opus packets (TX)
use std::collections::VecDeque;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@@ -26,6 +26,11 @@ use trx_core::decode::{
use trx_frontend::FrontendRuntimeContext;
const HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60);
/// Maximum number of raw AIS messages kept in the ring buffer.
/// AIS vessels can transmit every 2 s, so without a cap the buffer grows
/// unboundedly. 10 000 entries covers ~100 active vessels at 2-second intervals
/// for ~3 minutes — enough for a realistic snapshot while bounding memory use.
const AIS_HISTORY_MAX: usize = 10_000;
fn current_timestamp_ms() -> i64 {
let millis = SystemTime::now()
@@ -81,6 +86,9 @@ fn record_ais(context: &FrontendRuntimeContext, mut msg: AisMessage) {
.expect("ais history mutex poisoned");
history.push_back((Instant::now(), msg));
prune_ais_history(&mut history);
if history.len() > AIS_HISTORY_MAX {
history.pop_front();
}
}
fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) {
@@ -191,13 +199,28 @@ pub fn snapshot_hf_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPac
history.iter().map(|(_, pkt)| pkt.clone()).collect()
}
/// Return the latest message per MMSI seen within the retention window.
///
/// AIS vessels transmit every 230 s; returning every individual message would
/// produce a response too large to be useful. One entry per vessel matches
/// what the map shows (current position/state) and keeps the response compact.
/// The returned vec is sorted ascending by `ts_ms` so the client can replay
/// in chronological order.
pub fn snapshot_ais_history(context: &FrontendRuntimeContext) -> Vec<AisMessage> {
let mut history = context
.ais_history
.lock()
.expect("ais history mutex poisoned");
prune_ais_history(&mut history);
history.iter().map(|(_, msg)| msg.clone()).collect()
// Iterate oldest-first; later entries overwrite earlier ones so the
// HashMap always holds the newest message per MMSI.
let mut latest: HashMap<u32, AisMessage> = HashMap::new();
for (_, msg) in history.iter() {
latest.insert(msg.mmsi, msg.clone());
}
let mut out: Vec<AisMessage> = latest.into_values().collect();
out.sort_by_key(|m| m.ts_ms.unwrap_or(0));
out
}
pub fn snapshot_vdes_history(context: &FrontendRuntimeContext) -> Vec<VdesMessage> {