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));
+ }
+}