From 46c0f8d0bb32393107a9bddfd0a1330cce0fce16 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 10 Mar 2026 20:17:01 +0100 Subject: [PATCH] [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 Signed-off-by: Stan Grams --- .../trx-frontend-http/src/audio.rs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index 11bbd8e..627513f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -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 Vec { 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 = HashMap::new(); + for (_, msg) in history.iter() { + latest.insert(msg.mmsi, msg.clone()); + } + let mut out: Vec = latest.into_values().collect(); + out.sort_by_key(|m| m.ts_ms.unwrap_or(0)); + out } pub fn snapshot_vdes_history(context: &FrontendRuntimeContext) -> Vec {