Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Use ValueTask instead of custom awaiter for SocketAwaitableEventArgs #2998

Closed
wants to merge 7 commits into from

Conversation

davidfowl
Copy link
Member

  • This could improve the performance of async operations (reduce the number of allocations)

TODO: Measure performance, I may combine this with #2974

- This could improve the performance of async operations (reduce the number of allocations)
@davidfowl davidfowl requested review from halter73 and pakrym October 11, 2018 06:00
protected override void OnCompleted(SocketAsyncEventArgs _)
/// <summary>Initiates a send operation on the associated socket.</summary>
/// <returns>This instance.</returns>
public ValueTask<int> SendAsync(Socket socket)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for ReceiveAsync and SendAsync is quite similar. Can you refactor this to one method, to avoid the code-duplication?

@davidfowl
Copy link
Member Author

@dotnet-bot test OSX 10.12 Debug Build

{
ThrowSocketException(SocketError);
Debug.Fail("Multiple continuations registered!");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really hard to find this bug in release build.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats why we run tests in both. I'm not worried about this though, this isn't general purpose. This is specifically used in a single place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I see no reason to not just throw or even Environment.FailFast in this case.

{
OnCompleted(this);
Volatile.Write(ref _completion, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I guess theoretically without this volatile write, if OnCompleted(SocketAsyncEventArgs) was called from another thread before OnCompleted(Action<object> continuation, ..., the first OnCompleted call could observe a stale _completion callback and invoke it a second time.

{
Action<object> completion = _completion;

if (completion != null || (completion = Interlocked.CompareExchange(ref _completion, _callbackCompleted, null)) != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure completion != null is safe here. I think you need a memory barrier here for the same reason you need it in Reset(). For one, without a memory barrier, reading UserToken could be reordered to before reading _completion. There's also the issue with not transitioning into the _callbackCompleted state which would make GetStatus wrong momentarily, but I don't think that's very important.


private readonly PipeScheduler _ioScheduler;

private Action _callback;
private Action<object> _completion;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think _callback or _continuation is a better name

@halter73
Copy link
Member

If this turns out to be valuable, OutputFlowControlAwaitable, ThreadPoolAwaitable and even LibuvAwaitable could probably use this IValueTaskSource treatment. As this gets more complicated, I'm wondering if at least some of this logic could be shared.

}

protected override void OnCompleted(SocketAsyncEventArgs _)
private ValueTask<int> CompleteSocketOperation()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer GetCompletedValueTask() since this isn't used to complete socket operations in the async case.

if (ReferenceEquals(awaitableState, _callbackCompleted))
{
#if NETCOREAPP2_1
ThreadPool.QueueUserWorkItem(continuation, state, preferLocal: false);
Copy link
Member

@halter73 halter73 Oct 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get this should be rare, but why not UQUWI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The continuation is Action<object>, so it's not like we're boxing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you're right, I could just queue this instead of using the state object directly since the UserToken is a field.

Copy link
Member

@halter73 halter73 Oct 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use the UserToken field at all in this case? Just use the state object directly. UserToken is only necessary if OnCompleted(SocketAsyncEventArgs is responsible to running the continuation.

ThreadPool.QueueUserWorkItem(continuation, state, preferLocal: false);
#else
Task.Factory.StartNew(continuation, state);
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you null out UserToken in OnCompleted(SocketAsyncEventArgs it's probably best to also do that here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll null it out after the work item is invoked but before calling the continuation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to wait until after the work item invoked to null out UserToken for the same reason you don't need to in OnCompleted(SocketAsyncEventArgs. Just use the local state reference which isn't null. Technically you don't need to even set UserToken in this case, you could just set it in an else block at the end.

{
_ioScheduler.Schedule(state => ((Action)state)(), continuation);
}
Reset();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why call Reset() here? _completion must already be null, right? Which now that I think about it means GetStatus is wrong after sync completions. Again, I'm not sure how important that is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync completions don't use this code path. Unless I'm missing something...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??? This is only called when ReceiveAsync/SendAsync return false, right?

I guess it's confusing because of the name. That's why I recommend renaming this to GetCompletedValueTask().

var callbackState = args.UserToken;
args.UserToken = null;
var callback = args._callback;
callback(callbackState);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what was up now. I always seem to forget that you can't convert an Action<object> to a WaitCallback without allocating.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to change this back, it's a tad bit complicated and not worth saving the allocation.


if (SocketError != SocketError.Success)
public int GetResult(short token)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The active test run was aborted. Reason: Unhandled Exception: System.InvalidOperationException: Multiple continuations registered!

You still need to null out _callback here. It's GetCompletedValueTask() where nulling out _callback is unnecessary because it will always be null there. Assuming you null it out here that is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😬 ooops

int bytesTransferred = BytesTransferred;
SocketError error = SocketError;

Volatile.Write(ref _callback, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_callback should always already be null here.

@halter73
Copy link
Member

halter73 commented Oct 12, 2018

Both Jenkins Windows failures look really scary. All sockets functional tests and logs like the following:

16:05:36   Log Critical[ApplicationNeverCompleted]: Connection id "0HLHFPHRAPFNN" application never completed 
16:05:36   Log Critical[ApplicationNeverCompleted]: Connection id "0HLHFPHRAPFOC" application never completed 

That's the "you unrooted the request processing continuation and we know b/c of our weak reference" log. In one of the runs, this was logged 7 times. 😱

public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
protected override void OnCompleted(SocketAsyncEventArgs _)
{
var callback = Interlocked.CompareExchange(ref _callback, _callbackCompleted, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this from Interlocked.Exchange to Interlocked.CompareExchange? You want to move into the _callbackCompleted state no matter what. Here you're only transitioning it if a callback isn't registered yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was always using CompareExchange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it wasn't.

protected override void OnCompleted(SocketAsyncEventArgs _)
{
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);

I think the multiple OnCompleted methods make it easy to mix this kind of stuff up.

@davidfowl
Copy link
Member Author

🆙 📅

@davidfowl
Copy link
Member Author

Looks slightly worse:

RPS CPU (%) Memory (MB) Avg. Latency (ms) Startup (ms) First Request (ms) Latency (ms)
3,163,229 82 310 1.8 187 42.5 0.7

VS

RPS CPU (%) Memory (MB) Avg. Latency (ms) Startup (ms) First Request (ms) Latency (ms)
3,130,355 73 307 2.2 192 42.1 0.6

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we don't take it if there's no performance improvement and possibly even a regression. It doesn't seem to really simplify things either.

@davidfowl
Copy link
Member Author

davidfowl commented Oct 12, 2018

I guess we don't take it if there's no performance improvement and possibly even a regression. It doesn't seem to really simplify things either.

Yes that's the current conclusion. I need to see if there are debugging benefits here before I close it out (also future improvements that might be coming e.g. https://github.com/dotnet/coreclr/issues/20200).

@davidfowl davidfowl closed this Oct 13, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants