fix(spots): coalesce spot-marker rebuilds to stop multi-pan TCI freezes (#2481)#3310
Conversation
…es (aethersdr#2481) With 2 panadapters and 2 LogHX3 logs each running Spot Machine over TCI, the waterfall intermittently froze. The trigger (confirmed by the reporter: disabling Spot Machine in both logs resolved it) is the spot-marker render path on the main thread, not the TCI audio fan-out the first triage suspected. wirePanadapter() wired rebuildSpots directly to five SpotModel signals, once per panadapter. Every incoming spot emitted spotAdded/spotUpdated, firing rebuildSpots, which re-iterates every spot, runs DxccColorProvider::colorForSpot() (CTY resolution + allocations) per spot, and diffs the whole marker vector — so two simultaneous cluster feeds drove roughly O(spots x spots/sec x pans) main-thread work in bursts (the 10-170 ms paintEvent stalls in the logs). Coalesce the five signals through a single 50 ms (~20 Hz, below the FFT repaint cadence) single-shot timer per pan, so a burst of N spot events collapses to one rebuildSpots per pan per window instead of N x pans. Visual output is identical, just debounced. Timer is parented to the spectrum widget (per-pan lifetime) and guarded with QPointer, matching the sibling rebuildTnfMarkers pattern. Structural follow-up (flagged, not done here): colorForSpot()/DXCC resolution still runs on the GUI thread; moving it onto the existing SpotClients worker is the larger fix and a maintainer call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NF0T
left a comment
There was a problem hiding this comment.
Reviewed independently as @NF0T (Tier 3 reviewer).
Summary
Correct, well-scoped fix for the multi-pan TCI freeze reported in #2481. Root cause correctly diagnosed: wirePanadapter() connected five SpotModel signals directly to rebuildSpots, which iterates every spot, resolves DXCC color per entry, and diffs the full marker vector — once per signal, once per pan. Two simultaneous cluster feeds at N spots/sec across P pans produces O(N × P) main-thread work in bursts, stalling the waterfall render.
The fix
The five direct connects per pan are replaced with a single 50 ms debounce QTimer per pan. First signal in a burst starts the timer; subsequent signals within the window are no-ops; one rebuildSpots fires at +50 ms. At high spot rates the pattern repeats at ≤20 Hz — below the FFT repaint cadence.
Correctness
- Timer lifetime:
new QTimer(sw)with the SpectrumWidget as parent — identical to the existing resize-debounce timer at ~line 12494. Deleted when the pan is removed. - QPointer guard:
QPointer<QTimer>captured by value. Whenswdies the pointer nulls automatically; the lambda checksif (rebuildTimerGuard && ...)and returns safely — no use-after-free. - Connection context: Timer→rebuildSpots uses
swas context, so the connection auto-disconnects when the pan is removed. The existingif (!swGuard) return;at the top ofrebuildSpotsis correct defence-in-depth on top of that. - Five spot signals still use
thisas context (same as pre-PR) — afterswdies, theQPointercheck in the lambda returns silently. Equivalent to the previous behaviour. - No new includes needed —
<QTimer>and<QPointer>already present.
Non-blocking nit
The if inside scheduleRebuildSpots is missing braces:
if (rebuildTimerGuard && !rebuildTimerGuard->isActive())
rebuildTimerGuard->start();AGENTS.md requires braces on all control flow. The same pattern exists at several pre-existing sites in this file, so this doesn't stand out, but new code should set the right example.
Minor: the PR description states the pattern "matches the sibling rebuildTnfMarkers pattern" — rebuildTnfMarkers connects directly without a timer. The QPointer-guard part matches; the debounce part doesn't. Not a code issue.
Verdict: APPROVE
All project rules pass. CI green (build, check-macos, check-windows, analyze/cpp, CodeQL). Pure src/ — Tier 3. The brace nit is real but not blocking given the established codebase pattern.
…es (aethersdr#2481) (aethersdr#3310) Fixes aethersdr#2481 ## Problem With 2 panadapters and 2 LogHX3 logs each running Spot Machine over TCI, the waterfall intermittently froze. The reporter's isolation was decisive — disabling Spot Machine in **both** logs resolved it, and turning off the verbose logs did not. So the trigger is the **spot-marker render path on the main thread**, not the TCI audio fan-out the first triage suspected. `wirePanadapter()` wired `rebuildSpots` directly to five `SpotModel` signals, **once per panadapter**. Every incoming spot emitted `spotAdded`/`spotUpdated`, firing `rebuildSpots`, which re-iterates *every* spot, runs `DxccColorProvider::colorForSpot()` (CTY resolution + QString allocations) per spot, and diffs the whole marker vector. Two simultaneous cluster feeds drove roughly **O(spots × spots/sec × pans)** main-thread work in bursts — the 10–170 ms paintEvent stalls seen in the support-bundle logs. ## Fix Coalesce the five signals through a single **50 ms** (~20 Hz, below the FFT repaint cadence) single-shot timer per pan, so a burst of N spot events collapses to **one** `rebuildSpots` per pan per window instead of N × pans. Visual output is identical, just debounced. The timer is parented to the spectrum widget (per-pan lifetime) and `QPointer`-guarded, matching the sibling `rebuildTnfMarkers` pattern. No threading or signal-routing changes. ## Files - `src/gui/MainWindow.cpp` — `wirePanadapter()` spot-signal wiring ## Structural follow-up (flagged, not done here) `colorForSpot()` / DXCC resolution still runs on the GUI thread inside `rebuildSpots`. Moving it onto the existing `SpotClients` worker thread (PR aethersdr#2420 already moved the spot *sockets* there) is the larger, structural fix and a maintainer call — deliberately left out of this contained mitigation per the autonomous-agent boundaries. ## Testing Builds clean (985/985, macOS). Manual: 1. 2 panadapters + 2 LogHX3 logs (Spot Machine on) + 2 JTDX over TCI; let the cluster feeds run hot for several minutes. 2. Watch the waterfall for stutter and the title bar for "loose connection to radio" — both should be gone. 3. Confirm spots still appear/expire and DXCC colors render on both pans (refreshed at ≤20 Hz, visually indistinguishable). 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #2481
Problem
With 2 panadapters and 2 LogHX3 logs each running Spot Machine over TCI, the waterfall intermittently froze. The reporter's isolation was decisive — disabling Spot Machine in both logs resolved it, and turning off the verbose logs did not. So the trigger is the spot-marker render path on the main thread, not the TCI audio fan-out the first triage suspected.
wirePanadapter()wiredrebuildSpotsdirectly to fiveSpotModelsignals, once per panadapter. Every incoming spot emittedspotAdded/spotUpdated, firingrebuildSpots, which re-iterates every spot, runsDxccColorProvider::colorForSpot()(CTY resolution + QString allocations) per spot, and diffs the whole marker vector. Two simultaneous cluster feeds drove roughly O(spots × spots/sec × pans) main-thread work in bursts — the 10–170 ms paintEvent stalls seen in the support-bundle logs.Fix
Coalesce the five signals through a single 50 ms (~20 Hz, below the FFT repaint cadence) single-shot timer per pan, so a burst of N spot events collapses to one
rebuildSpotsper pan per window instead of N × pans. Visual output is identical, just debounced. The timer is parented to the spectrum widget (per-pan lifetime) andQPointer-guarded, matching the siblingrebuildTnfMarkerspattern. No threading or signal-routing changes.Files
src/gui/MainWindow.cpp—wirePanadapter()spot-signal wiringStructural follow-up (flagged, not done here)
colorForSpot()/ DXCC resolution still runs on the GUI thread insiderebuildSpots. Moving it onto the existingSpotClientsworker thread (PR #2420 already moved the spot sockets there) is the larger, structural fix and a maintainer call — deliberately left out of this contained mitigation per the autonomous-agent boundaries.Testing
Builds clean (985/985, macOS). Manual:
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat