diff --git a/documentation/api/definitions.md b/documentation/api/definitions.md index 7dc819b2b93..987ac1c1dd8 100644 --- a/documentation/api/definitions.md +++ b/documentation/api/definitions.md @@ -37,6 +37,7 @@ First Available: 8.0 Preview 7 | `typeName` | string | Name of the class for this frame. This includes generic parameters. | | `moduleName` | string | Name of the module for this frame. | | `moduleVersionId` | guid | Unique identifier used to distinguish between two versions of the same module. An empty value: `00000000-0000-0000-0000-000000000000`. | +| `hidden`| bool |(8.1+ and 9.0+) Whether this frame has the [StackTraceHiddenAttribute](https://learn.microsoft.com/dotnet/api/system.diagnostics.stacktracehiddenattribute) and should be omitted from stack trace text. | ## CallStackResult diff --git a/documentation/api/exceptions.md b/documentation/api/exceptions.md index 9a056528a78..5244906a982 100644 --- a/documentation/api/exceptions.md +++ b/documentation/api/exceptions.md @@ -91,8 +91,6 @@ System.InvalidOperationException: Operation is not valid due to the current stat First chance exception at 2023-07-13T21:46:18.7530773Z System.ObjectDisposedException: Cannot access a disposed object. Object name: 'System.Net.Sockets.NetworkStream'. - at System.ThrowHelper.ThrowObjectDisposedException(System.Object) - at System.ObjectDisposedException.ThrowIf(System.Boolean,System.Object) at System.Net.Sockets.NetworkStream.ReadAsync(System.Memory`1[[System.Byte, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]][System.Byte],System.Threading.CancellationToken) at System.Net.Http.HttpConnection+<g__ReadAheadWithZeroByteReadAsync|43_0>d.MoveNext() at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1+TResult,System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1+TStateMachine].ExecutionContextCallback(System.Object) @@ -140,7 +138,8 @@ Content-Type: application/x-ndjson "parameterTypes": [], "typeName": "WebApplication3.Pages.IndexModel\u002B\u003CGetData\u003Ed__3", "moduleName": "WebApplication3.dll", - "moduleVersionId": "bf769014-c2e2-496a-93b7-76fbbcd04be5" + "moduleVersionId": "bf769014-c2e2-496a-93b7-76fbbcd04be5", + "hidden": false }, ... // see stacks.md ] @@ -165,7 +164,8 @@ Content-Type: application/x-ndjson ], "typeName": "System.ThrowHelper", "moduleName": "System.Private.CoreLib.dll", - "moduleVersionId": "bf769014-c2e2-496a-93b7-76fbbcd04be5" + "moduleVersionId": "bf769014-c2e2-496a-93b7-76fbbcd04be5", + "hidden": true }, ... // see stacks.md ] diff --git a/documentation/api/stacks.md b/documentation/api/stacks.md index dd7e09c5307..0fe57711385 100644 --- a/documentation/api/stacks.md +++ b/documentation/api/stacks.md @@ -84,21 +84,24 @@ Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 "methodToken": 100663634, "typeName": "Interop\u002BKernel32", "moduleName": "System.Private.CoreLib.dll", - "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606" + "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606", + "hidden": false }, { "methodName": "WaitForSignal", "methodToken": 100663639, "typeName": "System.Threading.LowLevelLifoSemaphore", "moduleName": "System.Private.CoreLib.dll", - "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606" + "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606", + "hidden": false }, { "methodName": "Wait", "methodToken": 100663643, "typeName": "System.Threading.LowLevelLifoSemaphore", "moduleName": "System.Private.CoreLib.dll", - "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606" + "moduleVersionId": "194ddabd-a802-4520-90ef-854e2f1cd606", + "hidden": false } ] } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs index 31648a32ba9..98a786330d9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs @@ -4,11 +4,13 @@ using Microsoft.Diagnostics.Monitoring.WebApi.Stacks; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Text; using System.Text.Json.Serialization; namespace Microsoft.Diagnostics.Monitoring.WebApi.Models { + [DebuggerDisplay("{ModuleName,nq}!{TypeName,nq}.{MethodName,nq}")] public class CallStackFrame { [JsonPropertyName("methodName")] @@ -50,6 +52,9 @@ public string MethodNameWithGenericArgTypes [JsonPropertyName("moduleVersionId")] public Guid ModuleVersionId { get; set; } + [JsonPropertyName("hidden")] + public bool Hidden { get; set; } + [JsonIgnore] internal IList SimpleGenericArgTypes { get; set; } = new List(); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/SpeedScopeStacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/SpeedScopeStacksFormatter.cs index d33d356e200..00dc3137177 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/SpeedScopeStacksFormatter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/SpeedScopeStacksFormatter.cs @@ -78,6 +78,11 @@ public override async Task FormatStack(CallStackResult stackResult, Cancellation } else if (cache.FunctionData.TryGetValue(frame.FunctionId, out FunctionData? functionData)) { + if (StackUtilities.ShouldHideFunctionFromStackTrace(cache, functionData)) + { + continue; + } + if (!functionToSharedFrameMap.TryGetValue(frame.FunctionId, out int mapping)) { // Note this may imply some duplicate frames because we use FunctionId as a unique identifier for a frame, diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs index cbd79cf6fe1..31c4fc809bf 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs @@ -29,8 +29,10 @@ public override async Task FormatStack(CallStackResult stackResult, Cancellation { builder.Clear(); builder.Append(Indent); - BuildFrame(builder, stackResult.NameCache, frame); - await writer.WriteLineAsync(builder, token); + if (BuildFrame(builder, stackResult.NameCache, frame)) + { + await writer.WriteLineAsync(builder, token); + } } await writer.WriteLineAsync(); } @@ -42,7 +44,8 @@ public override async Task FormatStack(CallStackResult stackResult, Cancellation #endif } - private static void BuildFrame(StringBuilder builder, NameCache cache, CallStackFrame frame) + /// True if the frame should be included in the stack trace. + private static bool BuildFrame(StringBuilder builder, NameCache cache, CallStackFrame frame) { if (frame.FunctionId == 0) { @@ -50,6 +53,11 @@ private static void BuildFrame(StringBuilder builder, NameCache cache, CallStack } else if (cache.FunctionData.TryGetValue(frame.FunctionId, out FunctionData? functionData)) { + if (StackUtilities.ShouldHideFunctionFromStackTrace(cache, functionData)) + { + return false; + } + builder.Append(NameFormatter.GetModuleName(cache, functionData.ModuleId)); builder.Append(ModuleSeparator); NameFormatter.BuildTypeName(builder, cache, functionData); @@ -61,6 +69,8 @@ private static void BuildFrame(StringBuilder builder, NameCache cache, CallStack { builder.Append(UnknownFunction); } + + return true; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs index ad7e40797fd..b3552d696d8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs @@ -45,6 +45,7 @@ internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, Nam //Offset = frame.Offset, ModuleName = NameFormatter.UnknownModule, ModuleVersionId = Guid.Empty, + Hidden = false, SimpleParameterTypes = ensureParameterTypeFieldsNotNull ? [] : null, FullParameterTypes = ensureParameterTypeFieldsNotNull ? [] : null, @@ -59,6 +60,7 @@ internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, Nam { frameModel.MethodToken = functionData.MethodToken; frameModel.ModuleName = NameFormatter.GetModuleName(cache, functionData.ModuleId); + frameModel.Hidden = ShouldHideFunctionFromStackTrace(cache, functionData); if (cache.ModuleData.TryGetValue(functionData.ModuleId, out ModuleData? moduleData)) { @@ -90,6 +92,32 @@ internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, Nam return frameModel; } + public static bool ShouldHideFunctionFromStackTrace(NameCache cache, FunctionData functionData) + { + if (functionData.StackTraceHidden) + { + return true; + } + + if (cache.ClassData.TryGetValue(functionData.ParentClass, out ClassData? classData)) + { + if (classData.StackTraceHidden) + { + return true; + } + } + + if (cache.TokenData.TryGetValue(new ModuleScopedToken(functionData.ModuleId, functionData.ParentClassToken), out TokenData? tokenData)) + { + if (tokenData.StackTraceHidden) + { + return true; + } + } + + return false; + } + internal static StacksFormatter CreateFormatter(StackFormat format, Stream outputStream) => format switch { diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp index 01c047877ca..0f7507c21b3 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp @@ -86,7 +86,7 @@ HRESULT TypeNameUtilities::GetFunctionInfo(NameCache& nameCache, FunctionID id, IfFailRet(GetModuleInfo(nameCache, moduleId)); - bool stackTraceHidden = ShouldHideFromStackTrace(moduleId, classToken); + bool stackTraceHidden = ShouldHideFromStackTrace(moduleId, token); nameCache.AddFunctionData(moduleId, id, tstring(funcName), classId, token, classToken, typeArgs, typeArgsCount, stackTraceHidden); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs index fe4766ef238..d7dbccddc8e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs @@ -64,6 +64,7 @@ public static class SubScenarios public const string EclipsingExceptionFromMethodCall = nameof(EclipsingExceptionFromMethodCall); public const string AggregateException = nameof(AggregateException); public const string ReflectionTypeLoadException = nameof(ReflectionTypeLoadException); + public const string HiddenFramesExceptionCommand = nameof(HiddenFramesExceptionCommand); } public static class Commands diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ExceptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ExceptionsTests.cs index 7e70cf70c16..878a4173dae 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ExceptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ExceptionsTests.cs @@ -8,7 +8,9 @@ using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios; using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -28,6 +30,8 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests [Collection(DefaultCollectionFixture.Name)] public class ExceptionsTests { + private record class ExceptionFrame(string TypeName, string MethodName, List ParameterTypes); + private const string FrameTypeName = "Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios.ExceptionsScenario"; private const string FrameMethodName = "ThrowAndCatchInvalidOperationException"; private const string FrameParameterType = "System.Boolean"; @@ -748,6 +752,132 @@ await ScenarioRunner.SingleTarget( }); } + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public async Task Exceptions_HideHiddenFrames_Text(Architecture targetArchitecture) + { + const string HiddenMethodsParameterTypes = "(Action)"; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Exceptions.Name, + subScenarioName: TestAppScenarios.Exceptions.SubScenarios.HiddenFramesExceptionCommand, + appValidate: async (appRunner, apiClient) => + { + await GetExceptions(apiClient, appRunner, ExceptionFormat.PlainText); + ValidateSingleExceptionText( + SystemInvalidOperationException, + ExceptionMessage, + [ + new ExceptionFrame(FrameTypeName, FrameMethodName, [SimpleFrameParameterType, SimpleFrameParameterType]), + new ExceptionFrame(FrameTypeName, FrameMethodName, []), + new ExceptionFrame(typeof(HiddenFrameTestMethods).FullName, $"{nameof(HiddenFrameTestMethods.ExitPoint)}{HiddenMethodsParameterTypes}", []), + new ExceptionFrame(typeof(HiddenFrameTestMethods.PartiallyVisibleClass).FullName, $"{nameof(HiddenFrameTestMethods.PartiallyVisibleClass.DoWorkFromVisibleDerivedClass)}{HiddenMethodsParameterTypes}", []), + new ExceptionFrame(typeof(HiddenFrameTestMethods).FullName, $"{nameof(HiddenFrameTestMethods.EntryPoint)}{HiddenMethodsParameterTypes}", []), + ]); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + runner.EnableMonitorStartupHook = true; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + }); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public async Task Exceptions_HideHiddenFrames_Json(Architecture targetArchitecture) + { + ExceptionInstance expectedException = new() + { + Message = ExceptionMessage, + TypeName = SystemInvalidOperationException, + ModuleName = CoreLibModuleName, + CallStack = new() + { + Frames = + [ + new() + { + TypeName = FrameTypeName, + ModuleName = UnitTestAppModule, + MethodName = FrameMethodName, + FullParameterTypes = [FrameParameterType, FrameParameterType] + }, + new() + { + TypeName = FrameTypeName, + ModuleName = UnitTestAppModule, + MethodName = FrameMethodName, + }, + new() + { + TypeName = typeof(HiddenFrameTestMethods).FullName, + ModuleName = UnitTestAppModule, + MethodName = nameof(HiddenFrameTestMethods.ExitPoint), + FullParameterTypes = [typeof(Action).FullName], + }, + new() + { + TypeName = typeof(HiddenFrameTestMethods).FullName, + ModuleName = UnitTestAppModule, + MethodName = nameof(HiddenFrameTestMethods.DoWorkFromHiddenMethod), + FullParameterTypes = [typeof(Action).FullName], + Hidden = true + }, + new() + { + TypeName = typeof(HiddenFrameTestMethods.BaseHiddenClass).FullName, + ModuleName = UnitTestAppModule, + MethodName = nameof(HiddenFrameTestMethods.BaseHiddenClass.DoWorkFromHiddenBaseClass), + FullParameterTypes = [typeof(Action).FullName], + Hidden = true + }, + new() + { + TypeName = typeof(HiddenFrameTestMethods.PartiallyVisibleClass).FullName, + ModuleName = UnitTestAppModule, + MethodName = nameof(HiddenFrameTestMethods.PartiallyVisibleClass.DoWorkFromVisibleDerivedClass), + FullParameterTypes = [typeof(Action).FullName], + }, + new() + { + TypeName = typeof(HiddenFrameTestMethods).FullName, + ModuleName = UnitTestAppModule, + MethodName = nameof(HiddenFrameTestMethods.EntryPoint), + FullParameterTypes = [typeof(Action).FullName], + } + ] + } + }; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Exceptions.Name, + subScenarioName: TestAppScenarios.Exceptions.SubScenarios.HiddenFramesExceptionCommand, + appValidate: async (appRunner, apiClient) => + { + await GetExceptions(apiClient, appRunner, ExceptionFormat.NewlineDelimitedJson); + ValidateSingleExceptionJson(expectedException); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + runner.EnableMonitorStartupHook = true; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + }); + } + private void ValidateMultipleExceptionsText(int exceptionsCount, List exceptionTypes) { var exceptions = exceptionsResult.Split(new[] { FirstChanceExceptionMessage }, StringSplitOptions.RemoveEmptyEntries); @@ -759,14 +889,89 @@ private void ValidateMultipleExceptionsText(int exceptionsCount, List ex } } - private void ValidateSingleExceptionText(string exceptionType, string exceptionMessage, string frameTypeName, string frameMethodName, List parameterTypes) + private void ValidateSingleExceptionText(string exceptionType, string exceptionMessage, List topFrames) { var exceptionsLines = exceptionsResult.Split(Environment.NewLine, StringSplitOptions.None); - Assert.True(exceptionsLines.Length >= 3); + Assert.True(exceptionsLines.Length >= 2); Assert.Contains(FirstChanceExceptionMessage, exceptionsLines[0]); Assert.Equal($"{exceptionType}: {exceptionMessage}", exceptionsLines[1]); - Assert.Equal($" at {frameTypeName}.{frameMethodName}({string.Join(',', parameterTypes)})", exceptionsLines[2]); + int lineIndex = 2; + + Assert.True(exceptionsLines.Length - lineIndex >= topFrames.Count, "Not enough frames"); + foreach (ExceptionFrame expectedFrame in topFrames) + { + string parametersString = string.Empty; + if (expectedFrame.ParameterTypes.Count > 0) + { + parametersString = $"({string.Join(',', expectedFrame.ParameterTypes)})"; + } + Assert.Equal($" at {expectedFrame.TypeName}.{expectedFrame.MethodName}{parametersString}", exceptionsLines[lineIndex]); + lineIndex++; + } + } + + private void ValidateSingleExceptionJson(ExceptionInstance expectedException, bool onlyMatchTopFrames = true) + { + List exceptions = DeserializeJsonExceptions(); + ExceptionInstance exception = Assert.Single(exceptions); + + // We don't check all properties (e.g. timestamp or thread information) + Assert.Equal(expectedException.ModuleName, exception.ModuleName); + Assert.Equal(expectedException.TypeName, exception.TypeName); + Assert.Equal(expectedException.Message, exception.Message); + Assert.Equivalent(expectedException.Activity, exception.Activity); + + if (expectedException.CallStack == null) + { + Assert.Null(exception.CallStack); + return; + } + + Assert.NotNull(exception.CallStack); + if (onlyMatchTopFrames) + { + Assert.True(exception.CallStack.Frames.Count >= expectedException.CallStack.Frames.Count); + } + else + { + Assert.Equal(expectedException.CallStack.Frames.Count, exception.CallStack.Frames.Count); + } + + for (int i = 0; i < expectedException.CallStack.Frames.Count; i++) + { + CallStackFrame expectedFrame = expectedException.CallStack.Frames[i]; + CallStackFrame actualFrame = exception.CallStack.Frames[i]; + + // + // TODO: We don't currently check method tokens / mvid. + // This is tested by ExceptionsJsonTest. If/when that test is updated to use this method, + // we should resolve this todo. Until then the coverage is unnecessary so keep things simple. + // + Assert.Equal(expectedFrame.ModuleName, actualFrame.ModuleName); + Assert.Equal(expectedFrame.TypeName, actualFrame.TypeName); + Assert.Equal(expectedFrame.MethodName, actualFrame.MethodName); + Assert.Equal(expectedFrame.FullGenericArgTypes, actualFrame.FullGenericArgTypes); + Assert.Equal(expectedFrame.FullParameterTypes ?? [], actualFrame.FullParameterTypes ?? []); + Assert.Equal(expectedFrame.Hidden, actualFrame.Hidden); + } + } + + private void ValidateSingleExceptionText(string exceptionType, string exceptionMessage, string frameTypeName, string frameMethodName, List parameterTypes) + => ValidateSingleExceptionText(exceptionType, exceptionMessage, [new ExceptionFrame(frameTypeName, frameMethodName, parameterTypes)]); + + private List DeserializeJsonExceptions() + { + List exceptions = []; + + using StringReader reader = new StringReader(exceptionsResult); + + string line; + while (null != (line = reader.ReadLine())) + { + exceptions.Add(JsonSerializer.Deserialize(line)); + } + return exceptions; } private async Task GetExceptions(ApiClient apiClient, AppRunner appRunner, ExceptionFormat format, ExceptionsConfiguration configuration = null) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs index 44797425731..df8321358c8 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs @@ -7,6 +7,7 @@ using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -40,21 +41,30 @@ public class StacksTests private const string NativeFrame = "[NativeFrame]"; private const string ExpectedThreadName = "TestThread"; - private static MethodInfo GetMethodInfo(string methodName) + private static MethodInfo GetMethodInfo(string typeName, string methodName) { - // Strip off any generic type information. - if (methodName.Contains('[')) + static void removeGenericInformation(ref string name) { - methodName = methodName[..methodName.IndexOf('[')]; + if (name.Contains('[')) + { + name = name[..name.IndexOf('[')]; + } } + // Strip off any generic type information. + removeGenericInformation(ref typeName); + removeGenericInformation(ref methodName); + // Return null on pseudo frames (e.g. [NativeFrame]) if (methodName.Length == 0) { return null; } - return typeof(Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios.StacksWorker.StacksWorkerNested).GetMethod(methodName); + Type typeMatch = typeof(StacksWorker).Module.GetType(typeName); + Assert.NotNull(typeMatch); + + return typeMatch.GetMethod(methodName); } public StacksTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) @@ -90,6 +100,9 @@ private static async Task PlainTextValidation(AppRunner runner, ApiClient client string[] expectedFrames = { + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods).FullName, nameof(HiddenFrameTestMethods.ExitPoint)), + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods.PartiallyVisibleClass).FullName, nameof(HiddenFrameTestMethods.PartiallyVisibleClass.DoWorkFromVisibleDerivedClass)), + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods).FullName, nameof(HiddenFrameTestMethods.EntryPoint)), FormatFrame(ExpectedModule, ExpectedClass, ExpectedCallbackFunction), NativeFrame, FormatFrame(ExpectedModule, ExpectedClass, ExpectedTextFunction), @@ -185,14 +198,21 @@ private static async Task SpeedscopeStacksValidation(AppRunner runner, ApiClient WebApi.Models.SpeedscopeResult result = await JsonSerializer.DeserializeAsync(holder.Stream); - int bottomIndex = result.Shared.Frames.FindIndex(f => f.Name == FormatFrame(ExpectedModule, ExpectedClass, ExpectedFunction)); - Assert.NotEqual(-1, bottomIndex); - string topFrameName = FormatFrame(ExpectedModule, ExpectedClass, ExpectedCallbackFunction); - int topIndex = result.Shared.Frames.FindIndex(f => f.Name == topFrameName); - Assert.NotEqual(-1, topIndex); + string[] framesToFind = + [ + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods).FullName, nameof(HiddenFrameTestMethods.ExitPoint)), + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods.PartiallyVisibleClass).FullName, nameof(HiddenFrameTestMethods.PartiallyVisibleClass.DoWorkFromVisibleDerivedClass)), + FormatFrame(ExpectedModule, typeof(HiddenFrameTestMethods).FullName, nameof(HiddenFrameTestMethods.EntryPoint)), + FormatFrame(ExpectedModule, ExpectedClass, ExpectedCallbackFunction), + NativeFrame, + FormatFrame(ExpectedModule, ExpectedClass, ExpectedFunction) + ]; - WebApi.Models.ProfileEvent[] expectedFrames = ExpectedSpeedscopeFrames(topIndex, bottomIndex); - (WebApi.Models.Profile stack, IList actualFrames) = GetActualFrames(result, topFrameName, 3); + int[] indices = framesToFind.Select(frame => result.Shared.Frames.FindIndex(f => f.Name == frame)).ToArray(); + Assert.DoesNotContain(-1, indices); + + WebApi.Models.ProfileEvent[] expectedFrames = ExpectedSpeedscopeFrames(indices); + (WebApi.Models.Profile stack, IList actualFrames) = GetActualFrames(result, framesToFind[0], framesToFind.Length); Assert.NotNull(stack); @@ -471,7 +491,7 @@ private static string FormatFrame(string module, string @class, string function) private static bool AreFramesEqual(WebApi.Models.CallStackFrame expected, WebApi.Models.CallStackFrame actual) { - MethodInfo expectedMethodInfo = GetMethodInfo(expected.MethodName); + MethodInfo expectedMethodInfo = GetMethodInfo(expected.TypeName, expected.MethodName); return (expected.ModuleName == actual.ModuleName) && (expected.TypeName == actual.TypeName) && @@ -536,31 +556,48 @@ private static (WebApi.Models.CallStack, IList) Ge return (null, actualFrames); } - private static WebApi.Models.ProfileEvent[] ExpectedSpeedscopeFrames(int topFrameIndex, int bottomFrameIndex) => new WebApi.Models.ProfileEvent[] - { - new WebApi.Models.ProfileEvent + private static WebApi.Models.ProfileEvent[] ExpectedSpeedscopeFrames(int[] indices) + => indices.Select((i) => new WebApi.Models.ProfileEvent { - Frame = topFrameIndex, + Frame = i, At = 0.0, Type = WebApi.Models.ProfileEventType.O - }, - new WebApi.Models.ProfileEvent - { - Frame = 0, - At = 0.0, - Type = WebApi.Models.ProfileEventType.O - }, - new WebApi.Models.ProfileEvent - { - Frame = bottomFrameIndex, - At = 0.0, - Type = WebApi.Models.ProfileEventType.O - }, - - }; + }).ToArray(); private static WebApi.Models.CallStackFrame[] ExpectedFrames() => new WebApi.Models.CallStackFrame[] { + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + TypeName = typeof(HiddenFrameTestMethods).FullName, + MethodNameWithGenericArgTypes = nameof(HiddenFrameTestMethods.ExitPoint), + }, + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + TypeName = typeof(HiddenFrameTestMethods).FullName, + MethodNameWithGenericArgTypes = nameof(HiddenFrameTestMethods.DoWorkFromHiddenMethod), + Hidden = true, + }, + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + TypeName = typeof(HiddenFrameTestMethods.BaseHiddenClass).FullName, + MethodNameWithGenericArgTypes = nameof(HiddenFrameTestMethods.BaseHiddenClass.DoWorkFromHiddenBaseClass), + Hidden = true + }, + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + TypeName = typeof(HiddenFrameTestMethods.PartiallyVisibleClass).FullName, + MethodNameWithGenericArgTypes = nameof(HiddenFrameTestMethods.PartiallyVisibleClass.DoWorkFromVisibleDerivedClass), + }, + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + TypeName = typeof(HiddenFrameTestMethods).FullName, + MethodNameWithGenericArgTypes = nameof(HiddenFrameTestMethods.EntryPoint), + }, new WebApi.Models.CallStackFrame { ModuleName = ExpectedModule, diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/ExceptionsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/ExceptionsScenario.cs index 1b8eac7f820..a6bb8af039e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/ExceptionsScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/ExceptionsScenario.cs @@ -65,6 +65,9 @@ public static CliCommand Command() CliCommand reflectionTypeLoadExceptionCommand = new(TestAppScenarios.Exceptions.SubScenarios.ReflectionTypeLoadException); reflectionTypeLoadExceptionCommand.SetAction(ReflectionTypeLoadExceptionAsync); + CliCommand hiddenFramesExceptionCommand = new(TestAppScenarios.Exceptions.SubScenarios.HiddenFramesExceptionCommand); + hiddenFramesExceptionCommand.SetAction(HiddenFramesExceptionAsync); + CliCommand scenarioCommand = new(TestAppScenarios.Exceptions.Name); scenarioCommand.Subcommands.Add(singleExceptionCommand); scenarioCommand.Subcommands.Add(multipleExceptionsCommand); @@ -82,6 +85,7 @@ public static CliCommand Command() scenarioCommand.Subcommands.Add(eclipsingExceptionFromMethodCallCommand); scenarioCommand.Subcommands.Add(aggregateExceptionCommand); scenarioCommand.Subcommands.Add(reflectionTypeLoadExceptionCommand); + scenarioCommand.Subcommands.Add(hiddenFramesExceptionCommand); return scenarioCommand; } @@ -376,6 +380,30 @@ public static Task ReflectionTypeLoadExceptionAsync(ParseResult result, Can }, token); } + public static Task HiddenFramesExceptionAsync(ParseResult result, CancellationToken token) + { + return ScenarioHelpers.RunScenarioAsync(async logger => + { + await ScenarioHelpers.WaitForCommandAsync(TestAppScenarios.Exceptions.Commands.Begin, logger); + try + { + ThrowExceptionWithHiddenFrames(); + } + catch (Exception) + { + } + await ScenarioHelpers.WaitForCommandAsync(TestAppScenarios.Exceptions.Commands.End, logger); + return 0; + }, token); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowExceptionWithHiddenFrames() + { + HiddenFrameTestMethods.EntryPoint(ThrowAndCatchInvalidOperationException); + } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowAndCatchInvalidOperationException() { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/HiddenFrameTestMethods.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/HiddenFrameTestMethods.cs new file mode 100644 index 00000000000..393c8bc38e3 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/HiddenFrameTestMethods.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios +{ + internal static class HiddenFrameTestMethods + { + // We keep the entry and exit points visible so they act as sentinel frames + // when checking output that excludes hidden frames. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void EntryPoint(Action work) + { + PartiallyVisibleClass partiallyVisibleClass = new(); + partiallyVisibleClass.DoWorkFromVisibleDerivedClass(work); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ExitPoint(Action work) + { + work(); + } + + [StackTraceHidden] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void DoWorkFromHiddenMethod(Action work) + { + ExitPoint(work); + } + + [StackTraceHidden] + public abstract class BaseHiddenClass + { +#pragma warning disable CA1822 // Mark members as static + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWorkFromHiddenBaseClass(Action work) +#pragma warning restore CA1822 // Mark members as static + { + DoWorkFromHiddenMethod(work); + } + } + + public class PartiallyVisibleClass : BaseHiddenClass + { + // StackTraceHidden attributes are not inherited + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWorkFromVisibleDerivedClass(Action work) + { + DoWorkFromHiddenBaseClass(work); + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs index ef65fa2f1e3..bf9ba9b6e74 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs @@ -23,10 +23,13 @@ public void DoWork(U test, WaitHandle handle) public void Callback() { - using EventSource eventSource = new EventSource("StackScenario"); - using EventCounter eventCounter = new EventCounter("Ready", eventSource); - eventCounter.WriteMetric(1.0); - _handle.WaitOne(); + HiddenFrameTestMethods.EntryPoint(() => + { + using EventSource eventSource = new EventSource("StackScenario"); + using EventCounter eventCounter = new EventCounter("Ready", eventSource); + eventCounter.WriteMetric(1.0); + _handle.WaitOne(); + }); } } diff --git a/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs b/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs index ba48476fd01..d711f962139 100644 --- a/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs +++ b/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs @@ -256,6 +256,11 @@ private static async Task WriteTextInnerExceptionsAndStackFrames(TextWriter writ { foreach (CallStackFrame frame in currentInstance.CallStack.Frames) { + if (frame.Hidden) + { + continue; + } + await writer.WriteLineAsync(); await writer.WriteAsync(" at "); await writer.WriteAsync(frame.TypeName);