TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning - #6580
Open
Jidos86 wants to merge 3 commits into
Open
TUN inbound (Darwin): Wait() blocks via kqueue instead of busy-spinning#6580Jidos86 wants to merge 3 commits into
Jidos86 wants to merge 3 commits into
Conversation
DarwinTun.Wait() was procyield(1) -- a CPU-yield hint, not a real blocking wait. stack_gvisor_endpoint.go's dispatchLoop (a single dedicated goroutine) calls ReadPacket() then Wait() in a tight loop with no other throttling whenever the tun's non-blocking fd has nothing to read, so this pinned a full CPU core for the entire connected lifetime of the tunnel, independent of actual traffic volume -- observed causing severe, sustained device heating on a real iPhone 16 Pro (severe enough that iOS's own thermal management disabled the camera flash). tun_android.go builds its link endpoint via gVisor's own fdbased.New(...) -- a properly blocking fd-based endpoint -- and never had this issue. This adds a kqueue registered for EVFILT_READ on the tun fd, and has Wait() genuinely block on it (1s bounded timeout, so a racing Close() stays responsive) instead of yielding and immediately re-looping. Falls back to the original procyield behavior if kqueue setup ever fails, so this can only make things better or leave them unchanged, never worse. Verified locally: applies cleanly at this commit, and the patched package (proxy/tun) plus the whole dependent libXray module cross-compile successfully for GOOS=darwin GOARCH=arm64. Not a gVisor-internals expert -- there may be a more elegant fix (perhaps fdbased could be adapted for Darwin the way Android uses it, if the fd differences allow it). This is what fixed the issue in real-device testing; a maintainer may well prefer a different approach. Found and fixed with the help of Claude (Anthropic's AI coding assistant). Fixes XTLS#6579
Member
|
procyield 这里大抵是被滥用了 该删掉的 |
Reviewer feedback (Fangliding): the kqueue-setup-failure fallback still called procyield(1) -- the exact busy-spin this whole change exists to remove, just gated behind an edge case (kqueue setup failing, which practically never happens on a real Darwin system) instead of always. A genuine time.Sleep actually yields the CPU for a bounded duration, unlike procyield's near-instant scheduler hint, which would let the tight dispatchLoop caller (stack_gvisor_endpoint.go) spin just as hot as before if this path were ever actually hit. The now-unused //go:linkname procyield declaration is removed too rather than left as dead code. Verified via local cross-compile (darwin/arm64 and ios/arm64) -- go build/go vet both clean.
…usy-spin # Conflicts: # proxy/tun/tun_darwin.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6579.
What
DarwinTun.Wait()wasprocyield(1)-- a CPU-yield hint, not a real blocking wait.stack_gvisor_endpoint.go'sdispatchLoop(a single dedicated goroutine) callsReadPacket()thenWait()in a tight loop with no other throttling whenever the tun's non-blocking fd has nothing to read, so this pinned a full CPU core for the entire connected lifetime of the tunnel, independent of actual traffic volume.For comparison,
tun_android.gobuilds its link endpoint via gVisor's ownfdbased.New(...)-- a properly blocking fd-based endpoint -- and never had this issue.Impact
Observed causing severe, sustained device heating on a real iPhone 16 Pro during active tunnel connections -- severe enough that iOS's own thermal management disabled the camera flash ("iPhone needs to cool down before using flash"). See #6579 for the full writeup.
The fix
Adds a kqueue registered for
EVFILT_READon the tun fd, and hasWait()genuinely block on it (1s bounded timeout, so a racingClose()stays responsive) instead of yielding and immediately re-looping. Falls back to the originalprocyieldbehavior if kqueue setup ever fails, so this can only make things better or leave them unchanged, never worse.Testing
proxy/tunpackage (and the whole dependent libXray module) cross-compiles successfully forGOOS=darwin GOARCH=arm64.Note
This was found and fixed with the help of Claude (Anthropic's AI coding assistant), while debugging a real thermal-management report on iOS. I'm not a gVisor internals expert -- there may well be a more elegant fix (maybe even adapting
fdbasedfor Darwin the way Android uses it, if the fd differences allow it). I'm just sharing what fixed the issue in my own testing, not claiming this is the right solution -- happy for a maintainer to take a completely different approach.