|
//---------------------------------------------------------------------------
//
// <copyright file="WeakEventTable.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved.
// </copyright>
//
// Description: Storage for the "weak event listener" pattern.
// See WeakEventManager.cs for an overview.
//
//---------------------------------------------------------------------------
using System;
using System.Diagnostics; // Debug
using System.Collections; // Hashtable
using System.Collections.Generic; // List<T>
using System.Collections.Specialized; // HybridDictionary
using System.Runtime.CompilerServices; // RuntimeHelpers
using System.Security; // [SecurityCritical,SecurityTreatAsSafe]
using System.Threading; // [ThreadStatic]
using System.Windows; // WeakEventManager
using System.Windows.Threading; // DispatcherObject
using MS.Utility; // FrugalList
namespace MS.Internal
{
/// <summary>
/// This class manages the correspondence between event types and
/// event managers, in support of the "weak event listener" pattern.
/// It also stores data on behalf of the managers; a manager can store
/// data of its own choosing, indexed by the pair (manager, source).
/// </summary>
internal class WeakEventTable : DispatcherObject
{
#region Constructors
//
// Constructors
//
/// <summary>
/// Create a new instance of WeakEventTable.
/// </summary>
/// <SecurityNote>
/// Critical: This code calls into Link demanded methods
/// (AppDomain.DomainUnload and AppDomain.ProcessExit) to attach handlers
/// TreatAsSafe: This code does not take any parameter or return state.
/// It simply attaches private call back.
/// </SecurityNote>
[SecurityCritical,SecurityTreatAsSafe]
private WeakEventTable()
{
WeakEventTableShutDownListener listener = new WeakEventTableShutDownListener(this);
_cleanupHelper = new CleanupHelper(DoCleanup);
}
#endregion Constructors
#region Internal Properties
//
// Internal Properties
//
/// <summary>
/// Return the WeakEventTable for the current thread
/// </summary>
internal static WeakEventTable CurrentWeakEventTable
{
get
{
// _currentTable is [ThreadStatic], so there's one per thread
if (_currentTable == null)
{
_currentTable = new WeakEventTable();
}
return _currentTable;
}
}
/// <summary>
/// Take a read-lock on the table, and return the IDisposable.
/// Queries to the table should occur within a
/// "using (Table.ReadLock) { ... }" clause, except for queries
/// that are already within a write lock.
/// </summary>
internal IDisposable ReadLock
{
get
{
#if WeakEventTelemetry
++ _readCount;
#endif
return _lock.ReadLock;
}
}
/// <summary>
/// Take a write-lock on the table, and return the IDisposable.
/// All modifications to the table should occur within a
/// "using (Table.WriteLock) { ... }" clause.
/// </summary>
internal IDisposable WriteLock
{
get
{
#if WeakEventTelemetry
++ _writeCount;
#endif
return _lock.WriteLock;
}
}
/// <summary>
/// Get or set the manager instance for the given type.
/// </summary>
internal WeakEventManager this[Type managerType]
{
get { return (WeakEventManager)_managerTable[managerType]; }
set { _managerTable[managerType] = value; }
}
/// <summary>
/// Get or set the manager instance for the given event.
/// </summary>
internal WeakEventManager this[Type eventSourceType, string eventName]
{
get
{
EventNameKey key = new EventNameKey(eventSourceType, eventName);
return (WeakEventManager)_eventNameTable[key];
}
set
{
EventNameKey key = new EventNameKey(eventSourceType, eventName);
_eventNameTable[key] = value;
}
}
/// <summary>
/// Get or set the data stored by the given manager for the given source.
/// </summary>
internal object this[WeakEventManager manager, object source]
{
get
{
EventKey key = new EventKey(manager, source);
object result = _dataTable[key];
return result;
}
set
{
EventKey key = new EventKey(manager, source, true);
_dataTable[key] = value;
}
}
/// <summary>
/// Indicates whether cleanup is enabled.
/// </summary>
/// <remarks>
/// Normally cleanup is always enabled, but a perf test environment might
/// want to disable cleanup so that it doesn't interfere with the real
/// perf measurements.
/// </remarks>
internal bool IsCleanupEnabled
{
get { return _cleanupEnabled; }
set { _cleanupEnabled = value; }
}
#endregion Internal Properties
#region Internal Methods
//
// Internal Methods
//
/// <summary>
/// Remove the data for the given manager and source.
/// </summary>
internal void Remove(WeakEventManager manager, object source)
{
EventKey key = new EventKey(manager, source);
if (!_inPurge)
{
_dataTable.Remove(key);
}
else
{
_toRemove.Add(key);
}
}
/// <summary>
/// Schedule a cleanup pass. This can be called from any thread.
/// </summary>
internal void ScheduleCleanup()
{
if (!BaseAppContextSwitches.EnableCleanupSchedulingImprovements)
{
// only the first request after a previous cleanup should schedule real work
if (Interlocked.Increment(ref _cleanupRequests) == 1)
{
Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new DispatcherOperationCallback(CleanupOperation), null);
}
}
else
{
_cleanupHelper.ScheduleCleanup();
}
}
/// <summary>
/// Perform a cleanup pass.
/// </summary>
internal static bool Cleanup()
{
if (!BaseAppContextSwitches.EnableCleanupSchedulingImprovements)
{
return CurrentWeakEventTable.Purge(false);
}
else
{
return CurrentWeakEventTable._cleanupHelper.DoCleanup(forceCleanup:true);
}
}
bool DoCleanup(bool forceCleanup)
{
if (IsCleanupEnabled || forceCleanup)
{
return Purge(false);
}
else
{
return false;
}
}
#if WeakEventTelemetry
internal void LogAllocation(Type type, int count, int bytes)
{
LogAllocation(_allocations, type, count, bytes);
LogAllocation(_allocations, typeof(WeakEventLogger), count, bytes);
if (bytes/count >= LOH_Threshold)
{
LogAllocation(_LOHallocations, type, count, bytes);
LogAllocation(_LOHallocations, typeof(WeakEventLogger), count, bytes);
}
}
void LogAllocation(Dictionary<Type,AllocationRecord> dict, Type type, int count, int bytes)
{
AllocationRecord record;
if (!dict.TryGetValue(type, out record))
{
record = new AllocationRecord();
dict.Add(type, record);
}
record.Count += count;
record.Bytes += bytes;
}
#endif
#endregion Internal Methods
#region Private Methods
//
// Private Methods
//
// run a cleanup pass
private object CleanupOperation(object arg)
{
// allow new requests, even if cleanup is disabled
Interlocked.Exchange(ref _cleanupRequests, 0);
if (IsCleanupEnabled)
{
Purge(false);
}
return null;
}
// remove dead entries. When purgeAll is true, remove all entries.
private bool Purge(bool purgeAll)
{
bool foundDirt = false;
using (this.WriteLock)
{
#if WeakEventTelemetry
WeakEventLogger.LogSnapshot(this, "+Purge");
#endif
if (!BaseAppContextSwitches.EnableWeakEventMemoryImprovements)
{
// copy the keys into a separate array, so that later on
// we can change the table while iterating over the keys
ICollection ic = _dataTable.Keys;
EventKey[] keys = new EventKey[ic.Count];
ic.CopyTo(keys, 0);
for (int i=keys.Length-1; i>=0; --i)
{
object data = _dataTable[keys[i]];
// a purge earlier in the loop may have removed keys[i],
// in which case there's nothing more to do
if (data != null)
{
object source = keys[i].Source;
foundDirt |= keys[i].Manager.PurgeInternal(source, data, purgeAll);
// if source has been GC'd, remove its data
if (!purgeAll && source == null)
{
_dataTable.Remove(keys[i]);
}
}
}
#if WeakEventTelemetry
LogAllocation(ic.GetType(), 1, 12); // _dataTable.Keys - Hashtable+KeyCollection
LogAllocation(typeof(EventKey[]), 1, 12+ic.Count*12); // keys
LogAllocation(typeof(EventKey), ic.Count, 8+12); // box(key)
LogAllocation(typeof(ReaderWriterLockWrapper), 1, 12); // actually the RWLW+AutoWriterRelease from WriteLock
LogAllocation(typeof(Action), 2, 32); // anonymous delegates in RWLW
#endif
}
else
{
Debug.Assert(_toRemove.Count == 0, "to-remove list should be empty");
_inPurge = true;
// enumerate the dictionary using IDE explicitly rather than
// foreach, to avoid allocating temporary DictionaryEntry objects
IDictionaryEnumerator ide = _dataTable.GetEnumerator() as IDictionaryEnumerator;
while (ide.MoveNext())
{
EventKey key = (EventKey)ide.Key;
object source = key.Source;
foundDirt |= key.Manager.PurgeInternal(source, ide.Value, purgeAll);
// if source has been GC'd, remove its data
if (!purgeAll && source == null)
{
_toRemove.Add(key);
}
}
#if WeakEventTelemetry
LogAllocation(ide.GetType(), 1, 36); // Hashtable+HashtableEnumerator
#endif
_inPurge = false;
}
if (purgeAll)
{
_managerTable.Clear();
_dataTable.Clear();
}
else if (_toRemove.Count > 0)
{
foreach (EventKey key in _toRemove)
{
_dataTable.Remove(key);
}
_toRemove.Clear();
_toRemove.TrimExcess();
}
#if WeakEventTelemetry
++_purgeCount;
if (!foundDirt) ++_purgeNoops;
WeakEventLogger.LogSnapshot(this, "-Purge");
#endif
}
return foundDirt;
}
// do the final cleanup when the Dispatcher or AppDomain is shut down
private void OnShutDown()
{
if (CheckAccess())
{
Purge(true);
// remove the table from thread storage
_currentTable = null;
}
else
{
// if we're on the wrong thread, try asking the right thread
// to do the job. (DomainUnload arrives on finalizer thread - DDVSO 543980)
bool succeeded = false;
// DDVSO:606492
// In some cases, the extra delay from invocation can push applications with a race condition
// at shutdown into constant crashing. Due to this, respect a compat flag that allows these
// to skip the invocation. This causes us to do a partial cleanup (see the Purge call below)
// but avoids the significant timing changes that were adversely affecting the application.
// [DDVSO 655427] If the dispatcher has already shut down, the Invoke
// will throw an exception, so don't even bother trying. This can
// happen if the app does more work after dispatcher shutdown, creating
// a second WeakEventTable for the same thread.
//
if (!BaseAppContextSwitches.DoNotInvokeInWeakEventTableShutdownListener &&
!Dispatcher.HasShutdownFinished)
{
try
{
Dispatcher.Invoke((Action)OnShutDown, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(300));
succeeded = true;
}
catch (Exception ex) when (!CriticalExceptions.IsCriticalException(ex))
{
// Invoke can fail due to
// TimeoutException - the 300ms timeout expired
// TaskCanceledException - underlying Task didn't complete (DDVSO 655427)
// <other non-critical exception> - defense-in-depth
// These shouldn't crash the app or halt the shutdown processing.
// Instead, swallow the exception, but fall back to the
// "wrong thread" path (below).
}
}
// if that didn't work (because Dispatcher was busy or not pumping),
// do the work on the wrong thread, but don't touch thread-statics.
// This won't do everything, but it will do enough to support
// some useful scenarios (such as DevDiv Bugs 121070).
if (!succeeded)
{
Purge(true);
}
}
}
#endregion Private Methods
#region Private Fields
//
// Private Fields
//
private Hashtable _managerTable = new Hashtable(); // maps manager type -> instance
private Hashtable _dataTable = new Hashtable(); // maps EventKey -> data
private Hashtable _eventNameTable = new Hashtable(); // maps <Type,name> -> manager
ReaderWriterLockWrapper _lock = new ReaderWriterLockWrapper();
private int _cleanupRequests;
private bool _cleanupEnabled = true;
private CleanupHelper _cleanupHelper;
private bool _inPurge;
private List<EventKey> _toRemove = new List<EventKey>();
#if WeakEventTelemetry
const int LOH_Threshold = 85000; // per LOH docs
int _readCount, _writeCount;
int _purgeCount, _purgeNoops;
Dictionary<Type,AllocationRecord> _allocations = new Dictionary<Type,AllocationRecord>();
Dictionary<Type,AllocationRecord> _LOHallocations = new Dictionary<Type,AllocationRecord>();
class AllocationRecord
{
public int Count { get; set; }
public int Bytes { get; set; }
}
#endif
[ThreadStatic]
private static WeakEventTable _currentTable; // one table per thread
#endregion Private Fields
#region WeakEventTableShutDownListener
private sealed class WeakEventTableShutDownListener : ShutDownListener
{
/// <SecurityNote>
/// Critical: accesses AppDomain.DomainUnload event
/// TreatAsSafe: This code does not take any parameter or return state.
/// It simply attaches private callbacks.
/// </SecurityNote>
[SecurityCritical,SecurityTreatAsSafe]
public WeakEventTableShutDownListener(WeakEventTable target) : base(target)
{
}
internal override void OnShutDown(object target, object sender, EventArgs e)
{
WeakEventTable table = (WeakEventTable)target;
table.OnShutDown();
}
}
#endregion WeakEventTableShutDownListener
#region Table Keys
// the key for the data table: <manager, ((source)), hashcode>
private struct EventKey
{
internal EventKey(WeakEventManager manager, object source, bool useWeakRef)
{
_manager = manager;
_source = new WeakReference(source);
_hashcode = unchecked(manager.GetHashCode() + RuntimeHelpers.GetHashCode(source));
}
internal EventKey(WeakEventManager manager, object source)
{
_manager = manager;
_source = source;
_hashcode = unchecked(manager.GetHashCode() + RuntimeHelpers.GetHashCode(source));
}
internal object Source
{
get { return ((WeakReference)_source).Target; }
}
internal WeakEventManager Manager
{
get { return _manager; }
}
public override int GetHashCode()
{
#if DEBUG
WeakReference wr = _source as WeakReference;
object source = (wr != null) ? wr.Target : _source;
if (source != null)
{
int hashcode = unchecked(_manager.GetHashCode() + RuntimeHelpers.GetHashCode(source));
Debug.Assert(hashcode == _hashcode, "hashcodes disagree");
}
#endif
return _hashcode;
}
public override bool Equals(object o)
{
if (o is EventKey)
{
WeakReference wr;
EventKey ek = (EventKey)o;
if (_manager != ek._manager || _hashcode != ek._hashcode)
return false;
wr = this._source as WeakReference;
object s1 = (wr != null) ? wr.Target : this._source;
wr = ek._source as WeakReference;
object s2 = (wr != null) ? wr.Target : ek._source;
if (s1!=null && s2!=null)
return (s1 == s2);
else
return (_source == ek._source);
}
else
{
return false;
}
}
public static bool operator==(EventKey key1, EventKey key2)
{
return key1.Equals(key2);
}
public static bool operator!=(EventKey key1, EventKey key2)
{
return !key1.Equals(key2);
}
WeakEventManager _manager;
object _source; // lookup: direct ref; In table: WeakRef
int _hashcode; // cached, in case source is GC'd
}
// the key for the event name table: <ownerType, eventName>
private struct EventNameKey
{
public EventNameKey(Type eventSourceType, string eventName)
{
_eventSourceType = eventSourceType;
_eventName = eventName;
}
public override int GetHashCode()
{
return unchecked(_eventSourceType.GetHashCode() + _eventName.GetHashCode());
}
public override bool Equals(object o)
{
if (o is EventNameKey)
{
EventNameKey that = (EventNameKey)o;
return (this._eventSourceType == that._eventSourceType && this._eventName == that._eventName);
}
else
return false;
}
public static bool operator==(EventNameKey key1, EventNameKey key2)
{
return key1.Equals(key2);
}
public static bool operator!=(EventNameKey key1, EventNameKey key2)
{
return !key1.Equals(key2);
}
Type _eventSourceType;
string _eventName;
}
#endregion Table Keys
#if WeakEventTelemetry
#region Telemetry
static class WeakEventLogger
{
static System.Collections.Generic.List<Snapshot> _log = new System.Collections.Generic.List<Snapshot>();
public static void LogSnapshot(WeakEventTable table, string title)
{
_log.Add(new Snapshot(title).Populate(table));
if (_log.Count > 10000)
{
_log.RemoveRange(0, 7000);
}
}
class Snapshot
{
public int ThreadId { get; private set; }
public DateTime Timestamp { get; private set; }
public string Title { get; private set; }
public int Size { get; set; }
public int Reads { get; set; }
public int Writes { get; set; }
public int Purges { get; set; }
public int PurgeNoops { get; set; }
public int AllocationCount { get; set; }
public int AllocationBytes { get; set; }
public int LOHCount { get; set; }
public int LOHBytes { get; set; }
public System.Collections.Generic.Dictionary<Type, System.Collections.Generic.List<LoggerPair>>
Data { get; private set; }
public Snapshot(string title)
{
Title = title;
Timestamp = DateTime.Now;
Data = new System.Collections.Generic.Dictionary<Type, System.Collections.Generic.List<LoggerPair>>();
}
public Snapshot Populate(WeakEventTable table)
{
ThreadId = table.Dispatcher.Thread.ManagedThreadId;
Size = table._dataTable.Count;
Reads = table._readCount;
Writes = table._writeCount;
Purges = table._purgeCount;
PurgeNoops = table._purgeNoops;
AllocationRecord record;
if (table._allocations.TryGetValue(typeof(WeakEventLogger), out record))
{
AllocationCount = record.Count;
AllocationBytes = record.Bytes;
}
if (table._LOHallocations.TryGetValue(typeof(WeakEventLogger), out record))
{
LOHCount = record.Count;
LOHBytes = record.Bytes;
}
foreach (EventKey key in table._dataTable.Keys)
{
object source = key.Source;
Type type = (source == null) ? typeof(WeakEventTable) : source.GetType();
System.Collections.Generic.List<LoggerPair> list;
if (!Data.TryGetValue(type, out list))
{
list = new System.Collections.Generic.List<LoggerPair>();
list.Add(new LoggerPair());
Data[type] = list;
}
WeakEventManager manager = key.Manager;
bool found = false;
foreach (LoggerPair pair in list)
{
if (null == pair.Manager)
{
pair.Count += 1;
}
else if (manager == pair.Manager)
{
found = true;
pair.Count += 1;
break;
}
}
if (!found)
{
list.Add(new LoggerPair{ Manager=manager, Count=1 });
}
}
return this;
}
}
class LoggerPair
{
public WeakEventManager Manager { get; set; }
public int Count { get; set; }
public override string ToString() { return String.Format("{0}-{1}", Count, Manager?.GetType().Name); }
}
}
#endregion Telemetry
#endif
}
}
|