File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Utilities\EventMap.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.ErrorReporting;
 
namespace Roslyn.Utilities;
 
internal class EventMap
{
    private readonly NonReentrantLock _guard = new();
 
    private readonly Dictionary<string, object> _eventNameToRegistries = [];
 
    public EventMap()
    {
    }
 
    public void AddEventHandler<TEventHandler>(string eventName, TEventHandler eventHandler)
        where TEventHandler : class
    {
        using (_guard.DisposableWait())
        {
            var registries = GetRegistries_NoLock<TEventHandler>(eventName);
            var newRegistries = registries.Add(new Registry<TEventHandler>(eventHandler));
            SetRegistries_NoLock(eventName, newRegistries);
        }
    }
 
    public void RemoveEventHandler<TEventHandler>(string eventName, TEventHandler eventHandler)
        where TEventHandler : class
    {
        using (_guard.DisposableWait())
        {
            var registries = GetRegistries_NoLock<TEventHandler>(eventName);
 
            // remove disabled registrations from list
            var newRegistries = registries.RemoveAll(r => r.HasHandler(eventHandler));
 
            if (newRegistries != registries)
            {
                // disable all registrations of this handler (so pending raise events can be squelched)
                // This does not guarantee no race condition between Raise and Remove but greatly reduces it.
                foreach (var registry in registries.Where(r => r.HasHandler(eventHandler)))
                {
                    registry.Unregister();
                }
 
                SetRegistries_NoLock(eventName, newRegistries);
            }
        }
    }
 
    [PerformanceSensitive(
        "https://developercommunity.visualstudio.com/content/problem/854696/changing-target-framework-takes-10-minutes-with-10.html",
        AllowImplicitBoxing = false)]
    public EventHandlerSet<TEventHandler> GetEventHandlers<TEventHandler>(string eventName)
        where TEventHandler : class
    {
        return new EventHandlerSet<TEventHandler>(this.GetRegistries<TEventHandler>(eventName));
    }
 
    private ImmutableArray<Registry<TEventHandler>> GetRegistries<TEventHandler>(string eventName)
        where TEventHandler : class
    {
        using (_guard.DisposableWait())
        {
            return GetRegistries_NoLock<TEventHandler>(eventName);
        }
    }
 
    private ImmutableArray<Registry<TEventHandler>> GetRegistries_NoLock<TEventHandler>(string eventName)
        where TEventHandler : class
    {
        _guard.AssertHasLock();
        if (_eventNameToRegistries.TryGetValue(eventName, out var registries))
        {
            return (ImmutableArray<Registry<TEventHandler>>)registries;
        }
 
        return [];
    }
 
    private void SetRegistries_NoLock<TEventHandler>(string eventName, ImmutableArray<Registry<TEventHandler>> registries)
        where TEventHandler : class
    {
        _guard.AssertHasLock();
 
        _eventNameToRegistries[eventName] = registries;
    }
 
    internal class Registry<TEventHandler>(TEventHandler handler) : IEquatable<Registry<TEventHandler>?>
        where TEventHandler : class
    {
        private TEventHandler? _handler = handler;
 
        public void Unregister()
            => _handler = null;
 
        public void Invoke<TArg>(Action<TEventHandler, TArg> invoker, TArg arg)
        {
            var handler = _handler;
            if (handler != null)
            {
                invoker(handler, arg);
            }
        }
 
        public bool HasHandler(TEventHandler handler)
            => handler.Equals(_handler);
 
        public bool Equals(Registry<TEventHandler>? other)
        {
            if (other == null)
            {
                return false;
            }
 
            if (other._handler == null && _handler == null)
            {
                return true;
            }
 
            if (other._handler == null || _handler == null)
            {
                return false;
            }
 
            return other._handler.Equals(_handler);
        }
 
        public override bool Equals(object? obj)
            => Equals(obj as Registry<TEventHandler>);
 
        public override int GetHashCode()
            => _handler == null ? 0 : _handler.GetHashCode();
    }
 
    internal struct EventHandlerSet<TEventHandler>
        where TEventHandler : class
    {
        private readonly ImmutableArray<Registry<TEventHandler>> _registries;
 
        internal EventHandlerSet(ImmutableArray<Registry<TEventHandler>> registries)
            => _registries = registries;
 
        public readonly bool HasHandlers
        {
            get { return _registries != null && _registries.Length > 0; }
        }
 
        public readonly void RaiseEvent<TArg>(Action<TEventHandler, TArg> invoker, TArg arg)
        {
            // The try/catch here is to find additional telemetry for https://devdiv.visualstudio.com/DevDiv/_queries/query/71ee8553-7220-4b2a-98cf-20edab701fd1/.
            // We've realized there's a problem with our eventing, where if an exception is encountered while calling into subscribers to Workspace events,
            // we won't notify all of the callers. The expectation is such an exception would be thrown to the SafeStartNew in the workspace's event queue that
            // will raise that as a fatal exception, but OperationCancelledExceptions might be able to propagate through and fault the task we are using in the
            // chain. I'm choosing to use ReportWithoutCrashAndPropagate, because if our theory here is correct, it seems the first exception isn't actually
            // causing crashes, and so if it turns out this is a very common situation I don't want to make a often-benign situation fatal.
            try
            {
                if (this.HasHandlers)
                {
                    foreach (var registry in _registries)
                    {
                        registry.Invoke(invoker, arg);
                    }
                }
            }
            catch (Exception e) when (FatalError.ReportAndPropagate(e))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
    }
}