Repository navigation
Pointer stutters during long sessions; verboseDisplayLogging output is 100% <private> #202
Description
Pointer stutters on the extended display during long sessions — and verboseDisplayLogging output is 100% redacted, so stream snapshot is unreadable
Sender 3.5.2 (arm64 release asset) → Receiver 3.5.2, Thunderbolt Bridge, extendedDesktop / crisp2160p60, receiverDisplayProfile = lowLatency.
I'm on the setup that TargetBridge exists for, and video works, but the mouse pointer on the iMac is never smooth — it moves in visible steps and periodically freezes for a fraction of a second, all day long, on any preset. I turned on Settings → log virtual display events to Console hoping to find out why, and that's the second half of this report: the toggle produces no readable data at all.
Below is what the unified log actually contains for a single 6 h 47 min working session. Everything is from log show --predicate 'process == "TargetBridge"' --info --debug, no source-code reading, so it should be reproducible.
Environment
| Sender | MacBook Pro (M1 Pro, 16 GB), MacBookPro18,3, macOS 15.x |
| Receiver | 2019 27" 5K iMac (Radeon Pro 580X, Intel), TargetBridge Receiver 3.5.2 |
| Transport | thunderboltBridge over bridge0, link-local, MTU 1500 |
| Capture | captureSource = extendedDesktop, capturePreset = crisp2160p60 |
| Session flags | matchRenderToStream = true, inputGestureMode = native, inputControlRole = off, inputBindings = [] |
| Receiver profile | lowLatency |
| Addons | targetbridge.network-link disabled, targetbridge.audio-relay disabled |
| Verbose display logging | fd.tbdisplaysender.verboseDisplayLogging = true |
| Other display software | SwitchResX is installed on the sender |
The pointer is captured into the video stream (no receiver-master / cursor overlay in this configuration), so pointer smoothness is bounded by the frame rate the sender actually delivers. That framing matters for point 1.
1. The encoder delivered ~28.5 fps on average against a 60 fps preset
At session teardown:
2026-09-18 18:04:19.463 Df TargetBridge[5900:17a48] [com.apple.coremedia:VTCompression] <<<< VT-CS >>>>
VTCompressionSessionInvalidate: [0x7ae3fd1400:CS/YAO] invalidating compression session:
encoded 694814 dropped 2 duration 24389171 [ms]
694 814 frames ÷ 24 389 s = 28.5 fps sustained average for a session where the machine was in active use most of that time, with crisp2160p60 selected.
Importantly, the encoder itself looks healthy: 2 dropped frames out of 694 814. So the stutter is not VideoToolbox falling behind — whatever limits the frame rate is upstream (capture / pacing) or is the preset not actually being sustained.
I know SCStream is change-driven, so a low average over a whole day is expected; what I can't work out from the log is the instantaneous rate while I'm moving the mouse — which is exactly the number that determines whether the pointer looks smooth. TBFrameRatePacer / droppedPacing / droppedPre / droppedPost are all in the snapshot line (see point 5) and would answer it, but they're redacted.
Question: is crisp2160p60 expected to hold 60 fps of pacer-passed frames during active use on Apple Silicon, and what does TBFrameRatePacer do when matchRenderToStream = true?
2. Main-thread preference reads every ~1 s for the whole session
For the same 6.79 h session (pid 5900), the sender's own log output is dominated by two things:
| count | |
|---|---|
| structured log lines from the sender process | 5 726 |
Loading Preferences From User CFPrefsD |
1 763 |
… of those on the main thread (:17a48) |
1 762 |
(Foundation) <private> (app's own messages, redacted) |
1 491 |
| prefs reads + redacted lines | 56.8 % of all lines |
Median gap between consecutive main-thread preference reads: 0.77 s; 915 of them are less than 1 s apart, 490 less than 0.1 s apart. Peak minutes hit 71 reads/min. Sample, mid-session, with no clicking in the TargetBridge UI at all:
2026-09-18 17:13:21.210 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:21.220 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:22.070 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:22.980 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:22.988 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:23.944 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:23.953 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:25.307 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
2026-09-18 17:13:26.680 A TargetBridge[5900:17a48] (CoreFoundation) Loading Preferences From User CFPrefsD
Each of those is a synchronous XPC round-trip to cfprefsd. The binary contains an fpsTimer symbol, and this is a ~1 s main-thread cadence, so my guess is a status/fpsTimer path that re-reads UserDefaults (probably sender.label.sender_fps) every tick instead of caching it.
This is the same root-cause shape as #83 ("the sender app was on the same thread and refreshing often" → "Stopped needless refreshing of sender"), and #81/#82 moved the video pipeline off the main thread. I'm not reporting a regression so much as reporting that on 3.5.2 the main thread still appears to be doing per-tick work during streaming.
For completeness: with the app open and no session attached, I see 0 preference reads in a 70 s live capture — so this is tied to the streaming state, not launch.
3. Apple's own runtime asserts that TargetBridge blocks the main thread
48 Fault-level Performance Diagnostics entries over the window, always in bursts of exactly 6 — one burst at every session attach (8 attached sessions in the capture):
2026-09-18 11:17:50.094 F TargetBridge[5900:17ca7] [com.apple.runtime-issues:Security]
__delegate_identifier__:Performance Diagnostics__:::____message__:
This method should not be called on the main thread as it may lead to UI unresponsiveness.
(×6, within 13 ms of each other, immediately after SLSCompleteDisplayConfigurationWithOption for the virtual display.)
These come from the Security framework's own perf-diagnostics hook, so they're Apple telling us a Security API is being called synchronously on the main thread while the display is being attached. Combined with point 2, this is the only concrete main-thread-stall evidence in the log, and it's the kind of stall that shows up as a hitching pointer.
I can't see which call, because the entry that would name it is redacted.
4. Three TCP flows to port 54321 retry for the entire session and only connect at teardown
The session spent 6.79 h with three Network.framework connections pointed at the receiver's port 54321, on two interfaces:
| conn | endpoint | interface | outcome |
|---|---|---|---|
| C12, C14 | IPv4#d9f1b25f:54321 |
en7 |
never connected |
| C13 | IPv4#6cfb374d:54321 |
bridge0 |
never connected |
| C21, C22 | same two endpoints | — | ready at 18:04:20, i.e. during teardown — VTCompressionSessionInvalidate is at 18:04:19.463 and the CDVirtualDisplayDestroy at 18:04:19.553 |
Log totals for the session: 394 lines mentioning :54321, 513 state=SYN_SENT outputs, 392 SYN retransmits, 37 nw_protocol_tcp_log_summary blocks, 117 Operation timed out, and exactly 2 reporting state ready — both at the very end.
2026-09-18 12:14:33.628 Df TargetBridge[5900:2fd11] [com.apple.network:tcp] nw_protocol_tcp_log_summary [C14:3]
Init: 1, Conn_Time: 0.000ms, SYNs: 13, WR_T: 0/0, RD_T: 0/0, ...
rtt_cache: process, rtt_upd: 0, rtt: 0.000ms, rtt_var: 250.000ms ...
2026-09-18 12:14:33.630 E TargetBridge[5900:2fd11] [com.apple.network:connection] nw_endpoint_flow_failed_with_error
[C14 IPv4#d9f1b25f:54321 ... interface: en7] already failing, returning
2026-09-18 12:14:33.630 Df ... event: flow:failed_connect @3381.778s, error Operation timed out
2026-09-18 12:14:33.630 Df ... reporting state waiting error Operation timed out
Note @3381s, later @7376s, @10079s, @16312s, @22934s — the same connection objects are still retrying 6 hours later, and waiting is treated as a state to sit in rather than a failure to back off from. Also note that one of the two endpoints is on en7 (the LAN), which has no path to a link-local Thunderbolt receiver, and the network-link addon is disabled — so the sender is retrying an endpoint it can't possibly be meant to reach.
The video stream obviously did work during those hours, so this must be a control/secondary channel rather than the frame path. Two things I'd like to understand:
- Should a flow that never completes in ~30 s be retried for 6 hours with no backoff and no cancellation? 392 SYN retransmits is background churn on a machine that's already encoding 4K.
- Is it meaningful that the only two handshakes that ever succeeded happened right as the session was being torn down — i.e. do the stale retrying flows occupy the receiver's single
acceptslot, so a fresh connection can only land after the old ones are destroyed? That would fit what I see: sessions that attach fine one day and hang on "waiting for first frame" the next.
This looks adjacent to the open PR #197 ("remember receivers independently of IP") — my saved receiver has moved from 169.254.93.76 to 169.254.115.53, and the retries to the old identity kept going for hours afterwards.
5. verboseDisplayLogging = true yields zero readable app output — every line is <private>
This is the part that blocks me (and, I'd guess, anyone else) from filing a useful bug.
The sender's messages are in the binary as:
TargetBridge: stream snapshot streaming=%@ fps=%d virtualID=%u online=%@ pendingPackets=%d
inFlightEncode=%d ptsSeq=%lld captured=%d droppedPacing=%d droppedPre=%d droppedPost=%d
capture: pipeline started preset=%{public}s source=%{public}s codec=%{public}s rawNV12=%{bool,public}d
capture: first-frame timeout preset=%{public}s source=%{public}s connected=%{bool,public}d sentFrames=%{public}ld
Searching 20 days of the unified log for stream snapshot, capture: pipeline, first-frame, droppedPacing, or any message beginning TargetBridge: returns 0 matches. Instead, all 1 491 of the app's own lines in the session appear as:
2026-09-18 14:55:35.472 Df TargetBridge[5900:17a48] (Foundation) <private>
i.e. they arrive as a fully redacted NSLog("%@", <built string>), not as an os_log call with a literal format string. Because the whole message is one dynamic argument, even the %{public} integer fields in capture: pipeline started are hidden — so the one log line designed to answer "what fps and what drop counters" is exactly the line I can't read. The cadence is still visible (≈5 s during streaming, a 60 s heartbeat when idle), so the timer is running and emitting.
input-debug.log has the same problem from the other side. The whole file, across 3 weeks, contains one message shape and it's duplicated:
2026-09-18 02:58:36Z sender send control mode update off to 169.254.93.76
2026-09-18 02:58:36Z sender send control mode update off to 169.254.93.76
72 lines, 39 distinct, 33 of them written twice, and nothing about input, HID, or the pointer at all — in a file named input-debug.log.
Suggested fix (small, and it would make future reports self-diagnosing):
- Emit the snapshot with a literal format string and per-field annotations, e.g.
os_log("stream snapshot streaming=%{public}@ fps=%{public}d ... droppedPost=%{public}d", ...)
rather than building aStringand logging it as one%@argument. The integer counters don't need privacy. - Log the snapshot on the same path as
verboseDisplayLoggingso the toggle demonstrably produces output, and gate the 60 s idle heartbeat off (or label it). - De-duplicate the
input-debug.logwriter.
Happy to re-run a session with sudo log config --mode "private_data:on" and attach readable snapshots if that's the intended path — but I'd rather not require users to change global logging privacy to use a feature that's already labelled "verbose".
6. Two smaller things, same log
AppKit layout faults. 184 Fault entries, all in one window (Sep 18, 10:00–11:05, while I was attaching and re-attaching sessions), always in width/height pairs:
2026-09-18 10:53:43.N F TargetBridge[4263:f6ef] [com.apple.runtime-issues:AppKit] Invalid view geometry: width is negative.
2026-09-18 10:53:43.N F TargetBridge[4263:f6ef] [com.apple.runtime-issues:AppKit] Invalid view geometry: height is negative.
Some view is being laid out with a negative rect — likely the status-bar item / settings popover, given it correlates with sessions being started and stopped.
The shipped arm64 sender is a debug-dylib build. Contents/MacOS/TargetBridge is a 57 KB stub whose only LC_LOAD_DYLIB is @rpath/TargetBridge.debug.dylib, and __preview.dylib is alongside it; vmmap confirms TargetBridge.debug.dylib is mapped in the running process. If the release asset is genuinely a -Onone / Xcode-Preview build, that's a real cost on a 4K encode path, and probably not intended for a v3.5.2 download. (If ENABLE_DEBUG_DYLIB just survives into release archives in your CI, that's worth pinning down too.)
What I'd like out of this
- A pointer/cursor overlay path for the sender-master /
extendedDesktopconfiguration. Sender: use low-latency cursor overlay in Receiver Master #166 added a low-latency cursor overlay for Receiver-Master; in my configuration the cursor is burned into the encoded frames, so any dip below 60 fps is directly pointer stutter. Decoupling pointer compositing from the video frame rate would fix the symptom even if the encoder stays where it is. - Confirmation of whether ~28.5 fps average vs
crisp2160p60is expected, and whethermatchRenderToStream = trueintentionally caps render to stream rate. - Caching
UserDefaultsreads off thefpsTimertick (point 2) and moving the main-thread Security call off the main thread (point 3). These are cheap and they're the only measured main-thread stalls I have. - A backoff + cancel on the 54321 retry loop, and not retrying an endpoint on an interface that can't reach it (point 4).
- Fixing the snapshot logging (point 5) so that Memory leak fixes #6-level reports come with numbers next time.
Steps to reproduce the log capture
defaults write com.targetbridge.sender fd.tbdisplaysender.verboseDisplayLogging -bool true
# start a session, work for ~1 h, then:
log show --last 1h --style compact --info --debug \
--predicate 'process == "TargetBridge"' > tb-sender.log
grep -c 'Loading Preferences From User CFPrefsD' tb-sender.log # ~1 per second on the main thread
grep -c '(Foundation) <private>' tb-sender.log # every app message, redacted
grep 'VT-CS' tb-sender.log # encoded/dropped/durationFull log
The complete reduced log for this session is 617 lines / 134 KB (all Fault/Error entries, all :54321 network lines, the VT-CS and CDVirtualDisplay teardown lines, and a sample of the redacted app lines; the 1 763 Loading Preferences lines are summarised in the table rather than repeated, and remote endpoints appear only as Network.framework IPv4#<hash> — no hostname or LAN address). Say the word and I'll attach it here.
Related
- Stuttering after overnight use finally fixed! #83 — Stuttering after overnight use finally fixed! ("sender app was on the same thread and refreshing often")
- Sender: capture watchdog + UI re-render storm mitigations #81 — capture watchdog + UI re-render storm mitigations
- Sender: fix long-session stutter — real-time PTS + move video pipeline off the main thread #82 — fix long-session stutter — real-time PTS + move video pipeline off the main thread
- Sender: use low-latency cursor overlay in Receiver Master #166 — use low-latency cursor overlay in Receiver Master
- feat(discovery): remember receivers independently of IP #197 — remember receivers independently of IP (open)
- fix(sender): distinguish idle capture from a stalled pipeline #195 — distinguish idle capture from a stalled pipeline
Version 3.5.2 sender + receiver, macOS 15.x sender, debug-dylib build, bridge0 transport, crisp2160p60, lowLatency receiver profile.
Activity
estuo commentedon Oct 1, 2026
I may have a related but more specifically isolated pointer-stutter case on 3.5.2.
Setup:
The important part is that I can reproduce the pointer stutter without looking at the streamed iMac image at all.
With Receiver Master active, I move the mouse connected to the iMac while watching the pointer directly on the MacBook Air's built-in display. The pointer itself moves in clearly visible steps / jumps, as if mouse-motion events were being delivered at a very low polling rate.
If I instead use the mouse/trackpad locally on the MacBook Air, pointer motion on the same built-in display is perfectly smooth.
So in my case the stutter is independent of:
It appears to be specifically in the Receiver Master input path:
Receiver mouse capture → input relay → Sender event injection.
Keyboard input appears much less problematic; the continuous mouse-motion path is what is visibly coarse.
This may be different from the original issue here, because my test isolates the input relay itself rather than the video/capture cadence.
swellweb commentedon Oct 1, 2026
Thanks, @estuo. Your direct physical-display test is particularly helpful: it separates the Receiver Master input relay from the capture/encode path.
I have opened #205 for a focused 3.5.3 hotfix that removes synchronous debug-file I/O and repeated event-source allocation from the Sender mouse hot path. It deliberately does not alter capture, rendering, or the protocol.
The broader diagnostics, reconnect and logging items in this report remain tracked separately so we can measure their effect without masking this input regression.
swellweb commentedon Oct 14, 2026
Thanks again, @estuo. 3.6.0 is now published with standalone Release-built Sender packages, stable Receiver identities, and migration of saved preferences. The mouse-relay hot-path changes are also included, but they address Receiver-control input injection, not the captured-pointer path in your input-control-off setup.
I am not treating this report as fully fixed: the captured cursor, public numeric diagnostic logging, pacing, and long-session connection behavior still need separate measurement. A low whole-session average alone cannot establish the active-motion frame rate, as you noted.
If you can repeat the test on compatible 3.6.0 Sender and Receiver builds, please share the exact build numbers, preset, render-matching setting, session duration, and FPS during active movement. I do not need you to enable global private-data logging. Please review any logs for personal information before attaching them. Keeping this open for the stability follow-up.