From e63022b68fa7a872d8431d63d3d6367e7d036914 Mon Sep 17 00:00:00 2001 From: Alexey Sosnin Date: Sun, 28 Jan 2024 14:51:30 +0300 Subject: [PATCH] feat: publish native AOT --- .github/workflows/native-aot.yml | 42 + Directory.Build.props | 4 + Heartbeat.sln | 34 +- README.md | 2 +- scripts/publish-native-aot.ps1 | 69 ++ scripts/reinstall-dev-tool.ps1 | 23 +- scripts/update-ts-client.ps1 | 9 +- .../Heartbeat.Runtime.csproj | 4 - src/Heartbeat/ClientApp/api.yml | 191 ++-- .../client/api/dump/arrays/sparse/index.ts | 2 - .../api/dump/arrays/sparse/stat/index.ts | 2 - .../api/dump/heapDumpStatistics/index.ts | 2 - .../src/client/api/dump/info/index.ts | 2 - .../src/client/api/dump/modules/index.ts | 2 - .../api/dump/object/item/fields/index.ts | 2 - .../src/client/api/dump/object/item/index.ts | 2 - .../api/dump/object/item/roots/index.ts | 2 - .../api/dump/objectInstances/item/index.ts | 2 - .../src/client/api/dump/roots/index.ts | 2 - .../src/client/api/dump/segments/index.ts | 2 - .../client/api/dump/stringDuplicates/index.ts | 2 - .../src/client/api/dump/strings/index.ts | 2 - .../ClientApp/src/client/kiota-lock.json | 2 +- src/Heartbeat/Controllers/DumpController.cs | 864 +++++++++--------- src/Heartbeat/Controllers/Models.cs | 22 +- src/Heartbeat/Extensions/SwaggerExtensions.cs | 6 +- src/Heartbeat/Heartbeat.csproj | 65 +- src/Heartbeat/Program.cs | 599 ++++++++++-- 28 files changed, 1264 insertions(+), 698 deletions(-) create mode 100644 .github/workflows/native-aot.yml create mode 100644 scripts/publish-native-aot.ps1 diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml new file mode 100644 index 0000000..71daf43 --- /dev/null +++ b/.github/workflows/native-aot.yml @@ -0,0 +1,42 @@ +name: Publish native AOT + +on: + push: + branches: [ native-aot ] + # pull_request: + # # Sequence of patterns matched against refs/heads + # branches: + # - master + +jobs: + build: + runs-on: ubuntu-latest + env: + # temp fix frontend build + # Treating warnings as errors because process.env.CI = true. + # Most CI servers set it automatically. + CI: 'false' + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + global-json-file: global.json + - uses: actions/setup-node@v4 + with: + # Version Spec of the version to use in SemVer notation. + # It also emits such aliases as lts, latest, nightly and canary builds + # Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node + node-version: 20 + - name: Publish AOT version + shell: pwsh + run: | + ./scripts/publish-native-aot.ps1 + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: Heartbeat + path: artifacts/linux-x64/Heartbeat + retention-days: 1 + +# \artifacts\win-x64\Heartbeat.exe \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 2ac07e6..f221cac 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,6 +15,10 @@ ClrMd diagnostics Diagnostics utility to analyze memory dumps of a .NET application + + + Debug;Release;DebugOpenAPI;ReleaseAOT + diff --git a/Heartbeat.sln b/Heartbeat.sln index 71d269e..9551b77 100644 --- a/Heartbeat.sln +++ b/Heartbeat.sln @@ -47,6 +47,8 @@ Global Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 + DebugOpenAPI|Any CPU = DebugOpenAPI|Any CPU + ReleaseAOT|Any CPU = ReleaseAOT|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -57,14 +59,6 @@ Global {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x64.Build.0 = Debug|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x86.ActiveCfg = Debug|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Debug|x86.Build.0 = Debug|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|Any CPU.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|Any CPU.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|arm64.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|arm64.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x64.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x64.Build.0 = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x86.ActiveCfg = DebugLocal|Any CPU - {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugLocal|x86.Build.0 = DebugLocal|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|Any CPU.Build.0 = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|arm64.ActiveCfg = Release|Any CPU @@ -73,6 +67,10 @@ Global {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x64.Build.0 = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x86.ActiveCfg = Release|Any CPU {9E63F5A0-7695-474C-A946-64D75F8D9617}.Release|x86.Build.0 = Release|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.DebugOpenAPI|Any CPU.Build.0 = DebugOpenAPI|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU + {9E63F5A0-7695-474C-A946-64D75F8D9617}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -81,14 +79,6 @@ Global {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x64.Build.0 = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x86.ActiveCfg = Debug|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Debug|x86.Build.0 = Debug|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|Any CPU.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|Any CPU.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|arm64.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|arm64.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x64.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x64.Build.0 = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x86.ActiveCfg = DebugLocal|Any CPU - {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugLocal|x86.Build.0 = DebugLocal|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|Any CPU.Build.0 = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|arm64.ActiveCfg = Release|Any CPU @@ -97,6 +87,10 @@ Global {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x64.Build.0 = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x86.ActiveCfg = Release|Any CPU {D4060CFE-8141-49CE-99A5-559599D0E6B4}.Release|x86.Build.0 = Release|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.DebugOpenAPI|Any CPU.Build.0 = DebugOpenAPI|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU + {D4060CFE-8141-49CE-99A5-559599D0E6B4}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -121,6 +115,8 @@ Global {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x64.Build.0 = Release|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x86.ActiveCfg = Release|Any CPU {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.Release|x86.Build.0 = Release|Any CPU + {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {AC8E6790-14D5-42C5-AF51-98E8EB80644F}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|Any CPU.Build.0 = Debug|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -145,6 +141,8 @@ Global {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x64.Build.0 = Release|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x86.ActiveCfg = Release|Any CPU {789E65CA-B8F7-47B9-9013-B159D1E93F36}.Release|x86.Build.0 = Release|Any CPU + {789E65CA-B8F7-47B9-9013-B159D1E93F36}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {789E65CA-B8F7-47B9-9013-B159D1E93F36}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -169,6 +167,8 @@ Global {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x64.Build.0 = Release|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.ActiveCfg = Release|Any CPU {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.Build.0 = Release|Any CPU + {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -193,6 +193,8 @@ Global {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x64.Build.0 = Release|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.ActiveCfg = Release|Any CPU {3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.Build.0 = Release|Any CPU + {3D11554D-E09C-4710-B071-D90BB2447F46}.DebugOpenAPI|Any CPU.ActiveCfg = DebugOpenAPI|Any CPU + {3D11554D-E09C-4710-B071-D90BB2447F46}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 662973c..d2d4918 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Heartbeat [![NuGet Badge](https://buildstats.info/nuget/heartbeat?includePreReleases=true&dWidth=0)](https://www.nuget.org/packages/Heartbeat/) -Diagnostics utility with web UI to analyze memory dumps of a .NET application +Diagnostics utility with web UI to analyze memory dumps of .NET application ## Getting started diff --git a/scripts/publish-native-aot.ps1 b/scripts/publish-native-aot.ps1 new file mode 100644 index 0000000..8d6e549 --- /dev/null +++ b/scripts/publish-native-aot.ps1 @@ -0,0 +1,69 @@ +$ErrorActionPreference = "Stop" + +function Get-Runtime { + if ($IsWindows) { + return $env:PROCESSOR_ARCHITECTURE -eq 'AMD64' ? 'win-x64' : 'win-arm64' + } + + if ($IsLinux) { + return (uname -m) -eq 'aarch64' ? 'linux-arm64' : 'linux-x64' + } + + if ($IsMacOS) { + return (uname -m) -eq 'arm64' ? 'osx-arm64' : 'osx-x64' + } +} + +function Assert-ExitCode { + if (-not $?) { + throw 'Latest command failed' + } +} + +$Configuration = 'ReleaseAOT' +$RepositoryRoot = Split-Path $PSScriptRoot +$ArtifactsRoot = Join-Path $RepositoryRoot 'artifacts' + +Push-Location +try { + Set-Location $RepositoryRoot + + # [xml]$XmlConfig = Get-Content 'Directory.Build.props' + + # $XmlElement = Select-Xml '/Project/PropertyGroup/VersionPrefix' $XmlConfig | + # Select-Object -ExpandProperty Node + + # $VersionPrefix = $XmlElement.InnerText + # $VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')" + # $PackageVersion = "$VersionPrefix-$VersionSuffix" + + dotnet clean --configuration $Configuration + + # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog + # $Runtimes = @('win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64') + $Runtimes = @(Get-Runtime) + # TODO check `uname -m` for linux and macos + # $env:PROCESSOR_ARCHITECTURE + foreach ($Runtime in $Runtimes) { + $OutDir = Join-Path $ArtifactsRoot $Runtime + Write-Host "Publish AOT version for $Runtime to $OutDir" + dotnet publish --configuration $Configuration --runtime $Runtime --output $OutDir + Assert-ExitCode + + Write-Host "Files in $OutDir" + Get-ChildItem $OutDir + } + + # TODO zip? +} +catch { + Write-Host 'Publish AOT - FAILED!' -ForegroundColor Red + throw +} +finally { + Pop-Location +} + +# TODO Cross-OS native compilation +# C:\Users\Ne4to\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.1\build\Microsoft.NETCore.Native.Windows.targets(123,5): error : Platform linker not found. Ensure you have all the required prerequisites documented at https://aka.ms/nativeaot-prerequisites, in particular the Desktop Development for C++ workload in Visual Studio. For ARM64 development also install C++ ARM64 build tools. [C:\Users\Ne4to\projects\github.com\Ne4to\Heartbeat\src\Heartbeat\Heartbeat.csproj] +# C:\Users\Ne4to\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.1\build\Microsoft.NETCore.Native.Publish.targets(60,5): error : Cross-OS native compilation is not supported. [C:\Users\Ne4to\projects\github.com\Ne4to\Heartbeat\src\Heartbeat\Heartbeat.csproj] \ No newline at end of file diff --git a/scripts/reinstall-dev-tool.ps1 b/scripts/reinstall-dev-tool.ps1 index 07aa9e4..4f97073 100644 --- a/scripts/reinstall-dev-tool.ps1 +++ b/scripts/reinstall-dev-tool.ps1 @@ -1,5 +1,11 @@ $ErrorActionPreference = "Stop" +function Assert-ExitCode { + if (-not $?) { + throw 'Latest command failed' + } +} + $RepositoryRoot = Split-Path $PSScriptRoot Push-Location @@ -16,12 +22,14 @@ try dotnet tool uninstall -g Heartbeat dotnet clean --configuration Release - Get-Date -Format '' $VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')" - dotnet publish --runtime win-x64 - dotnet pack --runtime win-x64 --version-suffix $VersionSuffix -# $PackageVersion = "$VersionPrefix-$VersionSuffix" -# dotnet tool install --global --add-source ./src/Heartbeat/nupkg Heartbeat --version $PackageVersion + dotnet publish + Assert-ExitCode + dotnet pack --version-suffix $VersionSuffix + Assert-ExitCode + $PackageVersion = "$VersionPrefix-$VersionSuffix" + dotnet tool install --global --add-source ./src/Heartbeat/nupkg Heartbeat --version $PackageVersion + Assert-ExitCode } catch { Write-Host 'Install global tool - FAILED!' -ForegroundColor Red @@ -29,4 +37,7 @@ catch { } finally { Pop-Location -} \ No newline at end of file +} + + + diff --git a/scripts/update-ts-client.ps1 b/scripts/update-ts-client.ps1 index 718eaab..e2d4e9c 100644 --- a/scripts/update-ts-client.ps1 +++ b/scripts/update-ts-client.ps1 @@ -1,9 +1,10 @@ $ErrorActionPreference = "Stop" +$Configuration = 'DebugOpenAPI' $RepositoryRoot = Split-Path $PSScriptRoot $FrontendRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp' $ContractPath = Join-Path $FrontendRoot 'api.yml' -$DllPath = Join-Path $RepositoryRoot 'src/Heartbeat/bin/Debug/net8.0/Heartbeat.dll' +$DllPath = Join-Path $RepositoryRoot "src/Heartbeat/bin/$Configuration/net8.0/Heartbeat.dll" Push-Location try @@ -11,10 +12,9 @@ try Set-Location $RepositoryRoot dotnet tool restore - dotnet build --configuration Debug - + dotnet build --configuration $Configuration + Set-Location $FrontendRoot - $env:HEARTBEAT_GENERATE_CONTRACTS = 'true' dotnet swagger tofile --yaml --output $ContractPath $DllPath Heartbeat dotnet kiota generate -l typescript --openapi $ContractPath -c HeartbeatClient -o ./src/client --clean-output @@ -26,5 +26,4 @@ catch { } finally { Pop-Location - $env:HEARTBEAT_GENERATE_CONTRACTS = $null } \ No newline at end of file diff --git a/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj b/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj index 5c1d383..f29ff4c 100644 --- a/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj +++ b/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj @@ -4,8 +4,6 @@ - true - true true @@ -17,8 +15,6 @@ - - all diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index 1a3c09c..97c4ce8 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -12,68 +12,64 @@ paths: get: tags: - Dump - summary: Get dump info operationId: GetInfo responses: - '500': - description: Server Error + '200': + description: OK content: application/json: schema: - $ref: '#/components/schemas/ProblemDetails' - '200': - description: Success + $ref: '#/components/schemas/DumpInfo' + '500': + description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/DumpInfo' + $ref: '#/components/schemas/ProblemDetails' /api/dump/modules: get: tags: - Dump - summary: Get modules operationId: GetModules responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Module' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/segments: get: tags: - Dump - summary: Get segments operationId: GetSegments responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/HeapSegment' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/roots: get: tags: - Dump - summary: Get heap roots operationId: GetRoots parameters: - name: kind @@ -82,25 +78,24 @@ paths: schema: $ref: '#/components/schemas/ClrRootKind' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/RootInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/heap-dump-statistics: get: tags: - Dump - summary: Get heap dump statistics operationId: GetHeapDumpStat parameters: - name: gcStatus @@ -114,25 +109,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/ObjectTypeStatistics' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/strings: get: tags: - Dump - summary: Get heap dump statistics operationId: GetStrings parameters: - name: gcStatus @@ -146,25 +140,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/StringInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/string-duplicates: get: tags: - Dump - summary: Get string duplicates operationId: GetStringDuplicates parameters: - name: gcStatus @@ -178,25 +171,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/StringDuplicate' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object-instances/{mt}': get: tags: - Dump - summary: Get object instances operationId: GetObjectInstances parameters: - name: mt @@ -217,23 +209,22 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error + '200': + description: OK content: application/json: schema: - $ref: '#/components/schemas/ProblemDetails' - '200': - description: Success + $ref: '#/components/schemas/GetObjectInstancesResult' + '500': + description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/GetObjectInstancesResult' + $ref: '#/components/schemas/ProblemDetails' /api/dump/arrays/sparse: get: tags: - Dump - summary: Get sparse arrays operationId: GetSparseArrays parameters: - name: gcStatus @@ -247,25 +238,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/ArrayInfo' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /api/dump/arrays/sparse/stat: get: tags: - Dump - summary: Get arrays operationId: GetSparseArraysStat parameters: - name: gcStatus @@ -279,25 +269,24 @@ paths: schema: $ref: '#/components/schemas/Generation' responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/SparseArrayStatistics' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}': get: tags: - Dump - summary: Get object operationId: GetClrObject parameters: - name: address @@ -308,14 +297,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -326,11 +309,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/fields': get: tags: - Dump - summary: Get object fields operationId: GetClrObjectFields parameters: - name: address @@ -341,14 +329,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -361,11 +343,16 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/roots': get: tags: - Dump - summary: Get object roots operationId: GetClrObjectRoots parameters: - name: address @@ -376,14 +363,8 @@ paths: type: integer format: int64 responses: - '500': - description: Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ProblemDetails' '200': - description: Success + description: OK content: application/json: schema: @@ -396,6 +377,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' components: schemas: Architecture: diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts index 66a3615..de12874 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts @@ -28,7 +28,6 @@ export class SparseRequestBuilder extends BaseRequestBuilder new SparseRequestBuilder(x, y)); } /** - * Get sparse arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ArrayInfo */ @@ -42,7 +41,6 @@ export class SparseRequestBuilder extends BaseRequestBuilder(requestInfo, createArrayInfoFromDiscriminatorValue, errorMapping); } /** - * Get sparse arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts index 904f308..d653a82 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts @@ -21,7 +21,6 @@ export class StatRequestBuilder extends BaseRequestBuilder { super(pathParameters, requestAdapter, "{+baseurl}/api/dump/arrays/sparse/stat{?gcStatus*,generation*}", (x, y) => new StatRequestBuilder(x, y)); } /** - * Get arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of SparseArrayStatistics */ @@ -35,7 +34,6 @@ export class StatRequestBuilder extends BaseRequestBuilder { return this.requestAdapter.sendCollectionAsync(requestInfo, createSparseArrayStatisticsFromDiscriminatorValue, errorMapping); } /** - * Get arrays * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts index a840707..e4f268b 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/heapDumpStatistics/index.ts @@ -21,7 +21,6 @@ export class HeapDumpStatisticsRequestBuilder extends BaseRequestBuilder new HeapDumpStatisticsRequestBuilder(x, y)); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ObjectTypeStatistics */ @@ -35,7 +34,6 @@ export class HeapDumpStatisticsRequestBuilder extends BaseRequestBuilder(requestInfo, createObjectTypeStatisticsFromDiscriminatorValue, errorMapping); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts index 145d642..39b2050 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/info/index.ts @@ -17,7 +17,6 @@ export class InfoRequestBuilder extends BaseRequestBuilder { super(pathParameters, requestAdapter, "{+baseurl}/api/dump/info", (x, y) => new InfoRequestBuilder(x, y)); } /** - * Get dump info * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of DumpInfo */ @@ -31,7 +30,6 @@ export class InfoRequestBuilder extends BaseRequestBuilder { return this.requestAdapter.sendAsync(requestInfo, createDumpInfoFromDiscriminatorValue, errorMapping); } /** - * Get dump info * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts index b42906c..e0bb107 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/modules/index.ts @@ -17,7 +17,6 @@ export class ModulesRequestBuilder extends BaseRequestBuilder new ModulesRequestBuilder(x, y)); } /** - * Get modules * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of Module */ @@ -31,7 +30,6 @@ export class ModulesRequestBuilder extends BaseRequestBuilder(requestInfo, createModuleFromDiscriminatorValue, errorMapping); } /** - * Get modules * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts index 1d7c701..41ca43c 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts @@ -17,7 +17,6 @@ export class FieldsRequestBuilder extends BaseRequestBuilder new FieldsRequestBuilder(x, y)); } /** - * Get object fields * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ClrObjectField */ @@ -32,7 +31,6 @@ export class FieldsRequestBuilder extends BaseRequestBuilder(requestInfo, createClrObjectFieldFromDiscriminatorValue, errorMapping); } /** - * Get object fields * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts index b274c22..547c321 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts @@ -31,7 +31,6 @@ export class WithAddressItemRequestBuilder extends BaseRequestBuilder new WithAddressItemRequestBuilder(x, y)); } /** - * Get object * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of GetClrObjectResult */ @@ -46,7 +45,6 @@ export class WithAddressItemRequestBuilder extends BaseRequestBuilder(requestInfo, createGetClrObjectResultFromDiscriminatorValue, errorMapping); } /** - * Get object * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts index c224129..d6f83af 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/roots/index.ts @@ -17,7 +17,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder super(pathParameters, requestAdapter, "{+baseurl}/api/dump/object/{address}/roots", (x, y) => new RootsRequestBuilder(x, y)); } /** - * Get object roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of ClrObjectRootPath */ @@ -32,7 +31,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder return this.requestAdapter.sendCollectionAsync(requestInfo, createClrObjectRootPathFromDiscriminatorValue, errorMapping); } /** - * Get object roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts index 42321c2..45b0bb8 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/objectInstances/item/index.ts @@ -21,7 +21,6 @@ export class WithMtItemRequestBuilder extends BaseRequestBuilder new WithMtItemRequestBuilder(x, y)); } /** - * Get object instances * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of GetObjectInstancesResult */ @@ -35,7 +34,6 @@ export class WithMtItemRequestBuilder extends BaseRequestBuilder(requestInfo, createGetObjectInstancesResultFromDiscriminatorValue, errorMapping); } /** - * Get object instances * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts index e78f8d2..53001bc 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/roots/index.ts @@ -20,7 +20,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder super(pathParameters, requestAdapter, "{+baseurl}/api/dump/roots{?kind*}", (x, y) => new RootsRequestBuilder(x, y)); } /** - * Get heap roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of RootInfo */ @@ -34,7 +33,6 @@ export class RootsRequestBuilder extends BaseRequestBuilder return this.requestAdapter.sendCollectionAsync(requestInfo, createRootInfoFromDiscriminatorValue, errorMapping); } /** - * Get heap roots * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts index 3fcacc2..24adb7a 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts @@ -17,7 +17,6 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder new SegmentsRequestBuilder(x, y)); } /** - * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of HeapSegment */ @@ -31,7 +30,6 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder(requestInfo, createHeapSegmentFromDiscriminatorValue, errorMapping); } /** - * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts index 9297487..433614f 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/stringDuplicates/index.ts @@ -21,7 +21,6 @@ export class StringDuplicatesRequestBuilder extends BaseRequestBuilder new StringDuplicatesRequestBuilder(x, y)); } /** - * Get string duplicates * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of StringDuplicate */ @@ -35,7 +34,6 @@ export class StringDuplicatesRequestBuilder extends BaseRequestBuilder(requestInfo, createStringDuplicateFromDiscriminatorValue, errorMapping); } /** - * Get string duplicates * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts index 01bb66e..f259d74 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/strings/index.ts @@ -21,7 +21,6 @@ export class StringsRequestBuilder extends BaseRequestBuilder new StringsRequestBuilder(x, y)); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of StringInfo */ @@ -35,7 +34,6 @@ export class StringsRequestBuilder extends BaseRequestBuilder(requestInfo, createStringInfoFromDiscriminatorValue, errorMapping); } /** - * Get heap dump statistics * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 1671570..4104768 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "E058DCE3BA746E408EADE32EA9E442AFC9D72743F2398CD19A5CCA613B92AB688674BFB60B624AA3AD06461E16CA4C2EBC6F9C5C62315B4F01C073DEF44C1C6C", + "descriptionHash": "2680CD432C0BAF2EAD8335B87589C6613B8A32E7D90AAF6EC468A74F00CDC479E9320B872E8B6E0C6AE5CDD8A0F2251B0991E07026BB56A11771AE55760D7F36", "descriptionLocation": "..\\..\\api.yml", "lockFileVersion": "1.0.0", "kiotaVersion": "1.10.1", diff --git a/src/Heartbeat/Controllers/DumpController.cs b/src/Heartbeat/Controllers/DumpController.cs index fc96a50..1240e35 100644 --- a/src/Heartbeat/Controllers/DumpController.cs +++ b/src/Heartbeat/Controllers/DumpController.cs @@ -1,432 +1,432 @@ -using Heartbeat.Domain; -using Heartbeat.Runtime; -using Heartbeat.Runtime.Analyzers; -using Heartbeat.Runtime.Domain; -using Heartbeat.Runtime.Extensions; -using Heartbeat.Runtime.Proxies; - -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OutputCaching; -using Microsoft.Diagnostics.Runtime; - -using Swashbuckle.AspNetCore.Annotations; - -using System.Globalization; -using System.Net.Mime; - -namespace Heartbeat.Host.Controllers; - -[ApiController] -[OutputCache] -[Route("api/dump")] -[ApiExplorerSettings(GroupName = "Heartbeat")] -[Consumes(MediaTypeNames.Application.Json)] -[Produces(MediaTypeNames.Application.Json)] -[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] -public class DumpController : ControllerBase -{ - private readonly RuntimeContext _context; - - public DumpController(RuntimeContext context) - { - _context = context; - } - - [HttpGet] - [Route("info")] - [ProducesResponseType(typeof(DumpInfo), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get dump info")] - public DumpInfo GetInfo() - { - var clrHeap = _context.Heap; - var clrInfo = _context.Runtime.ClrInfo; - var dataReader = clrInfo.DataTarget.DataReader; - - return new DumpInfo( - _context.DumpPath, - clrHeap.CanWalkHeap, - clrHeap.IsServer, - clrInfo.ModuleInfo.FileName, - dataReader.Architecture, - dataReader.ProcessId, - dataReader.TargetPlatform.ToString(), - clrInfo.Version.ToString(2) - ); - } - - [HttpGet] - [Route("modules")] - [ProducesResponseType(typeof(Module[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get modules")] - public IEnumerable GetModules() - { - var modules = _context.Runtime - .EnumerateModules() - .Select(m => new Module(m.Address, m.Size, m.Name)) - .ToArray(); - - return modules; - } - - [HttpGet] - [Route("segments")] - [ProducesResponseType(typeof(HeapSegment[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get segments")] - public IEnumerable GetSegments() - { - var segments = - from s in _context.Heap.Segments - select new HeapSegment( - s.Start, - s.End, - s.Kind); - - return segments; - } - - [HttpGet] - [Route("roots")] - [ProducesResponseType(typeof(RootInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap roots")] - public IEnumerable GetRoots([FromQuery] ClrRootKind? kind = null) - { - return - from root in _context.Heap.EnumerateRoots() - where kind == null || root.RootKind == kind - let objectType = root.Object.Type - select new RootInfo( - root.Object.Address, - root.RootKind, - root.IsPinned, - root.Object.Size, - objectType.MethodTable, - objectType.Name - ); - } - - [HttpGet] - [Route("heap-dump-statistics")] - [ProducesResponseType(typeof(ObjectTypeStatistics[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics")] - public IEnumerable GetHeapDumpStat( - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - // TODO filter by just my code - how to filter Action? - // TODO filter by type name - { - var analyzer = new HeapDumpStatisticsAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; - - var statistics = analyzer.GetObjectTypeStatistics() - .OrderByDescending(s => s.TotalSize) - .Select(s => new ObjectTypeStatistics(s.MethodTable, s.TypeName, s.TotalSize, s.InstanceCount)) - .ToArray(); - - return statistics; - } - - [HttpGet] - [Route("strings")] - [ProducesResponseType(typeof(StringInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics")] - public IEnumerable GetStrings( - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - // TODO filter by min length - // TODO filter by max length - { - var query = from obj in _context.EnumerateStrings(gcStatus, generation) - let str = obj.AsString() - let length = obj.ReadField("_stringLength") - select new StringInfo(obj.Address, length, obj.Size, str); - - // TODO limit output qty - return query; - } - - [HttpGet] - [Route("string-duplicates")] - [ProducesResponseType(typeof(StringDuplicate[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get string duplicates")] - public IEnumerable GetStringDuplicates( - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - // TODO filter by min length - { - var analyzer = new StringDuplicateAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; - - return analyzer.GetStringDuplicates() - .Select(sd => new StringDuplicate(sd.Value, sd.Count, sd.FullLength, sd.WastedMemory)); - } - - [HttpGet] - [Route("object-instances/{mt}")] - [ProducesResponseType(typeof(GetObjectInstancesResult), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get object instances")] - public GetObjectInstancesResult GetObjectInstances( - ulong mt, - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - // TODO limit maxCount - { - var methodTable = new MethodTable(mt); - - var clrType = _context.Heap.FindTypeByMethodTable(methodTable); - - var instances = ( - from obj in _context.EnumerateObjects(gcStatus, generation) - where obj.Type != null - && obj.Type.MethodTable == methodTable - orderby obj.Size descending - select new ObjectInstance - ( - new Address(obj.Address), - new Size(obj.Size) - ) - ).ToArray(); - - return new GetObjectInstancesResult(methodTable, clrType?.Name, instances); - } - - [HttpGet] - [Route("arrays/sparse")] - [ProducesResponseType(typeof(ArrayInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get sparse arrays")] - // TODO add arrays - // TODO add arrays/sparse - // TODO add arrays/sparse/stat - public IEnumerable GetSparseArrays( - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - { - var query = from obj in _context.EnumerateObjects(gcStatus, generation) - where obj.IsArray - let proxy = new ArrayProxy(_context, obj) - where proxy.UnusedItemsPercent >= 0.2 - orderby proxy.Wasted descending - select new ArrayInfo(obj.Address, obj.Type.MethodTable, obj.Type.Name, proxy.Length, proxy.UnusedItemsCount, - proxy.UnusedItemsPercent, proxy.Wasted); - - return query.Take(100); - } - - [HttpGet] - [Route("arrays/sparse/stat")] - [ProducesResponseType(typeof(SparseArrayStatistics[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get arrays")] - public IEnumerable GetSparseArraysStat( - [FromQuery] ObjectGCStatus? gcStatus = null, - [FromQuery] Generation? generation = null) - { - var query = from obj in _context.EnumerateObjects(gcStatus, generation) - where obj.IsArray - let proxy = new ArrayProxy(_context, obj) - where proxy.UnusedItemsCount != 0 - group proxy by obj.Type.MethodTable - into grp - select new SparseArrayStatistics - ( - grp.Key, - grp.First().TargetObject.Type.Name, - grp.Count(), - Size.Sum(grp.Select(t => t.Wasted)) - ); - - return query; - } - - [HttpGet] - [Route("object/{address}")] - [ProducesResponseType(typeof(GetClrObjectResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object")] - public IActionResult GetClrObject(ulong address) - { - var clrObject = _context.Heap.GetObject(address); - if (clrObject.Type == null) - { - return NotFound(); - } - - var result = new GetClrObjectResult( - clrObject.Address, - clrObject.Type.Module.Name, - clrObject.Type.Name, - clrObject.Type.MethodTable, - clrObject.Size, - _context.Heap.GetGeneration(clrObject.Address), - clrObject.Type.IsString ? clrObject.AsString() : null); - - return Ok(result); - } - - [HttpGet] - [Route("object/{address}/fields")] - [ProducesResponseType(typeof(ClrObjectField[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object fields")] - public IActionResult GetClrObjectFields(ulong address) - { - var clrObject = _context.Heap.GetObject(address); - if (clrObject.Type == null) - { - return NotFound(); - } - - var fields = ( - from field in clrObject.Type.Fields - let mt = field.Type?.MethodTable ?? 0UL - let objectAddress = GetFieldObjectAddress(field, clrObject.Address) - let value = GetFieldValue(field, clrObject.Address) - select new ClrObjectField( - mt, - field.Type?.Name, - field.Offset, - field.IsValueType, - objectAddress, - value, - field.Name) - ).ToArray(); - - return Ok(fields); - } - - [HttpGet] - [Route("object/{address}/roots")] - [ProducesResponseType(typeof(ClrObjectRootPath[]), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object roots")] - public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) - { - var clrObject = _context.Heap.GetObject(address); - if (clrObject.Type == null) - { - return NotFound(); - } - - var result = new List(); - GCRoot gcRoot = new(_context.Heap, new[] { address }); - foreach ((ClrRoot root, GCRoot.ChainLink path) in gcRoot.EnumerateRootPaths(ct)) - { - var rootType = root.Object.Type!; - - var rootInfo = new RootInfo( - root.Object.Address, - root.RootKind, - root.IsPinned, - root.Object.Size, - rootType.MethodTable, - rootType.Name! - ); - - List pathItems = new(); - - GCRoot.ChainLink? current = path; - while (current != null) - { - var obj = _context.Heap.GetObject(current.Object); - - var item = new RootPathItem( - obj.Address, - obj.Type!.MethodTable, - obj.Type.Name, - obj.Size, - _context.Heap.GetGeneration(obj.Address)); - - pathItems.Add(item); - - current = current.Next; - } - - result.Add(new ClrObjectRootPath(rootInfo, pathItems)); - // TODO get only one root path - break; - } - - return Ok(result); - } - - private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address) - { - if (field.Type?.IsObjectReference ?? false) - { - return new Address(field.ReadObject(address, false).Address); - } - - return null; - } - - private static string GetFieldValue(ClrInstanceField field, ulong address) - { - if (field.IsPrimitive) - { - return field.ElementType switch - { - ClrElementType.Boolean => field.Read(address, false).ToString(), - ClrElementType.Char => field.Read(address, false).ToString(), - ClrElementType.Int8 => field.Read(address, false).ToString(), - ClrElementType.UInt8 => field.Read(address, false).ToString(), - ClrElementType.Int16 => field.Read(address, false).ToString(), - ClrElementType.UInt16 => field.Read(address, false).ToString(), - ClrElementType.Int32 => field.Read(address, false).ToString(), - ClrElementType.UInt32 => field.Read(address, false).ToString(), - ClrElementType.Int64 => field.Read(address, false).ToString(), - ClrElementType.UInt64 => field.Read(address, false).ToString(), - ClrElementType.Float => field.Read(address, false).ToString(CultureInfo.InvariantCulture), - ClrElementType.Double => field.Read(address, false).ToString(CultureInfo.InvariantCulture), - ClrElementType.NativeInt => field.Read(address, false).ToString(), - ClrElementType.NativeUInt => field.Read(address, false).ToString(), - _ => throw new ArgumentOutOfRangeException($"Unable to get primitive value for {field.ElementType} field") - }; - } - - return GetAddress(); - - string GetAddress() - { - if (field.Type?.IsEnum ?? false) - { - ClrEnum enumField = field.Type.AsEnum(); - // TODO handle other types - if (enumField.ElementType == ClrElementType.Int32) - { - var fieldValue = field.Read(address, false); - var name = enumField.EnumerateValues() - .FirstOrDefault(v => (int)v.Value == fieldValue) - .Name; - - return !string.IsNullOrEmpty(name) - ? name - : fieldValue.ToString(); - } - } - - if (field.Type?.IsObjectReference ?? false) - { - ClrObject clrObject = field.ReadObject(address, false); - if (clrObject.IsNull) - { - return ""; - } - - if (clrObject.Type?.IsString ?? false) - { - return clrObject.AsString(100); - } - - if (clrObject is { IsNull: false, Type.Name: "System.Version" }) - { - var major = clrObject.ReadField("_Major"); - var minor = clrObject.ReadField("_Minor"); - var build = clrObject.ReadField("_Build"); - var revision = clrObject.ReadField("_Revision"); - var version = new Version(major, minor, build, revision); - return version.ToString(); - } - - return clrObject.Address.ToString("x16"); - } - - return string.Empty; - } - } -} \ No newline at end of file +// using Heartbeat.Domain; +// using Heartbeat.Runtime; +// using Heartbeat.Runtime.Analyzers; +// using Heartbeat.Runtime.Domain; +// using Heartbeat.Runtime.Extensions; +// using Heartbeat.Runtime.Proxies; +// +// using Microsoft.AspNetCore.Mvc; +// using Microsoft.AspNetCore.OutputCaching; +// using Microsoft.Diagnostics.Runtime; +// +// using Swashbuckle.AspNetCore.Annotations; +// +// using System.Globalization; +// using System.Net.Mime; +// +// namespace Heartbeat.Host.Controllers; +// +// [ApiController] +// [OutputCache] +// [Route("api/dump")] +// [ApiExplorerSettings(GroupName = "Heartbeat")] +// [Consumes(MediaTypeNames.Application.Json)] +// [Produces(MediaTypeNames.Application.Json)] +// [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] +// public class DumpController : ControllerBase +// { +// private readonly RuntimeContext _context; +// +// public DumpController(RuntimeContext context) +// { +// _context = context; +// } +// +// [HttpGet] +// [Route("info")] +// [ProducesResponseType(typeof(DumpInfo), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get dump info")] +// public DumpInfo GetInfo() +// { +// var clrHeap = _context.Heap; +// var clrInfo = _context.Runtime.ClrInfo; +// var dataReader = clrInfo.DataTarget.DataReader; +// +// return new DumpInfo( +// _context.DumpPath, +// clrHeap.CanWalkHeap, +// clrHeap.IsServer, +// clrInfo.ModuleInfo.FileName, +// dataReader.Architecture, +// dataReader.ProcessId, +// dataReader.TargetPlatform.ToString(), +// clrInfo.Version.ToString(2) +// ); +// } +// +// [HttpGet] +// [Route("modules")] +// [ProducesResponseType(typeof(Module[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get modules")] +// public IEnumerable GetModules() +// { +// var modules = _context.Runtime +// .EnumerateModules() +// .Select(m => new Module(m.Address, m.Size, m.Name)) +// .ToArray(); +// +// return modules; +// } +// +// [HttpGet] +// [Route("segments")] +// [ProducesResponseType(typeof(HeapSegment[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get segments")] +// public IEnumerable GetSegments() +// { +// var segments = +// from s in _context.Heap.Segments +// select new HeapSegment( +// s.Start, +// s.End, +// s.Kind); +// +// return segments; +// } +// +// [HttpGet] +// [Route("roots")] +// [ProducesResponseType(typeof(RootInfo[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get heap roots")] +// public IEnumerable GetRoots([FromQuery] ClrRootKind? kind = null) +// { +// return +// from root in _context.Heap.EnumerateRoots() +// where kind == null || root.RootKind == kind +// let objectType = root.Object.Type +// select new RootInfo( +// root.Object.Address, +// root.RootKind, +// root.IsPinned, +// root.Object.Size, +// objectType.MethodTable, +// objectType.Name +// ); +// } +// +// [HttpGet] +// [Route("heap-dump-statistics")] +// [ProducesResponseType(typeof(ObjectTypeStatistics[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get heap dump statistics")] +// public IEnumerable GetHeapDumpStat( +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// // TODO filter by just my code - how to filter Action? +// // TODO filter by type name +// { +// var analyzer = new HeapDumpStatisticsAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; +// +// var statistics = analyzer.GetObjectTypeStatistics() +// .OrderByDescending(s => s.TotalSize) +// .Select(s => new ObjectTypeStatistics(s.MethodTable, s.TypeName, s.TotalSize, s.InstanceCount)) +// .ToArray(); +// +// return statistics; +// } +// +// [HttpGet] +// [Route("strings")] +// [ProducesResponseType(typeof(StringInfo[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get heap dump statistics")] +// public IEnumerable GetStrings( +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// // TODO filter by min length +// // TODO filter by max length +// { +// var query = from obj in _context.EnumerateStrings(gcStatus, generation) +// let str = obj.AsString() +// let length = obj.ReadField("_stringLength") +// select new StringInfo(obj.Address, length, obj.Size, str); +// +// // TODO limit output qty +// return query; +// } +// +// [HttpGet] +// [Route("string-duplicates")] +// [ProducesResponseType(typeof(StringDuplicate[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get string duplicates")] +// public IEnumerable GetStringDuplicates( +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// // TODO filter by min length +// { +// var analyzer = new StringDuplicateAnalyzer(_context) { ObjectGcStatus = gcStatus, Generation = generation }; +// +// return analyzer.GetStringDuplicates() +// .Select(sd => new StringDuplicate(sd.Value, sd.Count, sd.FullLength, sd.WastedMemory)); +// } +// +// [HttpGet] +// [Route("object-instances/{mt}")] +// [ProducesResponseType(typeof(GetObjectInstancesResult), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get object instances")] +// public GetObjectInstancesResult GetObjectInstances( +// ulong mt, +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// // TODO limit maxCount +// { +// var methodTable = new MethodTable(mt); +// +// var clrType = _context.Heap.FindTypeByMethodTable(methodTable); +// +// var instances = ( +// from obj in _context.EnumerateObjects(gcStatus, generation) +// where obj.Type != null +// && obj.Type.MethodTable == methodTable +// orderby obj.Size descending +// select new ObjectInstance +// ( +// new Address(obj.Address), +// new Size(obj.Size) +// ) +// ).ToArray(); +// +// return new GetObjectInstancesResult(methodTable, clrType?.Name, instances); +// } +// +// [HttpGet] +// [Route("arrays/sparse")] +// [ProducesResponseType(typeof(ArrayInfo[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get sparse arrays")] +// // TODO add arrays +// // TODO add arrays/sparse +// // TODO add arrays/sparse/stat +// public IEnumerable GetSparseArrays( +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// { +// var query = from obj in _context.EnumerateObjects(gcStatus, generation) +// where obj.IsArray +// let proxy = new ArrayProxy(_context, obj) +// where proxy.UnusedItemsPercent >= 0.2 +// orderby proxy.Wasted descending +// select new ArrayInfo(obj.Address, obj.Type.MethodTable, obj.Type.Name, proxy.Length, proxy.UnusedItemsCount, +// proxy.UnusedItemsPercent, proxy.Wasted); +// +// return query.Take(100); +// } +// +// [HttpGet] +// [Route("arrays/sparse/stat")] +// [ProducesResponseType(typeof(SparseArrayStatistics[]), StatusCodes.Status200OK)] +// [SwaggerOperation(summary: "Get arrays")] +// public IEnumerable GetSparseArraysStat( +// [FromQuery] ObjectGCStatus? gcStatus = null, +// [FromQuery] Generation? generation = null) +// { +// var query = from obj in _context.EnumerateObjects(gcStatus, generation) +// where obj.IsArray +// let proxy = new ArrayProxy(_context, obj) +// where proxy.UnusedItemsCount != 0 +// group proxy by obj.Type.MethodTable +// into grp +// select new SparseArrayStatistics +// ( +// grp.Key, +// grp.First().TargetObject.Type.Name, +// grp.Count(), +// Size.Sum(grp.Select(t => t.Wasted)) +// ); +// +// return query; +// } +// +// [HttpGet] +// [Route("object/{address}")] +// [ProducesResponseType(typeof(GetClrObjectResult), StatusCodes.Status200OK)] +// [ProducesResponseType(StatusCodes.Status404NotFound)] +// [SwaggerOperation(summary: "Get object")] +// public IActionResult GetClrObject(ulong address) +// { +// var clrObject = _context.Heap.GetObject(address); +// if (clrObject.Type == null) +// { +// return NotFound(); +// } +// +// var result = new GetClrObjectResult( +// clrObject.Address, +// clrObject.Type.Module.Name, +// clrObject.Type.Name, +// clrObject.Type.MethodTable, +// clrObject.Size, +// _context.Heap.GetGeneration(clrObject.Address), +// clrObject.Type.IsString ? clrObject.AsString() : null); +// +// return Ok(result); +// } +// +// [HttpGet] +// [Route("object/{address}/fields")] +// [ProducesResponseType(typeof(ClrObjectField[]), StatusCodes.Status200OK)] +// [ProducesResponseType(StatusCodes.Status404NotFound)] +// [SwaggerOperation(summary: "Get object fields")] +// public IActionResult GetClrObjectFields(ulong address) +// { +// var clrObject = _context.Heap.GetObject(address); +// if (clrObject.Type == null) +// { +// return NotFound(); +// } +// +// var fields = ( +// from field in clrObject.Type.Fields +// let mt = field.Type?.MethodTable ?? 0UL +// let objectAddress = GetFieldObjectAddress(field, clrObject.Address) +// let value = GetFieldValue(field, clrObject.Address) +// select new ClrObjectField( +// mt, +// field.Type?.Name, +// field.Offset, +// field.IsValueType, +// objectAddress, +// value, +// field.Name) +// ).ToArray(); +// +// return Ok(fields); +// } +// +// [HttpGet] +// [Route("object/{address}/roots")] +// [ProducesResponseType(typeof(ClrObjectRootPath[]), StatusCodes.Status200OK)] +// [ProducesResponseType(StatusCodes.Status404NotFound)] +// [SwaggerOperation(summary: "Get object roots")] +// public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) +// { +// var clrObject = _context.Heap.GetObject(address); +// if (clrObject.Type == null) +// { +// return NotFound(); +// } +// +// var result = new List(); +// GCRoot gcRoot = new(_context.Heap, new[] { address }); +// foreach ((ClrRoot root, GCRoot.ChainLink path) in gcRoot.EnumerateRootPaths(ct)) +// { +// var rootType = root.Object.Type!; +// +// var rootInfo = new RootInfo( +// root.Object.Address, +// root.RootKind, +// root.IsPinned, +// root.Object.Size, +// rootType.MethodTable, +// rootType.Name! +// ); +// +// List pathItems = new(); +// +// GCRoot.ChainLink? current = path; +// while (current != null) +// { +// var obj = _context.Heap.GetObject(current.Object); +// +// var item = new RootPathItem( +// obj.Address, +// obj.Type!.MethodTable, +// obj.Type.Name, +// obj.Size, +// _context.Heap.GetGeneration(obj.Address)); +// +// pathItems.Add(item); +// +// current = current.Next; +// } +// +// result.Add(new ClrObjectRootPath(rootInfo, pathItems)); +// // TODO get only one root path +// break; +// } +// +// return Ok(result); +// } +// +// private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address) +// { +// if (field.Type?.IsObjectReference ?? false) +// { +// return new Address(field.ReadObject(address, false).Address); +// } +// +// return null; +// } +// +// private static string GetFieldValue(ClrInstanceField field, ulong address) +// { +// if (field.IsPrimitive) +// { +// return field.ElementType switch +// { +// ClrElementType.Boolean => field.Read(address, false).ToString(), +// ClrElementType.Char => field.Read(address, false).ToString(), +// ClrElementType.Int8 => field.Read(address, false).ToString(), +// ClrElementType.UInt8 => field.Read(address, false).ToString(), +// ClrElementType.Int16 => field.Read(address, false).ToString(), +// ClrElementType.UInt16 => field.Read(address, false).ToString(), +// ClrElementType.Int32 => field.Read(address, false).ToString(), +// ClrElementType.UInt32 => field.Read(address, false).ToString(), +// ClrElementType.Int64 => field.Read(address, false).ToString(), +// ClrElementType.UInt64 => field.Read(address, false).ToString(), +// ClrElementType.Float => field.Read(address, false).ToString(CultureInfo.InvariantCulture), +// ClrElementType.Double => field.Read(address, false).ToString(CultureInfo.InvariantCulture), +// ClrElementType.NativeInt => field.Read(address, false).ToString(), +// ClrElementType.NativeUInt => field.Read(address, false).ToString(), +// _ => throw new ArgumentOutOfRangeException($"Unable to get primitive value for {field.ElementType} field") +// }; +// } +// +// return GetAddress(); +// +// string GetAddress() +// { +// if (field.Type?.IsEnum ?? false) +// { +// ClrEnum enumField = field.Type.AsEnum(); +// // TODO handle other types +// if (enumField.ElementType == ClrElementType.Int32) +// { +// var fieldValue = field.Read(address, false); +// var name = enumField.EnumerateValues() +// .FirstOrDefault(v => (int)v.Value == fieldValue) +// .Name; +// +// return !string.IsNullOrEmpty(name) +// ? name +// : fieldValue.ToString(); +// } +// } +// +// if (field.Type?.IsObjectReference ?? false) +// { +// ClrObject clrObject = field.ReadObject(address, false); +// if (clrObject.IsNull) +// { +// return ""; +// } +// +// if (clrObject.Type?.IsString ?? false) +// { +// return clrObject.AsString(100); +// } +// +// if (clrObject is { IsNull: false, Type.Name: "System.Version" }) +// { +// var major = clrObject.ReadField("_Major"); +// var minor = clrObject.ReadField("_Minor"); +// var build = clrObject.ReadField("_Build"); +// var revision = clrObject.ReadField("_Revision"); +// var version = new Version(major, minor, build, revision); +// return version.ToString(); +// } +// +// return clrObject.Address.ToString("x16"); +// } +// +// return string.Empty; +// } +// } +// } \ No newline at end of file diff --git a/src/Heartbeat/Controllers/Models.cs b/src/Heartbeat/Controllers/Models.cs index 06ccb75..7843591 100644 --- a/src/Heartbeat/Controllers/Models.cs +++ b/src/Heartbeat/Controllers/Models.cs @@ -1,6 +1,7 @@ using Microsoft.Diagnostics.Runtime; using System.Runtime.InteropServices; +using System.Text.Json.Serialization; namespace Heartbeat.Host.Controllers; @@ -56,8 +57,27 @@ public record StringDuplicate(string Value, int Count, int FullLength, ulong Was public record RootInfo(ulong Address, ClrRootKind Kind, bool IsPinned, ulong Size, ulong MethodTable, string TypeName); public record ClrObjectRootPath(RootInfo Root, IReadOnlyList PathItems); + public record RootPathItem(ulong Address, ulong MethodTable, string? TypeName, ulong Size, Generation Generation); public record ArrayInfo(ulong Address, ulong MethodTable, string? TypeName, int Length, int UnusedItemsCount, double UnusedPercent, ulong Wasted); -public record SparseArrayStatistics(ulong MethodTable, string? TypeName, int Count, ulong TotalWasted); \ No newline at end of file +public record SparseArrayStatistics(ulong MethodTable, string? TypeName, int Count, ulong TotalWasted); + +[JsonSourceGenerationOptions(UseStringEnumConverter = true)] +[JsonSerializable(typeof(DumpInfo))] +[JsonSerializable(typeof(GetObjectInstancesResult))] +[JsonSerializable(typeof(GetClrObjectResult))] +[JsonSerializable(typeof(Module[]))] +[JsonSerializable(typeof(ClrObjectField[]))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable>))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/src/Heartbeat/Extensions/SwaggerExtensions.cs b/src/Heartbeat/Extensions/SwaggerExtensions.cs index 779a7c2..95b4f4b 100644 --- a/src/Heartbeat/Extensions/SwaggerExtensions.cs +++ b/src/Heartbeat/Extensions/SwaggerExtensions.cs @@ -1,3 +1,4 @@ +#if OPENAPI using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -77,9 +78,7 @@ public class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter { public void Apply(OpenApiSchema model, SchemaFilterContext context) { -#if DEBUG FixNullableProperties(model, context); -#endif var additionalRequiredProps = model.Properties .Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key)) @@ -119,4 +118,5 @@ private static void FixNullableProperties(OpenApiSchema schema, SchemaFilterCont } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/Heartbeat/Heartbeat.csproj b/src/Heartbeat/Heartbeat.csproj index 2403617..2699432 100644 --- a/src/Heartbeat/Heartbeat.csproj +++ b/src/Heartbeat/Heartbeat.csproj @@ -13,51 +13,65 @@ https://localhost:44443 npm start - - - true + + + DEBUG;OPENAPI + + true + + + true - win-x64 + true + RELEASE;AOT + true + false + true + + + + true true - - - - heartbeat.exe + heartbeat ./nupkg README.md - + - + - - - - - + + + + + + + + + + + - - - - + + + + + - - - - + @@ -91,8 +105,9 @@ wwwroot\%(RecursiveDir)%(FileName)%(Extension) - PreserveNewest - true + Never + PreserveNewest + false diff --git a/src/Heartbeat/Program.cs b/src/Heartbeat/Program.cs index 9ea2b11..3a8d672 100644 --- a/src/Heartbeat/Program.cs +++ b/src/Heartbeat/Program.cs @@ -1,79 +1,82 @@ +using Heartbeat.Domain; using Heartbeat.Host.CommandLine; -using Heartbeat.Host.Extensions; +using Heartbeat.Host.Controllers; using Heartbeat.Runtime; +using Heartbeat.Runtime.Analyzers; using Heartbeat.Runtime.Domain; +using Heartbeat.Runtime.Extensions; +using Heartbeat.Runtime.Proxies; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; using Microsoft.Diagnostics.Runtime; +using Microsoft.Extensions.FileProviders; using System.CommandLine; +using System.Diagnostics; +using System.Globalization; using System.Net.Mime; -using System.Runtime.InteropServices; + +#if OPENAPI +using Heartbeat.Host.Extensions; using System.Text.Json.Serialization; +#endif + +using DumpInfo = Heartbeat.Host.Controllers.DumpInfo; +using HeapSegment = Heartbeat.Host.Controllers.HeapSegment; +using Module = Heartbeat.Host.Controllers.Module; +using ObjectTypeStatistics = Heartbeat.Host.Controllers.ObjectTypeStatistics; +using StringDuplicate = Heartbeat.Host.Controllers.StringDuplicate; +using StringInfo = Heartbeat.Host.Controllers.StringInfo; -#if DEBUG -if (Environment.GetEnvironmentVariable("HEARTBEAT_GENERATE_CONTRACTS") == "true") +PrintFiles(); + +#if OPENAPI +Console.WriteLine("Generating OpenAPI contract"); +var builder = WebApplication.CreateSlimBuilder(args); + +builder.Services.AddControllers().AddJsonOptions(options => { - var builder = WebApplication.CreateBuilder(args); + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); - builder.Services - .AddControllers() - .AddJsonOptions( - options => - { - // var enumConverter = new JsonStringEnumConverter(); - // options.JsonSerializerOptions.Converters.Add(enumConverter); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); - - builder.Services.AddSwagger(); - var app = builder.Build(); - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat"); - }); - app.MapControllers(); - app.Run(); - return; -} +// workaround for https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2550 +builder.Services.ConfigureHttpJsonOptions(options => +{ + // options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + // options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +builder.Services.AddSwagger(); + +var app = builder.Build(); +app.UseSwagger(); +MapEndpoints(app); +app.Run(); +return; #endif var (rootCommand, binder) = WebCommandOptions.RootCommand(); rootCommand.SetHandler((WebCommandOptions options) => MainWeb(options, args), binder); -//rootCommand.Add(AnalyzeCommandOptions.Command("analyze")); rootCommand.Invoke(args); -// TODO try native AOT - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0 static void MainWeb(WebCommandOptions options, string[] args) { -#if !DEBUG +#if !DEBUG && !AOT // fix for static files when running as dotnet tool string rootDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!; Directory.SetCurrentDirectory(rootDir); #endif - var builder = WebApplication.CreateBuilder(args); - - builder.Services - .AddControllers() - .AddJsonOptions( - options => - { - // var enumConverter = new JsonStringEnumConverter(); - // options.JsonSerializerOptions.Converters.Add(enumConverter); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); + var builder = WebApplication.CreateSlimBuilder(args); + + builder.Services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); + }); builder.Services.AddProblemDetails(); - builder.Services.AddSwagger(); builder.Services.AddOutputCache(); // TODO support auth @@ -82,40 +85,484 @@ static void MainWeb(WebCommandOptions options, string[] args) builder.Services.AddSingleton(runtimeContext); var app = builder.Build(); + +#if AOT + var fileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly, "ClientApp/build"); + app.UseDefaultFiles(new DefaultFilesOptions + { + FileProvider = fileProvider + }); + app.UseStaticFiles(new StaticFileOptions { FileProvider = fileProvider }); +#else app.UseDefaultFiles(); app.UseStaticFiles(); - app.UseSwagger(); - app.UseSwaggerUI(options => +#endif + app.UseStatusCodePages(async statusCodeContext + => await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode) + .ExecuteAsync(statusCodeContext.HttpContext)); + // app.UseExceptionHandler(exceptionHandlerApp => + // { + // exceptionHandlerApp.Run(async context => + // { + // context.Response.StatusCode = StatusCodes.Status500InternalServerError; + // context.Response.ContentType = MediaTypeNames.Application.Json; + // + // if (context.RequestServices.GetService() is { } problemDetailsService) + // { + // var exceptionHandlerFeature = context.Features.Get(); + // var exceptionType = exceptionHandlerFeature?.Error; + // await problemDetailsService.WriteAsync(new ProblemDetailsContext + // { + // HttpContext = context, + // ProblemDetails = + // { + // Title = "An error occurred while processing your request.", + // Detail = exceptionType?.Message, + // Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + // Status = 500 + // } + // }); + // } + // }); + // }); + app.UseOutputCache(); + MapEndpoints(app); + app.Run(); +} + +static void MapEndpoints(WebApplication app) +{ + var dumpGroup = app.MapGroup("api/dump") + .CacheOutput() + .WithTags("Dump") + .WithOpenApi(); + + dumpGroup.MapGet("info", DumpHandler.GetInfo) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetInfo"); + + dumpGroup.MapGet("modules", DumpHandler.GetModules) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetModules"); + + dumpGroup.MapGet("segments", DumpHandler.GetSegments) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSegments"); + + dumpGroup.MapGet("roots", DumpHandler.GetRoots) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetRoots"); + + dumpGroup.MapGet("heap-dump-statistics", DumpHandler.GetHeapDumpStat) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetHeapDumpStat"); + + dumpGroup.MapGet("strings", DumpHandler.GetStrings) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetStrings"); + + dumpGroup.MapGet("string-duplicates", DumpHandler.GetStringDuplicates) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetStringDuplicates"); + + dumpGroup.MapGet("object-instances/{mt}", DumpHandler.GetObjectInstances) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetObjectInstances"); + + dumpGroup.MapGet("arrays/sparse", DumpHandler.GetSparseArrays) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSparseArrays"); + + dumpGroup.MapGet("arrays/sparse/stat", DumpHandler.GetSparseArraysStat) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetSparseArraysStat"); + + dumpGroup.MapGet("object/{address}", DumpHandler.GetClrObject) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObject"); + + dumpGroup.MapGet("object/{address}/fields", DumpHandler.GetClrObjectFields) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObjectFields"); + + dumpGroup.MapGet("object/{address}/roots", DumpHandler.GetClrObjectRoots) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObjectRoots"); +} + +[Conditional("AOT")] +static void PrintFiles() +{ + IFileProvider fileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly); + var contents = fileProvider.GetDirectoryContents("/"); + using IEnumerator enumerator = contents.GetEnumerator(); + while (enumerator.MoveNext()) { - options.EnableTryItOutByDefault(); - options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat"); - }); - app.UseExceptionHandler(exceptionHandlerApp => + Console.WriteLine(enumerator.Current.Name); + if (enumerator.Current.Name == "Microsoft.Extensions.FileProviders.Embedded.Manifest.xml") + { + using var stream = enumerator.Current.CreateReadStream(); + using var reader = new StreamReader(stream); + Console.WriteLine(reader.ReadToEnd()); + } + } +} + +static class DumpHandler +{ + public static DumpInfo GetInfo([FromServices] RuntimeContext context) + { + var clrHeap = context.Heap; + var clrInfo = context.Runtime.ClrInfo; + var dataReader = clrInfo.DataTarget.DataReader; + + + var dumpInfo = new DumpInfo( + context.DumpPath, + clrHeap.CanWalkHeap, + clrHeap.IsServer, + clrInfo.ModuleInfo.FileName, + dataReader.Architecture, + dataReader.ProcessId, + dataReader.TargetPlatform.ToString(), + clrInfo.Version.ToString() + ); + + return dumpInfo; + } + + public static Module[] GetModules([FromServices] RuntimeContext context) + { + var modules = context.Runtime + .EnumerateModules() + .Select(m => new Module(m.Address, m.Size, m.Name)) + .ToArray(); + + return modules; + } + + public static IEnumerable GetSegments([FromServices] RuntimeContext context) { - exceptionHandlerApp.Run(async context => + var segments = + from s in context.Heap.Segments + select new HeapSegment( + s.Start, + s.End, + s.Kind + ); + + return segments; + } + + public static IEnumerable GetRoots([FromServices] RuntimeContext context, [FromQuery] ClrRootKind? kind = null) + { + return + from root in context.Heap.EnumerateRoots() + where kind == null || root.RootKind == kind + let objectType = root.Object.Type + select new RootInfo( + root.Object.Address, + root.RootKind, + root.IsPinned, + root.Object.Size, + objectType.MethodTable, + objectType.Name + ); + } + + public static IEnumerable GetHeapDumpStat( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + // TODO filter by just my code - how to filter Action? + // TODO filter by type name + { + var analyzer = new HeapDumpStatisticsAnalyzer(context) { ObjectGcStatus = gcStatus, Generation = generation }; + + var statistics = analyzer.GetObjectTypeStatistics() + .OrderByDescending(s => s.TotalSize) + .Select(s => new ObjectTypeStatistics(s.MethodTable, s.TypeName, s.TotalSize, s.InstanceCount)) + .ToArray(); + + return statistics; + } + + public static IEnumerable GetStrings( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + // TODO filter by min length + // TODO filter by max length + { + var query = from obj in context.EnumerateStrings(gcStatus, generation) + let str = obj.AsString() + let length = obj.ReadField("_stringLength") + select new StringInfo(obj.Address, length, obj.Size, str); + + // TODO limit output qty + return query; + } + + public static IEnumerable GetStringDuplicates( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + // TODO filter by min length + { + var analyzer = new StringDuplicateAnalyzer(context) { ObjectGcStatus = gcStatus, Generation = generation }; + + return analyzer.GetStringDuplicates() + .Select(sd => new StringDuplicate(sd.Value, sd.Count, sd.FullLength, sd.WastedMemory)); + } + + public static GetObjectInstancesResult GetObjectInstances( + [FromServices] RuntimeContext context, + ulong mt, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + // TODO limit maxCount + { + var methodTable = new MethodTable(mt); + + var clrType = context.Heap.FindTypeByMethodTable(methodTable); + + var instances = ( + from obj in context.EnumerateObjects(gcStatus, generation) + where obj.Type != null + && obj.Type.MethodTable == methodTable + orderby obj.Size descending + select new ObjectInstance + ( + new Address(obj.Address), + new Size(obj.Size) + ) + ).ToArray(); + + return new GetObjectInstancesResult(methodTable, clrType?.Name, instances); + } + + // TODO add arrays + // TODO add arrays/sparse + // TODO add arrays/sparse/stat + public static IEnumerable GetSparseArrays( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + { + var query = from obj in context.EnumerateObjects(gcStatus, generation) + where obj.IsArray + let proxy = new ArrayProxy(context, obj) + where proxy.UnusedItemsPercent >= 0.2 + orderby proxy.Wasted descending + select new ArrayInfo(obj.Address, obj.Type.MethodTable, obj.Type.Name, proxy.Length, proxy.UnusedItemsCount, + proxy.UnusedItemsPercent, proxy.Wasted); + + return query.Take(100); + } + + public static IEnumerable GetSparseArraysStat( + [FromServices] RuntimeContext context, + [FromQuery] ObjectGCStatus? gcStatus = null, + [FromQuery] Generation? generation = null) + { + var query = from obj in context.EnumerateObjects(gcStatus, generation) + where obj.IsArray + let proxy = new ArrayProxy(context, obj) + where proxy.UnusedItemsCount != 0 + group proxy by obj.Type.MethodTable + into grp + select new SparseArrayStatistics + ( + grp.Key, + grp.First().TargetObject.Type.Name, + grp.Count(), + Size.Sum(grp.Select(t => t.Wasted)) + ); + + return query; + } + + public static Results, NotFound> GetClrObject([FromServices] RuntimeContext context, ulong address) + { + var clrObject = context.Heap.GetObject(address); + if (clrObject.Type == null) + { + return TypedResults.NotFound(); + } + + var result = new GetClrObjectResult( + clrObject.Address, + clrObject.Type.Module.Name, + clrObject.Type.Name, + clrObject.Type.MethodTable, + clrObject.Size, + context.Heap.GetGeneration(clrObject.Address), + clrObject.Type.IsString ? clrObject.AsString() : null); + + return TypedResults.Ok(result); + } + + public static Results, NotFound> GetClrObjectFields([FromServices] RuntimeContext context, ulong address) + { + var clrObject = context.Heap.GetObject(address); + if (clrObject.Type == null) + { + return TypedResults.NotFound(); + } + + var fields = ( + from field in clrObject.Type.Fields + let mt = field.Type?.MethodTable ?? 0UL + let objectAddress = GetFieldObjectAddress(field, clrObject.Address) + let value = GetFieldValue(field, clrObject.Address) + select new ClrObjectField( + mt, + field.Type?.Name, + field.Offset, + field.IsValueType, + objectAddress, + value, + field.Name) + ).ToArray(); + + return TypedResults.Ok(fields); + } + + public static Results>, NotFound> GetClrObjectRoots([FromServices] RuntimeContext context, ulong address, CancellationToken ct) + { + var clrObject = context.Heap.GetObject(address); + if (clrObject.Type == null) + { + return TypedResults.NotFound(); + } + + var result = new List(); + GCRoot gcRoot = new(context.Heap, new[] { address }); + foreach ((ClrRoot root, GCRoot.ChainLink path) in gcRoot.EnumerateRootPaths(ct)) + { + var rootType = root.Object.Type!; + + var rootInfo = new RootInfo( + root.Object.Address, + root.RootKind, + root.IsPinned, + root.Object.Size, + rootType.MethodTable, + rootType.Name! + ); + + List pathItems = new(); + + GCRoot.ChainLink? current = path; + while (current != null) + { + var obj = context.Heap.GetObject(current.Object); + + var item = new RootPathItem( + obj.Address, + obj.Type!.MethodTable, + obj.Type.Name, + obj.Size, + context.Heap.GetGeneration(obj.Address)); + + pathItems.Add(item); + + current = current.Next; + } + + result.Add(new ClrObjectRootPath(rootInfo, pathItems)); + // TODO get only one root path + break; + } + + return TypedResults.Ok(result); + } + + private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address) + { + if (field.Type?.IsObjectReference ?? false) + { + return new Address(field.ReadObject(address, false).Address); + } + + return null; + } + + private static string GetFieldValue(ClrInstanceField field, ulong address) + { + if (field.IsPrimitive) + { + return field.ElementType switch + { + ClrElementType.Boolean => field.Read(address, false).ToString(), + ClrElementType.Char => field.Read(address, false).ToString(), + ClrElementType.Int8 => field.Read(address, false).ToString(), + ClrElementType.UInt8 => field.Read(address, false).ToString(), + ClrElementType.Int16 => field.Read(address, false).ToString(), + ClrElementType.UInt16 => field.Read(address, false).ToString(), + ClrElementType.Int32 => field.Read(address, false).ToString(), + ClrElementType.UInt32 => field.Read(address, false).ToString(), + ClrElementType.Int64 => field.Read(address, false).ToString(), + ClrElementType.UInt64 => field.Read(address, false).ToString(), + ClrElementType.Float => field.Read(address, false).ToString(CultureInfo.InvariantCulture), + ClrElementType.Double => field.Read(address, false).ToString(CultureInfo.InvariantCulture), + ClrElementType.NativeInt => field.Read(address, false).ToString(), + ClrElementType.NativeUInt => field.Read(address, false).ToString(), + _ => throw new ArgumentOutOfRangeException($"Unable to get primitive value for {field.ElementType} field") + }; + } + + return GetAddress(); + + string GetAddress() { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = MediaTypeNames.Application.Json; + if (field.Type?.IsEnum ?? false) + { + ClrEnum enumField = field.Type.AsEnum(); + // TODO handle other types + if (enumField.ElementType == ClrElementType.Int32) + { + var fieldValue = field.Read(address, false); + var name = enumField.EnumerateValues() + .FirstOrDefault(v => (int)v.Value == fieldValue) + .Name; + + return !string.IsNullOrEmpty(name) + ? name + : fieldValue.ToString(); + } + } - if (context.RequestServices.GetService() is { } problemDetailsService) + if (field.Type?.IsObjectReference ?? false) { - var exceptionHandlerFeature = context.Features.Get(); - var exceptionType = exceptionHandlerFeature?.Error; - await problemDetailsService.WriteAsync(new ProblemDetailsContext + ClrObject clrObject = field.ReadObject(address, false); + if (clrObject.IsNull) + { + return ""; + } + + if (clrObject.Type?.IsString ?? false) + { + return clrObject.AsString(100); + } + + if (clrObject is { IsNull: false, Type.Name: "System.Version" }) { - HttpContext = context, - ProblemDetails = - { - Title = "An error occurred while processing your request.", - Detail = exceptionType?.Message, - Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1", - Status = 500 - } - }); + var major = clrObject.ReadField("_Major"); + var minor = clrObject.ReadField("_Minor"); + var build = clrObject.ReadField("_Build"); + var revision = clrObject.ReadField("_Revision"); + var version = new Version(major, minor, build, revision); + return version.ToString(); + } + + return clrObject.Address.ToString("x16"); } - }); - }); - app.UseOutputCache(); - app.MapControllers(); - app.Run(); + + return string.Empty; + } + } } \ No newline at end of file