Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix CPU is consumed by polling frequently when there is no subscriber #4735

Merged
merged 12 commits into from
Nov 14, 2023
39 changes: 2 additions & 37 deletions playground/MSTest1/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.VisualStudio.TestTools.UnitTesting;
nohwnd marked this conversation as resolved.
Show resolved Hide resolved

using System.Threading;
namespace MSTest1;

[TestClass]
Expand All @@ -11,42 +12,6 @@ public class UnitTest1
[TestMethod]
public void TestMethod1()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod2()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod3()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod4()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod5()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod6()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod7()
{
// Thread.Sleep(1000);
Thread.Sleep(10_000);
}
}
2 changes: 1 addition & 1 deletion playground/MSTest2/MSTest2.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(TargetFrameworks);net472;net5.0;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworks);net462;net472;net5.0;net6.0;net7.0</TargetFrameworks>
<Prefer32Bit>false</Prefer32Bit>
<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
40 changes: 3 additions & 37 deletions playground/MSTest2/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using Microsoft.VisualStudio.TestTools.UnitTesting;

using System.Threading;

namespace MSTest2;

[TestClass]
Expand All @@ -11,42 +13,6 @@ public class UnitTest2
[TestMethod]
public void TestMethod1()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod2()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod3()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod4()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod5()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod6()
{
// Thread.Sleep(1000);
}

[TestMethod]
public void TestMethod7()
{
// Thread.Sleep(1000);
Thread.Sleep(10_000);
}
}
3 changes: 2 additions & 1 deletion playground/TestPlatform.Playground/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ internal class EnvironmentVariables
["VSTEST_RUNNER_DEBUG_ATTACHVS"] = "0",
["VSTEST_HOST_DEBUG_ATTACHVS"] = "0",
["VSTEST_DATACOLLECTOR_DEBUG_ATTACHVS"] = "0",
["VSTEST_EXPERIMENTAL_FORWARD_OUTPUT_FEATURE"] = "0"
["VSTEST_EXPERIMENTAL_FORWARD_OUTPUT_FEATURE"] = "0",
["VSTEST_HOSTPRESTART_COUNT"] = "1",
};

}
58 changes: 17 additions & 41 deletions playground/TestPlatform.Playground/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ static void Main()

var thisAssemblyPath = Assembly.GetEntryAssembly()!.Location;
var here = Path.GetDirectoryName(thisAssemblyPath)!;
var playground = Path.GetFullPath(Path.Combine(here, "..", "..", "..", ".."));

var console = Path.Combine(here, "vstest.console", "vstest.console.exe");

var sourceSettings = $$$"""
<RunSettings>
<RunConfiguration>

<!-- <MaxCpuCount>1</MaxCpuCount> -->
<MaxCpuCount>1</MaxCpuCount>
<!-- <DisableParallelization>True<DisableParallelization> -->
<!-- <TargetPlatform>x86</TargetPlatform> -->
<!-- <TargetFrameworkVersion>net472</TargetFrameworkVersion> -->
Expand Down Expand Up @@ -88,46 +87,24 @@ static void Main()
""";

var sources = new[] {
Path.Combine(playground, "bin", "MSTest1", "Debug", "net472", "MSTest1.dll"),
Path.Combine(playground, "bin", "MSTest2", "Debug", "net472", "MSTest2.dll"),
// @"S:\t\mstest85\bin\Debug\net7.0\mstest85.dll",
//@"S:\t\mstest85\bin\Debug\net6.0\mstest85.dll",
//@"S:\t\mstest85\bin\Debug\net8.0\mstest85.dll",
@"S:\t\nunit5\bin\Debug\net7.0\nunit5.dll",
@"S:\t\nunit5\bin\Debug\net8.0\nunit5.dll",
@"S:\t\nunit5\bin\Debug\net6.0\nunit5.dll",
// The built in .NET projects don't now work right now in Playground, there is some conflict with Arcade.
// But if you create one outside of Playground it will work.
//Path.Combine(playground, "bin", "MSTest1", "Debug", "net7.0", "MSTest1.dll"),
};

//// Console mode
//// Uncomment if providing command line parameters is easier for you
//// than converting them to settings, or when you debug command line scenario specifically.
//var settingsFile = Path.GetTempFileName();
//try
//{
// File.WriteAllText(settingsFile, sourceSettings);
// var processStartInfo = new ProcessStartInfo
// {
// FileName = console,
// Arguments = $"{string.Join(" ", sources)} --settings:{settingsFile} --listtests",
// UseShellExecute = false,
// };
// EnvironmentVariables.Variables.ToList().ForEach(processStartInfo.Environment.Add);
// var process = Process.Start(processStartInfo);
// process.WaitForExit();
// if (process.ExitCode != 0)
// {
// throw new Exception($"Process failed with {process.ExitCode}");
// }
//}
//finally
//{
// try { File.Delete(settingsFile); } catch { }
//}

// design mode
var detailedOutput = true;
var consoleOptions = new ConsoleParameters
{
EnvironmentVariables = EnvironmentVariables.Variables,
// LogFilePath = Path.Combine(here, "logs", "log.txt"),
// TraceLevel = TraceLevel.Verbose,
LogFilePath = Path.Combine(here, "logs", "log.txt"),
TraceLevel = TraceLevel.Verbose,
};
var options = new TestPlatformOptions
{
Expand All @@ -139,23 +116,22 @@ static void Main()
//// TestSessions
// r.StartTestSession(sources, sourceSettings, sessionHandler);
#pragma warning restore CS0618 // Type or member is obsolete
var discoveryHandler = new PlaygroundTestDiscoveryHandler(detailedOutput);
//var discoveryHandler = new PlaygroundTestDiscoveryHandler(detailedOutput);
var sw = Stopwatch.StartNew();
// Discovery
r.DiscoverTests(sources, sourceSettings, options, sessionHandler.TestSessionInfo, discoveryHandler);
var discoveryDuration = sw.ElapsedMilliseconds;
Console.WriteLine($"Discovery done in {discoveryDuration} ms");
//r.DiscoverTests(sources, sourceSettings, options, sessionHandler.TestSessionInfo, discoveryHandler);
//var discoveryDuration = sw.ElapsedMilliseconds;
//Console.WriteLine($"Discovery done in {discoveryDuration} ms");
sw.Restart();
// Run with test cases and custom testhost launcher
//r.RunTestsWithCustomTestHost(discoveryHandler.TestCases, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput), new DebuggerTestHostLauncher());
//// Run with test cases and without custom testhost launcher
r.RunTests(discoveryHandler.TestCases, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput));
//r.RunTests(discoveryHandler.TestCases, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput));
//// Run with sources and custom testhost launcher and debugging
//r.RunTestsWithCustomTestHost(sources, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput), new DebuggerTestHostLauncher());
// r.RunTestsWithCustomTestHost(sources, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput), new DebuggerTestHostLauncher());
//// Run with sources
//r.RunTests(sources, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput));
var rd = sw.ElapsedMilliseconds;
Console.WriteLine($"Discovery: {discoveryDuration} ms, Run: {rd} ms, Total: {discoveryDuration + rd} ms");
r.RunTests(sources, sourceSettings, options, sessionHandler.TestSessionInfo, new TestRunHandler(detailedOutput));
//Console.WriteLine($"Discovery: {discoveryDuration} ms, Run: {rd} ms, Total: {discoveryDuration + rd} ms");
// Console.WriteLine($"Settings:\n{sourceSettings}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;

public interface ICommunicationChannel : IDisposable
{
/// <summary>
/// Event raised when data is received on the communication channel.
/// </summary>
event EventHandler<MessageReceivedEventArgs> MessageReceived;
TrackableEvent<MessageReceivedEventArgs> MessageReceived { get; }

/// <summary>
/// Frames and sends the provided data over communication channel.
Expand All @@ -24,5 +27,66 @@ public interface ICommunicationChannel : IDisposable
/// Notification from server/client that data is available.
/// </summary>
/// <returns>A <see cref="Task"/> implying async nature of the function.</returns>
Task NotifyDataAvailable();
Task NotifyDataAvailable(CancellationToken cancellationToken);
}

#pragma warning disable CA1001 // Types that own disposable fields should be disposable
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
public class TrackableEvent<T>
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
{
private readonly ManualResetEventSlim _slim;

internal event EventHandler<T>? Event;

public TrackableEvent()
{
_slim = new ManualResetEventSlim(Event != null);
}

public void Notify(object sender, T eventArgs, string traceDisplayName)
{
var e = Event;
if (e != null)
{
e.SafeInvoke(sender, eventArgs!, traceDisplayName);
}
}

public bool WaitForSubscriber(int timeoutMilliseconds, CancellationToken cancellationToken)
{
var _ = timeoutMilliseconds;
var __ = cancellationToken;
return Event != null;
//return _slim.Wait(timeoutMilliseconds, cancellationToken);
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
}

public void Subscribe(EventHandler<T> eventHandler)
{
Event += eventHandler;
if (Event != null)
{
_slim.Set();
}
}

public void Unsubscribe(EventHandler<T> eventHandler)
{
Event -= eventHandler;
if (Event == null)
{
_slim.Reset();
}
}
}

internal class UnsubscribeToken : IDisposable
{
private readonly Action _unsubscribeCallback;
internal UnsubscribeToken(Action value) => _unsubscribeCallback = value;

public void Unsubscribe() => _unsubscribeCallback();
public void Dispose()
{
_unsubscribeCallback();
}
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;

Expand Down Expand Up @@ -37,7 +37,7 @@ public LengthPrefixCommunicationChannel(Stream stream)
}

/// <inheritdoc />
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
public TrackableEvent<MessageReceivedEventArgs> MessageReceived { get; } = new TrackableEvent<MessageReceivedEventArgs>();

/// <inheritdoc />
public Task Send(string data)
Expand Down Expand Up @@ -71,7 +71,7 @@ public Task Send(string data)
}

/// <inheritdoc />
public Task NotifyDataAvailable()
public Task NotifyDataAvailable(CancellationToken cancellationToken)
{
try
{
Expand All @@ -85,14 +85,10 @@ public Task NotifyDataAvailable()
// Try read data even if no one is listening to the data stream. Some server
// implementations (like Sockets) depend on the read operation to determine if a
// connection is closed.
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
if (MessageReceived != null)
if (MessageReceived.WaitForSubscriber(1000, cancellationToken))
{
var data = _reader.ReadString();
MessageReceived.SafeInvoke(this, new MessageReceivedEventArgs { Data = data }, "LengthPrefixCommunicationChannel: MessageReceived");
}
else
{
Copy link
Member Author

Choose a reason for hiding this comment

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

This message is unnecessary, and way too much detail. with prestart it is normal that client is waiting for his turn and this would just add yet another message to the log that would always repeat.

EqtTrace.Verbose("LengthPrefixCommunicationChannel.NotifyDataAvailable: New data are waiting to be received, but there is no subscriber to be notified. Not reading them from the stream.");
MessageReceived.Notify(this, new MessageReceivedEventArgs { Data = data }, "LengthPrefixCommunicationChannel: MessageReceived");
}
}
catch (ObjectDisposedException ex) when (!_reader.BaseStream.CanRead)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,6 @@ Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.Disconnect
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.DisconnectedEventArgs.Error.get -> System.Exception?
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.DisconnectedEventArgs.Error.set -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel.MessageReceived -> System.EventHandler<Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.MessageReceivedEventArgs!>!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel.NotifyDataAvailable() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel.Send(string! data) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationEndPoint
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationEndPoint.Connected -> System.EventHandler<Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ConnectedEventArgs!>!
Expand Down Expand Up @@ -189,8 +187,6 @@ Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.JsonDataSerializer.Se
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.Dispose() -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.LengthPrefixCommunicationChannel(System.IO.Stream! stream) -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.MessageReceived -> System.EventHandler<Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.MessageReceivedEventArgs!>?
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.NotifyDataAvailable() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.Send(string! data) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Message
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Message.Message() -> void
Expand Down Expand Up @@ -392,3 +388,13 @@ static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.JsonDataSerial
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.UnexpectedMessage.get -> string
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.VersionCheckFailed.get -> string
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.VersionCheckTimedout.get -> string
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>.TrackableEvent() -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>.Unsubscribe(System.EventHandler<T>! eventHandler) -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>.WaitForSubscriber(int timeoutMilliseconds, System.Threading.CancellationToken cancellationToken) -> bool
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>.Notify(object! sender, T eventArgs, string! traceDisplayName) -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<T>.Subscribe(System.EventHandler<T>! eventHandler) -> void
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel.MessageReceived.get -> Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.MessageReceivedEventArgs!>!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.MessageReceived.get -> Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.TrackableEvent<Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.MessageReceivedEventArgs!>!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ICommunicationChannel.NotifyDataAvailable(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.LengthPrefixCommunicationChannel.NotifyDataAvailable(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Loading