-
Notifications
You must be signed in to change notification settings - Fork 11
Performance Analyzers
Unity Helpers ships a Roslyn analyzer that reports footguns in code that already compiles and, for the most part, already works. It runs on your code as well as the package's, because the shapes it finds are not specific to either.
These are a different family from the WPROTO### serialization diagnostics, and they follow a
different policy on purpose:
WPROTO### |
WUH### |
|
|---|---|---|
| Reports | A serialization contract that cannot be honoured | An allocation or footgun in correct code |
| Severity | Error — the alternative is an exception from a shipped player | Warning, always |
| Can fail your build | Yes, and it should | No |
| Default | On | On |
A WUH### diagnostic will never fail your build. Taking a package upgrade cannot turn a green
build red over one of these. If your project treats warnings as errors, see
Turning one off.
C# does not cache a method-group conversion until C# 11, and Unity pins C# 9 on every version this package supports. So a method group written at a call site builds a new delegate on every call, including the lookups that hit — which is the case the lookup exists to make cheap.
// WUH001: a new Func<Type, Accessors> on every call, hits included.
return TypeAccessors.GetOrAdd(collectionType, CreateAccessors);
// No allocation on a hit: the delegate is built once.
private static readonly Func<Type, Accessors> AccessorFactory = CreateAccessors;
return TypeAccessors.GetOrAdd(collectionType, AccessorFactory);Measured on Unity 6000.4.6f1 over 400,000 warm-cache hits, against a control that moved 30.6 MB:
| factory shape | bytes/call |
|---|---|
| method group | 106.3 |
| lambda capturing a method parameter | 115.8 |
static lambda plus a state-taking overload |
0.0 |
cached static readonly Func<...> field |
0.1 |
-
ConcurrentDictionary<K, V>.GetOrAddand.AddOrUpdate ConditionalWeakTable<K, V>.GetValue-
Every delegate-taking member of this package's own
DictionaryExtensions—GetOrAdd,GetOrElse,AddOrUpdate,TryAdd,Merge,DifferenceandReverse— which extendIDictionaryandIReadOnlyDictionary, so a plainDictionary<K, V>is covered through them even though the BCL gives it no factory-taking member of its own.
That second bullet is matched by parameter type, not by method name. A name list was tried first
and was the wrong shape: it named three members and missed TryAdd, whose creator runs only when the
key is absent — exactly the defect — along with three more that take an optional Func creator.
Matching the delegate parameter means the next factory-taking extension is covered the day it is
written.
GetOrElse never adds anything, but it takes the same Func<V> and rebuilds it on every call that
finds the key. Same defect.
- A
staticlambda, or a delegate held in a field, a local, or a parameter. Those are built once. - Any method group on a compiler at C# 11 or newer, which caches the conversion. The analyzer checks the compilation's language version and stays silent above C# 10.
- A method named
GetOrAddon a type that is not in the list above. Your own cache type is yours.
Unity's serializer flattens a List<T> or a T[] into a repeated field, and it will not do that
twice. A field that resolves onto a collection of collections is dropped in full, with no error
and no warning: the asset records the outer structure and none of the inner values, and the
Inspector goes on accepting edits that vanish on the next reload.
// WUH002: backs onto List<Foo>[], so every value is lost on save.
[SerializeField] private SerializableDictionary<string, List<Foo>> _byTier;
// Saves: the outer array now holds a class, which Unity does serialize.
[SerializeField] private SerializableDictionary<string, SerializableList<Foo>> _byTier;SerializableList<T> ships for exactly this. It
is a [Serializable] class wrapping one List<T>, which is the layer of indirection Unity needs.
SerializableDictionary<string, List<Foo>> names one collection. The second appears only when its
backing TValueCache[] is substituted, two base classes further up. So the analyzer does not match
the declaration's syntax — it asks the symbol what Unity will actually serialize, walking the
serialized instance fields of the field's type and of theirs. That covers every adapter this package
ships, any it adds later, and a wrapper of your own, with no list to keep in sync.
Any field Unity will serialize:
- one carrying
[SerializeField], wherever it appears, or - a public instance field on a type deriving from
UnityEngine.Object, or - a public instance field of a
[Serializable]type the walk reached from one of those. A DTO written the ordinary way —[Serializable], public fields, no[SerializeField]anywhere — is exactly what a dictionary value usually is, and Unity serializes its public fields.
- A public field on a plain class that has no
[SerializeField], where nothing has established that Unity serializes the containing type. It may never reach Unity's serializer at all, and an ordinary algorithm'sList<List<int>>is not a serialization bug. - Anything marked
[NonSerialized],static, orconst. - A collection of
UnityEngine.Objectreferences. Those are serialized as references to a separate asset, so the nesting never happens. - A multi-dimensional array. Unity serializes
int[,]at no nesting at all, so reporting it here would name the wrong cause.
Suppress a single call site whose lookup is genuinely cold:
#pragma warning disable WUH001
return ColdCache.GetOrAdd(key, CreateOnce);
#pragma warning restore WUH001Or turn the rule off for the whole project in Assets/Default.ruleset:
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Project analyzer rules" ToolsVersion="15.0">
<Rules AnalyzerId="WallstopStudios.UnityHelpers.Analyzers"
RuleNamespace="WallstopStudios.UnityHelpers.Analyzers">
<Rule Id="WUH001" Action="None" />
<Rule Id="WUH002" Action="None" />
</Rules>
</RuleSet>An IDE or standalone .NET build can set dotnet_diagnostic.WUH001.severity = none in
.editorconfig instead.
-
Serialization diagnostics — the
WPROTO###family - Reflection performance — where these caches are used most
📦 Unity Helpers | 📖 Documentation | 🐛 Issues | 📜 MIT License
- Inspector Button
- Inspector Conditional Display
- Inspector Grouping Attributes
- Inspector Inline Editor
- Inspector Overview
- Inspector Selection Attributes
- Inspector Settings
- Inspector Validation Attributes
- Utility Components
- Visual Components
- Data Structures
- Helper Utilities
- Math And Extensions
- Pooling Guide
- Random Generators
- Reflection Helpers
- Singletons