The CreditManager implementation supports multiple awaiters, but Http2Stream's CreditManager never has more than one waiter at a time. We can instead just encode similar logic into Http2Stream, and make its waiter a reusable singleton, such that if we have to allocate it, we can just keep reusing it for all subsequent waits. This means we avoid the CreditManager allocation per Http2Stream as well as the Waiter allocation per wait (other than the first).
<PropertyGroup Condition=" '$(TargetsOSX)' == 'true' ">
<DefineConstants>$(DefineConstants);SYSNETHTTP_NO_OPENSSL</DefineConstants>
</PropertyGroup>
- <!-- Common -->
<ItemGroup>
<Compile Include="System\Net\Http\ByteArrayContent.cs" />
<Compile Include="System\Net\Http\ByteArrayHelpers.cs" />
<Compile Include="System\Net\Http\NetEventSource.Http.cs" />
<Compile Include="System\Net\Http\ReadOnlyMemoryContent.cs" />
<Compile Include="System\Net\Http\RequestRetryType.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\Http3Connection.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\Http3ConnectionException.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\Http3ProtocolException.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\Http3RequestStream.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthority.cs" />
<Compile Include="System\Net\Http\StreamContent.cs" />
<Compile Include="System\Net\Http\StreamToStreamCopy.cs" />
<Compile Include="System\Net\Http\StringContent.cs" />
<Compile Include="System\Net\Http\Headers\UriHeaderParser.cs" />
<Compile Include="System\Net\Http\Headers\ViaHeaderValue.cs" />
<Compile Include="System\Net\Http\Headers\WarningHeaderValue.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs" />
<Compile Include="$(CommonPath)System\IO\StreamHelpers.CopyValidation.cs">
<Link>Common\System\IO\StreamHelpers.CopyValidation.cs</Link>
</Compile>
<Compile Include="System\Net\Http\SocketsHttpHandler\ContentLengthWriteStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\CookieHelper.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\CreditManager.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\CreditWaiter.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\DecompressionHandler.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\EmptyReadStream.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Connection.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ConnectionException.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ProtocolErrorCode.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ProtocolException.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Stream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2StreamException.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\Http3Connection.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\Http3ConnectionException.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\Http3ProtocolException.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\Http3RequestStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthenticatedConnectionHandler.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthority.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpBaseStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnection.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionBase.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentWriteStream.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IHttpTrace.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpHandler.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
- <Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\IMultiWebProxy.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\MultiProxy.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpHandler.cs" />
+ <Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs" />
<Compile Include="$(CommonPath)System\Net\NTAuthentication.Common.cs">
<Link>Common\System\Net\NTAuthentication.Common.cs</Link>
</Compile>
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
-using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
-using System.Threading.Tasks.Sources;
namespace System.Net.Http
{
private bool _disposed;
/// <summary>Circular singly-linked list of active waiters.</summary>
/// <remarks>If null, the list is empty. If non-null, this is the tail. If the list has one item, its Next is itself.</remarks>
- private Waiter _waitersTail;
+ private CreditWaiter _waitersTail;
public CreditManager(IHttpTrace owner, string name, int initialCredit)
{
private object SyncObject
{
- get
- {
- // Generally locking on "this" is considered poor form, but this type is internal,
- // and it's unnecessary overhead to allocate another object just for this purpose.
- return this;
- }
+ // Generally locking on "this" is considered poor form, but this type is internal,
+ // and it's unnecessary overhead to allocate another object just for this purpose.
+ get => this;
}
public ValueTask<int> RequestCreditAsync(int amount, CancellationToken cancellationToken)
if (NetEventSource.IsEnabled) _owner.Trace($"{_name}. requested={amount}, no credit available.");
// Otherwise, create a new waiter.
- Waiter waiter = cancellationToken.CanBeCanceled ?
- new CancelableWaiter(amount, SyncObject, cancellationToken) :
- new Waiter(amount);
+ CreditWaiter waiter = cancellationToken.CanBeCanceled ?
+ new CancelableCreditWaiter(SyncObject, cancellationToken) :
+ new CreditWaiter();
+ waiter.Amount = amount;
// Add the waiter at the tail of the queue.
if (_waitersTail is null)
while (_current > 0 && _waitersTail != null)
{
// Get the waiter from the head of the queue.
- Waiter waiter = _waitersTail.Next;
+ CreditWaiter waiter = _waitersTail.Next;
int granted = Math.Min(waiter.Amount, _current);
// Remove the waiter from the list.
_disposed = true;
- Waiter waiter = _waitersTail;
+ CreditWaiter waiter = _waitersTail;
if (waiter != null)
{
do
{
- Waiter next = waiter.Next;
+ CreditWaiter next = waiter.Next;
waiter.Next = null;
waiter.Dispose();
waiter = next;
}
}
}
-
- /// <summary>Represents a waiter for credit.</summary>
- /// <remarks>All of the public members on the instance must only be accessed while holding the CreditManager's lock.</remarks>
- private class Waiter : IValueTaskSource<int>
- {
- public readonly int Amount;
- public Waiter Next;
- protected ManualResetValueTaskSourceCore<int> _source;
-
- public Waiter(int amount)
- {
- Amount = amount;
- _source.RunContinuationsAsynchronously = true;
- }
-
- public ValueTask<int> AsValueTask() => new ValueTask<int>(this, _source.Version);
-
- public bool IsPending => _source.GetStatus(_source.Version) == ValueTaskSourceStatus.Pending;
-
- public bool TrySetResult(int result)
- {
- if (IsPending)
- {
- _source.SetResult(result);
- return true;
- }
-
- return false;
- }
-
- public virtual void Dispose()
- {
- if (IsPending)
- {
- _source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new ObjectDisposedException(nameof(CreditManager), SR.net_http_disposed_while_in_use)));
- }
- }
-
- int IValueTaskSource<int>.GetResult(short token) =>
- _source.GetResult(token);
- ValueTaskSourceStatus IValueTaskSource<int>.GetStatus(short token) =>
- _source.GetStatus(token);
- void IValueTaskSource<int>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) =>
- _source.OnCompleted(continuation, state, token, flags);
- }
-
- private sealed class CancelableWaiter : Waiter
- {
- private readonly object _syncObj;
- private CancellationTokenRegistration _registration;
-
- public CancelableWaiter(int amount, object syncObj, CancellationToken cancellationToken) : base(amount)
- {
- _syncObj = syncObj;
- _registration = cancellationToken.UnsafeRegister(s =>
- {
- CancelableWaiter thisRef = (CancelableWaiter)s!;
- lock (thisRef._syncObj)
- {
- if (thisRef.IsPending)
- {
- thisRef._source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(thisRef._registration.Token)));
- thisRef._registration = default; // benign race with setting in the ctor
-
- // We don't remove it from the list as we lack a prev pointer that would enable us to do so correctly,
- // and it's not worth adding a prev pointer for the rare case of cancellation. We instead just
- // check when completing a waiter whether it's already been canceled. As such, we also do not
- // dispose it here.
- }
- }
- }, this);
- }
-
- public override void Dispose()
- {
- Monitor.IsEntered(_syncObj);
-
- _registration.Dispose();
- _registration = default;
-
- base.Dispose();
- }
- }
}
}
--- /dev/null
+// 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.Diagnostics;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Sources;
+
+namespace System.Net.Http
+{
+ /// <summary>Represents a waiter for credit.</summary>
+ internal class CreditWaiter : IValueTaskSource<int>
+ {
+ public int Amount;
+ public CreditWaiter Next;
+ protected ManualResetValueTaskSourceCore<int> _source;
+
+ public CreditWaiter() => _source.RunContinuationsAsynchronously = true;
+
+ public ValueTask<int> AsValueTask() => new ValueTask<int>(this, _source.Version);
+
+ public bool IsPending => _source.GetStatus(_source.Version) == ValueTaskSourceStatus.Pending;
+
+ public bool TrySetResult(int result)
+ {
+ if (IsPending)
+ {
+ _source.SetResult(result);
+ return true;
+ }
+
+ return false;
+ }
+
+ public virtual void CleanUp() { }
+
+ public void Dispose()
+ {
+ CleanUp();
+ if (IsPending)
+ {
+ _source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new ObjectDisposedException(nameof(CreditManager), SR.net_http_disposed_while_in_use)));
+ }
+ }
+
+ int IValueTaskSource<int>.GetResult(short token) =>
+ _source.GetResult(token);
+ ValueTaskSourceStatus IValueTaskSource<int>.GetStatus(short token) =>
+ _source.GetStatus(token);
+ void IValueTaskSource<int>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) =>
+ _source.OnCompleted(continuation, state, token, flags);
+ }
+
+ /// <summary>Represents a cancelable waiter for credit.</summary>
+ internal sealed class CancelableCreditWaiter : CreditWaiter
+ {
+ private readonly object _syncObj;
+ private CancellationTokenRegistration _registration;
+
+ public CancelableCreditWaiter(object syncObj, CancellationToken cancellationToken)
+ {
+ _syncObj = syncObj;
+ RegisterCancellation(cancellationToken);
+ }
+
+ public void ResetForAwait(CancellationToken cancellationToken)
+ {
+ Debug.Assert(Monitor.IsEntered(_syncObj));
+ Debug.Assert(!IsPending);
+ Debug.Assert(Next is null);
+ Debug.Assert(_registration == default);
+
+ _source.Reset();
+ RegisterCancellation(cancellationToken);
+ }
+
+ public override void CleanUp()
+ {
+ Monitor.IsEntered(_syncObj);
+ _registration.Dispose();
+ _registration = default;
+ }
+
+ private void RegisterCancellation(CancellationToken cancellationToken)
+ {
+ _registration = cancellationToken.UnsafeRegister(s =>
+ {
+ CancelableCreditWaiter thisRef = (CancelableCreditWaiter)s!;
+ lock (thisRef._syncObj)
+ {
+ if (thisRef.IsPending)
+ {
+ thisRef._registration = default; // benign race with setting in the ctor
+ thisRef._source.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(thisRef._registration.Token)));
+
+ // We don't remove it from the list as we lack a prev pointer that would enable us to do so correctly,
+ // and it's not worth adding a prev pointer for the rare case of cancellation. We instead just
+ // check when completing a waiter whether it's already been canceled. As such, we also do not
+ // dispose it here.
+ }
+ }
+ }, this);
+ }
+ }
+}
private readonly Http2Connection _connection;
private readonly int _streamId;
- private readonly CreditManager _streamWindow;
private readonly HttpRequestMessage _request;
private HttpResponseMessage _response;
/// <summary>Stores any trailers received after returning the response content to the caller.</summary>
private ArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
private int _pendingWindowUpdate;
+ private CancelableCreditWaiter _creditWaiter;
+ private int _availableCredit;
private StreamCompletionState _requestCompletionState;
private StreamCompletionState _responseCompletionState;
_responseBuffer = new ArrayBuffer(InitialStreamBufferSize, usePool: true);
_pendingWindowUpdate = 0;
-
- _streamWindow = new CreditManager(this, nameof(_streamWindow), initialWindowSize);
+ _availableCredit = initialWindowSize;
_headerBudgetRemaining = connection._pool.Settings._maxResponseHeadersLength * 1024;
if (NetEventSource.IsEnabled) Trace($"{request}, {nameof(initialWindowSize)}={initialWindowSize}");
}
- private object SyncObject => _streamWindow;
+ private object SyncObject => this; // this isn't handed out to code that may lock on it
public int StreamId => _streamId;
_connection.RemoveStream(this);
- _streamWindow.Dispose();
+ lock (SyncObject)
+ {
+ CreditWaiter w = _creditWaiter;
+ if (w != null)
+ {
+ w.Dispose();
+ _creditWaiter = null;
+ }
+ }
}
private void Cancel()
return (signalWaiter, sendReset);
}
- public void OnWindowUpdate(int amount) => _streamWindow.AdjustCredit(amount);
+ public void OnWindowUpdate(int amount)
+ {
+ lock (SyncObject)
+ {
+ _availableCredit = checked(_availableCredit + amount);
+ if (_availableCredit > 0 && _creditWaiter != null && _creditWaiter.IsPending)
+ {
+ int granted = Math.Min(_availableCredit, _creditWaiter.Amount);
+ if (_creditWaiter.TrySetResult(granted))
+ {
+ _availableCredit -= granted;
+ }
+ }
+ }
+ }
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
{
while (buffer.Length > 0)
{
- int sendSize = await _streamWindow.RequestCreditAsync(buffer.Length, cancellationToken).ConfigureAwait(false);
+ int sendSize = -1;
+ lock (SyncObject)
+ {
+ if (_availableCredit > 0)
+ {
+ sendSize = Math.Min(buffer.Length, _availableCredit);
+ _availableCredit -= sendSize;
+ }
+ else
+ {
+ if (_creditWaiter is null)
+ {
+ _creditWaiter = new CancelableCreditWaiter(SyncObject, cancellationToken);
+ }
+ else
+ {
+ Debug.Assert(!_creditWaiter.IsPending);
+ _creditWaiter.ResetForAwait(cancellationToken);
+ }
+ _creditWaiter.Amount = buffer.Length;
+ }
+ }
+
+ if (sendSize == -1)
+ {
+ sendSize = await _creditWaiter.AsValueTask().ConfigureAwait(false);
+ _creditWaiter.CleanUp();
+ }
ReadOnlyMemory<byte> current;
(current, buffer) = SplitBuffer(buffer, sendSize);