Skip to content

refactor: extract testable MeshTrafficGate; MeshTrafficMonitor singleton - #2218

Open
garthvh wants to merge 1 commit into
mainfrom
refactor/mesh-traffic-gate
Open

refactor: extract testable MeshTrafficGate; MeshTrafficMonitor singleton#2218
garthvh wants to merge 1 commit into
mainfrom
refactor/mesh-traffic-gate

Conversation

@garthvh

@garthvh garthvh commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Finishes an in-progress refactor of the map's trace-route-flyover traffic gate. The branch had been left with two coexisting copies of MeshTrafficMonitor.swift — an untracked WIP under Helpers/ plus the committed one under Accessory Manager/ — which produced duplicate .stringsdata output and broke the build on every platform.

Changes

  • Extract the hysteresis + debounce decision into a pure, value-typed MeshTrafficGate state machine. It's clock-free (callers pass now), so it's deterministically unit-testable.
  • Rework MeshTrafficMonitor as a main-actor singleton (MeshTrafficMonitor.shared) that self-starts its decay timer on the first inbound packet, replacing the earlier EWMA-counter + explicit start()/stop() model.
  • Relocate the file from Accessory Manager/ to Helpers/ and update the AccessoryManager call sites: record()recordInboundPacket(), stop()reset(), and drop the now-unnecessary explicit start().
  • Add MeshTrafficGateTests covering: a brief spike is ignored (debounce), a sustained high rate trips only after the debounce window, and the flag doesn't chatter around the threshold (hysteresis).

Testing

  • Builds for Mac Catalyst.
  • MeshTrafficGateTests run green on the iOS 26.5 simulator.

Summary by CodeRabbit

  • Bug Fixes

    • Improved sustained high-traffic detection using a debounced hysteresis approach, reducing false flips during short fluctuations.
    • High-traffic status now remains stable while traffic stays within the hysteresis band.
    • Traffic monitoring auto-activates on inbound traffic and clears correctly when connections close.
  • Tests

    • Added unit tests for the high-traffic gate timing, thresholds, and recovery behavior.
    • Removed the prior traffic-monitor sampling test suite.

Copilot AI review requested due to automatic review settings July 27, 2026 21:39
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fd48953-8feb-4ad5-b171-bf43a3b690a9

📥 Commits

Reviewing files that changed from the base of the PR and between ce7d50b and 3f1e227.

📒 Files selected for processing (6)
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift
  • Meshtastic/Accessory/Accessory Manager/MeshTrafficMonitor.swift
  • Meshtastic/Helpers/MeshTrafficMonitor.swift
  • MeshtasticTests/MeshTrafficGateTests.swift
  • MeshtasticTests/MeshTrafficMonitorTests.swift
💤 Files with no reviewable changes (2)
  • MeshtasticTests/MeshTrafficMonitorTests.swift
  • Meshtastic/Accessory/Accessory Manager/MeshTrafficMonitor.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
  • Meshtastic/Helpers/MeshTrafficMonitor.swift
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift
  • MeshtasticTests/MeshTrafficGateTests.swift

📝 Walkthrough

Walkthrough

The pull request replaces the accessory-local traffic monitor with a shared packet-driven monitor, adds hysteresis/debounce gating and tests, and updates connection lifecycle handling to record inbound packets and reset monitoring during teardown.

Changes

Mesh traffic monitoring

Layer / File(s) Summary
Traffic gate state machine and tests
Meshtastic/Helpers/MeshTrafficMonitor.swift, MeshtasticTests/MeshTrafficGateTests.swift
Adds MeshTrafficGate with sustained high-rate and low-threshold hysteresis transitions, with tests covering spikes, thresholds, persistence, and debounce restarts.
Shared packet monitor
Meshtastic/Helpers/MeshTrafficMonitor.swift
Adds a shared @MainActor monitor with rolling packet-rate tracking, decay timing, published high-traffic state, transition logging, and reset behavior.
Accessory connection lifecycle integration
Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift, Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
Uses the shared monitor, records inbound packets, resets monitoring during teardown, and removes explicit monitor startup during connection setup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AccessoryManager
  participant MeshTrafficMonitor
  participant MeshTrafficGate
  participant DecayTimer

  AccessoryManager->>MeshTrafficMonitor: recordInboundPacket()
  MeshTrafficMonitor->>MeshTrafficGate: evaluate averaged packet rate
  MeshTrafficMonitor->>DecayTimer: schedule rolling-window decay
  DecayTimer->>MeshTrafficMonitor: re-evaluate after packets stop
  MeshTrafficMonitor-->>AccessoryManager: update isHighTraffic
  AccessoryManager->>MeshTrafficMonitor: reset() on closeConnection()
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

I’m a rabbit with packets to spare,
Watching traffic hop through the air.
Gates wait, then glow bright,
Timers fade through the night,
And resets leave clean burrows there.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main refactor: extracting MeshTrafficGate and converting MeshTrafficMonitor to a singleton.
Description check ✅ Passed The description covers the change, rationale, and testing; it is mostly complete despite using custom headings instead of the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

📄 Docs staleness warning

This PR modifies user-facing Swift source files but does not update any page under docs/user/ or docs/developer/.

Changed source files:

Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift
Meshtastic/Accessory/Accessory Manager/MeshTrafficMonitor.swift

What to check:

Changed area Likely doc page
Views/Messages/ docs/user/messages.md
Views/Nodes/ docs/user/nodes.md
Views/Map/ docs/user/map.md
Views/Settings/Bluetooth/ docs/user/bluetooth.md
Views/Settings/Discovery/ docs/user/discovery.md
Views/Settings/MQTT/ docs/user/mqtt.md
Views/Settings/TAK/ docs/user/tak.md
Views/Settings/Firmware/ docs/user/firmware.md
Views/Settings/ (telemetry/sensor) docs/user/telemetry.md
Views/Settings/ (general) docs/user/settings.md
Meshtastic Watch App/ docs/user/watch.md
Model/ docs/developer/swiftdata.md or docs/developer/architecture.md
Accessory/Transports/ docs/developer/transport.md

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this warning.

After updating docs/, re-run bash scripts/build-docs.sh --output Meshtastic/Resources/docs locally and commit the regenerated HTML bundle.

Finish an in-progress refactor of the map's trace-route-flyover traffic gate:

- Extract the hysteresis + debounce decision into a pure, value-typed
  MeshTrafficGate state machine (clock-free, unit-tested in MeshTrafficGateTests).
- Rework MeshTrafficMonitor as a main-actor singleton (MeshTrafficMonitor.shared)
  that self-starts its decay timer on the first inbound packet, replacing the
  earlier EWMA-counter + explicit start()/stop() model.
- Relocate the file from Accessory Manager/ to Helpers/ and update the
  AccessoryManager call sites: record() -> recordInboundPacket(),
  stop() -> reset(), and drop the explicit start() (it auto-starts now).
- Remove the old MeshTrafficMonitorTests, which tested the removed
  record()/sample()/watermark API; the debounce + hysteresis behaviour it
  covered now lives in MeshTrafficGateTests against the extracted gate.

Two copies of MeshTrafficMonitor.swift had been coexisting — an untracked WIP in
Helpers/ plus the committed one in Accessory Manager/ — producing duplicate
.stringsdata output that broke every build; this consolidates onto the new version.
@garthvh
garthvh force-pushed the refactor/mesh-traffic-gate branch from ce7d50b to 3f1e227 Compare July 27, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors the map trace-route flyover traffic gating by extracting a deterministic, testable hysteresis/debounce state machine (MeshTrafficGate) and reworking MeshTrafficMonitor into a main-actor singleton that self-starts on first inbound packet (while removing the duplicated monitor file that was breaking builds).

Changes:

  • Added MeshTrafficGate (pure value-type) and new unit tests covering debounce + hysteresis behavior.
  • Replaced the prior EWMA/sampling-timer MeshTrafficMonitor with a windowed rolling-rate monitor that self-manages its decay timer and exposes a coarse isHighTraffic flag.
  • Updated AccessoryManager call sites to use the singleton and the new APIs (recordInboundPacket(), reset()), and removed the old monitor implementation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
MeshtasticTests/MeshTrafficGateTests.swift Adds Swift Testing coverage for the new gate’s debounce and hysteresis rules.
Meshtastic/Helpers/MeshTrafficMonitor.swift Introduces the new MeshTrafficGate + singleton MeshTrafficMonitor implementation and timer-driven decay.
Meshtastic/Accessory/Accessory Manager/MeshTrafficMonitor.swift Removes the previous EWMA + explicit start/stop monitor implementation.
Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift Drops explicit monitor start() call and updates explanatory comments.
Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift Switches to MeshTrafficMonitor.shared, updates packet-recording call, and resets monitor on disconnect.

Comment on lines +98 to +101
private var decayTimer: Timer?

private init() {}

Comment on lines +132 to +133
let rate = Double(timestamps.count) / Self.windowSeconds
packetsPerSecond = rate
Copilot AI review requested due to automatic review settings July 27, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

Meshtastic/Helpers/MeshTrafficMonitor.swift:154

  • The decay timer is scheduled on RunLoop.main, but the callback currently spawns a new Task every tick just to hop back to @MainActor. This adds unnecessary task allocation/latency and can also make timer-driven decay less predictable under load.

Since the timer already fires on the main run loop, use MainActor.assumeIsolated (as the previous implementation did) to call evaluate(now:) directly without creating a new task each time.

		let timer = Timer(timeInterval: Self.tickInterval, repeats: true) { [weak self] _ in
			// Fires on the main run loop; hop to the main actor to touch isolated state.
			Task { @MainActor in self?.evaluate(now: CACurrentMediaTime()) }
		}

Comment on lines +123 to +130
private func evaluate(now: TimeInterval) {
// Age packets out of the trailing window.
let cutoff = now - Self.windowSeconds
if let firstFresh = timestamps.firstIndex(where: { $0 >= cutoff }) {
if firstFresh > 0 { timestamps.removeFirst(firstFresh) }
} else {
timestamps.removeAll(keepingCapacity: true)
}
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.

2 participants