perf(device): an idle connected device no longer burns CPU every second - #514
Conversation
Two continuous background costs that every connected device paid whether or not anything was happening (issue #491, items 1 and 2): 1. MessageProducer's background loop woke ten times a second, forever, per device. The 100 ms timeout on its wait was never load-bearing — every state change the loop must observe (a send, a stop) sets the event first, and ManualResetEventSlim is sticky — so the wait is now unbounded and an idle producer wakes zero times. 2. The serial presence probe ran a whole-system SerialPort.GetPortNames() per connected device per second. The filesystem check now runs first, so on Unix (where a port name is a device node path) the enumeration does not run at all while the port is there; when it does run it comes from one snapshot shared by every polling transport in the process. The two sources are OR-ed, so the reordering cannot change the answer. A cached "yes" may be up to a second old, which moves worst-case unplug detection from ~3 s to ~4 s. A cached "no" is never reused: it would report a port plugged in since the snapshot as absent, which would stop a transport arming its presence check at connect time. Measured on the bench (Nq1 fw 3.7.2, serial, 30 s idle connected, 3 runs each): 230/255/257 ms CPU -> 93/79/86 ms, and 68.5 KiB allocated -> 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoPerf: eliminate idle device wakeups and cache serial-port enumeration
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1.
|
Qodo round 1: the XML doc said "since Start()" but nothing resets the counter. Corrected the doc rather than adding the reset — a stop whose Join times out leaves the previous background thread alive and still incrementing, so zeroing in Start() would be a counter with a race in it. Added a test pinning the cumulative semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 10a4218 |
|
Qodo-clean, CI green — ready for review. (2 rounds on head |
What was wrong
A DAQiFi device that is connected but doing absolutely nothing was not free. Every connected device woke a background thread ten times a second forever, and every second it also ran a whole-system scan of the machine's serial ports just to confirm its own port was still there. On a long-running consumer — the desktop app or the MCP server, which hold devices open for hours — that is continuous background CPU and garbage for no work, and it got worse with every extra device connected.
Measured on the bench with one device connected and completely idle, that came to ~250 ms of CPU and 68 KiB of garbage per 30 seconds, per device.
How it was fixed
Two independent changes, neither of which alters what the library does:
SerialPort.GetPortNames()and only falling back to "does the device node still exist". Those two are OR-ed, so the order cannot change the answer — and on macOS/Linux, where a port name is a path, the filesystem check answers outright and the system-wide enumeration never runs. When it does run (Windows), it comes from one snapshot shared by every polling transport instead of one scan per device.The one thing worth pushing back on is the snapshot's staleness. A cached "this port is present" can be up to a second old, so worst-case unplug detection moves from roughly three seconds to roughly four. A cached "this port is absent" is deliberately never reused — that direction is the dangerous one (it would report a port plugged in since the snapshot as missing, which would stop a transport from arming its presence check at all), so a cached miss always forces a fresh enumeration before answering.
Item 3 of the issue — the reader thread created per SCPI exchange — is not in this PR, so #491 stays open. It is not an idle cost (it only happens when you send a command), and removing it means reworking
StreamMessageConsumer's thread lifecycle, including the stale-reader guard that stops two readers ever landing on one stream. That deserves its own change.Verification
/dev/cu.usbmodem1101. Harness alternating against anorigin/mainbaseline worktree, 30 s idle with the transport connected and a producer running, 3 runs each: 230/255/257 ms CPU → 93/79/86 ms (7.7-8.6 → 2.6-4.0 ms per second) and 68.5 KiB allocated → 0, connection still healthy at the end of every run. OneGetPortNames()costs 0.11 ms on this machine, so the enumeration alone was ~0.11 ms per device per second. Health checks after: serial discovery findssn=9090539562006014104, 3 s @ 500 Hz on channels 0-2 → 1187 samples (this unit's known ~79 % clock ratio), SD storage query 7.80 GB, clean disconnect. No reboot, format, delete,SD:GET, firmware or LAN writes.Addresses items 1 and 2 of #491.
Not merging — this is for your review.