Skip to content

Commit

Permalink
Add spin wait to the desktop compilation service client (#11709)
Browse files Browse the repository at this point in the history
On .NET 4.5 the implementation for NamedPipeClientStream busy waits
during the timeout period, blocking the running CPU. Fixes #10413.
  • Loading branch information
agocke committed Jun 3, 2016
1 parent 5130487 commit 98de71e
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 1 deletion.
44 changes: 44 additions & 0 deletions src/Compilers/Server/VBCSCompilerTests/DesktopBuildClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using System.Threading;
using System.IO.Pipes;

namespace Microsoft.CodeAnalysis.CompilerServer.UnitTests
{
Expand Down Expand Up @@ -56,6 +57,14 @@ protected override Task<BuildResponse> RunServerCompilation(List<string> argumen

return base.RunServerCompilation(arguments, buildPaths, sessionKey, keepAlive, libDirectory, cancellationToken);
}

public bool TryConnectToNamedPipeWithSpinWait(int timeoutMs, CancellationToken cancellationToken)
{
using (var pipeStream = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous))
{
return TryConnectToNamedPipeWithSpinWait(pipeStream, timeoutMs, cancellationToken);
}
}
}

public sealed class ServerTests : DesktopBuildClientTests
Expand Down Expand Up @@ -136,6 +145,41 @@ public void ConnectToServerFails()
}
}

[Fact]
public async Task ConnectToPipeWithSpinWait()
{
// No server should be started with the current pipe name
var client = CreateClient();
var oneSecondInMs = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
Assert.False(client.TryConnectToNamedPipeWithSpinWait(oneSecondInMs,
default(CancellationToken)));

// Try again with infinite timeout and cancel
var cts = new CancellationTokenSource();
var connection = Task.Run(() => client.TryConnectToNamedPipeWithSpinWait(Timeout.Infinite,
cts.Token),
cts.Token);
Assert.False(connection.IsCompleted);
// Spin for a little bit
await Task.Delay(TimeSpan.FromMilliseconds(100));
cts.Cancel();
await Task.WhenAny(connection, Task.Delay(TimeSpan.FromMilliseconds(100)))
.ConfigureAwait(false);
Assert.True(connection.IsCompleted);
Assert.True(connection.IsCanceled);

// Create server and try again
Assert.True(TryCreateServer(_pipeName));
Assert.True(client.TryConnectToNamedPipeWithSpinWait(oneSecondInMs,
default(CancellationToken)));
// With infinite timeout
connection = Task.Run(() =>
client.TryConnectToNamedPipeWithSpinWait(Timeout.Infinite, default(CancellationToken)));
await Task.WhenAny(connection, Task.Delay(oneSecondInMs)).ConfigureAwait(false);
Assert.True(connection.IsCompleted);
Assert.True(await connection.ConfigureAwait(false));
}

[Fact]
public void OnlyStartsOneServer()
{
Expand Down
57 changes: 56 additions & 1 deletion src/Compilers/Shared/DesktopBuildClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,11 @@ protected override int RunLocalCompilation(string[] arguments, BuildPaths buildP
cancellationToken.ThrowIfCancellationRequested();

Log("Attempt to connect named pipe '{0}'", pipeName);
pipeStream.Connect(timeoutMs);
if (!TryConnectToNamedPipeWithSpinWait(pipeStream, timeoutMs, cancellationToken))
{
Log($"Connecting to server timed out after {timeoutMs} ms");
return null;
}
Log("Named pipe '{0}' connected", pipeName);

cancellationToken.ThrowIfCancellationRequested();
Expand All @@ -313,6 +317,57 @@ protected override int RunLocalCompilation(string[] arguments, BuildPaths buildP
}
}

// Protected for testing
protected static bool TryConnectToNamedPipeWithSpinWait(NamedPipeClientStream pipeStream,
int timeoutMs,
CancellationToken cancellationToken)
{
Debug.Assert(timeoutMs == Timeout.Infinite || timeoutMs > 0);

// .NET 4.5 implementation of NamedPipeStream.Connect busy waits the entire time.
// Work around is to spin wait.
const int maxWaitIntervalMs = 50;
int elapsedMs = 0;
var sw = new SpinWait();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();

int waitTime;
if (timeoutMs == Timeout.Infinite)
{
waitTime = maxWaitIntervalMs;
}
else
{
waitTime = Math.Min(timeoutMs - elapsedMs, maxWaitIntervalMs);
if (waitTime <= 0)
{
return false;
}
}

try
{
pipeStream.Connect(waitTime);
break;
}
catch (Exception e) when (e is IOException || e is TimeoutException)
{
// Ignore timeout

// Note: IOException can also indicate timeout. From docs:
// TimeoutException: Could not connect to the server within the
// specified timeout period.
// IOException: The server is connected to another client and the
// time-out period has expired.
}
unchecked { elapsedMs += waitTime; }
sw.SpinOnce();
}
return true;
}

/// <summary>
/// Create a new instance of the server process, returning true on success
/// and false otherwise.
Expand Down

0 comments on commit 98de71e

Please sign in to comment.