Skip to content

On netstandard builds, ContiguousMap (and ContiguousCollection/Set of reference-holding structs) never release removed or cleared items #79

Description

@matt-edmondson

What's wrong

The code that clears slots on Remove/Clear only runs when this condition holds:

#if NET5_0_OR_GREATER
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
#else
if (!typeof(T).IsValueType)
#endif

On the netstandard2.0/2.1 targets, the fallback is false for every struct, including structs that hold references.

  • ContiguousMap.Entry is always a struct, so on the netstandard builds ContiguousMap<TKey, TValue> never clears a slot, whatever TKey/TValue are. See ContiguousMap.cs:376 (Remove) and :448 (Clear).
  • ContiguousCollection<T> (:171, :252) and ContiguousSet<T> (:228, :308) have the same problem for struct T that holds references, such as KeyValuePair<int, object> or (string, int).

Removed and cleared keys and values stay reachable from the backing array until their slot is overwritten.

Reproduction

The library was built for netstandard2.0 and consumed from a net10 app. Each case holds a WeakReference to a value, then runs GC.Collect() followed by GC.WaitForPendingFinalizers():

  • ContiguousMap<int, object> → Clear() → value still alive
  • ContiguousMap<int, object> → remove every key → value still alive
  • ContiguousCollection<KeyValuePair<int, object>> → Clear() → value still alive

The same program run against the net10 build reports every value as collected.

Why it matters

Consumers of the netstandard assemblies, such as .NET Framework, Unity and Mono, see memory leaks that net5+ users don't. A map used as a cache that is periodically cleared keeps every value it has ever held alive. This is the same class of bug as #72 (RingBuffer.Clear), but it affects different types and has a different root cause: the TFM guard.

Suggested fix

  • Use RuntimeHelpers.IsReferenceOrContainsReferences<T>() under #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER; the API exists in netstandard2.1.
  • On netstandard2.0, drop the condition and always clear. Clearing is cheap and always correct.
  • Apply the fix to every occurrence of the pattern (grep -n "IsValueType" Containers/).

Acceptance criteria

  • A test with a WeakReference shows that values are collectable after Remove/Clear for ContiguousMap<int, object> and ContiguousCollection<KeyValuePair<int, object>>.
  • The test runs on the netstandard-consuming leg if CI has one. Otherwise, verify it manually against the netstandard2.0 build.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingreadyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions