diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs new file mode 100644 index 000000000..f173eca91 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskInternals.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.Threading; + +using System.ComponentModel; + +#pragma warning disable RS0016 // Add public types and members to the declared API +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +/// +/// A helper class for integration with Visual Studio. +/// APIs in this file are intended for Microsoft internal use only +/// and are subject to change without notice. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class JoinableTaskInternals +{ + public static bool IsMainThreadBlockedByAnyJoinableTask(JoinableTaskContext? joinableTaskContext) + { + return joinableTaskContext?.IsMainThreadBlockedByAnyJoinableTask == true; + } +} diff --git a/src/Microsoft.VisualStudio.Threading/VSThreadHelper.cs b/src/Microsoft.VisualStudio.Threading/VSThreadHelper.cs deleted file mode 100644 index f2ce67d80..000000000 --- a/src/Microsoft.VisualStudio.Threading/VSThreadHelper.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.VisualStudio.Threading -{ -#pragma warning disable RS0016 //Add public types and members to the declared API - /// - /// A helper class for integration with Visual Studio. - /// APIs in this file are intended for Microsoft internal use only. - /// - public static class VSThreadHelper - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static bool IsMainThreadBlockedByAnyJoinableTask(JoinableTaskContext joinableTaskContext) -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - { - return joinableTaskContext?.IsMainThreadBlockedByAnyJoinableTask == true; - } - } -#pragma warning restore RS0016 // Add public types and members to the declared API -} diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs index f74ea8d18..d0b00c42f 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskContextTests.cs @@ -45,85 +45,6 @@ await Task.Run(delegate }); } - [Fact] - public void IsMainThreadBlockedByAnyJoinableTask_True() - { - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - AsyncManualResetEvent mainThreadBlockerEvent = new AsyncManualResetEvent(false); - AsyncManualResetEvent backgroundThreadMonitorEvent = new AsyncManualResetEvent(false); - - // Start task to monitor IsMainThreadBlockedByAnyJoinableTask - Task monitorTask = Task.Run(async () => - { - await mainThreadBlockerEvent.WaitAsync(this.TimeoutToken); - - while (!VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)) - { - // Give the main thread time to enter a blocking state, if the test hasn't already timed out. - await Task.Delay(50, this.TimeoutToken); - } - - backgroundThreadMonitorEvent.Set(); - }); - - JoinableTask? joinable = this.Factory.RunAsync(async delegate - { - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - await this.Factory.SwitchToMainThreadAsync(this.TimeoutToken); - - this.Factory.Run(async () => - { - await TaskScheduler.Default.SwitchTo(alwaysYield: true); - mainThreadBlockerEvent.Set(); - await backgroundThreadMonitorEvent.WaitAsync(this.TimeoutToken); - }); - }); - - joinable.Join(); - monitorTask.WaitWithoutInlining(throwOriginalException: true); - - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - } - - [Fact] - public void IsMainThreadBlockedByAnyJoinableTask_False() - { - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - ManualResetEventSlim backgroundThreadBlockerEvent = new(); - - JoinableTask? joinable = this.Factory.RunAsync(async delegate - { - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - await TaskScheduler.Default.SwitchTo(alwaysYield: true); - - this.Factory.Run(async () => - { - backgroundThreadBlockerEvent.Set(); - - // Set a delay sufficient for the other thread to have noticed if IsMainThreadBlockedByAnyJoinableTask is true - // while we're suspended. - await Task.Delay(AsyncDelay); - }); - }); - - backgroundThreadBlockerEvent.Wait(UnexpectedTimeout); - - do - { - // Give the background thread time to enter a blocking state, if the test hasn't already timed out. - this.TimeoutToken.ThrowIfCancellationRequested(); - - // IsMainThreadBlockedByAnyJoinableTask should be false when a background thread is blocked. - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - Thread.Sleep(10); - } - while (!joinable.IsCompleted); - - joinable.Join(); - - Assert.False(VSThreadHelper.IsMainThreadBlockedByAnyJoinableTask(this.Context)); - } - [Fact] public void ReportHangOnRun() { @@ -489,8 +410,8 @@ public void GetHangReportProducesDgmlWithNamedJoinableCollections() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Collection" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Collection" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label == jtcName); return Task.CompletedTask; }); @@ -512,8 +433,8 @@ public void GetHangReportProducesDgmlWithMethodNameRequestingMainThread() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Task" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Task" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label.Contains(nameof(this.GetHangReportProducesDgmlWithMethodNameRequestingMainThread))); } @@ -535,8 +456,8 @@ public void GetHangReportProducesDgmlWithMethodNameYieldingOnMainThread() this.Logger.WriteLine(report.Content); var dgml = XDocument.Parse(report.Content); IEnumerable? collectionLabels = from node in dgml.Root!.Element(XName.Get("Nodes", DgmlNamespace))!.Elements() - where node.Attribute(XName.Get("Category"))?.Value == "Task" - select node.Attribute(XName.Get("Label"))?.Value; + where node.Attribute(XName.Get("Category"))?.Value == "Task" + select node.Attribute(XName.Get("Label"))?.Value; Assert.Contains(collectionLabels, label => label.Contains(nameof(this.YieldingMethodAsync))); }); } diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs new file mode 100644 index 000000000..6544cce31 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskInternalsTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; +using Xunit; +using Xunit.Abstractions; + +public class JoinableTaskInternalsTests : JoinableTaskTestBase +{ + public JoinableTaskInternalsTests(ITestOutputHelper logger) + : base(logger) + { + } + + [Fact] + public void IsMainThreadBlockedByAnyJoinableTask_True() + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + AsyncManualResetEvent mainThreadBlockerEvent = new AsyncManualResetEvent(false); + AsyncManualResetEvent backgroundThreadMonitorEvent = new AsyncManualResetEvent(false); + + // Start task to monitor IsMainThreadBlockedByAnyJoinableTask + Task monitorTask = Task.Run(async () => + { + await mainThreadBlockerEvent.WaitAsync(this.TimeoutToken); + + while (!JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)) + { + // Give the main thread time to enter a blocking state, if the test hasn't already timed out. + await Task.Delay(50, this.TimeoutToken); + } + + backgroundThreadMonitorEvent.Set(); + }); + + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + await this.asyncPump.SwitchToMainThreadAsync(this.TimeoutToken); + + this.asyncPump.Run(async () => + { + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + mainThreadBlockerEvent.Set(); + await backgroundThreadMonitorEvent.WaitAsync(this.TimeoutToken); + }); + }); + + joinable.Join(); + monitorTask.WaitWithoutInlining(throwOriginalException: true); + + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + } + + [Fact] + public void IsMainThreadBlockedByAnyJoinableTask_False() + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + ManualResetEventSlim backgroundThreadBlockerEvent = new(); + + JoinableTask? joinable = this.asyncPump.RunAsync(async delegate + { + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + + this.asyncPump.Run(async () => + { + backgroundThreadBlockerEvent.Set(); + + // Set a delay sufficient for the other thread to have noticed if IsMainThreadBlockedByAnyJoinableTask is true + // while we're suspended. + await Task.Delay(AsyncDelay); + }); + }); + + backgroundThreadBlockerEvent.Wait(UnexpectedTimeout); + + do + { + // Give the background thread time to enter a blocking state, if the test hasn't already timed out. + this.TimeoutToken.ThrowIfCancellationRequested(); + + // IsMainThreadBlockedByAnyJoinableTask should be false when a background thread is blocked. + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + Thread.Sleep(10); + } + while (!joinable.IsCompleted); + + joinable.Join(); + + Assert.False(JoinableTaskInternals.IsMainThreadBlockedByAnyJoinableTask(this.context)); + } +}