Skip to content

Avoid delegate dispatch in DetectBuildingAuth - #75

Open
dexsper wants to merge 1 commit into
rust-app-io:mainfrom
dexsper:main
Open

Avoid delegate dispatch in DetectBuildingAuth#75
dexsper wants to merge 1 commit into
rust-app-io:mainfrom
dexsper:main

Conversation

@dexsper

@dexsper dexsper commented Aug 19, 2026

Copy link
Copy Markdown

Summary

DetectBuildingAuth used BaseEntity.Query.Server.GetInSphereFast(pos, radius, buffer, Func<BaseEntity,bool> filter), which runs the supplied delegate against every candidate entity the spatial grid returns before the caller can filter/cast. Replaced it with the generic, type-filtered overload GetInSphere<BuildingPrivlidge>(pos, radius, list, DistanceCheckType.None), which filters by type inside the grid query itself, instead of returning all BaseEntity candidates and filtering afterwards via a delegate.

See plugins/rust/RustApp.cs, DetectBuildingAuth (~line 4725). Key changes:

- BaseEntity.Query.Server.GetInSphereFast(pos, SearchRadius, _buildAuthArr, _buildAuthFilter);
+ BaseEntity.Query.Server.GetInSphere(pos, SearchRadius, list, BaseEntity.Query.DistanceCheckType.None);

The fixed-size _buildAuthArr/_buildAuthFilter buffer + Array.Clear cleanup were replaced with a pooled List<BuildingPrivlidge> (Pool.Get / Pool.FreeUnmanaged), matching the pooling convention already used elsewhere in RustApp.cs. No other behavioral change: same radius, same sqrMagnitude check, same early-return-on-first-match logic.

Why GetInSphere<T> and not GetInSphereFast

GetInSphereFast narrows candidates with a Func<BaseEntity, bool> delegate that is invoked once per entity the grid cell returns, regardless of type. Each candidate requires a delegate invocation, adding an indirect managed call on top of the is BuildingPrivlidge check it wraps. The generic overload does the type check inside Grid<T>.Query, so there is no per-candidate delegate call. It also does not walk every BaseEntity in range: only BuildingPrivlidge.

This matches an earlier local call-stack capture (23 calls in one window): the delegate <>c::<.cctor>b__178_0 (the invocation itself plus the is BuildingPrivlidge check it wraps; the profiler can't separate the two, both are the lambda's self-time) was 0.718ms of the 0.942ms inclusive time in DetectBuildingAuth.

Verification

Built a standalone benchmark plugin (RustAppBench.cs, not part of this PR) exposing both implementations behind chat commands, driven 5000 iterations each, wrapped in ServerProfiler.RecordScope to get an exact call-tree via profile.perfsnapshot.

Aggregated over 5000 calls (Main Thread). Snapshot ts is microseconds (ServerProfiler.TimestampToMicros):

Metric GetInSphereFast (before) GetInSphere<T> (after)
Grid<T>::Query total (self + lambda) 27.802 ms 11.362 ms
lambda self-time (invocation + is BuildingPrivlidge, 455000 calls, 91 per call) 11.328 ms gone
DetectBuildingAuth inclusive, 5000 calls 29.596 ms 13.552 ms
DetectBuildingAuth inclusive per call (avg) 5.92 µs 2.71 µs

Raw snapshot lines (call 100 of 5000, exported .json trace, one BuildingPrivlidge in range):

Before (GetInSphereFast): one call from ts 599 to ts 604 (5 µs), filter lambda entered/exited 91 times:

{"name": "a21g2bi2.adl!RustAppBench::DetectBuildingAuth_Original", "ph": "B", "ts": 599, "tid": 1}
{"name": "Assembly-CSharp!EntityTree::GetInSphereFast", "ph": "B", "ts": 599, "tid": 1}
{"name": "Facepunch.System!Grid`1::Query", "ph": "B", "ts": 599, "tid": 1}
{"name": "a21g2bi2.adl!<>c::<.cctor>b__17_0", "ph": "B", "ts": 599, "tid": 1}
{"ph": "E", "ts": 599, "tid": 1}
{"name": "a21g2bi2.adl!<>c::<.cctor>b__17_0", "ph": "B", "ts": 599, "tid": 1}
{"ph": "E", "ts": 599, "tid": 1}
... (repeats 91 times per call, one B/E pair per grid candidate) ...
{"ph": "E", "ts": 603, "tid": 1}
{"ph": "E", "ts": 603, "tid": 1}

After (GetInSphere<BuildingPrivlidge>): no lambda frames, call from ts 298 to ts 301 (3 µs):

{"name": "a21g2bi2.adl!RustAppBench::DetectBuildingAuth_Optimized", "ph": "B", "ts": 298, "tid": 1}
{"name": "Assembly-CSharp!EntityTree::GetInSphere", "ph": "B", "ts": 298, "tid": 1}
{"name": "Facepunch.System!Grid`1::Query", "ph": "B", "ts": 298, "tid": 1}
{"ph": "E", "ts": 300, "tid": 1}
{"ph": "E", "ts": 301, "tid": 1}
{"name": "Assembly-CSharp!BuildingPrivlidge::IsAuthed", "ph": "B", "ts": 301, "tid": 1}
{"ph": "E", "ts": 301, "tid": 1}
{"ph": "E", "ts": 301, "tid": 1}

Pooling (Pool.Get<List<BuildingPrivlidge>>() / Pool.FreeUnmanaged) follows the same convention already used everywhere else in RustApp.cs (15+ call sites), so no new allocation pattern is introduced.

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.

1 participant