perf(cluster): widen the spot batching window with the rate of the feed

Every flush commits state the whole window re-renders on, and the window was a
flat 50 ms — twenty full renders a second under an RBN firehose. That is paid
everywhere else in the interface, and it is what a dropdown highlighting its
entries a beat behind the mouse actually is.

The window now follows the rate: 50 ms on a quiet cluster, where a spot should
appear the moment it arrives; 200 ms past five a second; 500 ms past twenty,
where half a second's delay on a line already scrolling past is invisible.
This commit is contained in:
2026-08-28 15:06:08 +02:00
parent 30f74583ff
commit 3e794f57e0
2 changed files with 24 additions and 6 deletions
+4 -2
View File
@@ -9,7 +9,8 @@
"Two screens: OpsLog no longer walks off the desktop. On a monitor placed left of the primary one, the saved position was being added to that monitors own origin at every launch, so the window moved one screen further out each time until it was invisible.",
"CW over TCI: a SunSDR can now be keyed through its own macro keyer — pick TCI as the keyer engine (Settings → CW Keyer) and macros, auto-call and the speed control all work over the link already open, with no WinKeyer and no second serial port. NOT TESTED on the air yet.",
"LoTW upload: a refusal now shows TQSLs own explanation — which contacts were already uploaded, which fell outside the certificates dates — instead of the bare “no QSOs processed”, and names the two settings that cause it.",
"Main tab: the docked cluster now has ONE header row — its title, live count and Filters button sit with Clear filters and Columns, as Recent QSOs beside it already did. The pane is titled DX Cluster."
"Main tab: the docked cluster now has ONE header row — its title, live count and Filters button sit with Clear filters and Columns, as Recent QSOs beside it already did. The pane is titled DX Cluster.",
"A busy cluster no longer makes the rest of the interface sluggish: incoming spots are grouped into fewer, larger updates as the feed gets faster (up to half a second), instead of redrawing the window twenty times a second. A quiet cluster still shows each spot as it lands."
],
"fr": [
"Téléchargement LoTW : les détails QSL (date du QSL, locator, état, comté) deviennent optionnels et désactivés par défaut — LoTW met environ dix fois plus longtemps à construire ce rapport, vingt minutes contre deux sur le même compte, et marquer une confirmation n'en a pas besoin. Toujours demandés automatiquement quand on ajoute les QSO absents du log.",
@@ -18,7 +19,8 @@
"Deux écrans : OpsLog ne s'échappe plus du bureau. Sur un écran placé à gauche de l'écran principal, la position enregistrée était ajoutée à l'origine de cet écran à chaque lancement, si bien que la fenêtre s'éloignait d'un écran à chaque fois jusqu'à devenir invisible.",
"CW en TCI : un SunSDR peut désormais être manipulé par son propre keyer à macros — choisissez TCI comme moteur (Réglages → Manipulateur CW) et les macros, l'appel automatique et le réglage de vitesse passent par la liaison déjà ouverte, sans WinKeyer ni second port série. PAS ENCORE TESTÉ sur l'air.",
"Envoi LoTW : un refus affiche désormais l'explication de TQSL — quels contacts étaient déjà envoyés, lesquels tombaient hors des dates du certificat — au lieu du seul « no QSOs processed », et nomme les deux réglages qui en sont la cause.",
"Onglet Main : le cluster ancré n'a plus qu'UNE ligne d'en-tête — son titre, le compteur live et le bouton Filtres rejoignent Effacer les filtres et Colonnes, comme le faisait déjà la liste des QSO récents à côté. Le panneau s'intitule DX Cluster."
"Onglet Main : le cluster ancré n'a plus qu'UNE ligne d'en-tête — son titre, le compteur live et le bouton Filtres rejoignent Effacer les filtres et Colonnes, comme le faisait déjà la liste des QSO récents à côté. Le panneau s'intitule DX Cluster.",
"Un cluster chargé ne ralentit plus le reste de l'interface : les spots entrants sont regroupés en mises à jour moins nombreuses à mesure que le flux s'accélère (jusqu'à une demi-seconde), au lieu de redessiner la fenêtre vingt fois par seconde. Sur un cluster calme, chaque spot s'affiche toujours dès son arrivée."
]
},
{
+20 -4
View File
@@ -2068,6 +2068,9 @@ export default function App() {
useEffect(() => { spotStatusRef.current = spotStatus; }, [spotStatus]);
// Mirror of spots so the log-triggered refresh reads the current list without
// a stale closure.
// Arrival times of the last second's spots, for the adaptive batching window
// in the cluster:spot listener.
const spotRateRef = useRef<number[]>([]);
const spotsRef = useRef(spots);
useEffect(() => { spotsRef.current = spots; }, [spots]);
// The decoded stations, for the same reason: the status refresh and the cache
@@ -3595,11 +3598,24 @@ export default function App() {
const unsubSpot = EventsOn('cluster:spot', (sp: ClusterSpot) => {
// Stage the spot; a short timer resolves its status then commits it.
pendingSpotsRef.current.push(sp);
// 50 ms is enough to coalesce an RBN burst into one status lookup (the
// worked-index is in memory, so resolving is near-instant) while staying
// imperceptible.
// The window WIDENS with the rate of the feed.
//
// Every flush commits state that the whole window re-renders on, so a
// fixed 50 ms means twenty full renders a second under an RBN firehose —
// and that is felt everywhere else: a dropdown highlighting its entries a
// beat late as the mouse moves down them, which is what was reported.
//
// A quiet cluster keeps the 50 ms: a handful of spots an hour should
// appear the moment they arrive. A busy one is coalesced instead, and half
// a second's delay on a line in a list that is already scrolling past is
// not something anyone can see.
const now = Date.now();
spotRateRef.current = spotRateRef.current.filter((t) => now - t < 1000);
spotRateRef.current.push(now);
const perSec = spotRateRef.current.length;
const window_ms = perSec > 20 ? 500 : perSec > 5 ? 200 : 50;
if (pendingSpotTimer.current === undefined) {
pendingSpotTimer.current = window.setTimeout(flushPendingSpots, 50);
pendingSpotTimer.current = window.setTimeout(flushPendingSpots, window_ms);
}
// Self-spot: someone spotted OUR callsign — show it in the shared header
// toast (same place as the other notifications), not a separate banner.