From 81ce4f6c1acd08e0d014734ebf806475fb46c2da Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 19 Jun 2025 15:48:42 -0600 Subject: [PATCH 1/3] Add trim and NativeAOT safety attributes This gets vs-threading to self-identify as a NativeAOT-ready assembly, and adds the attributes on the specific methods that are *not* safe. Incidentally, all the trim-unsafe methods are strictly for runtime diagnostics rather than critical functionality. --- Microsoft.VisualStudio.Threading.slnx | 1 + azure-pipelines/build.yml | 3 + azure-pipelines/dotnet.yml | 11 + ...cReaderWriterLock+HangReportContributor.cs | 11 +- .../IHangReportContributor.cs | 7 +- .../InternalUtilities.cs | 241 ----------------- ...inableTaskContext+HangReportContributor.cs | 6 +- .../JoinableTaskFactory.cs | 7 +- .../MemoryInspection.cs | 252 ++++++++++++++++++ .../Microsoft.VisualStudio.Threading.csproj | 3 + .../Reasons.cs | 9 + .../ThreadingEventSource.cs | 4 +- ...rosoft.VisualStudio.Threading.Tests.csproj | 9 +- .../NativeAOTCompatibility.csproj | 21 ++ test/NativeAOTCompatibility/Program.cs | 4 + 15 files changed, 328 insertions(+), 261 deletions(-) create mode 100644 src/Microsoft.VisualStudio.Threading/MemoryInspection.cs create mode 100644 src/Microsoft.VisualStudio.Threading/Reasons.cs create mode 100644 test/NativeAOTCompatibility/NativeAOTCompatibility.csproj create mode 100644 test/NativeAOTCompatibility/Program.cs diff --git a/Microsoft.VisualStudio.Threading.slnx b/Microsoft.VisualStudio.Threading.slnx index 62b62e80b..c5f2e42b1 100644 --- a/Microsoft.VisualStudio.Threading.slnx +++ b/Microsoft.VisualStudio.Threading.slnx @@ -40,5 +40,6 @@ + diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 0f62763b6..d16bcb0cc 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -197,6 +197,7 @@ jobs: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} IsOptProf: ${{ parameters.IsOptProf }} + osRID: win - ${{ if and(parameters.EnableDotNetFormatCheck, not(parameters.EnableLinuxBuild)) }}: - script: dotnet format --verify-no-changes @@ -241,6 +242,7 @@ jobs: parameters: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} + osRID: linux - ${{ if parameters.EnableDotNetFormatCheck }}: - script: dotnet format --verify-no-changes displayName: 💅 Verify formatted code @@ -276,6 +278,7 @@ jobs: parameters: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} + osRID: osx - job: WrapUp dependsOn: diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 2abca9eb2..8fc6e7d55 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -5,6 +5,8 @@ parameters: default: false - name: Is1ESPT type: boolean +- name: osRID + type: string steps: @@ -16,6 +18,15 @@ steps: displayName: 🧪 dotnet test condition: and(succeeded(), ${{ parameters.RunTests }}) +- powershell: | + dotnet publish -f net9.0 -c Release -r ${{ parameters.osRID }}-x64 -warnaserror + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (!($IsMacOS -or $IsLinux)) { + dotnet publish -f net9.0-windows -c Release -r ${{ parameters.osRID }}-x64 -warnaserror + } + displayName: 🧪 NativeAOT test + workingDirectory: test/NativeAOTCompatibility + - ${{ if parameters.IsOptProf }}: - script: dotnet pack src\VSInsertionMetadata -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/VSInsertion-Pack.binlog" displayName: 🔧 dotnet pack VSInsertionMetadata diff --git a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs index cbed5dad5..08f4ffcbf 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncReaderWriterLock+HangReportContributor.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Xml.Linq; namespace Microsoft.VisualStudio.Threading @@ -46,6 +46,7 @@ protected internal virtual SynchronizationContext NoMessagePumpSynchronizationCo /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] HangReportContribution IHangReportContributor.GetHangReport() { return this.GetHangReport(); @@ -55,7 +56,8 @@ HangReportContribution IHangReportContributor.GetHangReport() /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] protected virtual HangReportContribution GetHangReport() { using (this.NoMessagePumpSynchronizationContext.Apply()) @@ -125,6 +127,7 @@ private static XDocument CreateDgml(out XElement nodes, out XElement links) /// /// Appends details of a given collection of awaiters to the hang report. /// + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] private static XElement CreateAwaiterNode(Awaiter awaiter) { Requires.NotNull(awaiter, nameof(awaiter)); @@ -211,9 +214,13 @@ public IEnumerable Categories { get { +#if NET + foreach (AwaiterCollection value in Enum.GetValues()) +#else #pragma warning disable CS8605 // Unboxing a possibly null value. foreach (AwaiterCollection value in Enum.GetValues(typeof(AwaiterCollection))) #pragma warning restore CS8605 // Unboxing a possibly null value. +#endif { if (this.Membership.HasFlag(value)) { diff --git a/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs index 7c320ff33..7b5cac146 100644 --- a/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/IHangReportContributor.cs @@ -1,11 +1,7 @@ // 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; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.VisualStudio.Threading; @@ -18,5 +14,6 @@ public interface IHangReportContributor /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] HangReportContribution GetHangReport(); } diff --git a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs index ad95047e6..f9eaba00a 100644 --- a/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs +++ b/src/Microsoft.VisualStudio.Threading/InternalUtilities.cs @@ -1,14 +1,7 @@ // 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; -using System.Collections; using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; namespace Microsoft.VisualStudio.Threading; @@ -17,15 +10,6 @@ namespace Microsoft.VisualStudio.Threading; /// internal static class InternalUtilities { - /// - /// The substring that should be inserted before each async return stack frame. - /// - /// - /// When printing synchronous callstacks, .NET begins each frame with " at ". - /// When printing async return stack, we use this to indicate continuations. - /// - private const string AsyncReturnStackPrefix = " -> "; - /// /// Removes an element from the middle of a queue without disrupting the other elements. /// @@ -60,229 +44,4 @@ internal static bool RemoveMidQueue(this Queue queue, T valueToRemove) return found; } - - /// - /// Walk the continuation objects inside "async state machines" to generate the return call stack. - /// FOR DIAGNOSTIC PURPOSES ONLY. - /// - /// The delegate that represents the head of an async continuation chain. - internal static IEnumerable GetAsyncReturnStackFrames(this Delegate continuationDelegate) - { - IAsyncStateMachine? stateMachine = FindAsyncStateMachine(continuationDelegate); - if (stateMachine is null) - { - // Did not find the async state machine, so returns the method name as top frame and stop walking. - yield return GetDelegateLabel(continuationDelegate); - yield break; - } - - do - { - object? state = GetStateMachineFieldValueOnSuffix(stateMachine, "__state"); - yield return string.Format( - CultureInfo.CurrentCulture, - "{2}{0} (state: {1}, address: 0x{3:X8})", - stateMachine.GetType().FullName, - state, - AsyncReturnStackPrefix, - (long)GetAddress(stateMachine)); // the long cast allows hex formatting - - Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); - if (continuationDelegates.Length == 0) - { - break; - } - - // Consider: It's possible but uncommon scenario to have multiple "async methods" being awaiting for one "async method". - // Here we just choose the first awaiting "async method" as that should be good enough for postmortem. - // In future we might want to revisit this to cover the other awaiting "async methods". - stateMachine = continuationDelegates.Select((d) => FindAsyncStateMachine(d)) - .FirstOrDefault((s) => s is object); - if (stateMachine is null) - { - yield return GetDelegateLabel(continuationDelegates.First()); - } - } - while (stateMachine is object); - } - - /// - /// A helper method to get the label of the given delegate. - /// - private static string GetDelegateLabel(Delegate invokeDelegate) - { - Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); - - MethodInfo? method = invokeDelegate.GetMethodInfo(); - if (invokeDelegate.Target is object) - { - string instanceType = string.Empty; - if (!(method?.DeclaringType?.Equals(invokeDelegate.Target.GetType()) ?? false)) - { - instanceType = " (" + invokeDelegate.Target.GetType().FullName + ")"; - } - - return string.Format( - CultureInfo.CurrentCulture, - "{3}{0}.{1}{2} (target address: 0x{4:X" + (IntPtr.Size * 2) + "})", - method?.DeclaringType?.FullName, - method?.Name, - instanceType, - AsyncReturnStackPrefix, - GetAddress(invokeDelegate.Target).ToInt64()); // the cast allows hex formatting - } - - return string.Format( - CultureInfo.CurrentCulture, - "{2}{0}.{1}", - method?.DeclaringType?.FullName, - method?.Name, - AsyncReturnStackPrefix); - } - - /// - /// Gets the memory address of a given object. - /// - /// The object to get the address for. - /// The memory address. - /// - /// This method works when GCHandle will refuse because the type of object is a non-blittable type. - /// However, this method provides no guarantees that the address will remain valid for the caller, - /// so it is only useful for diagnostics and when we don't expect addresses to be changing much any more. - /// - private static unsafe IntPtr GetAddress(object value) => new IntPtr(Unsafe.AsPointer(ref value)); - - /// - /// A helper method to find the async state machine from the given delegate. - /// - private static IAsyncStateMachine? FindAsyncStateMachine(Delegate invokeDelegate) - { - Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); - - if (invokeDelegate.Target is object) - { - // Some delegates are wrapped with a ContinuationWrapper object. We have to unwrap that in those cases. - // In testing, this m_continuation field jump is only required when the debugger is attached -- weird. - // I suspect however that it's a natural behavior of the async state machine (when there are >1 continuations perhaps). - // So we check for the case in all cases. - if (GetFieldValue(invokeDelegate.Target, "m_continuation") is Action continuation) - { - invokeDelegate = continuation; - if (invokeDelegate.Target is null) - { - return null; - } - } - - var stateMachine = GetFieldValue(invokeDelegate.Target, "m_stateMachine") as IAsyncStateMachine; - return stateMachine; - } - - return null; - } - - /// - /// This is the core to find the continuation delegate(s) inside the given async state machine. - /// The chain of objects is like this: async state machine -> async method builder -> task -> continuation object -> action. - /// - /// - /// There are 3 types of "async method builder": AsyncVoidMethodBuilder, AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<T>. - /// We don't cover AsyncVoidMethodBuilder as it is used rarely and it can't be awaited either; - /// AsyncTaskMethodBuilder is a wrapper on top of AsyncTaskMethodBuilder<VoidTaskResult>. - /// - private static IEnumerable FindContinuationDelegates(IAsyncStateMachine stateMachine) - { - Requires.NotNull(stateMachine, nameof(stateMachine)); - - object? builder = GetStateMachineFieldValueOnSuffix(stateMachine, "__builder"); - if (builder is null) - { - yield break; - } - - object? task = GetFieldValue(builder, "m_task"); - if (task is null) - { - // Probably this builder is an instance of "AsyncTaskMethodBuilder", so we need to get its inner "AsyncTaskMethodBuilder" - builder = GetFieldValue(builder, "m_builder"); - if (builder is object) - { - task = GetFieldValue(builder, "m_task"); - } - } - - if (task is null) - { - yield break; - } - - // "task" might be an instance of the type deriving from "Task", but "m_continuationObject" is a private field in "Task", - // so we need to use "typeof(Task)" to access "m_continuationObject". - FieldInfo? continuationField = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); - if (continuationField is null) - { - yield break; - } - - object? continuationObject = continuationField.GetValue(task); - if (continuationObject is null) - { - yield break; - } - - if (continuationObject is IEnumerable items) - { - foreach (object? item in items) - { - Delegate? action = item as Delegate ?? GetFieldValue(item!, "m_action") as Delegate; - if (action is object) - { - yield return action; - } - } - } - else - { - Delegate? action = continuationObject as Delegate ?? GetFieldValue(continuationObject, "m_action") as Delegate; - if (action is object) - { - yield return action; - } - } - } - - /// - /// A helper method to get field's value given the object and the field name. - /// - private static object? GetFieldValue(object obj, string fieldName) - { - Requires.NotNull(obj, nameof(obj)); - Requires.NotNullOrEmpty(fieldName, nameof(fieldName)); - - FieldInfo? field = obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); - if (field is object) - { - return field.GetValue(obj); - } - - return null; - } - - /// - /// The field names of "async state machine" are not fixed; the workaround is to find the field based on the suffix. - /// - private static object? GetStateMachineFieldValueOnSuffix(IAsyncStateMachine stateMachine, string suffix) - { - Requires.NotNull(stateMachine, nameof(stateMachine)); - Requires.NotNullOrEmpty(suffix, nameof(suffix)); - - IEnumerable? fields = stateMachine.GetType().GetTypeInfo().DeclaredFields; - FieldInfo? field = fields.FirstOrDefault((f) => f.Name.EndsWith(suffix, StringComparison.Ordinal)); - if (field is object) - { - return field.GetValue(stateMachine); - } - - return null; - } } diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs index 7f96afed4..caad35d6c 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskContext+HangReportContributor.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Xml.Linq; namespace Microsoft.VisualStudio.Threading; @@ -17,6 +17,7 @@ public partial class JoinableTaskContext : IHangReportContributor /// Contributes data for a hang report. /// /// The hang report contribution. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] HangReportContribution IHangReportContributor.GetHangReport() { return this.GetHangReport(); @@ -26,6 +27,7 @@ HangReportContribution IHangReportContributor.GetHangReport() /// Contributes data for a hang report. /// /// The hang report contribution. Null values should be ignored. + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] protected virtual HangReportContribution GetHangReport() { using (this.NoMessagePumpSynchronizationContext.Apply()) @@ -116,6 +118,7 @@ private static Dictionary CreateNodesForJoinab return result; } + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] private static List> CreateNodeLabels(Dictionary tasksAndElements) { Requires.NotNull(tasksAndElements, nameof(tasksAndElements)); @@ -146,6 +149,7 @@ private static List> CreateNodeLabels(Dictionary internal string DelegateLabel { + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] get { return this.WalkAsyncReturnStackFrames().First(); // Top frame of the return callstack. @@ -1214,6 +1210,7 @@ internal void RemoveExecutingCallback(JoinableTask.ExecutionQueue callbackReceiv /// Walk the continuation objects inside "async state machines" to generate the return callstack. /// FOR DIAGNOSTIC PURPOSES ONLY. /// + [RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] internal IEnumerable WalkAsyncReturnStackFrames() { // This instance might be a wrapper of another instance of "SingleExecuteProtector". diff --git a/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs b/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs new file mode 100644 index 000000000..b9f9ca85b --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/MemoryInspection.cs @@ -0,0 +1,252 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Threading; + +[RequiresUnreferencedCode(Reasons.DiagnosticAnalysisOnly)] +internal static class MemoryInspection +{ + /// + /// The substring that should be inserted before each async return stack frame. + /// + /// + /// When printing synchronous callstacks, .NET begins each frame with " at ". + /// When printing async return stack, we use this to indicate continuations. + /// + private const string AsyncReturnStackPrefix = " -> "; + + /// + /// Walk the continuation objects inside "async state machines" to generate the return call stack. + /// FOR DIAGNOSTIC PURPOSES ONLY. + /// + /// The delegate that represents the head of an async continuation chain. + internal static IEnumerable GetAsyncReturnStackFrames(this Delegate continuationDelegate) + { + IAsyncStateMachine? stateMachine = FindAsyncStateMachine(continuationDelegate); + if (stateMachine is null) + { + // Did not find the async state machine, so returns the method name as top frame and stop walking. + yield return GetDelegateLabel(continuationDelegate); + yield break; + } + + do + { + object? state = GetStateMachineFieldValueOnSuffix(stateMachine, "__state"); + yield return string.Format( + CultureInfo.CurrentCulture, + "{2}{0} (state: {1}, address: 0x{3:X8})", + stateMachine.GetType().FullName, + state, + AsyncReturnStackPrefix, + (long)GetAddress(stateMachine)); // the long cast allows hex formatting + + Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); + if (continuationDelegates.Length == 0) + { + break; + } + + // Consider: It's possible but uncommon scenario to have multiple "async methods" being awaiting for one "async method". + // Here we just choose the first awaiting "async method" as that should be good enough for postmortem. + // In future we might want to revisit this to cover the other awaiting "async methods". + stateMachine = continuationDelegates.Select((d) => FindAsyncStateMachine(d)) + .FirstOrDefault((s) => s is object); + if (stateMachine is null) + { + yield return GetDelegateLabel(continuationDelegates.First()); + } + } + while (stateMachine is object); + } + + /// + /// A helper method to get the label of the given delegate. + /// + private static string GetDelegateLabel(Delegate invokeDelegate) + { + Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); + + MethodInfo? method = invokeDelegate.GetMethodInfo(); + if (invokeDelegate.Target is object) + { + string instanceType = string.Empty; + if (!(method?.DeclaringType?.Equals(invokeDelegate.Target.GetType()) ?? false)) + { + instanceType = " (" + invokeDelegate.Target.GetType().FullName + ")"; + } + + return string.Format( + CultureInfo.CurrentCulture, + "{3}{0}.{1}{2} (target address: 0x{4:X" + (IntPtr.Size * 2) + "})", + method?.DeclaringType?.FullName, + method?.Name, + instanceType, + AsyncReturnStackPrefix, + GetAddress(invokeDelegate.Target).ToInt64()); // the cast allows hex formatting + } + + return string.Format( + CultureInfo.CurrentCulture, + "{2}{0}.{1}", + method?.DeclaringType?.FullName, + method?.Name, + AsyncReturnStackPrefix); + } + + /// + /// A helper method to find the async state machine from the given delegate. + /// + private static IAsyncStateMachine? FindAsyncStateMachine(Delegate invokeDelegate) + { + Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); + + if (invokeDelegate.Target is object) + { + // Some delegates are wrapped with a ContinuationWrapper object. We have to unwrap that in those cases. + // In testing, this m_continuation field jump is only required when the debugger is attached -- weird. + // I suspect however that it's a natural behavior of the async state machine (when there are >1 continuations perhaps). + // So we check for the case in all cases. + if (GetFieldValue(invokeDelegate.Target, "m_continuation") is Action continuation) + { + invokeDelegate = continuation; + if (invokeDelegate.Target is null) + { + return null; + } + } + + var stateMachine = GetFieldValue(invokeDelegate.Target, "m_stateMachine") as IAsyncStateMachine; + return stateMachine; + } + + return null; + } + + /// + /// This is the core to find the continuation delegate(s) inside the given async state machine. + /// The chain of objects is like this: async state machine -> async method builder -> task -> continuation object -> action. + /// + /// + /// There are 3 types of "async method builder": AsyncVoidMethodBuilder, AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<T>. + /// We don't cover AsyncVoidMethodBuilder as it is used rarely and it can't be awaited either; + /// AsyncTaskMethodBuilder is a wrapper on top of AsyncTaskMethodBuilder<VoidTaskResult>. + /// + private static IEnumerable FindContinuationDelegates(IAsyncStateMachine stateMachine) + { + Requires.NotNull(stateMachine, nameof(stateMachine)); + + object? builder = GetStateMachineFieldValueOnSuffix(stateMachine, "__builder"); + if (builder is null) + { + yield break; + } + + object? task = GetFieldValue(builder, "m_task"); + if (task is null) + { + // Probably this builder is an instance of "AsyncTaskMethodBuilder", so we need to get its inner "AsyncTaskMethodBuilder" + builder = GetFieldValue(builder, "m_builder"); + if (builder is object) + { + task = GetFieldValue(builder, "m_task"); + } + } + + if (task is null) + { + yield break; + } + + // "task" might be an instance of the type deriving from "Task", but "m_continuationObject" is a private field in "Task", + // so we need to use "typeof(Task)" to access "m_continuationObject". + FieldInfo? continuationField = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); + if (continuationField is null) + { + yield break; + } + + object? continuationObject = continuationField.GetValue(task); + if (continuationObject is null) + { + yield break; + } + + if (continuationObject is IEnumerable items) + { + foreach (object? item in items) + { + Delegate? action = item as Delegate ?? GetFieldValue(item!, "m_action") as Delegate; + if (action is object) + { + yield return action; + } + } + } + else + { + Delegate? action = continuationObject as Delegate ?? GetFieldValue(continuationObject, "m_action") as Delegate; + if (action is object) + { + yield return action; + } + } + } + + /// + /// A helper method to get field's value given the object and the field name. + /// + private static object? GetFieldValue(object obj, string fieldName) + { + Requires.NotNull(obj, nameof(obj)); + Requires.NotNullOrEmpty(fieldName, nameof(fieldName)); + + FieldInfo? field = obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); + if (field is object) + { + return field.GetValue(obj); + } + + return null; + } + + /// + /// The field names of "async state machine" are not fixed; the workaround is to find the field based on the suffix. + /// + private static object? GetStateMachineFieldValueOnSuffix(IAsyncStateMachine stateMachine, string suffix) + { + Requires.NotNull(stateMachine, nameof(stateMachine)); + Requires.NotNullOrEmpty(suffix, nameof(suffix)); + + IEnumerable? fields = stateMachine.GetType().GetTypeInfo().DeclaredFields; + FieldInfo? field = fields.FirstOrDefault((f) => f.Name.EndsWith(suffix, StringComparison.Ordinal)); + if (field is object) + { + return field.GetValue(stateMachine); + } + + return null; + } + + /// + /// Gets the memory address of a given object. + /// + /// The object to get the address for. + /// The memory address. + /// + /// This method works when GCHandle will refuse because the type of object is a non-blittable type. + /// However, this method provides no guarantees that the address will remain valid for the caller, + /// so it is only useful for diagnostics and when we don't expect addresses to be changing much any more. + /// + private static unsafe IntPtr GetAddress(object value) => new IntPtr(Unsafe.AsPointer(ref value)); +} diff --git a/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj b/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj index 3e5321d89..14acd335d 100644 --- a/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj +++ b/src/Microsoft.VisualStudio.Threading/Microsoft.VisualStudio.Threading.csproj @@ -7,6 +7,9 @@ Use the Microsoft.VisualStudio.Threading package to get the library and analyzers together. true + + true + true true diff --git a/src/Microsoft.VisualStudio.Threading/Reasons.cs b/src/Microsoft.VisualStudio.Threading/Reasons.cs new file mode 100644 index 000000000..8d8189e95 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/Reasons.cs @@ -0,0 +1,9 @@ +// 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; + +internal static class Reasons +{ + internal const string DiagnosticAnalysisOnly = "This API performs analysis of runtime objects for diagnostic purposes only, and isn't required for correct functionality."; +} diff --git a/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs b/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs index 890718f74..59ae498a6 100644 --- a/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs +++ b/src/Microsoft.VisualStudio.Threading/ThreadingEventSource.cs @@ -1,7 +1,7 @@ // 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; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; namespace Microsoft.VisualStudio.Threading; @@ -104,6 +104,7 @@ public void WaitReaderWriterLockStop(int lockId, AsyncReaderWriterLock.LockKind /// Hash code of the task. /// Whether the task is on the main thread. [Event(CompleteOnCurrentThreadStartEvent)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "We're only serializing primitive types.")] public void CompleteOnCurrentThreadStart(int taskId, bool isOnMainThread) { this.WriteEvent(CompleteOnCurrentThreadStartEvent, taskId, Boxed.Box(isOnMainThread)); @@ -143,6 +144,7 @@ public void WaitSynchronouslyStop() /// The request id. /// The execution need happen on the main thread. [Event(PostExecutionStartEvent, Level = EventLevel.Verbose)] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "We're only serializing primitive types.")] public void PostExecutionStart(int requestId, bool mainThreadAffinitized) { this.WriteEvent(PostExecutionStartEvent, requestId, Boxed.Box(mainThreadAffinitized)); diff --git a/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj b/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj index b4bc53556..b17c4787e 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj +++ b/test/Microsoft.VisualStudio.Threading.Tests/Microsoft.VisualStudio.Threading.Tests.csproj @@ -22,8 +22,7 @@ RarelyRemoveItemSet`1.cs - + WeakKeyDictionary`2.cs @@ -39,8 +38,7 @@ - + @@ -48,8 +46,7 @@ true - + false true diff --git a/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj b/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj new file mode 100644 index 000000000..47a89a6d7 --- /dev/null +++ b/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + true + false + + + $(TargetFrameworks);net9.0-windows + + + + + + + + + + + diff --git a/test/NativeAOTCompatibility/Program.cs b/test/NativeAOTCompatibility/Program.cs new file mode 100644 index 000000000..0c1d2e38a --- /dev/null +++ b/test/NativeAOTCompatibility/Program.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +System.Console.WriteLine("This test is run by \"dotnet publish -c release -r [RID]-x64\" rather than by executing the program."); From c8124cf9002d6df309941c9f7dcbfe42d68f14a9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 19 Jun 2025 17:31:00 -0600 Subject: [PATCH 2/3] Consolidate build and NativeAOT publish --- Directory.Build.props | 13 +++++++++++++ Directory.Build.targets | 2 ++ Directory.Traversal.targets | 15 +++++++++++++++ Microsoft.VisualStudio.Threading.slnx | 7 +++++++ azure-pipelines/dotnet.yml | 11 +---------- global.json | 3 ++- src/dirs.proj | 5 +++++ test/AOT.props | 16 ++++++++++++++++ .../NativeAOTCompatibility.csproj | 4 +--- test/dirs.proj | 8 ++++++++ tools/dirs.proj | 6 ++++++ 11 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 Directory.Traversal.targets create mode 100644 src/dirs.proj create mode 100644 test/AOT.props create mode 100644 test/dirs.proj create mode 100644 tools/dirs.proj diff --git a/Directory.Build.props b/Directory.Build.props index 30f2da1db..ca9be7049 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -41,6 +41,19 @@ 16.9 + + win + linux + osx + + x64 + arm64 + + $(RidOsPrefix)-$(RidOsArchitecture) + + $(DefaultRuntimeIdentifier) + + diff --git a/Directory.Build.targets b/Directory.Build.targets index b4afce0a7..dbcb085dc 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -4,4 +4,6 @@ + + diff --git a/Directory.Traversal.targets b/Directory.Traversal.targets new file mode 100644 index 000000000..805bd6e92 --- /dev/null +++ b/Directory.Traversal.targets @@ -0,0 +1,15 @@ + + + true + + false + false + true + true + + + + + + diff --git a/Microsoft.VisualStudio.Threading.slnx b/Microsoft.VisualStudio.Threading.slnx index c5f2e42b1..c7243f9f5 100644 --- a/Microsoft.VisualStudio.Threading.slnx +++ b/Microsoft.VisualStudio.Threading.slnx @@ -11,16 +11,21 @@ + + + + + @@ -33,8 +38,10 @@ + + diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 8fc6e7d55..f9ca66e10 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -10,7 +10,7 @@ parameters: steps: -- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" +- script: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" displayName: 🛠 dotnet build - ${{ if not(parameters.IsOptProf) }}: @@ -18,15 +18,6 @@ steps: displayName: 🧪 dotnet test condition: and(succeeded(), ${{ parameters.RunTests }}) -- powershell: | - dotnet publish -f net9.0 -c Release -r ${{ parameters.osRID }}-x64 -warnaserror - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if (!($IsMacOS -or $IsLinux)) { - dotnet publish -f net9.0-windows -c Release -r ${{ parameters.osRID }}-x64 -warnaserror - } - displayName: 🧪 NativeAOT test - workingDirectory: test/NativeAOTCompatibility - - ${{ if parameters.IsOptProf }}: - script: dotnet pack src\VSInsertionMetadata -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/VSInsertion-Pack.binlog" displayName: 🔧 dotnet pack VSInsertionMetadata diff --git a/global.json b/global.json index 6e89f4181..5fce40099 100644 --- a/global.json +++ b/global.json @@ -6,6 +6,7 @@ }, "msbuild-sdks": { "MSBuild.Sdk.Extras": "3.0.44", - "Microsoft.Build.NoTargets": "3.7.56" + "Microsoft.Build.NoTargets": "3.7.56", + "Microsoft.Build.Traversal": "4.1.82" } } diff --git a/src/dirs.proj b/src/dirs.proj new file mode 100644 index 000000000..b6fa81f93 --- /dev/null +++ b/src/dirs.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/AOT.props b/test/AOT.props new file mode 100644 index 000000000..27d5cbf21 --- /dev/null +++ b/test/AOT.props @@ -0,0 +1,16 @@ + + + Exe + true + true + true + $(AvailableRuntimeIdentifiers) + $(DefaultRuntimeIdentifier) + + + + + RuntimeIdentifier + + + diff --git a/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj b/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj index 47a89a6d7..f663beb88 100644 --- a/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj +++ b/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj @@ -1,9 +1,7 @@ - + - Exe net9.0 - true false diff --git a/test/dirs.proj b/test/dirs.proj new file mode 100644 index 000000000..eec473e2a --- /dev/null +++ b/test/dirs.proj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tools/dirs.proj b/tools/dirs.proj new file mode 100644 index 000000000..8f1d6787a --- /dev/null +++ b/tools/dirs.proj @@ -0,0 +1,6 @@ + + + + + + From 5cb439e79c6bd3b9feeb69516f1210cca94731a2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 19 Jun 2025 17:41:44 -0600 Subject: [PATCH 3/3] Fix symbol classification for test project --- Microsoft.VisualStudio.Threading.slnx | 2 +- .../NativeAOTCompatibility.Test.csproj} | 0 .../Program.cs | 0 test/dirs.proj | 6 +++--- 4 files changed, 4 insertions(+), 4 deletions(-) rename test/{NativeAOTCompatibility/NativeAOTCompatibility.csproj => NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj} (100%) rename test/{NativeAOTCompatibility => NativeAOTCompatibility.Test}/Program.cs (100%) diff --git a/Microsoft.VisualStudio.Threading.slnx b/Microsoft.VisualStudio.Threading.slnx index c7243f9f5..037ae9ddc 100644 --- a/Microsoft.VisualStudio.Threading.slnx +++ b/Microsoft.VisualStudio.Threading.slnx @@ -47,6 +47,6 @@ - + diff --git a/test/NativeAOTCompatibility/NativeAOTCompatibility.csproj b/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj similarity index 100% rename from test/NativeAOTCompatibility/NativeAOTCompatibility.csproj rename to test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj diff --git a/test/NativeAOTCompatibility/Program.cs b/test/NativeAOTCompatibility.Test/Program.cs similarity index 100% rename from test/NativeAOTCompatibility/Program.cs rename to test/NativeAOTCompatibility.Test/Program.cs diff --git a/test/dirs.proj b/test/dirs.proj index eec473e2a..16cfe03d2 100644 --- a/test/dirs.proj +++ b/test/dirs.proj @@ -1,8 +1,8 @@ - + - - + +