diff --git a/.github/dependabot.template.yml b/.github/dependabot.template.yml index bb7abccc365..749547109ac 100644 --- a/.github/dependabot.template.yml +++ b/.github/dependabot.template.yml @@ -8,6 +8,7 @@ #! to the list of TFMs that the major version uses for build and test. #@ def getTfms(): #@ return { +#@ "9": [ "net9.0", "net8.0", "net7.0", "net6.0" ], #@ "8": [ "net8.0", "net7.0", "net6.0" ], #@ "6": [ "net6.0", "netcoreapp3.1" ] #@ } @@ -18,6 +19,7 @@ #@ def getBranches(): #@ return [ #@ struct.encode({"name": "main", "majorVersion": "8"}), +#@ struct.encode({"name": "feature/9.x", "majorVersion": "9"}), #@ struct.encode({"name": "release/8.0", "majorVersion": "8"}), #@ struct.encode({"name": "release/6.x", "majorVersion": "6"}), #@ ] @@ -49,9 +51,6 @@ updates: - "version-update:semver-major" - "version-update:semver-minor" #@ end - - dependency-name: System.Text.Json - update-types: - - "version-update:semver-major" - dependency-name: "Moq" commit-message: prefix: #@ commit_prefix @@ -88,6 +87,7 @@ updates: runtime-dependencies: patterns: - "Microsoft.Extensions.*" - - "Microsoft.NETCore.App.Runtime.*" + - "Microsoft.NETCore.DotNetHost" + - "System.Text.Json" #@ end #@ end diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 981c1c9b829..ca31b0ba295 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,9 +14,6 @@ updates: - dependency-name: Microsoft.Extensions.* update-types: - version-update:semver-major - - dependency-name: System.Text.Json - update-types: - - version-update:semver-major - dependency-name: Moq commit-message: prefix: '[main] ' @@ -52,7 +49,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/net7.0 schedule: @@ -68,7 +66,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/net6.0 schedule: @@ -84,17 +83,112 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/independent schedule: interval: daily - target-branch: release/8.0 + target-branch: feature/9.x ignore: - dependency-name: Microsoft.Extensions.* update-types: - version-update:semver-major - - dependency-name: System.Text.Json + - dependency-name: Moq + commit-message: + prefix: '[feature/9.x] ' + groups: + azure-sdk-dependencies: + patterns: + - Azure.Core + - Azure.Identity + - Azure.Storage.* + identity-dependencies: + patterns: + - Microsoft.Identity.* + - Microsoft.IdentityModel.* +- package-ecosystem: nuget + directory: /eng/dependabot/nuget.org + schedule: + interval: daily + target-branch: feature/9.x + commit-message: + prefix: '[feature/9.x] ' +- package-ecosystem: nuget + directory: /eng/dependabot/net9.0 + schedule: + interval: daily + target-branch: feature/9.x + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + commit-message: + prefix: '[feature/9.x] ' + groups: + runtime-dependencies: + patterns: + - Microsoft.Extensions.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json +- package-ecosystem: nuget + directory: /eng/dependabot/net8.0 + schedule: + interval: daily + target-branch: feature/9.x + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + commit-message: + prefix: '[feature/9.x] ' + groups: + runtime-dependencies: + patterns: + - Microsoft.Extensions.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json +- package-ecosystem: nuget + directory: /eng/dependabot/net7.0 + schedule: + interval: daily + target-branch: feature/9.x + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + commit-message: + prefix: '[feature/9.x] ' + groups: + runtime-dependencies: + patterns: + - Microsoft.Extensions.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json +- package-ecosystem: nuget + directory: /eng/dependabot/net6.0 + schedule: + interval: daily + target-branch: feature/9.x + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + commit-message: + prefix: '[feature/9.x] ' + groups: + runtime-dependencies: + patterns: + - Microsoft.Extensions.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json +- package-ecosystem: nuget + directory: /eng/dependabot/independent + schedule: + interval: daily + target-branch: release/8.0 + ignore: + - dependency-name: Microsoft.Extensions.* update-types: - version-update:semver-major - dependency-name: Moq @@ -132,7 +226,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/net7.0 schedule: @@ -148,7 +243,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/net6.0 schedule: @@ -164,7 +260,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/independent schedule: @@ -178,9 +275,6 @@ updates: update-types: - version-update:semver-major - version-update:semver-minor - - dependency-name: System.Text.Json - update-types: - - version-update:semver-major - dependency-name: Moq commit-message: prefix: '[release/6.x] ' @@ -216,7 +310,8 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json - package-ecosystem: nuget directory: /eng/dependabot/netcoreapp3.1 schedule: @@ -232,4 +327,5 @@ updates: runtime-dependencies: patterns: - Microsoft.Extensions.* - - Microsoft.NETCore.App.Runtime.* + - Microsoft.NETCore.DotNetHost + - System.Text.Json diff --git a/.github/workflows/scan-for-to-do-comments.yml b/.github/workflows/scan-for-to-do-comments.yml index 9790dbbbe7f..75628d54abe 100644 --- a/.github/workflows/scan-for-to-do-comments.yml +++ b/.github/workflows/scan-for-to-do-comments.yml @@ -27,7 +27,7 @@ jobs: COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - name: Upload artifacts - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 with: name: issue-todo path: issue/ diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index cb0a75444cb..18b3feff59e 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -17,7 +17,7 @@ jobs: with: persist-credentials: false - - uses: streetsidesoftware/cspell-action@934c74da3775ac844ec89503f666f67efb427fed + - uses: streetsidesoftware/cspell-action@9759be9ad475fe8145f8d2a1bf29a1c4d1c6f18d name: Documentation spellcheck if: ${{ !cancelled() }} with: @@ -25,7 +25,7 @@ jobs: inline: error incremental_files_only: true - - uses: streetsidesoftware/cspell-action@934c74da3775ac844ec89503f666f67efb427fed + - uses: streetsidesoftware/cspell-action@9759be9ad475fe8145f8d2a1bf29a1c4d1c6f18d name: Resx spellcheck if: ${{ !cancelled() }} with: @@ -33,7 +33,7 @@ jobs: inline: error incremental_files_only: true - - uses: streetsidesoftware/cspell-action@934c74da3775ac844ec89503f666f67efb427fed + - uses: streetsidesoftware/cspell-action@9759be9ad475fe8145f8d2a1bf29a1c4d1c6f18d name: Source code spellcheck if: ${{ !cancelled() }} with: diff --git a/documentation/api/definitions.md b/documentation/api/definitions.md index 92a3f8c5c33..987ac1c1dd8 100644 --- a/documentation/api/definitions.md +++ b/documentation/api/definitions.md @@ -33,10 +33,11 @@ First Available: 8.0 Preview 7 |---|---|---| | `methodName` | string | Name of the method for this frame. This includes generic parameters. | | `methodToken` | int | TypeDef token for the method. | -| `parameterTypes` | string[] | Array of parameter types. Empty array if none. | +| `parameterTypes` | string[] | Array of parameter types. Empty array if none. Field does not exist when this information is not available. | | `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 776cd0bd1a2..0fe57711385 100644 --- a/documentation/api/stacks.md +++ b/documentation/api/stacks.md @@ -42,6 +42,9 @@ Allowed schemes: ## Responses +> [!NOTE] +> Parameter type information is not available through this feature. The `parameterTypes` field of [CallStackFrame](definitions.md#callstackframe) is omitted. + | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | [CallStackResult](definitions.md#callstackresult) | Callstacks for all managed threads in the process. | `application/json` | @@ -79,30 +82,26 @@ Location: localhost:52323/operations/67f07e40-5cca-4709-9062-26302c484f18 { "methodName": "GetQueuedCompletionStatus", "methodToken": 100663634, - "parameterTypes": [], "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, - "parameterTypes": [ - "System.Threading.ExecutionContext", - "System.Threading.ContextCallback", - "System.Object" - ], "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, - "parameterTypes": [], "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/eng/Version.Details.xml b/eng/Version.Details.xml index 286d1173b2a..53e2c176c89 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,17 +4,17 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore 8e941eb42f819adb116b881195158b3887a70a1c - + https://github.com/dotnet/diagnostics - 412c0e46f9db6cabc3123d77cc2cfcbc5d9f133a + da9510470934a71eaa1165e3276a57e08148e061 - + https://github.com/dotnet/diagnostics - 412c0e46f9db6cabc3123d77cc2cfcbc5d9f133a + da9510470934a71eaa1165e3276a57e08148e061 - + https://github.com/dotnet/command-line-api - 803d8598f98fb4efd94604b32627ee9407f246db + feb61c7f328a2401d74f4317b39d02126cfdfe24 @@ -22,33 +22,33 @@ https://github.com/dotnet/roslyn-analyzers b4d9a1334d5189172977ba8fddd00bda70161e4a - + https://github.com/dotnet/arcade - 103916ccdbe7f4ab2e194068a1a3cd330542601f + 24e02f80c5458d1f75240ae57fc2a98fb8a9022a - + https://github.com/dotnet/arcade - 103916ccdbe7f4ab2e194068a1a3cd330542601f + 24e02f80c5458d1f75240ae57fc2a98fb8a9022a - + https://github.com/dotnet/arcade - 103916ccdbe7f4ab2e194068a1a3cd330542601f + 24e02f80c5458d1f75240ae57fc2a98fb8a9022a - + https://github.com/dotnet/arcade - 103916ccdbe7f4ab2e194068a1a3cd330542601f + 24e02f80c5458d1f75240ae57fc2a98fb8a9022a https://github.com/dotnet/installer 68e8abb1d3e1a240a6e4c29dcd220aae91681676 - + https://github.com/dotnet/arcade - 103916ccdbe7f4ab2e194068a1a3cd330542601f + 24e02f80c5458d1f75240ae57fc2a98fb8a9022a - + https://github.com/dotnet/diagnostics - 412c0e46f9db6cabc3123d77cc2cfcbc5d9f133a + da9510470934a71eaa1165e3276a57e08148e061 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime diff --git a/eng/Versions.props b/eng/Versions.props index 66035371369..76602779d3e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -48,17 +48,17 @@ --> - 8.0.0-beta.24504.2 - 8.0.0-beta.24504.2 - 8.0.0-beta.24504.2 + 8.0.0-beta.24525.2 + 8.0.0-beta.24525.2 + 8.0.0-beta.24525.2 8.0.1 8.0.1-servicing.23580.8 - 2.0.0-beta4.24324.3 + 2.0.0-beta4.24528.1 - 8.0.0-preview.24507.1 - 8.0.0-preview.24507.1 + 9.0.0-preview.24554.2 + 9.0.0-preview.24554.2 8.0.103-servicing.24114.15 @@ -67,7 +67,7 @@ 8.0.1 8.0.1-servicing.23580.1 - 1.0.550701 + 1.0.555402 $(MicrosoftNETCoreApp31Version) diff --git a/eng/common/templates-official/steps/get-delegation-sas.yml b/eng/common/templates-official/steps/get-delegation-sas.yml index c0e8f91317f..c690cc0a070 100644 --- a/eng/common/templates-official/steps/get-delegation-sas.yml +++ b/eng/common/templates-official/steps/get-delegation-sas.yml @@ -28,7 +28,16 @@ steps: # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + # Temporarily work around a helix issue where SAS tokens with / in them will cause incorrect downloads + # of correlation payloads. https://github.com/dotnet/dnceng/issues/3484 + $sas = "" + do { + $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to generate SAS token." + exit 1 + } + } while($sas.IndexOf('/') -ne -1) if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." diff --git a/eng/common/templates/steps/get-delegation-sas.yml b/eng/common/templates/steps/get-delegation-sas.yml index c0e8f91317f..c690cc0a070 100644 --- a/eng/common/templates/steps/get-delegation-sas.yml +++ b/eng/common/templates/steps/get-delegation-sas.yml @@ -28,7 +28,16 @@ steps: # Calculate the expiration of the SAS token and convert to UTC $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + # Temporarily work around a helix issue where SAS tokens with / in them will cause incorrect downloads + # of correlation payloads. https://github.com/dotnet/dnceng/issues/3484 + $sas = "" + do { + $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to generate SAS token." + exit 1 + } + } while($sas.IndexOf('/') -ne -1) if ($LASTEXITCODE -ne 0) { Write-Error "Failed to generate SAS token." diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index eb188cfda41..a2dedaa5297 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -892,7 +892,7 @@ function IsWindowsPlatform() { } function Get-Darc($version) { - $darcPath = "$TempDir\darc\$(New-Guid)" + $darcPath = "$TempDir\darc\$([guid]::NewGuid())" if ($version -ne $null) { & $PSScriptRoot\darc-init.ps1 -toolpath $darcPath -darcVersion $version | Out-Host } else { diff --git a/eng/dependabot/independent/Versions.props b/eng/dependabot/independent/Versions.props index f15cc0bad52..c1031b3033a 100644 --- a/eng/dependabot/independent/Versions.props +++ b/eng/dependabot/independent/Versions.props @@ -2,11 +2,11 @@ - 1.44.0 - 1.12.1 - 12.22.1 - 12.20.0 - 3.2.1 + 1.44.1 + 1.13.1 + 12.22.2 + 12.20.1 + 3.3.1 1.6.22 4.3.2 5.0.0 @@ -14,7 +14,7 @@ 13.0.3 11.0.0 - 6.8.1 + 6.9.0 3.7.305.7 3.7.300.33 diff --git a/eng/dependabot/net8.0/Packages.props b/eng/dependabot/net8.0/Packages.props index 2943b192b67..039efe2cfb3 100644 --- a/eng/dependabot/net8.0/Packages.props +++ b/eng/dependabot/net8.0/Packages.props @@ -13,6 +13,6 @@ with the same version number. --> - diff --git a/eng/dependabot/net8.0/Versions.props b/eng/dependabot/net8.0/Versions.props index a034a83518a..1488808d877 100644 --- a/eng/dependabot/net8.0/Versions.props +++ b/eng/dependabot/net8.0/Versions.props @@ -4,13 +4,14 @@ 8.0.0 - 8.0.0 + 8.0.1 - 8.0.1 + 8.0.2 - 8.0.0 + 8.0.1 - 8.0.8 - 8.0.4 + 8.0.10 + + 8.0.5 diff --git a/eng/pipelines/jobs/build.yml b/eng/pipelines/jobs/build.yml index 56708ffe01a..8c655fbcf22 100644 --- a/eng/pipelines/jobs/build.yml +++ b/eng/pipelines/jobs/build.yml @@ -70,7 +70,7 @@ jobs: ${{ if in(parameters.osGroup, 'MacOS') }}: pool: name: Azure Pipelines - vmImage: macos-12 + vmImage: macos-15 os: macOS ${{ if ne(parameters.container, '') }}: diff --git a/global.json b/global.json index 43f5a6eb792..bb843f8dab7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "tools": { - "dotnet": "8.0.108", + "dotnet": "8.0.110", "runtimes": { "aspnetcore": [ "$(MicrosoftAspNetCoreApp60Version)", @@ -26,7 +26,7 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.0", - "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24504.2", - "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24504.2" + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.24525.2", + "Microsoft.DotNet.Helix.Sdk": "8.0.0-beta.24525.2" } } diff --git a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSource.cs b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSource.cs index 95806453b32..00668ee1553 100644 --- a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSource.cs +++ b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSource.cs @@ -79,9 +79,10 @@ public void ClassDescription( ulong ModuleId, uint Token, uint Flags, + uint StackTraceHidden, ulong[] TypeArgs) { - Span data = stackalloc EventData[5]; + Span data = stackalloc EventData[6]; Span typeArgsSpan = stackalloc byte[GetArrayDataSize(TypeArgs)]; FillArrayData(typeArgsSpan, TypeArgs); @@ -89,6 +90,7 @@ public void ClassDescription( SetValue(ref data[NameIdentificationEvents.ClassDescPayloads.ModuleId], ModuleId); SetValue(ref data[NameIdentificationEvents.ClassDescPayloads.Token], Token); SetValue(ref data[NameIdentificationEvents.ClassDescPayloads.Flags], Flags); + SetValue(ref data[NameIdentificationEvents.ClassDescPayloads.StackTraceHidden], StackTraceHidden); SetValue(ref data[NameIdentificationEvents.ClassDescPayloads.TypeArgs], typeArgsSpan); WriteEventWithFlushing(ExceptionEvents.EventIds.ClassDescription, data); @@ -101,11 +103,12 @@ public void FunctionDescription( ulong ClassId, uint ClassToken, ulong ModuleId, + uint StackTraceHidden, string Name, ulong[] TypeArgs, ulong[] ParameterTypes) { - Span data = stackalloc EventData[8]; + Span data = stackalloc EventData[9]; using PinnedData namePinned = PinnedData.Create(Name); Span typeArgsSpan = stackalloc byte[GetArrayDataSize(TypeArgs)]; FillArrayData(typeArgsSpan, TypeArgs); @@ -117,6 +120,7 @@ public void FunctionDescription( SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.ClassId], ClassId); SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.ClassToken], ClassToken); SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.ModuleId], ModuleId); + SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.StackTraceHidden], StackTraceHidden); SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.Name], namePinned); SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.TypeArgs], typeArgsSpan); SetValue(ref data[NameIdentificationEvents.FunctionDescPayloads.ParameterTypes], parameterTypesSpan); @@ -160,16 +164,18 @@ public void TokenDescription( ulong ModuleId, uint Token, uint OuterToken, + uint StackTraceHidden, string Name, string Namespace) { - Span data = stackalloc EventData[5]; + Span data = stackalloc EventData[6]; using PinnedData namePinned = PinnedData.Create(Name); using PinnedData namespacePinned = PinnedData.Create(Namespace); SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.ModuleId], ModuleId); SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.Token], Token); SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.OuterToken], OuterToken); + SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.StackTraceHidden], StackTraceHidden); SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.Name], namePinned); SetValue(ref data[NameIdentificationEvents.TokenDescPayloads.Namespace], namespacePinned); diff --git a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSourceIdentifierCacheCallback.cs b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSourceIdentifierCacheCallback.cs index 5a76d8ea968..97575217b2c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSourceIdentifierCacheCallback.cs +++ b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Eventing/ExceptionsEventSourceIdentifierCacheCallback.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Diagnostics.Monitoring.StartupHook.Exceptions.Identification; +using System; namespace Microsoft.Diagnostics.Monitoring.StartupHook.Exceptions.Eventing { @@ -22,6 +23,7 @@ public override void OnClassData(ulong classId, ClassData data) data.ModuleId, data.Token, (uint)data.Flags, + Convert.ToUInt32(data.StackTraceHidden), data.TypeArgs); } @@ -42,6 +44,7 @@ public override void OnFunctionData(ulong functionId, FunctionData data) data.ParentClass, data.ParentClassToken, data.ModuleId, + Convert.ToUInt32(data.StackTraceHidden), data.Name, data.TypeArgs, data.ParameterTypes); @@ -69,6 +72,7 @@ public override void OnTokenData(ulong moduleId, uint typeToken, TokenData data) moduleId, typeToken, data.OuterToken, + Convert.ToUInt32(data.StackTraceHidden), data.Name, data.Namespace); } diff --git a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Identification/ExceptionGroupIdentifierCache.cs b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Identification/ExceptionGroupIdentifierCache.cs index 99ef3d1f5e6..2ce0b9e545d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Identification/ExceptionGroupIdentifierCache.cs +++ b/src/Microsoft.Diagnostics.Monitoring.StartupHook/Exceptions/Identification/ExceptionGroupIdentifierCache.cs @@ -96,6 +96,13 @@ public ulong GetOrAdd(MethodBase method) parentClassToken = Convert.ToUInt32(method.DeclaringType.MetadataToken); } + bool stackTraceHidden = false; + try + { + stackTraceHidden = method.GetCustomAttribute(inherit: false) != null; + } + catch (Exception) { } + // RTDynamicMethod does not implement GetGenericArguments. Type[] genericArguments = Array.Empty(); try @@ -111,8 +118,8 @@ public ulong GetOrAdd(MethodBase method) parentClassToken, GetOrAdd(method.Module), GetOrAdd(genericArguments), - GetOrAdd(method.GetParameters()) - ); + GetOrAdd(method.GetParameters()), + stackTraceHidden); if (_nameCache.FunctionData.TryAdd(methodId, data)) { @@ -205,11 +212,20 @@ public ulong GetOrAdd(Type type) { ulong moduleId = GetOrAdd(type.Module); uint typeToken = Convert.ToUInt32(type.MetadataToken); + + bool stackTraceHidden = false; + try + { + stackTraceHidden = type.GetCustomAttribute(inherit: false) != null; + } + catch (Exception) { } + ClassData classData = new( typeToken, moduleId, ClassFlags.None, - GetOrAdd(type.GetGenericArguments())); + GetOrAdd(type.GetGenericArguments()), + stackTraceHidden); if (!_nameCache.ClassData.TryAdd(classId, classData)) break; @@ -228,7 +244,8 @@ public ulong GetOrAdd(Type type) TokenData tokenData = new( type.Name, null == type.DeclaringType ? type.Namespace ?? string.Empty : string.Empty, - parentClassToken); + parentClassToken, + stackTraceHidden); if (!_nameCache.TokenData.TryAdd(key, tokenData)) break; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/Activity.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/Activity.cs new file mode 100644 index 00000000000..f64632d6384 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/Activity.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Models +{ + public class Activity + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("idFormat")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ActivityIdFormat IdFormat { get; set; } = ActivityIdFormat.Unknown; + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs index 87a29455177..98a786330d9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs @@ -1,20 +1,48 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +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")] - public string MethodName { get; set; } = string.Empty; + public string MethodNameWithGenericArgTypes + { + get + { + StringBuilder builder = new(MethodName); + NameFormatter.BuildGenericArgTypes(builder, FullGenericArgTypes); + return builder.ToString(); + } + // Only intended for test code. + set + { + MethodName = NameFormatter.RemoveGenericArgTypes(value, out string[] genericArgTypes); + FullGenericArgTypes = genericArgTypes; + } + } + + [JsonIgnore] + internal string MethodName { get; set; } = string.Empty; [JsonPropertyName("methodToken")] public uint MethodToken { get; set; } + [JsonPropertyName("parameterTypes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList? FullParameterTypes { get; set; } + + [JsonIgnore] + internal IList? SimpleParameterTypes { get; set; } + [JsonPropertyName("typeName")] public string TypeName { get; set; } = string.Empty; @@ -24,17 +52,15 @@ public class CallStackFrame [JsonPropertyName("moduleVersionId")] public Guid ModuleVersionId { get; set; } + [JsonPropertyName("hidden")] + public bool Hidden { get; set; } + [JsonIgnore] internal IList SimpleGenericArgTypes { get; set; } = new List(); [JsonIgnore] internal IList FullGenericArgTypes { get; set; } = new List(); - [JsonIgnore] - internal IList SimpleParameterTypes { get; set; } = new List(); - - [JsonIgnore] - internal IList FullParameterTypes { get; set; } = new List(); //TODO Bring this back once we have a relative il offset value. //[JsonPropertyName("offset")] //public ulong Offset { get; set; } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ExceptionInstance.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ExceptionInstance.cs new file mode 100644 index 00000000000..13a04a48af1 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ExceptionInstance.cs @@ -0,0 +1,49 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Models +{ + public class ExceptionInstance + { + [JsonPropertyName("id")] + public ulong Id { get; set; } + + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } + + [JsonPropertyName("typeName")] + public string TypeName { get; set; } = string.Empty; + + [JsonPropertyName("moduleName")] + public string ModuleName { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("activity")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Activity? Activity { get; set; } + + [JsonPropertyName("innerExceptions")] + public InnerExceptionId[] InnerExceptionIds { get; set; } = []; + + [JsonPropertyName("stack")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CallStack? CallStack { get; set; } + } + + public class InnerExceptionId + { + public static explicit operator InnerExceptionId(ulong id) + => new InnerExceptionId() + { + Id = id, + }; + + [JsonPropertyName("id")] + public ulong Id { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs index cc891648e96..c0448c8e330 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs @@ -114,7 +114,8 @@ private void Callback(TraceEvent action) action.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ClassToken), action.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ModuleId), action.GetPayload(NameIdentificationEvents.FunctionDescPayloads.TypeArgs) ?? Array.Empty(), - action.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ParameterTypes) ?? Array.Empty() + action.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ParameterTypes) ?? Array.Empty(), + action.GetBoolPayload(NameIdentificationEvents.FunctionDescPayloads.StackTraceHidden) ); _result.NameCache.FunctionData.TryAdd(id, functionData); @@ -126,7 +127,8 @@ private void Callback(TraceEvent action) action.GetPayload(NameIdentificationEvents.ClassDescPayloads.Token), action.GetPayload(NameIdentificationEvents.ClassDescPayloads.ModuleId), (ClassFlags)action.GetPayload(NameIdentificationEvents.ClassDescPayloads.Flags), - action.GetPayload(NameIdentificationEvents.ClassDescPayloads.TypeArgs) ?? Array.Empty() + action.GetPayload(NameIdentificationEvents.ClassDescPayloads.TypeArgs) ?? Array.Empty(), + action.GetBoolPayload(NameIdentificationEvents.ClassDescPayloads.StackTraceHidden) ); _result.NameCache.ClassData.TryAdd(id, classData); @@ -148,7 +150,8 @@ private void Callback(TraceEvent action) var tokenData = new TokenData( action.GetPayload(NameIdentificationEvents.TokenDescPayloads.Name), action.GetPayload(NameIdentificationEvents.TokenDescPayloads.Namespace), - action.GetPayload(NameIdentificationEvents.TokenDescPayloads.OuterToken) + action.GetPayload(NameIdentificationEvents.TokenDescPayloads.OuterToken), + action.GetBoolPayload(NameIdentificationEvents.TokenDescPayloads.StackTraceHidden) ); _result.NameCache.TokenData.TryAdd(new ModuleScopedToken(modId, token), tokenData); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs index b6e9fb722be..a65a98850bc 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs @@ -21,7 +21,7 @@ public override async Task FormatStack(CallStackResult stackResult, Cancellation foreach (CallStack stack in stackResult.Stacks) { - stackResultModel.Stacks.Add(StackUtilities.TranslateCallStackToModel(stack, cache)); + stackResultModel.Stacks.Add(StackUtilities.TranslateCallStackToModel(stack, cache, ensureParameterTypeFieldsNotNull: false)); } await JsonSerializer.SerializeAsync(OutputStream, stackResultModel, cancellationToken: token); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameCache.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameCache.cs index 6207e0837a5..308d5208679 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameCache.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameCache.cs @@ -32,16 +32,18 @@ internal enum ClassFlags : uint /// The identifier of the module that contains the class. /// The flags for the class. /// The class identifiers of the generic type arguments of the class. + /// If the class has . /// /// The name of the class can be retrieved from the corresponding . /// - internal sealed record class ClassData(uint Token, ulong ModuleId, ClassFlags Flags, ulong[] TypeArgs); + internal sealed record class ClassData(uint Token, ulong ModuleId, ClassFlags Flags, ulong[] TypeArgs, bool StackTraceHidden); /// The name of the token. /// The namespace of the Name. /// The metadata token of the parent container. + /// If the token has . [DebuggerDisplay("{Name}")] - internal sealed record class TokenData(string Name, string Namespace, uint OuterToken); + internal sealed record class TokenData(string Name, string Namespace, uint OuterToken, bool StackTraceHidden); /// The name of the function. /// The method token of the function (methodDef token). @@ -50,11 +52,12 @@ internal sealed record class TokenData(string Name, string Namespace, uint Outer /// The identifier of the module that contains the function. /// The class identifiers of the generic type arguments of the function. /// The class identifiers of the parameter types of the function. + /// If the function has . /// /// If is 0, then use . /// [DebuggerDisplay("{Name}")] - internal sealed record class FunctionData(string Name, uint MethodToken, ulong ParentClass, uint ParentClassToken, ulong ModuleId, ulong[] TypeArgs, ulong[] ParameterTypes); + internal sealed record class FunctionData(string Name, uint MethodToken, ulong ParentClass, uint ParentClassToken, ulong ModuleId, ulong[] TypeArgs, ulong[] ParameterTypes, bool StackTraceHidden); /// The name of the module. /// The version identifier of the module. diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameFormatter.cs index 050ab763dc8..1554ba58267 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameFormatter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameFormatter.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.Text; @@ -126,6 +127,26 @@ public static void BuildGenericArgTypes(StringBuilder builder, IList typ WriteTypeNamesList(builder, typeNames, GenericStart, GenericEnd, GenericSeparator); } + public static string RemoveGenericArgTypes(string name, out string[] genericArgTypes) + { + int genericsStartIndex = name.IndexOf(GenericStart); + // Not found or an annotated frame + if (genericsStartIndex <= 0) + { + genericArgTypes = []; + return name; + } + + int genericEndIndex = name.IndexOf(GenericEnd); + if (genericEndIndex != name.Length - 1) + { + throw new InvalidOperationException("Malformed name"); + } + + genericArgTypes = name[(genericsStartIndex + 1)..genericEndIndex].Split(GenericSeparator); + return name[..genericsStartIndex]; + } + public static void BuildMethodParameterTypes(StringBuilder builder, IList typeNames) { WriteTypeNamesList(builder, typeNames, MethodParameterTypesStart, MethodParameterTypesEnd, GenericSeparator); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameIdentificationEvents.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameIdentificationEvents.cs index 91394002118..50ed1b5bf37 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameIdentificationEvents.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/NameIdentificationEvents.cs @@ -16,9 +16,10 @@ public static class FunctionDescPayloads public const int ClassId = 2; public const int ClassToken = 3; public const int ModuleId = 4; - public const int Name = 5; - public const int TypeArgs = 6; - public const int ParameterTypes = 7; + public const int StackTraceHidden = 5; + public const int Name = 6; + public const int TypeArgs = 7; + public const int ParameterTypes = 8; } public static class ClassDescPayloads @@ -27,7 +28,8 @@ public static class ClassDescPayloads public const int ModuleId = 1; public const int Token = 2; public const int Flags = 3; - public const int TypeArgs = 4; + public const int StackTraceHidden = 4; + public const int TypeArgs = 5; } public static class ModuleDescPayloads @@ -42,8 +44,9 @@ public static class TokenDescPayloads public const int ModuleId = 0; public const int Token = 1; public const int OuterToken = 2; - public const int Name = 3; - public const int Namespace = 4; + public const int StackTraceHidden = 3; + public const int Name = 4; + public const int Namespace = 5; } } } 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/Strings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs index a9b823d6b86..335715f0f3b 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs @@ -312,6 +312,15 @@ internal static string FeatureName_ParameterCapturing { } } + /// + /// Looks up a localized string similar to Server Endpoint Pruning Algorithm V2. + /// + internal static string FeatureName_ServerEndpointPruningAlgorithmV2 { + get { + return ResourceManager.GetString("FeatureName_ServerEndpointPruningAlgorithmV2", resourceCulture); + } + } + /// /// Looks up a localized string similar to The counter {0} ended and is no longer receiving metrics.. /// diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx index d8f2ca1fc88..a0a3b45ea73 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx @@ -332,4 +332,7 @@ Finished generating in-process artifact + + Server Endpoint Pruning Algorithm V2 + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs index 042a7975f1d..b3552d696d8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs @@ -17,30 +17,22 @@ internal enum StackFormat internal static class StackUtilities { - public static Models.CallStack TranslateCallStackToModel(CallStack stack, NameCache cache, bool methodNameIncludesGenericParameters = true) + public static Models.CallStack TranslateCallStackToModel(CallStack stack, NameCache cache, bool ensureParameterTypeFieldsNotNull = true) { Models.CallStack stackModel = new Models.CallStack(); stackModel.ThreadId = stack.ThreadId; stackModel.ThreadName = stack.ThreadName; - StringBuilder builder = new(); foreach (CallStackFrame frame in stack.Frames) { - var frameModel = CreateFrameModel(frame, cache); - if (methodNameIncludesGenericParameters) - { - builder.Append(frameModel.MethodName); - NameFormatter.BuildGenericArgTypes(builder, frameModel.FullGenericArgTypes); - frameModel.MethodName = builder.ToString(); - builder.Clear(); - } + var frameModel = CreateFrameModel(frame, cache, ensureParameterTypeFieldsNotNull); stackModel.Frames.Add(frameModel); } return stackModel; } - internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, NameCache cache) + internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, NameCache cache, bool ensureParameterTypeFieldsNotNull) { var builder = new StringBuilder(); @@ -52,7 +44,11 @@ internal static Models.CallStackFrame CreateFrameModel(CallStackFrame frame, Nam //TODO Bring this back once we have a useful offset value //Offset = frame.Offset, ModuleName = NameFormatter.UnknownModule, - ModuleVersionId = Guid.Empty + ModuleVersionId = Guid.Empty, + Hidden = false, + + SimpleParameterTypes = ensureParameterTypeFieldsNotNull ? [] : null, + FullParameterTypes = ensureParameterTypeFieldsNotNull ? [] : null, }; if (frame.FunctionId == 0) { @@ -64,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)) { @@ -95,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/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs index 5ddfea78713..c6d03a0f617 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs @@ -2,11 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Diagnostics.Tracing; +using System; namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class TraceEventExtensions { + public static bool GetBoolPayload(this TraceEvent traceEvent, int index) + { + return Convert.ToBoolean(GetPayload(traceEvent, index)); + } + public static T GetPayload(this TraceEvent traceEvent, int index) { return (T)traceEvent.PayloadValue(index); diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/ClrData.h b/src/Profilers/CommonMonitorProfiler/CommonUtilities/ClrData.h index c243c31a63c..22965e39d83 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/ClrData.h +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/ClrData.h @@ -36,14 +36,15 @@ enum class ClassFlags : UINT32 class ClassData { public: - ClassData(ModuleID moduleId, mdTypeDef token, ClassFlags flags) : - _moduleId(moduleId), _token(token), _flags(flags) + ClassData(ModuleID moduleId, mdTypeDef token, ClassFlags flags, bool stackTraceHidden) : + _moduleId(moduleId), _token(token), _flags(flags), _stackTraceHidden(stackTraceHidden) { } const ModuleID GetModuleId() const { return _moduleId; } const mdTypeDef GetToken() const { return _token; } const ClassFlags GetFlags() const { return _flags; } + const bool GetStackTraceHidden() const { return _stackTraceHidden; } const std::vector& GetTypeArgs() const { return _typeArgs; } void AddTypeArg(ClassID id) { _typeArgs.push_back(static_cast(id)); } @@ -51,30 +52,34 @@ class ClassData ModuleID _moduleId; mdTypeDef _token; ClassFlags _flags; + bool _stackTraceHidden; std::vector _typeArgs; }; class TokenData { public: - TokenData(tstring&& name, tstring&& Namespace, mdTypeDef outerClass) : _name(name), _namespace(Namespace), _outerClass(outerClass) + TokenData(tstring&& name, tstring&& Namespace, mdTypeDef outerClass, bool stackTraceHidden) : + _name(name), _namespace(Namespace), _outerClass(outerClass), _stackTraceHidden(stackTraceHidden) { } const tstring& GetName() const { return _name; } const tstring& GetNamespace() const { return _namespace; } const mdTypeDef& GetOuterToken() const { return _outerClass; } + const bool GetStackTraceHidden() const { return _stackTraceHidden; } private: tstring _name; tstring _namespace; mdTypeDef _outerClass; + bool _stackTraceHidden; }; class FunctionData { public: - FunctionData(ModuleID moduleId, ClassID containingClass, tstring&& name, mdToken methodToken, mdTypeDef classToken) : - _moduleId(moduleId), _class(containingClass), _functionName(name), _methodToken(methodToken), _classToken(classToken) + FunctionData(ModuleID moduleId, ClassID containingClass, tstring&& name, mdToken methodToken, mdTypeDef classToken, bool stackTraceHidden) : + _moduleId(moduleId), _class(containingClass), _functionName(name), _methodToken(methodToken), _classToken(classToken), _stackTraceHidden(stackTraceHidden) { } @@ -83,6 +88,7 @@ class FunctionData const ClassID GetClass() const { return _class; } const mdToken GetMethodToken() const { return _methodToken; } const mdTypeDef GetClassToken() const { return _classToken; } + const bool GetStackTraceHidden() const { return _stackTraceHidden; } const std::vector& GetTypeArgs() const { return _typeArgs; } const std::vector& GetParameterTypes() const { return _parameterTypes; } void AddTypeArg(ClassID classID) { _typeArgs.push_back(static_cast(classID)); } @@ -93,6 +99,7 @@ class FunctionData tstring _functionName; mdToken _methodToken; mdTypeDef _classToken; + bool _stackTraceHidden; std::vector _typeArgs; std::vector _parameterTypes; }; diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.cpp b/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.cpp index dd0f99ab3e1..5d059eea630 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.cpp +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.cpp @@ -193,9 +193,9 @@ const std::unordered_map, std::shared_ptr functionData = std::make_shared(moduleId, parent, std::move(name), methodToken, parentToken); + std::shared_ptr functionData = std::make_shared(moduleId, parent, std::move(name), methodToken, parentToken, stackTraceHidden); for (int i = 0; i < typeArgsCount; i++) { functionData->AddTypeArg(typeArgs[i]); @@ -203,9 +203,9 @@ void NameCache::AddFunctionData(ModuleID moduleId, FunctionID id, tstring&& name _functionNames.emplace(id, functionData); } -void NameCache::AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount) +void NameCache::AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount, bool stackTraceHidden) { - std::shared_ptr classData = std::make_shared(moduleId, typeDef, flags); + std::shared_ptr classData = std::make_shared(moduleId, typeDef, flags, stackTraceHidden); for (int i = 0; i < typeArgsCount; i++) { classData->AddTypeArg(typeArgs[i]); @@ -213,9 +213,9 @@ void NameCache::AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, C _classNames.emplace(id, classData); } -void NameCache::AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name, tstring&& Namespace) +void NameCache::AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name, tstring&& Namespace, bool stackTraceHidden) { - std::shared_ptr tokenData = std::make_shared(std::move(name), std::move(Namespace), outerToken); + std::shared_ptr tokenData = std::make_shared(std::move(name), std::move(Namespace), outerToken, stackTraceHidden); _names.emplace(std::make_pair(moduleId, typeDef), tokenData); } diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.h b/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.h index d1549d071bc..0c8ed5c5db7 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.h +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/NameCache.h @@ -25,9 +25,9 @@ class NameCache bool TryGetTokenData(ModuleID modId, mdTypeDef token, std::shared_ptr& data); void AddModuleData(ModuleID moduleId, tstring&& name, GUID mvid); - void AddFunctionData(ModuleID moduleId, FunctionID id, tstring&& name, ClassID parent, mdToken methodToken, mdTypeDef parentToken, ClassID* typeArgs, int typeArgsCount); - void AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount); - void AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name, tstring&& Namespace); + void AddFunctionData(ModuleID moduleId, FunctionID id, tstring&& name, ClassID parent, mdToken methodToken, mdTypeDef parentToken, ClassID* typeArgs, int typeArgsCount, bool stackTraceHidden); + void AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount, bool stackTraceHidden); + void AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name, tstring&& Namespace, bool stackTraceHidden); HRESULT GetFullyQualifiedName(FunctionID id, tstring& name); HRESULT GetFullyQualifiedTypeName(ClassID classId, tstring& name); diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp index 647cdc10f99..0f7507c21b3 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.cpp @@ -86,7 +86,9 @@ HRESULT TypeNameUtilities::GetFunctionInfo(NameCache& nameCache, FunctionID id, IfFailRet(GetModuleInfo(nameCache, moduleId)); - nameCache.AddFunctionData(moduleId, id, tstring(funcName), classId, token, classToken, typeArgs, typeArgsCount); + bool stackTraceHidden = ShouldHideFromStackTrace(moduleId, token); + + nameCache.AddFunctionData(moduleId, id, tstring(funcName), classId, token, classToken, typeArgs, typeArgsCount, stackTraceHidden); // If the ClassID returned from GetFunctionInfo is 0, then the function is a shared generic function. if (classId != 0) @@ -169,7 +171,9 @@ HRESULT TypeNameUtilities::GetClassInfo(NameCache& nameCache, ClassID classId) } } - nameCache.AddClassData(modId, classId, classToken, flags, typeArgs, typeArgsCount); + bool stackTraceHidden = ShouldHideFromStackTrace(modId, classToken); + + nameCache.AddClassData(modId, classId, classToken, flags, typeArgs, typeArgsCount, stackTraceHidden); return S_OK; } @@ -193,6 +197,8 @@ HRESULT TypeNameUtilities::GetTypeDefName(NameCache& nameCache, ModuleID moduleI break; } + bool stackTraceHidden = ShouldHideFromStackTrace(moduleId, tokenToProcess); + WCHAR wName[256]; DWORD dwTypeDefFlags = 0; @@ -222,7 +228,7 @@ HRESULT TypeNameUtilities::GetTypeDefName(NameCache& nameCache, ModuleID moduleI wNameString = wNameString.substr(found + 1); } } - nameCache.AddTokenData(moduleId, tokenToProcess, outerTokenType, tstring(wNameString), tstring(wNamespaceString)); + nameCache.AddTokenData(moduleId, tokenToProcess, outerTokenType, tstring(wNameString), tstring(wNamespaceString), stackTraceHidden); tokenToProcess = outerTokenType; } @@ -283,3 +289,37 @@ HRESULT TypeNameUtilities::GetModuleInfo(NameCache& nameCache, ModuleID moduleId return S_OK; } + +bool TypeNameUtilities::ShouldHideFromStackTrace(ModuleID moduleId, mdToken token) +{ + bool hasAttribute = false; + if (HasStackTraceHiddenAttribute(moduleId, token, hasAttribute) != S_OK) { + // When encountering an error while checking for the attribute show the frame. + return false; + } + + return hasAttribute; +} + +HRESULT TypeNameUtilities::HasStackTraceHiddenAttribute(ModuleID moduleId, mdToken token, bool& hasAttribute) +{ + HRESULT hr; + hasAttribute = false; + + ComPtr pIMDImport; + IfFailRet(_profilerInfo->GetModuleMetaData(moduleId, + ofRead, + IID_IMetaDataImport, + (IUnknown**)&pIMDImport)); + + // GetCustomAttributeByName will return S_FALSE if the attribute is not found. + IfFailRet(pIMDImport->GetCustomAttributeByName( + token, + _T("System.Diagnostics.StackTraceHiddenAttribute"), + nullptr, + nullptr)); + + hasAttribute = (hr == S_OK); + + return S_OK; +} diff --git a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.h b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.h index 4690da28add..dc3df626813 100644 --- a/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.h +++ b/src/Profilers/CommonMonitorProfiler/CommonUtilities/TypeNameUtilities.h @@ -24,6 +24,10 @@ class TypeNameUtilities HRESULT GetClassInfo(NameCache& nameCache, ClassID classId); HRESULT GetModuleInfo(NameCache& nameCache, ModuleID moduleId); HRESULT GetTypeDefName(NameCache& nameCache, ModuleID moduleId, mdTypeDef classToken); + HRESULT HasStackTraceHiddenAttribute(ModuleID moduleId, mdToken token, bool& hasAttribute); + // A wrapper around HasStackTraceHiddenAttribute to ensure consistent behavior when checking for the attribute + // encounters errors. + bool ShouldHideFromStackTrace(ModuleID moduleId, mdToken token); private: ComPtr _profilerInfo; }; diff --git a/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.cpp b/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.cpp index 5f148ee4520..84e98a6739a 100644 --- a/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.cpp +++ b/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.cpp @@ -46,6 +46,7 @@ HRESULT StacksEventProvider::WriteClassData(ClassID classId, const ClassData& cl static_cast(classData.GetModuleId()), classData.GetToken(), static_cast(classData.GetFlags()), + classData.GetStackTraceHidden(), classData.GetTypeArgs()); } @@ -57,6 +58,7 @@ HRESULT StacksEventProvider::WriteFunctionData(FunctionID functionId, const Func static_cast(functionData.GetClass()), functionData.GetClassToken(), static_cast(functionData.GetModuleId()), + functionData.GetStackTraceHidden(), functionData.GetName(), functionData.GetTypeArgs(), functionData.GetParameterTypes()); @@ -72,7 +74,13 @@ HRESULT StacksEventProvider::WriteModuleData(ModuleID moduleId, const ModuleData HRESULT StacksEventProvider::WriteTokenData(ModuleID moduleId, mdTypeDef typeDef, const TokenData& tokenData) { - return _tokenEvent->WritePayload(moduleId, typeDef, tokenData.GetOuterToken(), tokenData.GetName(), tokenData.GetNamespace()); + return _tokenEvent->WritePayload( + moduleId, + typeDef, + tokenData.GetOuterToken(), + tokenData.GetStackTraceHidden(), + tokenData.GetName(), + tokenData.GetNamespace()); } HRESULT StacksEventProvider::WriteEndEvent() diff --git a/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.h b/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.h index e2d6af6ce45..8f0d527767f 100644 --- a/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.h +++ b/src/Profilers/MonitorProfiler/Stacks/StacksEventProvider.h @@ -37,20 +37,20 @@ class StacksEventProvider ComPtr _profilerInfo; std::unique_ptr _provider; - + const WCHAR* CallstackPayloads[4] = { _T("ThreadId"), _T("ThreadName"), _T("FunctionIds"), _T("IpOffsets")}; std::unique_ptr, std::vector>> _callstackEvent; //Note we will either send a ClassId or a ClassToken. For Shared generic functions, there is no ClassID. - const WCHAR* FunctionPayloads[8] = { _T("FunctionId"), _T("MethodToken"), _T("ClassId"), _T("ClassToken"), _T("ModuleId"), _T("Name"), _T("TypeArgs"), _T("ParameterTypes") }; - std::unique_ptr, std::vector>> _functionEvent; + const WCHAR* FunctionPayloads[9] = { _T("FunctionId"), _T("MethodToken"), _T("ClassId"), _T("ClassToken"), _T("ModuleId"), _T("StackTraceHidden"), _T("Name"), _T("TypeArgs"), _T("ParameterTypes") }; + std::unique_ptr, std::vector>> _functionEvent; //We cannot retrieve detailed information for some ClassIds. Flags is used to indicate these conditions. - const WCHAR* ClassPayloads[5] = { _T("ClassId"), _T("ModuleId"), _T("Token"), _T("Flags"), _T("TypeArgs") }; - std::unique_ptr>> _classEvent; + const WCHAR* ClassPayloads[6] = { _T("ClassId"), _T("ModuleId"), _T("Token"), _T("Flags"), _T("StackTraceHidden"), _T("TypeArgs") }; + std::unique_ptr>> _classEvent; - const WCHAR* TokenPayloads[5] = { _T("ModuleId"), _T("Token"), _T("OuterToken"), _T("Name"), _T("Namespace") }; - std::unique_ptr> _tokenEvent; + const WCHAR* TokenPayloads[6] = { _T("ModuleId"), _T("Token"), _T("OuterToken"), _T("StackTraceHidden"), _T("Name"), _T("Namespace") }; + std::unique_ptr> _tokenEvent; const WCHAR* ModulePayloads[3] = { _T("ModuleId"), _T("ModuleVersionId"), _T("Name") }; std::unique_ptr> _moduleEvent; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/BackgroundService/BackgroundServiceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/BackgroundService/BackgroundServiceTests.cs index bd4be15f272..e7cb23f53ec 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/BackgroundService/BackgroundServiceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/BackgroundService/BackgroundServiceTests.cs @@ -52,7 +52,7 @@ public async Task Stop_TriggersCancellation() await Assert.ThrowsAnyAsync(() => service.ExecutingTask); } - [Fact] + [Fact(Skip = "Flaky, the background service only waits up to 1 second when stopping")] public async Task Stop_WaitsForTheBackgroundTask() { // Arrange diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventListener.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventListener.cs index c9444ab823d..66304b0e10b 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventListener.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventListener.cs @@ -61,7 +61,8 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) ToUInt32(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.Token]), ToUInt64(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.ModuleId]), (ClassFlags)ToUInt32(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.Flags]), - ToArray(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.TypeArgs]))); + ToArray(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.TypeArgs]), + ToBool(eventData.Payload[NameIdentificationEvents.ClassDescPayloads.StackTraceHidden]))); break; case ExceptionEvents.EventIds.FunctionDescription: NameCache.FunctionData.TryAdd( @@ -73,7 +74,8 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) ToUInt32(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.ClassToken]), ToUInt64(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.ModuleId]), ToArray(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.TypeArgs]), - ToArray(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.ParameterTypes]))); + ToArray(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.ParameterTypes]), + ToBool(eventData.Payload[NameIdentificationEvents.FunctionDescPayloads.StackTraceHidden]))); break; case ExceptionEvents.EventIds.ModuleDescription: NameCache.ModuleData.TryAdd( @@ -99,7 +101,8 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) new TokenData( ToString(eventData.Payload[NameIdentificationEvents.TokenDescPayloads.Name]), ToString(eventData.Payload[NameIdentificationEvents.TokenDescPayloads.Namespace]), - ToUInt32(eventData.Payload[NameIdentificationEvents.TokenDescPayloads.OuterToken]))); + ToUInt32(eventData.Payload[NameIdentificationEvents.TokenDescPayloads.OuterToken]), + ToBool(eventData.Payload[NameIdentificationEvents.TokenDescPayloads.StackTraceHidden]))); break; default: throw new NotSupportedException(); @@ -107,6 +110,11 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) } } + private static bool ToBool(object? value) + { + return Convert.ToBoolean(ToType(value)); + } + private static Guid ToGuid(object? value) { return ToType(value); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventSourceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventSourceTests.cs index 2fbf7109167..bb487938066 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventSourceTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.StartupHook.UnitTests/Exceptions/Eventing/ExceptionsEventSourceTests.cs @@ -143,14 +143,15 @@ public void ExceptionsEventSource_WriteStackFrame_Event(ulong id, ulong methodId } [Theory] - [InlineData(0, 0, 0, 0, 0, "", new ulong[0], new ulong[0])] - [InlineData(1, 100663639, 128, 256, 512, "ThrowObjectDisposedException", new ulong[1] { 1024 }, new ulong[2] { 2048, 4096 })] + [InlineData(0, 0, 0, 0, 0, true, "", new ulong[0], new ulong[0])] + [InlineData(1, 100663639, 128, 256, 512, false, "ThrowObjectDisposedException", new ulong[1] { 1024 }, new ulong[2] { 2048, 4096 })] public void ExceptionsEventSource_WriteFunction_Event( ulong functionId, uint methodToken, ulong classId, uint classToken, ulong moduleId, + bool stackTraceHidden, string name, ulong[] typeArgs, ulong[] parameterTypes) @@ -160,7 +161,7 @@ public void ExceptionsEventSource_WriteFunction_Event( using ExceptionsEventListener listener = new(); listener.EnableEvents(source, EventLevel.Informational); - source.FunctionDescription(functionId, methodToken, classId, classToken, moduleId, name, typeArgs, parameterTypes); + source.FunctionDescription(functionId, methodToken, classId, classToken, moduleId, Convert.ToUInt32(stackTraceHidden), name, typeArgs, parameterTypes); Assert.True(listener.NameCache.FunctionData.TryGetValue(functionId, out FunctionData? function)); Assert.Equal(methodToken, function.MethodToken); @@ -168,6 +169,7 @@ public void ExceptionsEventSource_WriteFunction_Event( Assert.Equal(classToken, function.ParentClassToken); Assert.Equal(moduleId, function.ModuleId); Assert.Equal(name, function.Name); + Assert.Equal(stackTraceHidden, function.StackTraceHidden); // We would normally expect the following to return an array of the stack frame IDs // but in-process listener doesn't decode non-byte arrays correctly. Assert.Equal(Array.Empty(), function.TypeArgs); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/RetryUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/RetryUtilities.cs index d44ac1fc63d..b736d90e69f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/RetryUtilities.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/RetryUtilities.cs @@ -27,7 +27,7 @@ public static void Retry(Action func, Func shouldRetry, ITestOu } } - public static async Task RetryAsync(Func func, Func shouldRetry, ITestOutputHelper outputHelper, int maxRetryCount = 3) + public static async Task RetryAsync(Func> func, Func shouldRetry, ITestOutputHelper outputHelper, int maxRetryCount = 3) { int attemptIteration = 0; while (true) @@ -36,13 +36,19 @@ public static async Task RetryAsync(Func func, Func shoul outputHelper.WriteLine("===== Attempt #{0} =====", attemptIteration); try { - await func(); - break; + return await func(); } catch (Exception ex) when (attemptIteration < maxRetryCount && shouldRetry(ex)) { } } } + + public static async Task RetryAsync(Func func, Func shouldRetry, ITestOutputHelper outputHelper, int maxRetryCount = 3) + => await RetryAsync(async () => + { + await func(); + return null; + }, shouldRetry, outputHelper, maxRetryCount); } } 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/EgressTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs index 8ff9160aa35..4341bab18e4 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs @@ -235,7 +235,7 @@ await ScenarioRunner.SingleTarget( int processId = await appRunner.ProcessIdTask; OperationResponse response1 = await EgressTraceWithDelay(apiClient, processId); - OperationResponse response3 = await EgressTraceWithDelay(apiClient, processId); + OperationResponse response2 = await EgressTraceWithDelay(apiClient, processId); using HttpResponseMessage traceDirect1 = await TraceWithDelay(apiClient, processId); Assert.Equal(HttpStatusCode.OK, traceDirect1.StatusCode); @@ -253,10 +253,10 @@ await ScenarioRunner.SingleTarget( Assert.Equal(await egressDirect.Content.ReadAsStringAsync(), await traceDirect.Content.ReadAsStringAsync()); await CancelEgressOperation(apiClient, response1); - OperationResponse response4 = await EgressTraceWithDelay(apiClient, processId, delay: false); + OperationResponse response3 = await EgressTraceWithDelay(apiClient, processId, delay: false); + await CancelEgressOperation(apiClient, response2); await CancelEgressOperation(apiClient, response3); - await CancelEgressOperation(apiClient, response4); await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }, @@ -426,9 +426,13 @@ await ScenarioRunner.SingleTarget( }); } - private static async Task TraceWithDelay(ApiClient client, int processId, bool delay = true) + private async Task TraceWithDelay(ApiClient client, int processId, bool delay = true) { - HttpResponseMessage message = await client.ApiCall(FormattableString.Invariant($"/trace?pid={processId}&durationSeconds=-1")); + HttpResponseMessage message = await RetryUtilities.RetryAsync( + func: () => client.ApiCall(FormattableString.Invariant($"/trace?pid={processId}&durationSeconds=-1")), + shouldRetry: IsTransientApiFailure, + outputHelper: _outputHelper); + if (delay) { await Task.Delay(TimeSpan.FromSeconds(1)); @@ -436,17 +440,22 @@ private static async Task TraceWithDelay(ApiClient client, return message; } - private static Task EgressDirect(ApiClient client, int processId) + private async Task EgressDirect(ApiClient client, int processId) { - return client.ApiCall(FormattableString.Invariant($"/trace?pid={processId}&egressProvider={FileProviderName}")); + return await RetryUtilities.RetryAsync( + func: () => client.ApiCall(FormattableString.Invariant($"/trace?pid={processId}&egressProvider={FileProviderName}")), + shouldRetry: IsTransientApiFailure, + outputHelper: _outputHelper); } - private static async Task EgressTraceWithDelay(ApiClient apiClient, int processId, bool delay = true) + private async Task EgressTraceWithDelay(ApiClient apiClient, int processId, bool delay = true) { try { - OperationResponse response = await apiClient.EgressTraceAsync(processId, durationSeconds: -1, FileProviderName); - return response; + return await RetryUtilities.RetryAsync( + func: () => apiClient.EgressTraceAsync(processId, durationSeconds: -1, FileProviderName), + shouldRetry: IsTransientApiFailure, + outputHelper: _outputHelper); } finally { @@ -473,6 +482,11 @@ private static void ValidateOperation(OperationStatus expected, OperationSummary Assert.Equal(expected.IsStoppable, summary.IsStoppable); } + // When the process could not be found (due to transient responsiveness issues), dotnet-monitor APIs will return a 400 status code. + private static bool IsTransientApiFailure(Exception ex) + => ex is ValidationProblemDetailsException validationException + && validationException.StatusCode == HttpStatusCode.BadRequest; + public void Dispose() { _tempDirectory.Dispose(); 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 a78f444cdd2..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,48 +556,65 @@ 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, TypeName = ExpectedClass, - MethodName = ExpectedCallbackFunction, + MethodNameWithGenericArgTypes = ExpectedCallbackFunction, }, new WebApi.Models.CallStackFrame { ModuleName = NativeFrame, TypeName = NativeFrame, - MethodName = NativeFrame, + MethodNameWithGenericArgTypes = NativeFrame, }, new WebApi.Models.CallStackFrame { ModuleName = ExpectedModule, TypeName = ExpectedClass, - MethodName = ExpectedFunction, + MethodNameWithGenericArgTypes = ExpectedFunction, } }; } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTestCommon/EndpointUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTestCommon/EndpointUtilities.cs index dd3ff579421..af62bbdff85 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTestCommon/EndpointUtilities.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTestCommon/EndpointUtilities.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using System; @@ -72,9 +73,21 @@ public async Task StartServerAsync(EndpointInfoSourceCallbac .Setup(factory => factory.CreateScope()) .Returns(serviceScopeMock.Object); - ServerEndpointInfoSource source = new(scopeFactoryMock.Object, portOptions, callbacks, operationTrackerService); + IServerEndpointStateChecker endpointChecker = new ServerEndpointStateChecker(operationTrackerService); - await source.StartAsync(CancellationToken.None); + ServerEndpointTracker endpointTracker = new ServerEndpointTracker(endpointChecker, portOptions); + + ServerEndpointInfoSource source = new( + scopeFactoryMock.Object, + endpointTracker, + portOptions, + NullLogger.Instance, + callbacks); + + await Task.WhenAll( + endpointTracker.StartAsync(CancellationToken.None), + source.StartAsync(CancellationToken.None) + ); return new ServerSourceHolder(source, transportName); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfo/ServerEndpointTrackerV2Tests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfo/ServerEndpointTrackerV2Tests.cs new file mode 100644 index 00000000000..f0827c27df4 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EndpointInfo/ServerEndpointTrackerV2Tests.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; +using Microsoft.Extensions.Options; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests.EndpointInfo +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + public sealed class ServerEndpointTrackerV2Tests + { + private readonly MockTimeProvider _timeProvider = new(); + private readonly IOptions _diagPortOptions = Extensions.Options.Options.Create(new DiagnosticPortOptions() + { + ConnectionMode = DiagnosticPortConnectionMode.Listen + }); + + private static IServerEndpointStateChecker CreateStateChecker(params ServerEndpointState[] stateCheckResults) + { + Assert.NotEmpty(stateCheckResults); + Mock stateChecker = new(); + var sequence = stateChecker.SetupSequence(s => s.GetEndpointStateAsync(It.IsAny(), It.IsAny())); + + for (int i = 0; i < stateCheckResults.Length; i++) + { + sequence.ReturnsAsync(stateCheckResults[i]); + } + + sequence.ThrowsAsync(new InvalidOperationException("End of test")); + + return stateChecker.Object; + } + + private ServerEndpointTrackerV2 CreateTracker(ServerEndpointState stateCheckResult = ServerEndpointState.Active) + => new ServerEndpointTrackerV2(CreateStateChecker(stateCheckResult), _timeProvider, _diagPortOptions); + + private ServerEndpointTrackerV2 CreateTracker(IServerEndpointStateChecker stateChecker) + => new ServerEndpointTrackerV2(stateChecker, _timeProvider, _diagPortOptions); + + + [Fact] + public async Task Add_RegistersEndpoint() + { + // Arrange + using CancellationTokenSource cts = new CancellationTokenSource(CommonTestTimeouts.GeneralTimeout); + using ServerEndpointTrackerV2 tracker = CreateTracker(); + IEndpointInfo endpoint = Mock.Of(); + + // Act + await tracker.AddAsync(endpoint, cts.Token); + + // Assert + IEnumerable activeEndpoints = await tracker.GetEndpointInfoAsync(cts.Token); + IEndpointInfo activeEndpoint = Assert.Single(activeEndpoints); + Assert.Equal(endpoint, activeEndpoint); + } + + [Fact] + public async Task PruneEndpointsAsync_Prunes_ErrorEndpoint() + { + // Arrange + using CancellationTokenSource cts = new CancellationTokenSource(CommonTestTimeouts.GeneralTimeout); + using ServerEndpointTrackerV2 tracker = CreateTracker(ServerEndpointState.Error); + + IEndpointInfo endpoint = Mock.Of(); + await tracker.AddAsync(endpoint, cts.Token); + + EndpointRemovedEventArgs endpointRemovedArgs = null; + tracker.EndpointRemoved += (_, args) => endpointRemovedArgs = args; + + // Act + await tracker.PruneEndpointsAsync(cts.Token); + + // Assert + IEnumerable activeEndpoints = await tracker.GetEndpointInfoAsync(cts.Token); + Assert.Empty(activeEndpoints); + + Assert.NotNull(endpointRemovedArgs); + Assert.Equal(ServerEndpointState.Error, endpointRemovedArgs.State); + Assert.Equal(endpoint, endpointRemovedArgs.Endpoint); + } + + [Fact] + public async Task PruneEndpointsAsync_DoesNotPrune_ActiveEndpoint() + { + // Arrange + using CancellationTokenSource cts = new CancellationTokenSource(CommonTestTimeouts.GeneralTimeout); + using ServerEndpointTrackerV2 tracker = CreateTracker(ServerEndpointState.Active); + await tracker.AddAsync(Mock.Of(), cts.Token); + + EndpointRemovedEventArgs endpointRemovedArgs = null; + tracker.EndpointRemoved += (_, args) => endpointRemovedArgs = args; + + // Act + await tracker.PruneEndpointsAsync(cts.Token); + + // Assert + IEnumerable activeEndpoints = await tracker.GetEndpointInfoAsync(cts.Token); + Assert.NotEmpty(activeEndpoints); + + Assert.Null(endpointRemovedArgs); + } + + [Fact] + public async Task PruneEndpointsAsync_DoesNotPrune_TransientlyUnresponsiveEndpoint() + { + // Arrange + ServerEndpointState[] states = [ServerEndpointState.Active, ServerEndpointState.Unresponsive, ServerEndpointState.Active]; + + using CancellationTokenSource cts = new CancellationTokenSource(CommonTestTimeouts.GeneralTimeout); + using ServerEndpointTrackerV2 tracker = CreateTracker(CreateStateChecker(states)); + + IEndpointInfo endpoint = Mock.Of(); + await tracker.AddAsync(endpoint, cts.Token); + + EndpointRemovedEventArgs endpointRemovedArgs = null; + tracker.EndpointRemoved += (_, args) => endpointRemovedArgs = args; + + // Act & Assert + for (int i = 0; i < states.Length; i++) + { + _timeProvider.Increment(ServerEndpointTrackerV2.UnresponsiveGracePeriod / 2); + await tracker.PruneEndpointsAsync(cts.Token); + + IEnumerable activeEndpoints = await tracker.GetEndpointInfoAsync(cts.Token); + IEndpointInfo activeEndpoint = Assert.Single(activeEndpoints); + Assert.Equal(endpoint, activeEndpoint); + } + + Assert.Null(endpointRemovedArgs); + } + + [Fact] + public async Task PruneEndpointsAsync_Prunes_UnresponsiveEndpointAfterGracePeriod() + { + // Arrange + using CancellationTokenSource cts = new CancellationTokenSource(CommonTestTimeouts.GeneralTimeout); + using ServerEndpointTrackerV2 tracker = CreateTracker(ServerEndpointState.Unresponsive); + + IEndpointInfo endpoint = Mock.Of(); + await tracker.AddAsync(endpoint, cts.Token); + + EndpointRemovedEventArgs endpointRemovedArgs = null; + tracker.EndpointRemoved += (_, args) => endpointRemovedArgs = args; + + _timeProvider.Increment(ServerEndpointTrackerV2.UnresponsiveGracePeriod * 2); + + // Act + await tracker.PruneEndpointsAsync(cts.Token); + + // Assert + IEnumerable activeEndpoints = await tracker.GetEndpointInfoAsync(cts.Token); + Assert.Empty(activeEndpoints); + + Assert.NotNull(endpointRemovedArgs); + Assert.Equal(ServerEndpointState.Unresponsive, endpointRemovedArgs.State); + Assert.Equal(endpoint, endpointRemovedArgs.Endpoint); + } + } +} 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/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Models/CallStackResultsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Models/CallStackResultsTests.cs new file mode 100644 index 00000000000..d8f9cbe224c --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Models/CallStackResultsTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using System; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.UnitTests.Models +{ + public class CallStackResultsTests + { + [Theory] + [InlineData("Test", "Test")] + [InlineData("Test[System.String]", "Test", "System.String")] + [InlineData("Test[System.String,System.Object]", "Test", "System.String", "System.Object")] + public void MethodNameWithGenericArgTypes_Get(string expectedName, string methodName, params string[] genericArgTypes) + { + CallStackFrame frame = new() + { + MethodName = methodName, + FullGenericArgTypes = genericArgTypes + }; + + Assert.Equal(expectedName, frame.MethodNameWithGenericArgTypes); + } + + [Theory] + [InlineData("Test", "Test")] + [InlineData("[NativeFrame]", "[NativeFrame]")] + [InlineData("Test[System.String]", "Test", "System.String")] + [InlineData("Test[System.String,System.Object]", "Test", "System.String", "System.Object")] + public void MethodNameWithGenericArgTypes_Set(string serializedName, string expectedMethodName, params string[] expectedGenericArgTypes) + { + CallStackFrame frame = new() + { + MethodNameWithGenericArgTypes = serializedName + }; + + Assert.Equal(expectedMethodName, frame.MethodName); + Assert.Equal(expectedGenericArgTypes, frame.FullGenericArgTypes); + } + + [Theory] + [InlineData("Test[")] + [InlineData("Test[System.String,System.Object")] + public void MethodNameWithGenericArgTypes_Set_InvalidInput_Throws(string serializedName) + { + CallStackFrame frame = new(); + Assert.Throws(() => frame.MethodNameWithGenericArgTypes = serializedName); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Operation/EgressOperationStoreTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Operation/EgressOperationStoreTests.cs index eed1a1e6858..544d3b97180 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Operation/EgressOperationStoreTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/Operation/EgressOperationStoreTests.cs @@ -69,7 +69,7 @@ public async Task StopOperation_OnException_InvokesCallback() // Assert Exception hitException = await exceptionHit.Task.WaitAsync(CommonTestTimeouts.GeneralTimeout); Assert.NotNull(hitException); - Assert.Equal(Models.OperationState.Stopping, store.GetOperationStatus(operationId).Status); + Assert.Equal(WebApi.Models.OperationState.Stopping, store.GetOperationStatus(operationId).Status); } [Fact(Skip = "Flaky")] @@ -96,14 +96,14 @@ public async Task CancelOperation_Supports_StoppingState() Guid operationId = await store.AddOperation(mockOperation.Object, AllowOperationKey); store.StopOperation(operationId, (ex) => { }); - Assert.Equal(Models.OperationState.Stopping, store.GetOperationStatus(operationId).Status); + Assert.Equal(WebApi.Models.OperationState.Stopping, store.GetOperationStatus(operationId).Status); // Act store.CancelOperation(operationId); // Assert await stopCancelled.Task.WaitAsync(CommonTestTimeouts.GeneralTimeout); - Assert.Equal(Models.OperationState.Cancelled, store.GetOperationStatus(operationId).Status); + Assert.Equal(WebApi.Models.OperationState.Cancelled, store.GetOperationStatus(operationId).Status); } } } diff --git a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs index b9fae5571f1..cdae1908e94 100644 --- a/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs +++ b/src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs @@ -113,9 +113,8 @@ private static IHostBuilder Configure(this IHostBuilder builder, StartupAuthenti services.ConfigureDotnetMonitorDebug(context.Configuration); - services.AddSingleton(); - services.AddSingleton(); - services.AddHostedServiceForwarder(); + services.ConfigureEndpointInfoSource(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointStateChecker.cs b/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointStateChecker.cs new file mode 100644 index 00000000000..aa322051de0 --- /dev/null +++ b/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointStateChecker.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System.Threading.Tasks; +using System.Threading; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal enum ServerEndpointState + { + Active, + Unresponsive, + Error + } + + internal interface IServerEndpointStateChecker + { + Task GetEndpointStateAsync(IEndpointInfo info, CancellationToken token); + } +} diff --git a/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointTracker.cs b/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointTracker.cs new file mode 100644 index 00000000000..d43d05649a4 --- /dev/null +++ b/src/Tools/dotnet-monitor/EndpointInfo/IServerEndpointTracker.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal record class EndpointRemovedEventArgs(IEndpointInfo Endpoint, ServerEndpointState State); + + internal interface IServerEndpointTracker + { + Task AddAsync(IEndpointInfo endpointInfo, CancellationToken token); + Task> GetEndpointInfoAsync(CancellationToken token); + + event EventHandler? EndpointRemoved; + } +} diff --git a/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs index 4a925a8faf8..4e9f9a17e8d 100644 --- a/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs +++ b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointInfoSource.cs @@ -32,29 +32,20 @@ internal sealed class ServerEndpointInfoSource : // the writer to wait for capacity to be available. private const int PendingRemovalChannelCapacity = 1000; - // The amount of time to wait when checking if the a endpoint info should be - // pruned from the list of endpoint infos. If the runtime doesn't have a viable connection within - // this time, it will be pruned from the list. - private static readonly TimeSpan PruneWaitForConnectionTimeout = TimeSpan.FromMilliseconds(250); - - // The amount of time to wait between pruning operations. - private static readonly TimeSpan PruningInterval = TimeSpan.FromSeconds(3); - - private readonly List _activeEndpoints = new(); private readonly SemaphoreSlim _activeEndpointsSemaphore = new(1); private readonly Dictionary _activeEndpointServiceScopes = new(); private readonly IServiceScopeFactory _scopeFactory; - private readonly ChannelReader _pendingRemovalReader; - private readonly ChannelWriter _pendingRemovalWriter; + private readonly ChannelReader _pendingRemovalReader; + private readonly ChannelWriter _pendingRemovalWriter; - private readonly CancellationTokenSource _cancellation = new(); private readonly IEnumerable _callbacks; private readonly DiagnosticPortOptions _portOptions; - private readonly OperationTrackerService _operationTrackerService; private readonly ILogger _logger; + private readonly IServerEndpointTracker _endpointTracker; + private long _disposalState; /// @@ -63,15 +54,15 @@ internal sealed class ServerEndpointInfoSource : /// public ServerEndpointInfoSource( IServiceScopeFactory scopeFactory, + IServerEndpointTracker endpointTracker, IOptions portOptions, - IEnumerable callbacks = null, - OperationTrackerService operationTrackerService = null, - ILogger logger = null) + ILogger logger, + IEnumerable callbacks = null) { _callbacks = callbacks ?? Enumerable.Empty(); - _operationTrackerService = operationTrackerService; _portOptions = portOptions.Value; _logger = logger; + _endpointTracker = endpointTracker; _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); BoundedChannelOptions channelOptions = new(PendingRemovalChannelCapacity) @@ -79,9 +70,11 @@ public ServerEndpointInfoSource( SingleReader = true, SingleWriter = true }; - Channel pendingRemovalChannel = Channel.CreateBounded(channelOptions); + Channel pendingRemovalChannel = Channel.CreateBounded(channelOptions); _pendingRemovalReader = pendingRemovalChannel.Reader; _pendingRemovalWriter = pendingRemovalChannel.Writer; + + _endpointTracker.EndpointRemoved += OnEndpointRemoved; } public async ValueTask DisposeAsync() @@ -92,6 +85,7 @@ public async ValueTask DisposeAsync() // Makes sure the background task is canceled. Dispose(); + _endpointTracker.EndpointRemoved -= OnEndpointRemoved; _pendingRemovalWriter.TryComplete(); // Wait for background services to complete. @@ -158,7 +152,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.WhenAll( ListenAsync(server, stoppingToken), - MonitorEndpointsAsync(stoppingToken), NotifyAndRemoveAsync(server, stoppingToken) ); } @@ -170,13 +163,10 @@ await Task.WhenAll( /// The token to monitor for cancellation requests. /// A list of active instances. public async Task> GetEndpointInfoAsync(CancellationToken token) - { - using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); + => await _endpointTracker.GetEndpointInfoAsync(token); - List validEndpoints = new(); - await PruneEndpointsAsync(validEndpoints, linkedSource.Token); - return validEndpoints; - } + private void OnEndpointRemoved(object sender, EndpointRemovedEventArgs args) + => _pendingRemovalWriter.TryWrite(args); /// /// Accepts endpoint infos from the reversed diagnostics server. @@ -276,7 +266,7 @@ private async Task ResumeAndQueueEndpointInfo(ReversedDiagnosticsServer server, await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); try { - _activeEndpoints.Add(endpointInfo); + await _endpointTracker.AddAsync(endpointInfo, token).ConfigureAwait(false); foreach (IEndpointInfoSourceCallbacks callback in _callbacks) { @@ -306,27 +296,24 @@ private async Task ResumeAndQueueEndpointInfo(ReversedDiagnosticsServer server, } } - private async Task MonitorEndpointsAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - await Task.Delay(PruningInterval, token); - - await PruneEndpointsAsync(validEndpoints: null, token); - } - } - private async Task NotifyAndRemoveAsync(ReversedDiagnosticsServer server, CancellationToken token) { while (!token.IsCancellationRequested) { - IEndpointInfo endpoint = await _pendingRemovalReader.ReadAsync(token); + EndpointRemovedEventArgs args = await _pendingRemovalReader.ReadAsync(token); + IEndpointInfo endpoint = args.Endpoint; + ServerEndpointState state = args.State; List exceptions = new(); AsyncServiceScope serviceScope; bool isServiceScopeValid = false; + if (state == ServerEndpointState.Unresponsive) + { + _logger.EndpointTimeout(endpoint.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + await _activeEndpointsSemaphore.WaitAsync(token); try { @@ -385,84 +372,6 @@ private async Task NotifyAndRemoveAsync(ReversedDiagnosticsServer server, Cancel } } - private async Task PruneEndpointsAsync(List validEndpoints, CancellationToken token) - { - // Prune connections that no longer have an active runtime instance before - // returning the list of connections. - await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); - - try - { - // Check the transport for each endpoint info and remove it if the check fails. - List> checkTasks = new(); - foreach (EndpointInfo info in _activeEndpoints) - { - checkTasks.Add(Task.Run(() => CheckEndpointAsync(info, token), token)); - } - - // Wait for all checks to complete - bool[] results = await Task.WhenAll(checkTasks).ConfigureAwait(false); - - // Remove failed endpoints from active list; record the failed endpoints - // for removal after releasing the active endpoints semaphore. - int endpointIndex = 0; - for (int resultIndex = 0; resultIndex < results.Length; resultIndex++) - { - IEndpointInfo endpoint = _activeEndpoints[endpointIndex]; - if (results[resultIndex]) - { - validEndpoints?.Add(endpoint); - endpointIndex++; - } - else - { - _activeEndpoints.RemoveAt(endpointIndex); - - await _pendingRemovalWriter.WriteAsync(endpoint, token); - } - } - } - finally - { - _activeEndpointsSemaphore.Release(); - } - } - - /// - /// Tests the endpoint to see if its connection is viable. - /// - private async Task CheckEndpointAsync(EndpointInfo info, CancellationToken token) - { - // If a dump operation is in progress, the runtime is likely to not respond to - // diagnostic requests. Do not check for responsiveness while the dump operation - // is in progress. - if (_operationTrackerService?.IsExecutingOperation(info) == true) - { - return true; - } - - using var timeoutSource = new CancellationTokenSource(); - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token); - - try - { - timeoutSource.CancelAfter(PruneWaitForConnectionTimeout); - - await info.Endpoint.WaitForConnectionAsync(linkedSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) when (timeoutSource.IsCancellationRequested) - { - _logger?.EndpointTimeout(info.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture)); - return false; - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - return false; - } - - return true; - } - private IDisposable SetupDiagnosticPortWatcher() { // If running on Windows, a named pipe is used so there is no need to watch it. diff --git a/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointStateChecker.cs b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointStateChecker.cs new file mode 100644 index 00000000000..a39fd2aaed4 --- /dev/null +++ b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointStateChecker.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal class ServerEndpointStateChecker(OperationTrackerService? operationTracker) : IServerEndpointStateChecker + { + // The amount of time to wait when checking if the a endpoint info is active. + private static readonly TimeSpan WaitForConnectionTimeout = TimeSpan.FromMilliseconds(250); + + public async Task GetEndpointStateAsync(IEndpointInfo info, CancellationToken token) + { + // If a dump operation is in progress, the runtime is likely to not respond to + // diagnostic requests. Do not check for responsiveness while the dump operation + // is in progress. + if (operationTracker?.IsExecutingOperation(info) == true) + { + return ServerEndpointState.Active; + } + + using var timeoutSource = new CancellationTokenSource(); + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token); + + try + { + timeoutSource.CancelAfter(WaitForConnectionTimeout); + + await info.Endpoint.WaitForConnectionAsync(linkedSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (timeoutSource.IsCancellationRequested) + { + return ServerEndpointState.Unresponsive; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return ServerEndpointState.Error; + } + + return ServerEndpointState.Active; + } + } +} diff --git a/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTracker.cs b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTracker.cs new file mode 100644 index 00000000000..3b022056fed --- /dev/null +++ b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTracker.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class ServerEndpointTracker(IServerEndpointStateChecker endpointChecker, IOptions portOptions) : + BackgroundService, + IServerEndpointTracker + { + // The amount of time to wait between pruning operations. + private static readonly TimeSpan PruningInterval = TimeSpan.FromSeconds(3); + + private readonly List _activeEndpoints = new(); + private readonly SemaphoreSlim _activeEndpointsSemaphore = new(1); + + private readonly CancellationTokenSource _cancellation = new(); + + private readonly DiagnosticPortOptions _portOptions = portOptions.Value; + + public event EventHandler? EndpointRemoved; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (_portOptions.ConnectionMode != DiagnosticPortConnectionMode.Listen) + { + return; + } + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(PruningInterval, stoppingToken); + + await PruneEndpointsAsync(validEndpoints: null, stoppingToken); + } + } + + /// + /// Gets the list of served from the reversed diagnostics server. + /// + /// The token to monitor for cancellation requests. + /// A list of active instances. + public async Task> GetEndpointInfoAsync(CancellationToken token) + { + using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); + + List validEndpoints = new(); + await PruneEndpointsAsync(validEndpoints, linkedSource.Token); + return validEndpoints; + } + + private async Task PruneEndpointsAsync(List? validEndpoints, CancellationToken token) + { + // Prune connections that no longer have an active runtime instance before + // returning the list of connections. + await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); + + try + { + // Check the transport for each endpoint info and remove it if the check fails. + List> checkTasks = new(); + foreach (EndpointInfo info in _activeEndpoints) + { + checkTasks.Add(Task.Run(() => endpointChecker.GetEndpointStateAsync(info, token), token)); + } + + // Wait for all checks to complete + ServerEndpointState[] states = await Task.WhenAll(checkTasks).ConfigureAwait(false); + + // Remove failed endpoints from active list; record the failed endpoints + // for removal after releasing the active endpoints semaphore. + int endpointIndex = 0; + for (int resultIndex = 0; resultIndex < states.Length; resultIndex++) + { + IEndpointInfo endpoint = _activeEndpoints[endpointIndex]; + ServerEndpointState state = states[resultIndex]; + + if (state == ServerEndpointState.Active) + { + validEndpoints?.Add(endpoint); + endpointIndex++; + } + else + { + _activeEndpoints.RemoveAt(endpointIndex); + EndpointRemoved?.Invoke(this, new(endpoint, state)); + } + } + } + finally + { + _activeEndpointsSemaphore.Release(); + } + } + + public async Task AddAsync(IEndpointInfo endpointInfo, CancellationToken token) + { + await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); + try + { + _activeEndpoints.Add(endpointInfo); + } + finally + { + _activeEndpointsSemaphore.Release(); + } + } + } +} diff --git a/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTrackerv2.cs b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTrackerv2.cs new file mode 100644 index 00000000000..b19c496e0f0 --- /dev/null +++ b/src/Tools/dotnet-monitor/EndpointInfo/ServerEndpointTrackerv2.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal sealed class ServerEndpointTrackerV2(IServerEndpointStateChecker endpointChecker, TimeProvider timeProvider, IOptions portOptions) : + BackgroundService, + IServerEndpointTracker + { + private record class ActiveEndpoint(IEndpointInfo Endpoint, DateTimeOffset LastContact) + { + // LastContact should be mutable + public DateTimeOffset LastContact { get; set; } = LastContact; + } + + private static readonly TimeSpan PruningInterval = TimeSpan.FromSeconds(3); + // Internal for testing + internal static readonly TimeSpan UnresponsiveGracePeriod = TimeSpan.FromMinutes(1); + + + private readonly List _activeEndpoints = new(); + private readonly SemaphoreSlim _activeEndpointsSemaphore = new(1); + + private readonly DiagnosticPortOptions _portOptions = portOptions.Value; + + public event EventHandler? EndpointRemoved; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (_portOptions.ConnectionMode != DiagnosticPortConnectionMode.Listen) + { + return; + } + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(PruningInterval, stoppingToken); + + await PruneEndpointsAsync(stoppingToken); + } + } + + public async Task> GetEndpointInfoAsync(CancellationToken token) + { + await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); + try + { + return _activeEndpoints.Select(a => a.Endpoint); + } + finally + { + _activeEndpointsSemaphore.Release(); + } + } + + /// + /// Internal for testing. Do not directly call outside of tests or from this class. + /// + internal async Task PruneEndpointsAsync(CancellationToken token) + { + await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); + + try + { + // Check the transport for each endpoint info and remove it if the check fails. + List> checkTasks = []; + foreach (ActiveEndpoint activeEndpoint in _activeEndpoints) + { + checkTasks.Add(Task.Run(() => endpointChecker.GetEndpointStateAsync(activeEndpoint.Endpoint, token), token)); + } + + // Wait for all checks to complete + ServerEndpointState[] states = await Task.WhenAll(checkTasks).ConfigureAwait(false); + + // Remove failed endpoints from active list; record the failed endpoints + // for removal after releasing the active endpoints semaphore. + int endpointIndex = 0; + + void removeEndpoint(IEndpointInfo endpoint, ServerEndpointState state) + { + _activeEndpoints.RemoveAt(endpointIndex); + EndpointRemoved?.Invoke(this, new(endpoint, state)); + } + + DateTimeOffset now = timeProvider.GetUtcNow(); + + for (int resultIndex = 0; resultIndex < states.Length; resultIndex++) + { + ActiveEndpoint activeEndpoint = _activeEndpoints[endpointIndex]; + ServerEndpointState state = states[resultIndex]; + + switch (state) + { + case ServerEndpointState.Active: + activeEndpoint.LastContact = now; + endpointIndex++; + break; + + case ServerEndpointState.Unresponsive: + if (now - activeEndpoint.LastContact > UnresponsiveGracePeriod) + { + removeEndpoint(activeEndpoint.Endpoint, state); + } + else + { + endpointIndex++; + } + break; + + case ServerEndpointState.Error: + removeEndpoint(activeEndpoint.Endpoint, state); + break; + } + } + } + finally + { + _activeEndpointsSemaphore.Release(); + } + } + + public async Task AddAsync(IEndpointInfo endpointInfo, CancellationToken token) + { + await _activeEndpointsSemaphore.WaitAsync(token).ConfigureAwait(false); + try + { + _activeEndpoints.Add(new(endpointInfo, timeProvider.GetUtcNow())); + } + finally + { + _activeEndpointsSemaphore.Release(); + } + } + } +} diff --git a/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipeline.cs b/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipeline.cs index d302e5d6c02..cd3f76addcc 100644 --- a/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipeline.cs +++ b/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipeline.cs @@ -63,7 +63,8 @@ private void Callback(TraceEvent traceEvent) traceEvent.GetPayload(NameIdentificationEvents.ClassDescPayloads.Token), traceEvent.GetPayload(NameIdentificationEvents.ClassDescPayloads.ModuleId), traceEvent.GetPayload(NameIdentificationEvents.ClassDescPayloads.Flags), - traceEvent.GetPayload(NameIdentificationEvents.ClassDescPayloads.TypeArgs) + traceEvent.GetPayload(NameIdentificationEvents.ClassDescPayloads.TypeArgs), + traceEvent.GetBoolPayload(NameIdentificationEvents.ClassDescPayloads.StackTraceHidden) ); break; case "ExceptionGroup": @@ -100,7 +101,8 @@ private void Callback(TraceEvent traceEvent) traceEvent.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ModuleId), traceEvent.GetPayload(NameIdentificationEvents.FunctionDescPayloads.Name), traceEvent.GetPayload(NameIdentificationEvents.FunctionDescPayloads.TypeArgs), - traceEvent.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ParameterTypes) + traceEvent.GetPayload(NameIdentificationEvents.FunctionDescPayloads.ParameterTypes), + traceEvent.GetBoolPayload(NameIdentificationEvents.FunctionDescPayloads.StackTraceHidden) ); break; case "ModuleDescription": @@ -123,7 +125,8 @@ private void Callback(TraceEvent traceEvent) traceEvent.GetPayload(NameIdentificationEvents.TokenDescPayloads.Token), traceEvent.GetPayload(NameIdentificationEvents.TokenDescPayloads.OuterToken), traceEvent.GetPayload(NameIdentificationEvents.TokenDescPayloads.Name), - traceEvent.GetPayload(NameIdentificationEvents.TokenDescPayloads.Namespace) + traceEvent.GetPayload(NameIdentificationEvents.TokenDescPayloads.Namespace), + traceEvent.GetBoolPayload(NameIdentificationEvents.TokenDescPayloads.StackTraceHidden) ); break; case "Flush": diff --git a/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipelineNameCache.cs b/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipelineNameCache.cs index 5cb3169509a..667d1446064 100644 --- a/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipelineNameCache.cs +++ b/src/Tools/dotnet-monitor/Exceptions/EventExceptionsPipelineNameCache.cs @@ -16,9 +16,9 @@ internal sealed class EventExceptionsPipelineNameCache : IExceptionsNameCache public NameCache NameCache => _nameCache; - public void AddClass(ulong id, uint token, ulong moduleId, ClassFlags flags, ulong[] typeArgs) + public void AddClass(ulong id, uint token, ulong moduleId, ClassFlags flags, ulong[] typeArgs, bool stackTraceHidden) { - _nameCache.ClassData.TryAdd(id, new ClassData(token, moduleId, flags, typeArgs ?? Array.Empty())); + _nameCache.ClassData.TryAdd(id, new ClassData(token, moduleId, flags, typeArgs, stackTraceHidden)); } public void AddExceptionGroup(ulong id, ulong exceptionClassId, ulong throwingMethodId, int ilOffset) @@ -26,9 +26,9 @@ public void AddExceptionGroup(ulong id, ulong exceptionClassId, ulong throwingMe _exceptionGroupMap.Add(id, new ExceptionGroup(exceptionClassId, throwingMethodId, ilOffset)); } - public void AddFunction(ulong id, uint methodToken, ulong classId, uint classToken, ulong moduleId, string name, ulong[] typeArgs, ulong[] parameterTypes) + public void AddFunction(ulong id, uint methodToken, ulong classId, uint classToken, ulong moduleId, string name, ulong[] typeArgs, ulong[] parameterTypes, bool stackTraceHidden) { - _nameCache.FunctionData.TryAdd(id, new FunctionData(name, methodToken, classId, classToken, moduleId, typeArgs ?? Array.Empty(), parameterTypes ?? Array.Empty())); + _nameCache.FunctionData.TryAdd(id, new FunctionData(name, methodToken, classId, classToken, moduleId, typeArgs, parameterTypes, stackTraceHidden)); } public void AddStackFrame(ulong id, ulong functionId, int ilOffset) @@ -41,11 +41,11 @@ public void AddModule(ulong id, Guid moduleVersionId, string moduleName) _nameCache.ModuleData.TryAdd(id, new ModuleData(moduleName, moduleVersionId)); } - public void AddToken(ulong moduleId, uint token, uint outerToken, string name, string @namespace) + public void AddToken(ulong moduleId, uint token, uint outerToken, string name, string @namespace, bool stackTraceHidden) { _nameCache.TokenData.TryAdd( new ModuleScopedToken(moduleId, token), - new TokenData(name, @namespace, outerToken)); + new TokenData(name, @namespace, outerToken, stackTraceHidden)); } public bool TryGetExceptionGroup(ulong groupId, out ulong exceptionClassId, out ulong throwingMethodId, out int ilOffset) diff --git a/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs b/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs index 257d8f43256..d711f962139 100644 --- a/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs +++ b/src/Tools/dotnet-monitor/Exceptions/ExceptionsOperation.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Utils = Microsoft.Diagnostics.Monitoring.WebApi.Utilities; +using Models = Microsoft.Diagnostics.Monitoring.WebApi.Models; using NameFormatter = Microsoft.Diagnostics.Monitoring.WebApi.Stacks.NameFormatter; namespace Microsoft.Diagnostics.Tools.Monitor.Exceptions @@ -25,9 +26,6 @@ internal sealed class ExceptionsOperation : IArtifactOperation private static byte[] JsonSequenceRecordSeparator = new byte[] { 0x1E }; - private const char GenericSeparator = ','; - private const char GenericStart = '['; - private const char GenericEnd = ']'; private readonly ExceptionsConfigurationSettings _configuration; private readonly IEndpointInfo _endpointInfo; @@ -141,81 +139,28 @@ private async Task WriteJsonInstance(Stream stream, IExceptionInstance instance, await stream.WriteAsync(JsonSequenceRecordSeparator, token); } - // Make sure dotnet-monitor is self-consistent with other features that print type and stack information. - // For example, the stacks and exceptions features should print structured stack traces exactly the same way. - // CONSIDER: Investigate if other tools have "standard" formats for printing structured stacks and exceptions. - await using (Utf8JsonWriter writer = new(stream, new JsonWriterOptions() { Indented = false })) + Models.ExceptionInstance model = new() { - writer.WriteStartObject(); - writer.WriteNumber("id", instance.Id); - // Writes the timestamp in ISO 8601 format - writer.WriteString("timestamp", instance.Timestamp); - writer.WriteString("typeName", instance.TypeName); - writer.WriteString("moduleName", instance.ModuleName); - writer.WriteString("message", instance.Message); - - if (IncludeActivityId(instance)) - { - writer.WriteStartObject("activity"); - writer.WriteString("id", instance.ActivityId); - writer.WriteString("idFormat", instance.ActivityIdFormat.ToString("G")); - writer.WriteEndObject(); - } - - writer.WriteStartArray("innerExceptions"); - foreach (ulong innerExceptionId in instance.InnerExceptionIds) - { - writer.WriteStartObject(); - writer.WriteNumber("id", innerExceptionId); - writer.WriteEndObject(); - } - writer.WriteEndArray(); - - if (null != instance.CallStack) + Id = instance.Id, + Timestamp = instance.Timestamp, + TypeName = instance.TypeName, + ModuleName = instance.ModuleName, + Message = instance.Message, + InnerExceptionIds = Array.ConvertAll(instance.InnerExceptionIds, id => (InnerExceptionId)id), + CallStack = instance.CallStack, + }; + + if (IncludeActivityId(instance)) + { + model.Activity = new() { - writer.WriteStartObject("stack"); - writer.WriteNumber("threadId", instance.CallStack.ThreadId); - writer.WriteString("threadName", instance.CallStack.ThreadName); - - writer.WriteStartArray("frames"); - - StringBuilder builder = new StringBuilder(); - - foreach (var frame in instance.CallStack.Frames) - { - writer.WriteStartObject(); - - string assembledMethodName = frame.MethodName; - if (frame.FullGenericArgTypes.Count > 0) - { - builder.Clear(); - builder.Append(GenericStart); - builder.Append(string.Join(GenericSeparator, frame.FullGenericArgTypes)); - builder.Append(GenericEnd); - assembledMethodName += builder.ToString(); - } - writer.WriteString("methodName", assembledMethodName); - writer.WriteNumber("methodToken", frame.MethodToken); - writer.WriteStartArray("parameterTypes"); - foreach (string parameterType in frame.FullParameterTypes) - { - writer.WriteStringValue(parameterType); - } - writer.WriteEndArray(); // end parameterTypes - writer.WriteString("typeName", frame.TypeName); - writer.WriteString("moduleName", frame.ModuleName); - writer.WriteString("moduleVersionId", frame.ModuleVersionId.ToString("D")); - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); // end frames - writer.WriteEndObject(); // end callStack - } - - writer.WriteEndObject(); // end. + Id = instance.ActivityId, + IdFormat = instance.ActivityIdFormat + }; } + await JsonSerializer.SerializeAsync(stream, model, cancellationToken: token); + await stream.WriteAsync(JsonRecordDelimiter, token); } @@ -311,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); @@ -321,7 +271,7 @@ private static async Task WriteTextInnerExceptionsAndStackFrames(TextWriter writ await writer.WriteAsync(builder); builder.Clear(); - NameFormatter.BuildMethodParameterTypes(builder, frame.SimpleParameterTypes); + NameFormatter.BuildMethodParameterTypes(builder, frame.SimpleParameterTypes ?? []); await writer.WriteAsync(builder); builder.Clear(); } diff --git a/src/Tools/dotnet-monitor/Exceptions/ExceptionsStore.cs b/src/Tools/dotnet-monitor/Exceptions/ExceptionsStore.cs index e1849aa2330..ad04893cc72 100644 --- a/src/Tools/dotnet-monitor/Exceptions/ExceptionsStore.cs +++ b/src/Tools/dotnet-monitor/Exceptions/ExceptionsStore.cs @@ -220,7 +220,7 @@ private async Task ProcessEntriesAsync(CancellationToken token) } } - return StackUtilities.TranslateCallStackToModel(callStack, cache.NameCache, methodNameIncludesGenericParameters: false); + return StackUtilities.TranslateCallStackToModel(callStack, cache.NameCache); } private sealed class ExceptionInstanceEntry diff --git a/src/Tools/dotnet-monitor/Experimental/ExperimentalFeatureIdentifiers.cs b/src/Tools/dotnet-monitor/Experimental/ExperimentalFeatureIdentifiers.cs new file mode 100644 index 00000000000..7b2f08bf644 --- /dev/null +++ b/src/Tools/dotnet-monitor/Experimental/ExperimentalFeatureIdentifiers.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.Tools.Monitor +{ + internal static class ExperimentalFeatureIdentifiers + { + public static class EnvironmentVariables + { + private const string Prefix = ToolIdentifiers.StandardPrefix + "Experimental_"; + + public const string ServerEndpointPruningAlgorithmV2 = Prefix + nameof(ServerEndpointPruningAlgorithmV2); + } + } +} diff --git a/src/Tools/dotnet-monitor/Experimental/ExperimentalStartupLogger.cs b/src/Tools/dotnet-monitor/Experimental/ExperimentalStartupLogger.cs index 0a69300b6d5..406b57c00eb 100644 --- a/src/Tools/dotnet-monitor/Experimental/ExperimentalStartupLogger.cs +++ b/src/Tools/dotnet-monitor/Experimental/ExperimentalStartupLogger.cs @@ -11,12 +11,17 @@ internal sealed class ExperimentalStartupLogger : IStartupLogger { private readonly ILogger _logger; - private ParameterCapturingOptions _parameterCapturingOptions; + private readonly ParameterCapturingOptions _parameterCapturingOptions; + private readonly ServerEndpointTrackerV2? _serverEndpointTrackerV2; - public ExperimentalStartupLogger(ILogger logger, IOptions parameterCapturingOptions) + public ExperimentalStartupLogger( + ILogger logger, + IOptions parameterCapturingOptions, + ServerEndpointTrackerV2? serverEndpointTrackerV2 = null) { _logger = logger; _parameterCapturingOptions = parameterCapturingOptions.Value; + _serverEndpointTrackerV2 = serverEndpointTrackerV2; } public void Log() @@ -26,6 +31,11 @@ public void Log() _logger.ExperimentalFeatureEnabled(Microsoft.Diagnostics.Monitoring.WebApi.Strings.FeatureName_ParameterCapturing); } + if (_serverEndpointTrackerV2 != null) + { + _logger.ExperimentalFeatureEnabled(Microsoft.Diagnostics.Monitoring.WebApi.Strings.FeatureName_ServerEndpointPruningAlgorithmV2); + } + // Experimental features should log a warning when they are activated e.g. // _logger.ExperimentalFeatureEnabled("CallStacks"); diff --git a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs index 834966d70e5..e28c9ce54ca 100644 --- a/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs +++ b/src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs @@ -404,6 +404,30 @@ public static IServiceCollection ConfigureParameterCapturing(this IServiceCollec services.AddTransient(); return services; } + public static IServiceCollection ConfigureEndpointInfoSource(this IServiceCollection services) + { + services.AddSingleton(); + + services.AddSingleton(); + + if (ToolIdentifiers.IsEnvVarEnabled(ExperimentalFeatureIdentifiers.EnvironmentVariables.ServerEndpointPruningAlgorithmV2)) + { + services.AddSingleton(); + services.AddSingletonForwarder(); + services.AddHostedServiceForwarder(); + } + else + { + services.AddSingleton(); + services.AddSingletonForwarder(); + services.AddHostedServiceForwarder(); + } + + services.AddSingleton(); + services.AddHostedServiceForwarder(); + + return services; + } public static void AddScopedForwarder(this IServiceCollection services) where TImplementation : class, TService where TService : class {