Repository navigation
perf: [AI-written] 5 performance optimizations - #58
T3ST3ST3R0N merged 14 commits into
Conversation
…mpling Removed the 1-second time.Sleep inside getBandwidthSpeed() that was blocking the system stats collection goroutine. Replaced with persistent package-level state (lastRxBytes, lastTxBytes, lastTime) that computes bandwidth deltas from cumulative network counters on each call. This eliminates the CPU wait-state caused by the sleep, reduces the effective stats collection cycle from ~2.5s to 1.5s, and keeps the goroutine fully non-blocking.
…ontention Replaced the long critical section in Config.ToBytes() that held read locks on ALL inbound mutexes during JSON serialization. Now each inbound is snapshotted individually under a microsecond lock/unlock, producing a detached Inbound copy. The heavy json.Marshal runs on the detached copy with zero locks held. This prevents concurrent SyncUser API calls from being blocked by config serialization, reducing lock contention on multi-inbound configurations.
… pressure Added a package-level sync.Pool that recycles the 64KB bufio.Scanner buffer across Xray process restarts instead of allocating a fresh buffer on every captureProcessLogs() call. This eliminates one 64KB heap allocation per Start() invocation, reducing GC pressure on memory-constrained VPS instances.
…timize startup context core.go: Added cleanExit flag to Core struct. When Xray exits cleanly (expected shutdown), the flag is set so the next Start() skips the expensive /proc or ps-based orphan process scan. The scan only runs when Xray crashed unexpectedly, avoiding unnecessary CPU spikes on normal restarts. controller.go: Removed the redundant context.WithCancel() allocation in New() that was immediately discarded and replaced in Connect(). Added a nil-guard on cancelFunc in Disconnect() for safe pre-Connect teardown.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR makes five independent improvements: refactors Xray config serialization to use read-locked snapshots instead of mutating live ChangesBackend and System Improvements
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1
✨ Finishing Touches
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. 🐇 No more blocking for a second just to measure the byte, Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
backend/xray/config.go (1)
327-330: 💤 Low valueShallow copy of
Settingsmap shares nested references.The for-range copy creates a new map but values remain shared references. If any value is a mutable type (nested map/slice) that gets modified concurrently elsewhere, the snapshot could observe partial updates. This appears acceptable given that Settings values are typically primitives or read-only config data, but worth noting if the data model evolves.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/xray/config.go` around lines 327 - 330, The for-range copy of inbound.Settings creates a new map but leaves values as shared references, which could cause issues if any value is a mutable type (nested map/slice) that gets modified concurrently. While this appears acceptable since Settings values are typically primitives or read-only config data, add a comment above the snap.Settings map copy loop in the range assignment block explaining this shallow copy behavior and the conditions under which it would need to be changed to a deep copy (i.e., if the data model evolves to include mutable nested types).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/xray/config.go`:
- Around line 425-428: Add a nil check for snapConfig.LogConfig before
attempting to access the AccessLog and ErrorLog fields on lines 425-428. Wrap
the code block that accesses snapConfig.LogConfig.AccessLog and
snapConfig.LogConfig.ErrorLog with a guard condition that verifies
snapConfig.LogConfig is not nil first. This prevents a nil dereference panic
when the parsed config JSON lacks a log section.
- Around line 425-433: The snapConfig.LogConfig pointer references the live
c.LogConfig object, causing mutations during serialization to race with
concurrent readers and creating a nil dereference risk if c.LogConfig is not
initialized. Replace the approach of temporarily blanking the AccessLog and
ErrorLog fields by instead creating a copy of the LogConfig struct within the
snapshot with the log fields set to empty strings. Add a nil guard to check if
c.LogConfig exists before attempting to copy it, returning appropriate handling
(such as using an empty LogConfig struct or skipping the copy) when c.LogConfig
is nil. This eliminates both the race condition and the potential panic from
dereferencing a nil LogConfig pointer.
In `@controller/controller.go`:
- Around line 68-70: The `Disconnect()` method reads `c.cancelFunc` without
holding the lock `c.mu`, while the `Connect()` method writes to `c.cancelFunc`
under lock synchronization. This creates a data race on concurrent access. Wrap
the read of `c.cancelFunc` in the `Disconnect()` method (the nil check and the
call to `c.cancelFunc()`) by acquiring `c.mu` lock before the access and
releasing it after, matching the synchronization pattern used in `Connect()` to
ensure thread-safe access to this shared field.
---
Nitpick comments:
In `@backend/xray/config.go`:
- Around line 327-330: The for-range copy of inbound.Settings creates a new map
but leaves values as shared references, which could cause issues if any value is
a mutable type (nested map/slice) that gets modified concurrently. While this
appears acceptable since Settings values are typically primitives or read-only
config data, add a comment above the snap.Settings map copy loop in the range
assignment block explaining this shallow copy behavior and the conditions under
which it would need to be changed to a deep copy (i.e., if the data model
evolves to include mutable nested types).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7c115af5-bfc3-4639-b127-91462ae04ae8
📒 Files selected for processing (5)
backend/xray/config.gobackend/xray/core.gobackend/xray/log.gocontroller/controller.gopkg/sysstats/sys.go
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@controller/controller.go`:
- Around line 68-84: The Disconnect() method has duplicate code blocks that
cause compilation errors. Remove the second backend variable declaration block
starting at line 81 (the duplicate c.mu.Lock(), backend := c.backend,
c.mu.Unlock() sequence) to eliminate the duplicate `:=` assignment of the
backend variable in the same scope and complete the Disconnect() function
properly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: da52e867-2f31-426c-a11c-5a9bad2d1be9
📒 Files selected for processing (1)
controller/controller.go
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@controller/controller.go`:
- Around line 68-84: The Disconnect() method has duplicate code blocks that
cause compilation errors. Remove the second backend variable declaration block
starting at line 81 (the duplicate c.mu.Lock(), backend := c.backend,
c.mu.Unlock() sequence) to eliminate the duplicate `:=` assignment of the
backend variable in the same scope and complete the Disconnect() function
properly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: da52e867-2f31-426c-a11c-5a9bad2d1be9
📒 Files selected for processing (1)
controller/controller.go
🛑 Comments failed to post (1)
controller/controller.go (1)
68-84:
⚠️ Potential issue | 🔴 Critical | ⚡ Quick winRemove duplicated
Disconnect()block — current code does not compile.At Line 68,
Disconnect()is declared a second time inside itself, which causes the parser error reported by typecheck. Also, Line 83 re-declaresbackendwith:=in the same scope as Line 79, which is another compile-time failure. Keep only one function header and one backend snapshot block.Proposed minimal fix
func (c *Controller) Disconnect() { -func (c *Controller) Disconnect() { c.mu.Lock() cancel := c.cancelFunc c.cancelFunc = nil c.mu.Unlock() if cancel != nil { cancel() } c.mu.Lock() backend := c.backend c.mu.Unlock() - - c.mu.Lock() - backend := c.backend - c.mu.Unlock() // Shutdown backend outside of lock to avoid deadlock // Shutdown() will wait for process termination to complete if backend != nil { backend.Shutdown()🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 68-68: : # github.com/pasarguard/node/controller [github.com/pasarguard/node/controller.test]
controller/controller.go:68:35: syntax error: unexpected { at end of statement(typecheck)
[error] 68-68: expected ';', found '{'
(typecheck)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/controller.go` around lines 68 - 84, The Disconnect() method has duplicate code blocks that cause compilation errors. Remove the second backend variable declaration block starting at line 81 (the duplicate c.mu.Lock(), backend := c.backend, c.mu.Unlock() sequence) to eliminate the duplicate `:=` assignment of the backend variable in the same scope and complete the Disconnect() function properly.Source: Linters/SAST tools
SnapshotToBytes: replaced shared pointer copy of LogConfig with a value copy. When c.LogConfig is non-nil, the struct is dereferenced and copied locally so the AccessLog/ErrorLog fields can be blanked without mutating the live object. When c.LogConfig is nil, the field is left nil and skipped, preventing a nil pointer dereference panic. Also added a shallow-copy comment on the Settings map loop documenting the current safety assumption and the conditions that would require a deep copy in the future.
…d fix cancelFunc race CodeRabbit's auto-commit duplicated the Disconnect() function signature and the backend read block. Removed the duplicates. The cancelFunc race fix is retained: read under lock, nil out, call outside lock.
Resolve the conflicts against current dev: - backend/xray/config.go: keep the ToBytes snapshot on top of dev's config.go. - backend/xray/log.go: keep dev's isAccessLog; keep the scanner buffer pool. - controller/controller.go, pkg/sysstats/sys.go: take dev's versions. Dev already nil-checks cancelFunc in Disconnect under the lock and samples bandwidth once per GetSystemStats call with context support, which covers these changes.
…napshot The snapshot wrote an empty clients list into excluded inbounds, wiping the clients an admin wrote by hand, and had no WireGuard case, so Xray WireGuard inbounds got no peers and a stray clients key. Serialize excluded inbounds unchanged, write WireGuard users as peers, and copy the whole Config so new top-level fields are not dropped. Tests cover excluded and managed inbounds, WireGuard peers, the log file handling, that ToBytes leaves the stored settings alone, and concurrent calls under -race.
A replaced process's exit handler can run after the next Start and mark the running core as cleanly exited, so a later start after a crash would skip the scan. The scan is one /proc read per start, so keep it unconditional.
When an interface is recreated between the two samples its counters restart, and the unsigned subtraction wrapped around to a huge bandwidth value. Skip that direction for that sample.
The snapshot copies Inbound fields by hand because Inbound holds a mutex, so a field added later without a matching copy would silently vanish from the Xray config. Fill every exported field by reflection and compare.
A counter that went down between samples was reset, so its current value is the traffic since the reset; add it instead of skipping the interface for that sample (review suggestion).
SyncUsers clones the live config under syncMu while a health-check restart serializes it under x.mu. Dev's ToBytes wrote settings maps that Clone reads without a lock (14 race reports with -race; without it Go can abort with a concurrent map read and write). The snapshot leaves them alone.
|
Thanks for this, @wahh3b-lgtm!
Thanks again! |
…ts owner Main's decision G1 (B). None of these lines claims a port, or coverage by a branch that is not on main. - a3d2d2f (PasarGuard#101): owned by NODE-WG (wgnft), until 2026-11-10. - 33ab5fb (PasarGuard#58): owned by NODE-SYNC-055 and NODE-XRAY-RACE, until 2026-11-10. - xray 7da5dae: its verified port by NODE-MISC-XMUX is queued after this cut, until 2026-11-20. - xray b291961, 8d0a7cf and 72b4d0c, each on its upstream branch and scoped to it: reachability awaits NODE-MISC-UPX, until 2026-11-10. - sing-box 42cae14 and release v1.14.4: PUSH-GATE, with the sing-box 1.14 move, until 2026-11-20.
… (upstream PasarGuard#114) Upstream PasarGuard/node 946ab6a (PasarGuard#114, released in v0.5.5) ships these tests with its fix. On the parent: - TestShadowsocks2022InboundStartedWithoutUsers (real xray, v26.3.27 here): "the bare server key opened a connection and received 478773248 bytes", then "adding the first user to the inbound failed". The pinned Xray (infra/conf/shadowsocks.go buildShadowsocks2022) builds an inbound with an empty client list as a single-user ServerConfig on the server key, which every Shadowsocks 2022 link carries, and that server refuses every AddUser. - TestToBytesStartsEmptyShadowsocks2022InboundsMultiUser: "aes128: an inbound without users was built as *shadowsocks_2022.ServerConfig, want a multi-user server", and the same for aes256. - TestThePlaceholderLeavesOnceAShadowsocks2022InboundGetsAUserAndIsNeverAUser (fork test): an empty aes128 inbound serialises no client at all. TestToBytesWritesOnlyRealShadowsocks2022Users passes on the parent and pins that real users replace the placeholder. shadowsocks2022_test.go is upstream's apart from its comments, which are not carried (no explanatory comments in this fork). Upstream's two ToBytes tests use PasarGuard#58's config_tobytes_test.go helpers, which the fork does not carry, so they are rewritten on ToBytes' own output with the same assertions. The third ToBytes test is the fork's: a clone taken after ToBytes holds no client, a real user replaces the placeholder, and the placeholder comes back when the last user leaves. Upstream: 946ab6a
…empty (upstream PasarGuard#114) Port of upstream PasarGuard/node 946ab6a (PasarGuard#114, released in v0.5.5). Xray builds a Shadowsocks 2022 inbound whose client list is empty as a single-user server. That server accepts the bare server key, which every Shadowsocks 2022 link carries, so anyone who ever held a link could use the inbound without being counted, and it refuses every later AddUser, so users given the inbound after the core started never got access. An empty 2022-blake3-aes inbound now serialises one placeholder client whose random key never leaves the node process, so Xray always builds the multi-user server. The placeholder exists only in the serialised config: it is never a runtime client, never reported, and gone once the inbound has a real user. chacha20 has no multi-user mode in Xray and is left alone. The placeholder code is upstream's. Upstream adds it to PasarGuard#58's snapshot serialiser; this fork's ToBytes still writes the client lists in place (the snapshot serialiser is the fork's own 57958d9 on release/v0.6.21-acct-xrayrace, not on this base), so the placeholder goes into the empty-client branch of ToBytes. Merging 57958d9 must move it into wireClientsLocked's empty case. Upstream's comments are not carried (no explanatory comments in this fork). Production has no Shadowsocks 2022 inbound today (core 1's only Shadowsocks inbound has no 2022 method), so this is latent there. Upstream: 946ab6a
…hileCloning Port of the two -race tests of upstream PasarGuard/node backend/xray/config_tobytes_test.go (v0.5.5/v0.5.6, PasarGuard#58). The upstream fixture carries a WireGuard inbound whose peers depend on upstream PasarGuard#64, which this fork has not ported (pending the xray pin move), so the fork fixture swaps it for an empty Shadowsocks 2022 inbound that exercises the PasarGuard#114 placeholder path, and the concurrent writer alternates a full user set with an empty one. Measured: on 526beaf (ToBytes writes the live inbound settings under a read lock) both tests fail under -race with 52 DATA RACE reports; on this head (snapshot serialiser) both pass.
…ps the bandwidth around Upstream's TestBandwidthDeltaCountsTrafficSinceACounterReset from PasarGuard#58 (33ab5fb, released in v0.5.5), without its explanatory comment line. On main bandwidthDelta subtracts a smaller counter from a larger one and reports 18446744073709546756 incoming bytes for wg0 after a re-creation.
… xray log scanner buffer Ports the two parts of upstream PasarGuard#58 (33ab5fb, released in v0.5.5) that main did not carry yet: counterDelta in pkg/sysstats/sys.go, so an interface re-created between samples reports the traffic since its reset instead of wrapping around, and the sync.Pool for the 64 KiB scanner buffer in backend/xray/log.go. Upstream's doc comment on counterDelta is left out. The snapshot serialisation of PasarGuard#58 is already on main as the fork's 797adcf with two of upstream's ToBytes tests (9c99b8a); the other seven ToBytes tests need the fork's test helpers adapted (wireguardUser does not exist in the fork's xray tests) and stay pending, so this commit carries no Upstream: trailer. TestBandwidthDeltaCountsTrafficSinceACounterReset from the previous commit passes; pkg/sysstats -race and the xray log tests pass.
…rted on main 71466f2 (PasarGuard#113) is ported on main by the series pushed in 26bba5a, and c2ed985 carries its Upstream: line, so the check already reports it PORTED; its pending waiver, which still said this branch did not contain the port, is removed and the parity table gets its row. 33ab5fb (PasarGuard#58) is only partly on main: the snapshot serialisation (797adcf), two of upstream's nine ToBytes tests (9c99b8a), and now counterDelta and the log buffer pool (83932cb, RED f7891b1). Its waiver is rewritten to say exactly that, owned by NODE-LEAD, with the same 2026-11-10 date; the seven remaining ToBytes tests need the fork's xray test helpers adapted.
The seven tests of upstream config_tobytes_test.go (33ab5fb) that 9c99b8a did not take, from NODE-VERIFY's round 4 test file. The WireGuard inbound, the wireguardUser and the peer assertions of TestToBytesWritesSyncedClientsAndPeers and TestToBytesWritesEmptyListsWithoutUsers are left out: they test the xray WireGuard peer serialisation that upstream PasarGuard#64 (e03754d) added and the fork has not taken. TestSnapshotInboundCopiesEveryExportedField calls the fork's (*Inbound).snapshotLocked under the inbound's read lock in place of upstream's snapshotInboundForSerialization; both copy the exported fields by hand.
…sarGuard#58 as ported except its WireGuard parts Upstream PasarGuard#113 (71466f2) also adds Inbound.canHoldUsers and two tests. The fork sends a delta-sync removal only where the inbound held the user or a call for it is unsettled, so the hunk is superseded; 921eb1d pins the behaviour (finding F-R4-3). Upstream PasarGuard#58 (33ab5fb) is now ported except its xray WireGuard parts, which rewrite the peer serialisation of the unported PasarGuard#64 (e03754d). Its TestSnapshotInboundCopiesEveryExportedField is ported, not waived: the fork's snapshotLocked copies the exported Inbound fields by hand like upstream's snapshotInboundForSerialization, and the test fails when one copy is dropped. The 33ab5fb waiver stays, with its due date, and now says exactly what is left.
waiver Upstream PasarGuard#58 (33ab5fb) is ported except its xray WireGuard parts. The fork commits that carry it are 797adcf (the config snapshot serialisation), 9c99b8a (TestToBytesConcurrentCalls and TestToBytesWhileCloning), 83932cb (counterDelta and the xray log scanner buffer pool, with its RED test f7891b1) and 0687f7e (the other seven tests of upstream config_tobytes_test.go, among them TestSnapshotInboundCopiesEveryExportedField against the fork's snapshotLocked). Not ported: the Wireguard peers case of the config snapshot, and the wg inbound, wireguardUser and peer assertions of TestToBytesWritesSyncedClientsAndPeers and TestToBytesWritesEmptyListsWithoutUsers. They rewrite the WireGuard peer serialisation that PasarGuard#64 (e03754d) adds and the fork does not have, so they go in with the PasarGuard#64 port. By the main session's decision of 2026-11-03 the e03754d waiver now tracks them, with its owner NODE-WG and its date 2026-11-20 unchanged, and the separate 33ab5fb waiver (NODE-LEAD, due 2026-11-10) is removed. The trailer below makes the drift check count PasarGuard#58 as ported; the WireGuard remainder stays open under the e03754d waiver until the PasarGuard#64 port. Upstream: 33ab5fb
…d#114 placeholder's home and the coalesce test debt F-VSB-1: d189e94 names sing-box 52f9bad0 as ported, but a930417d2 carries only its sing-box source hunks. The library halves of the same fix, SagerNet/sing 8d939df03478, sing-mux caf09fe32475 and sing-tun 0bdadeb4c934, are not in the image: the fork's go.mod at 3f35dc15e0b4 takes sing v0.8.14, sing-mux v0.3.5 and sing-tun v0.8.15 with no replace, and each commit is ahead of that tag (GitHub compare, 2026-11-03). The drift check does not watch those repositories, so the sing-box section now records them, owned by NODE-LEAD, to be taken with the next sing-box pin move and no later than 2026-11-20. The PasarGuard#114 row still said the fork had not taken PasarGuard#58's snapshot serialiser. Since 797adcf the Shadowsocks 2022 placeholder sits in the empty-client case of wireClientsLocked, and ToBytes marshals a snapshot; the row now says so, with the byte-for-byte test and the PasarGuard#114 tests that pass on it. F-R5-SB-1: the three Linux-only coalesce tests fail identically on both sing-box pins because they read the config file that the hot-reload path never rewrites. The section now records the evidence, that CI never runs them (the live step names only the Hysteria2 tests, and the race step has no sing-box binary, so they skip), and NODE-LEAD as owner of the rewrite. The tests are unchanged.
…test.go A rename with no change to its content. TestToBytesConcurrentCalls and TestToBytesWhileCloning, which 9c99b8a ported from upstream PasarGuard#58, sit in backend/xray/config_tobytes_test.go in upstream, and upstream PasarGuard#114 (946ab6a) adds its two Shadowsocks 2022 ToBytes tests to that file, after TestToBytesWhileCloning. The fork had filed the race tests as config_tobytes_race_test.go, so the next commit can carry PasarGuard#114's hunks of that file only once it has upstream's name.
… empty (PasarGuard#114) Upstream PasarGuard#114's hunks of backend/xray/config_tobytes_test.go, carried as upstream wrote them: the Shadowsocks 2022 config fixture, buildShadowsocksInbound, which runs Xray's own config builder on the serialized inbound, and TestToBytesStartsEmptyShadowsocks2022InboundsMultiUser, which requires an empty 2022-blake3-aes inbound to be built as a multi-user server around one placeholder that is not the server key, leaves chacha20 and legacy Shadowsocks without clients, and stores no client on the live inbound. Its two import lines are upstream's; the encoding/json import comes from upstream's PasarGuard#58 import block of the same file, which the fork's copy did not need before. Upstream's comment lines are left out under the fork's no-comments rule. The test passes on the fork's placeholder code (3d18e70, moved into wireClientsLocked by 797adcf). The fork's rewritten copies of the fixture, the helper and this test in shadowsocks2022_tobytes_test.go are removed; that file keeps the fork's placeholder lifecycle test. (cherry picked from commit 946ab6a)
Summary
This PR implements 5 targeted performance optimizations for PasarGuard Node, designed for deployment on small 1-Core / 2GB RAM VPS instances where CPU spikes, GC pressure, and lock contention degrade performance under high traffic.
Changes
1. Non-blocking bandwidth sampling (
pkg/sysstats/sys.go)time.SleepinsidegetBandwidthSpeed()that blocked the stats collection goroutine2. Lock-free config serialization (
backend/xray/config.go)ToBytes()no longer holds read locks on ALL inbound mutexes during JSON marshalingjson.Marshalruns on a detached copy with zero locks heldSyncUserAPI calls from being blocked by serialization3. sync.Pool for scanner buffers (
backend/xray/log.go)sync.Poolthat recycles the 64KBbufio.Scannerbuffer across Xray restartsStart()invocation4. Conditional orphan process scan (
backend/xray/core.go)cleanExitflag toCorestruct/procorps-based global process scan when Xray exited cleanly5. Eliminated unused context allocation (
controller/controller.go)context.WithCancel()inNew()that was immediately discardedcancelFuncinDisconnect()for safe pre-Connect teardownTest Results
go build ./...compiles successfully (exit code 0)xray/api,wireguard,controller,pkg/statsImpact
Summary by CodeRabbit