Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix EntityWhitelist causing huge performance drop #33322

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 106 additions & 41 deletions Content.Shared/Whitelist/EntityWhitelistSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,78 @@ public sealed class EntityWhitelistSystem : EntitySystem
[Dependency] private readonly TagSystem _tag = default!;

private EntityQuery<ItemComponent> _itemQuery;
private readonly Dictionary<string, (ComponentAvailability availability, ComponentRegistration? registration)> _componentCache = new();

public override void Initialize()
{
base.Initialize();
_itemQuery = GetEntityQuery<ItemComponent>();

foreach (var registration in _factory.GetAllRegistrations())
{
_componentCache[registration.Name] = (ComponentAvailability.Available, registration);
}
}
Comment on lines +21 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't a frozendictionary going to be faster than this


/// <summary>
/// Converts strings to registrations once and caches the result
/// </summary>
private void EnsureRegistrations(EntityWhitelist list)
{
// If we've already converted the Components, no need to do it again
if (list.Registrations != null)
return;

// Initialize registrations list
var capacity = (list.Components?.Length ?? 0) + (list.MindRoles?.Length ?? 0);
list.Registrations = new List<ComponentRegistration>(capacity);

// Process both component lists
ProcessNames(list.Components);
ProcessNames(list.MindRoles);
return;

void ProcessNames(string[]? names)
{
if (names == null)
return;

foreach (var name in names)
{
// Skip if we already know it's unknown (cached with null registration)
if (_componentCache.TryGetValue(name, out var cached))
{
if (cached.registration != null && !list.Registrations.Contains(cached.registration))
{
list.Registrations.Add(cached.registration);
}
continue;
}

// First time seeing this component name
var availability = _factory.GetComponentAvailability(name);
ComponentRegistration? registration = null;

if (availability == ComponentAvailability.Available)
{
_factory.TryGetRegistration(name, out registration);
}

else if (availability == ComponentAvailability.Unknown)
{
// Only log unknown components once when we first see them
Log.Error($"Unknown component name {name} passed to EntityWhitelist!");
}

// Cache the result (including nulls for unknown components)
_componentCache[name] = (availability, registration);

if (registration != null && !list.Registrations.Contains(registration))
{
list.Registrations.Add(registration);
}
}
}
}

/// <inheritdoc cref="IsValid(Content.Shared.Whitelist.EntityWhitelist,Robust.Shared.GameObjects.EntityUid)"/>
Expand Down Expand Up @@ -47,21 +114,28 @@ public bool CheckBoth([NotNullWhen(true)] EntityUid? uid, EntityWhitelist? black
/// </summary>
public bool IsValid(EntityWhitelist list, EntityUid uid)
{
if (list.Components != null)
// Fast path for empty lists
if ((list.Components == null || list.Components.Length == 0) &&
(list.MindRoles == null || list.MindRoles.Length == 0) &&
(list.Tags == null || list.Tags.Count == 0) &&
(list.Registrations == null || list.Registrations.Count == 0) &&
list.Sizes == null)
{
var regs = StringsToRegs(list.Components);

list.Registrations ??= new List<ComponentRegistration>();
list.Registrations.AddRange(regs);
return list.RequireAll;
}

if (list.MindRoles != null)
{
var regs = StringsToRegs(list.MindRoles);
// Convert Components and MindRoles to Registrations
EnsureRegistrations(list);

foreach (var role in regs)
// Check mind roles first
if (list.MindRoles is { Length: > 0 })
{
foreach (var roleName in list.MindRoles)
{
if ( _roles.MindHasRole(uid, role.Type, out _))
if (!_componentCache.TryGetValue(roleName, out var cached) || cached.registration == null)
continue;

if (_roles.MindHasRole(uid, cached.registration.Type, out _))
{
if (!list.RequireAll)
return true;
Expand All @@ -71,6 +145,7 @@ public bool IsValid(EntityWhitelist list, EntityUid uid)
}
}

// Check registrations
if (list.Registrations != null && list.Registrations.Count > 0)
{
foreach (var reg in list.Registrations)
Expand All @@ -85,21 +160,35 @@ public bool IsValid(EntityWhitelist list, EntityUid uid)
}
}

// Check sizes
if (list.Sizes != null && _itemQuery.TryComp(uid, out var itemComp))
{
if (list.Sizes.Contains(itemComp.Size))
return true;
{
if (!list.RequireAll)
return true;
}
else if (list.RequireAll)
return false;
}

if (list.Tags != null)
{
return list.RequireAll
? _tag.HasAllTags(uid, list.Tags)
: _tag.HasAnyTag(uid, list.Tags);
}
// Check tags
if (list.Tags == null)
return list.RequireAll;

var hasTag = list.RequireAll
? _tag.HasAllTags(uid, list.Tags)
: _tag.HasAnyTag(uid, list.Tags);

if (!list.RequireAll && hasTag)
return true;

if (list.RequireAll && !hasTag)
return false;

return list.RequireAll;
}

/// The following are a list of "helper functions" that are basically the same as each other
/// to help make code that uses EntityWhitelist a bit more readable because at the moment
/// it is quite clunky having to write out component.Whitelist == null ? true : _whitelist.IsValid(component.Whitelist, uid)
Expand Down Expand Up @@ -184,28 +273,4 @@ public bool IsBlacklistFailOrNull(EntityWhitelist? blacklist, EntityUid uid)
{
return IsWhitelistFailOrNull(blacklist, uid);
}

private List<ComponentRegistration> StringsToRegs(string[]? input)
{
var list = new List<ComponentRegistration>();

if (input == null || input.Length == 0)
return list;

foreach (var name in input)
{
var availability = _factory.GetComponentAvailability(name);
if (_factory.TryGetRegistration(name, out var registration)
&& availability == ComponentAvailability.Available)
{
list.Add(registration);
}
else if (availability == ComponentAvailability.Unknown)
{
Log.Error($"StringsToRegs failed: Unknown component name {name} passed to EntityWhitelist!");
}
}

return list;
}
}
Loading