Skip to content

fix(spots): coalesce spot-marker rebuilds to stop multi-pan TCI freezes (#2481)#3310

Merged
NF0T merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/issue-2481-tci-multipan-freeze
May 31, 2026
Merged

fix(spots): coalesce spot-marker rebuilds to stop multi-pan TCI freezes (#2481)#3310
NF0T merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/issue-2481-tci-multipan-freeze

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

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() 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.cppwirePanadapter() 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 #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

…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>
@jensenpat jensenpat marked this pull request as ready for review May 31, 2026 01:38
@jensenpat jensenpat requested a review from a team as a code owner May 31, 2026 01:38
@NF0T NF0T self-assigned this May 31, 2026

@NF0T NF0T left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. When sw dies the pointer nulls automatically; the lambda checks if (rebuildTimerGuard && ...) and returns safely — no use-after-free.
  • Connection context: Timer→rebuildSpots uses sw as context, so the connection auto-disconnects when the pan is removed. The existing if (!swGuard) return; at the top of rebuildSpots is correct defence-in-depth on top of that.
  • Five spot signals still use this as context (same as pre-PR) — after sw dies, the QPointer check 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.

@NF0T NF0T merged commit bc2084f into aethersdr:main May 31, 2026
5 checks passed
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

More investigation about Aether SDR freezes when using 2 Panadapters and 2 TCI connections

2 participants