question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

IdentityWeakReference should be IDisposable

See original GitHub issue

IdentityWeakReference inherits from WeakReference, which has a finalizer, but its finalizer is especially handled by CLR to be ignored:

image https://referencesource.microsoft.com/#mscorlib/system/weakreference.cs,108

When you inherit the WeakReference CLR doesn’t ignore the finalizer anymore and the object become finalizable. IdentityWeakReference doesn’t supress finalization causing a huge pressure on finalizer thread.

In one of my traces I found a 5 seconds GC pause due to that, the pause was not in GC itself, but in the suspension phase before starting the GC. I tracked what the SuspendEE was waiting, and it was the finalizer thread. When investigating why finalizer was hanging, I found this:

image

IdentityWeakReference should implement IDisposable and call GC.SuppressFinalize(this) in Dispose method. And all instances of IdentityWeakReference needs to call Dispose before losing the reference, for example:

image

You can easily simulate the issue using this code and uncomment the lines to fix it:

static object x = new object();

static async Task Main(string[] args)
{
    const int count = 1000000;

    var weaks = new List<IdentityWeakReference<object>>(count);
    for (int i = 0; i < count; i++)
    {
        weaks.Add(new IdentityWeakReference<object>(x));
    }

    Console.WriteLine("Start Perfview collection and press Enter");
    Console.ReadLine();

    GC.KeepAlive(weaks);

    for (int i = 0; i < count; i++)
    {
        //weaks[i].Dispose();
    }

    weaks = null;

    Stopwatch c = Stopwatch.StartNew();

    GC.Collect(2, GCCollectionMode.Default, true, true);
    GC.WaitForPendingFinalizers();
    GC.Collect(2, GCCollectionMode.Default, true, true);
    GC.WaitForPendingFinalizers();

    Console.WriteLine("GC DONE. Stop Perfview");
    Console.WriteLine($"{c.ElapsedMilliseconds}ms");
    Console.ReadLine();
}

internal class IdentityWeakReference<T> : WeakReference
    //, IDisposable 
    where T : class
{
    private readonly int hash;
    private static readonly object NULL = new object();
    //private bool disposedValue;

    public IdentityWeakReference(T target)
        : base(target ?? NULL)
    {
        hash = RuntimeHelpers.GetHashCode(target);
    }

    public override int GetHashCode()
    {
        return hash;
    }

    public override bool Equals(object o)
    {
        if (ReferenceEquals(this, o))
        {
            return true;
        }
        if (o is IdentityWeakReference<T> iwr && ReferenceEquals(this.Target, iwr.Target))
        {
            return true;
        }
        return false;
    }

    public new T Target
    {   // note: if this.NULL is the target, it will not cast to T, so the "as" will return null as we would expect.
        get => base.Target as T;
        set => base.Target = value;
    }

    //protected virtual void Dispose(bool disposing)
    //{
    //    if (!disposedValue)
    //    {
    //        if (disposing)
    //        {
    //            // TODO: dispose managed state (managed objects)
    //        }

    //        // TODO: free unmanaged resources (unmanaged objects) and override finalizer
    //        // TODO: set large fields to null
    //        disposedValue = true;
    //    }
    //}

    //public void Dispose()
    //{
    //    // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
    //    Dispose(disposing: true);
    //    GC.SuppressFinalize(this);
    //}
}

GC Stats without the fix:

image

And even this application doing nothing. GC takes 5 seconds (technically not GC, but Finalizer):

image

image

GC Stats with the fix:

No Finalized Objects

GC is almost instantaneous, as expected

image

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:10 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
NightOwl888commented, Aug 12, 2021

I have submitted #509 with a patch to fix the issues with using the wrong collection type.

  • .NET Standard 2.1 uses ConditionalWeakTable<TKey, TValue>
  • Lower .NET versions use WeakDictionary<TKey, TValue>
    • The WeakKey<T> class (used by WeakDictionary<TKey, TValue>) was modified to be backed by WeakReference<T> rather than WeakReference
    • WeakDictionary<TKey, TValue> was modified to be backed by ConcurrentDictionary<TKey, TValue> to allow the reap to happen while doing a forward iteration

You can download a CI build of the changes from here, and I would appreciate your help testing them to ensure the problem is resolved.

When you inherit the WeakReference CLR doesn’t ignore the finalizer anymore and the object become finalizable.

Being that we switched to a wrapped (rather than inherited) instance, I suspect the finalizer issue has been sidestepped. However, if there is still an issue with it, please let us know.

Do note that because of the conditional compilation branching it would be helpful to know which .NET platform you are targeting going forward so we know which implementation you are referring to.

0reactions
felipepessotocommented, Sep 16, 2021

@NightOwl888 do you have any ETA when next release will be done?

Read more comments on GitHub >

github_iconTop Results From Across the Web

c# - Should IDisposable be applied cascadingly?
Yes, your class needs to be IDisposable if it needs to dispose of any objects that it uses. An example of this is...
Read more >
What is the use of IDisposable interface? : r/csharp
You should implement IDisposable if your class holds a reference to either an unmanaged resource or a managed resource that implements ...
Read more >
CA1063: Implement IDisposable correctly (code analysis)
Every unsealed type that declares and implements the IDisposable interface must provide its own protected virtual void Dispose(bool) method.
Read more >
Using objects that implement IDisposable
IDisposable or System.IAsyncDisposable should always be properly disposed of, regardless of variable scoping, unless otherwise explicitly ...
Read more >
c# - Would it be bad design to use IDisposable on a class ...
The only concern I have with some of these uses is that they don't convey the correct semantic meaning. The programmer coming after...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found