// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
//
// TargetRegistry.cs
//
//
// A store of registered targets with a target block.
//
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
namespace System.Threading.Tasks.Dataflow.Internal
{
/// Stores targets registered with a source.
/// Specifies the type of data accepted by the targets.
/// This type is not thread-safe.
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(TargetRegistry<>.DebugView))]
internal sealed class TargetRegistry
{
///
/// Information about a registered target. This class represents a self-sufficient node in a linked list.
///
internal sealed class LinkedTargetInfo
{
/// Initializes the LinkedTargetInfo.
/// The target block reference for this entry.
/// The link options.
internal LinkedTargetInfo(ITargetBlock target, DataflowLinkOptions linkOptions)
{
Contract.Requires(target != null, "The target that is supposed to be linked must not be null.");
Contract.Requires(linkOptions != null, "The linkOptions must not be null.");
Target = target;
PropagateCompletion = linkOptions.PropagateCompletion;
RemainingMessages = linkOptions.MaxMessages;
}
/// The target block reference for this entry.
internal readonly ITargetBlock Target;
/// The value of the PropagateCompletion link option.
internal readonly bool PropagateCompletion;
/// Number of remaining messages to propagate.
/// This counter is initialized to the MaxMessages option and
/// gets decremented after each successful propagation.
internal int RemainingMessages;
/// The previous node in the list.
internal LinkedTargetInfo Previous;
/// The next node in the list.
internal LinkedTargetInfo Next;
}
/// A reference to the owning source block.
private readonly ISourceBlock _owningSource;
/// A mapping of targets to information about them.
private readonly Dictionary, LinkedTargetInfo> _targetInformation;
/// The first node of an ordered list of targets. Messages should be offered to targets starting from First and following Next.
private LinkedTargetInfo _firstTarget;
/// The last node of the ordered list of targets. This field is used purely as a perf optimization to avoid traversing the list for each Add.
private LinkedTargetInfo _lastTarget;
/// Number of links with positive RemainingMessages counters.
/// This is an optimization that allows us to skip dictionary lookup when this counter is 0.
private int _linksWithRemainingMessages;
/// Initializes the registry.
internal TargetRegistry(ISourceBlock owningSource)
{
Contract.Requires(owningSource != null, "The TargetRegistry instance must be owned by a source block.");
_owningSource = owningSource;
_targetInformation = new Dictionary, LinkedTargetInfo>();
}
/// Adds a target to the registry.
/// The target to add.
/// The link options.
internal void Add(ref ITargetBlock target, DataflowLinkOptions linkOptions)
{
Contract.Requires(target != null, "The target that is supposed to be linked must not be null.");
Contract.Requires(linkOptions != null, "The link options must not be null.");
LinkedTargetInfo targetInfo;
// If the target already exists in the registry, replace it with a new NopLinkPropagator to maintain uniqueness
if (_targetInformation.TryGetValue(target, out targetInfo)) target = new NopLinkPropagator(_owningSource, target);
// Add the target to both stores, the list and the dictionary, which are used for different purposes
var node = new LinkedTargetInfo(target, linkOptions);
AddToList(node, linkOptions.Append);
_targetInformation.Add(target, node);
// Increment the optimization counter if needed
Debug.Assert(_linksWithRemainingMessages >= 0, "_linksWithRemainingMessages must be non-negative at any time.");
if (node.RemainingMessages > 0) _linksWithRemainingMessages++;
#if FEATURE_TRACING
DataflowEtwProvider etwLog = DataflowEtwProvider.Log;
if (etwLog.IsEnabled())
{
etwLog.DataflowBlockLinking(_owningSource, target);
}
#endif
}
/// Gets whether the registry contains a particular target.
/// The target.
/// true if the registry contains the target; otherwise, false.
internal bool Contains(ITargetBlock target)
{
return _targetInformation.ContainsKey(target);
}
/// Removes the target from the registry.
/// The target to remove.
///
/// Only remove the target if it's configured to be unlinked after one propagation.
///
internal void Remove(ITargetBlock target, bool onlyIfReachedMaxMessages = false)
{
Contract.Requires(target != null, "Target to remove is required.");
// If we are implicitly unlinking and there is nothing to be unlinked implicitly, bail
Debug.Assert(_linksWithRemainingMessages >= 0, "_linksWithRemainingMessages must be non-negative at any time.");
if (onlyIfReachedMaxMessages && _linksWithRemainingMessages == 0) return;
// Otherwise take the slow path
Remove_Slow(target, onlyIfReachedMaxMessages);
}
/// Actually removes the target from the registry.
/// The target to remove.
///
/// Only remove the target if it's configured to be unlinked after one propagation.
///
private void Remove_Slow(ITargetBlock target, bool onlyIfReachedMaxMessages)
{
Contract.Requires(target != null, "Target to remove is required.");
// Make sure we've intended to go the slow route
Debug.Assert(_linksWithRemainingMessages >= 0, "_linksWithRemainingMessages must be non-negative at any time.");
Debug.Assert(!onlyIfReachedMaxMessages || _linksWithRemainingMessages > 0, "We shouldn't have ended on the slow path.");
// If the target is registered...
LinkedTargetInfo node;
if (_targetInformation.TryGetValue(target, out node))
{
Debug.Assert(node != null, "The LinkedTargetInfo node referenced in the Dictionary must be non-null.");
// Remove the target, if either there's no constraint on the removal
// or if this was the last remaining message.
if (!onlyIfReachedMaxMessages || node.RemainingMessages == 1)
{
RemoveFromList(node);
_targetInformation.Remove(target);
// Decrement the optimization counter if needed
if (node.RemainingMessages == 0) _linksWithRemainingMessages--;
Debug.Assert(_linksWithRemainingMessages >= 0, "_linksWithRemainingMessages must be non-negative at any time.");
#if FEATURE_TRACING
DataflowEtwProvider etwLog = DataflowEtwProvider.Log;
if (etwLog.IsEnabled())
{
etwLog.DataflowBlockUnlinking(_owningSource, target);
}
#endif
}
// If the target is to stay and we are counting the remaining messages for this link, decrement the counter
else if (node.RemainingMessages > 0)
{
Debug.Assert(node.RemainingMessages > 1, "The target should have been removed, because there are no remaining messages.");
node.RemainingMessages--;
}
}
}
/// Clears the target registry entry points while allowing subsequent traversals of the linked list.
internal LinkedTargetInfo ClearEntryPoints()
{
// Save _firstTarget so we can return it
LinkedTargetInfo firstTarget = _firstTarget;
// Clear out the entry points
_firstTarget = _lastTarget = null;
_targetInformation.Clear();
Debug.Assert(_linksWithRemainingMessages >= 0, "_linksWithRemainingMessages must be non-negative at any time.");
_linksWithRemainingMessages = 0;
return firstTarget;
}
/// Propagated completion to the targets of the given linked list.
/// The head of a saved linked list.
internal void PropagateCompletion(LinkedTargetInfo firstTarget)
{
Debug.Assert(_owningSource.Completion.IsCompleted, "The owning source must have completed before propagating completion.");
// Cache the owning source's completion task to avoid calling the getter many times
Task owningSourceCompletion = _owningSource.Completion;
// Propagate completion to those targets that have requested it
for (LinkedTargetInfo node = firstTarget; node != null; node = node.Next)
{
if (node.PropagateCompletion) Common.PropagateCompletion(owningSourceCompletion, node.Target, Common.AsyncExceptionHandler);
}
}
/// Gets the first node of the ordered target list.
internal LinkedTargetInfo FirstTargetNode { get { return _firstTarget; } }
/// Adds a LinkedTargetInfo node to the doubly-linked list.
/// The node to be added.
/// Whether to append or to prepend the node.
internal void AddToList(LinkedTargetInfo node, bool append)
{
Contract.Requires(node != null, "Requires a node to be added.");
// If the list is empty, assign the ends to point to the new node and we are done
if (_firstTarget == null && _lastTarget == null)
{
_firstTarget = _lastTarget = node;
}
else
{
Debug.Assert(_firstTarget != null && _lastTarget != null, "Both first and last node must either be null or non-null.");
Debug.Assert(_lastTarget.Next == null, "The last node must not have a successor.");
Debug.Assert(_firstTarget.Previous == null, "The first node must not have a predecessor.");
if (append)
{
// Link the new node to the end of the existing list
node.Previous = _lastTarget;
_lastTarget.Next = node;
_lastTarget = node;
}
else
{
// Link the new node to the front of the existing list
node.Next = _firstTarget;
_firstTarget.Previous = node;
_firstTarget = node;
}
}
Debug.Assert(_firstTarget != null && _lastTarget != null, "Both first and last node must be non-null after AddToList.");
}
/// Removes the LinkedTargetInfo node from the doubly-linked list.
/// The node to be removed.
internal void RemoveFromList(LinkedTargetInfo node)
{
Contract.Requires(node != null, "Node to remove is required.");
Debug.Assert(_firstTarget != null && _lastTarget != null, "Both first and last node must be non-null before RemoveFromList.");
LinkedTargetInfo previous = node.Previous;
LinkedTargetInfo next = node.Next;
// Remove the node by linking the adjacent nodes
if (node.Previous != null)
{
node.Previous.Next = next;
node.Previous = null;
}
if (node.Next != null)
{
node.Next.Previous = previous;
node.Next = null;
}
// Adjust the list ends
if (_firstTarget == node) _firstTarget = next;
if (_lastTarget == node) _lastTarget = previous;
Debug.Assert((_firstTarget != null) == (_lastTarget != null), "Both first and last node must either be null or non-null after RemoveFromList.");
}
/// Gets the number of items in the registry.
private int Count { get { return _targetInformation.Count; } }
/// Converts the linked list of targets to an array for rendering in a debugger.
private ITargetBlock[] TargetsForDebugger
{
get
{
var targets = new ITargetBlock[Count];
int i = 0;
for (LinkedTargetInfo node = _firstTarget; node != null; node = node.Next)
{
targets[i++] = node.Target;
}
return targets;
}
}
/// Provides a nop passthrough for use with TargetRegistry.
[DebuggerDisplay("{DebuggerDisplayContent,nq}")]
[DebuggerTypeProxy(typeof(TargetRegistry<>.NopLinkPropagator.DebugView))]
private sealed class NopLinkPropagator : IPropagatorBlock, ISourceBlock, IDebuggerDisplay
{
/// The source that encapsulates this block.
private readonly ISourceBlock _owningSource;
/// The target with which this block is associated.
private readonly ITargetBlock _target;
/// Initializes the passthrough.
/// The source that encapsulates this block.
/// The target to which messages should be forwarded.
internal NopLinkPropagator(ISourceBlock owningSource, ITargetBlock target)
{
Contract.Requires(owningSource != null, "Propagator must be associated with a source.");
Contract.Requires(target != null, "Target to propagate to is required.");
// Store the arguments
_owningSource = owningSource;
_target = target;
}
///
DataflowMessageStatus ITargetBlock.OfferMessage(DataflowMessageHeader messageHeader, T messageValue, ISourceBlock source, Boolean consumeToAccept)
{
Debug.Assert(source == _owningSource, "Only valid to be used with the source for which it was created.");
return _target.OfferMessage(messageHeader, messageValue, this, consumeToAccept);
}
///
T ISourceBlock.ConsumeMessage(DataflowMessageHeader messageHeader, ITargetBlock target, out Boolean messageConsumed)
{
return _owningSource.ConsumeMessage(messageHeader, this, out messageConsumed);
}
///
bool ISourceBlock.ReserveMessage(DataflowMessageHeader messageHeader, ITargetBlock target)
{
return _owningSource.ReserveMessage(messageHeader, this);
}
///
void ISourceBlock.ReleaseReservation(DataflowMessageHeader messageHeader, ITargetBlock target)
{
_owningSource.ReleaseReservation(messageHeader, this);
}
///
Task IDataflowBlock.Completion { get { return _owningSource.Completion; } }
///
void IDataflowBlock.Complete() { _target.Complete(); }
///
void IDataflowBlock.Fault(Exception exception) { _target.Fault(exception); }
///
IDisposable ISourceBlock.LinkTo(ITargetBlock target, DataflowLinkOptions linkOptions) { throw new NotSupportedException(SR.NotSupported_MemberNotNeeded); }
/// The data to display in the debugger display attribute.
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider")]
private object DebuggerDisplayContent
{
get
{
var displaySource = _owningSource as IDebuggerDisplay;
var displayTarget = _target as IDebuggerDisplay;
return string.Format("{0} Source=\"{1}\", Target=\"{2}\"",
Common.GetNameForDebugger(this),
displaySource != null ? displaySource.Content : _owningSource,
displayTarget != null ? displayTarget.Content : _target);
}
}
/// Gets the data to display in the debugger display attribute for this instance.
object IDebuggerDisplay.Content { get { return DebuggerDisplayContent; } }
/// Provides a debugger type proxy for a passthrough.
private sealed class DebugView
{
/// The passthrough.
private readonly NopLinkPropagator _passthrough;
/// Initializes the debug view.
/// The passthrough to view.
public DebugView(NopLinkPropagator passthrough)
{
Contract.Requires(passthrough != null, "Need a propagator with which to construct the debug view.");
_passthrough = passthrough;
}
/// The linked target for this block.
public ITargetBlock LinkedTarget { get { return _passthrough._target; } }
}
}
/// Provides a debugger type proxy for the target registry.
private sealed class DebugView
{
/// The registry being debugged.
private readonly TargetRegistry _registry;
/// Initializes the type proxy.
/// The target registry.
public DebugView(TargetRegistry registry)
{
Contract.Requires(registry != null, "Need a registry with which to construct the debug view.");
_registry = registry;
}
/// Gets a list of all targets to show in the debugger.
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public ITargetBlock[] Targets { get { return _registry.TargetsForDebugger; } }
}
}
}