diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml
new file mode 100644
index 0000000..978e456
--- /dev/null
+++ b/.github/workflows/native-aot.yml
@@ -0,0 +1,50 @@
+name: Publish native AOT
+
+on:
+ push:
+ branches: [ disabled ]
+
+jobs:
+ build-aot:
+ name: Build Native AOT
+ strategy:
+ matrix:
+ # https://github.com/actions/runner-images
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ 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
+ # TODO join steps
+ - name: 'Upload Artifact linux-x64'
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Heartbeat-linux-x64
+ path: artifacts/linux-x64/
+ retention-days: 1
+ - name: 'Upload Artifact win-x64'
+ if: ${{ matrix.os == 'windows-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Heartbeat-win-x64
+ path: artifacts/win-x64/
+ retention-days: 1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..dde65aa
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,73 @@
+name: Create release
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+
+jobs:
+ build-aot:
+ name: Build Native AOT
+ strategy:
+ matrix:
+ # https://github.com/actions/runner-images
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ 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
+ # TODO join steps
+ - name: 'Upload Artifact linux-x64'
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Heartbeat-linux-x64
+ path: artifacts/linux-x64/
+ retention-days: 1
+ - name: 'Upload Artifact win-x64'
+ if: ${{ matrix.os == 'windows-latest' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: Heartbeat-win-x64
+ path: artifacts/win-x64/
+ retention-days: 1
+
+ release:
+ needs: build-aot
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ # https://github.com/actions/download-artifact
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: release-artifacts
+ merge-multiple: true
+ # https://github.com/softprops/action-gh-release
+ - name: Release
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ generate_release_notes: true
+ files: release-artifacts/**
+
+ # TODO try https://github.com/marketplace/actions/release-drafter
diff --git a/Directory.Build.props b/Directory.Build.props
index 2ac07e6..f538a75 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,7 +7,7 @@
- 0.4.0
+ 0.5.0
https://github.com/Ne4to/Heartbeat
true
MIT
@@ -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.draft.md b/README.draft.md
new file mode 100644
index 0000000..4194b4d
--- /dev/null
+++ b/README.draft.md
@@ -0,0 +1,93 @@
+## Troubleshooting
+
+Unable to connect to [symbol server](https://github.com/microsoft/clrmd/blob/437022b361da20cf5f02d401a01c5e2c6c366097/src/Microsoft.Diagnostics.Runtime/Implementation/SymbolServer.cs#L17)
+[clrmd docs](https://github.com/microsoft/clrmd/blob/437022b361da20cf5f02d401a01c5e2c6c366097/doc/GettingStarted.md?plain=1#L67)
+
+```
+Unhandled exception: System.AggregateException: One or more errors occurred. (The SSL connection could not be established, see inner exception.)
+ ---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
+ ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot
+```
+
+TODO find minimum requirement
+
+`apt update & apt install -y curl`
+
+```
+Selecting previously unselected package openssl.
+(Reading database ... 4393 files and directories currently installed.)
+Preparing to unpack .../00-openssl_3.0.2-0ubuntu1.13_amd64.deb ...
+Unpacking openssl (3.0.2-0ubuntu1.13) ...
+Selecting previously unselected package ca-certificates.
+Preparing to unpack .../01-ca-certificates_20230311ubuntu0.22.04.1_all.deb ...
+Unpacking ca-certificates (20230311ubuntu0.22.04.1) ...
+Selecting previously unselected package libnghttp2-14:amd64.
+Preparing to unpack .../02-libnghttp2-14_1.43.0-1ubuntu0.1_amd64.deb ...
+Unpacking libnghttp2-14:amd64 (1.43.0-1ubuntu0.1) ...
+Selecting previously unselected package libpsl5:amd64.
+Preparing to unpack .../03-libpsl5_0.21.0-1.2build2_amd64.deb ...
+Unpacking libpsl5:amd64 (0.21.0-1.2build2) ...
+Selecting previously unselected package publicsuffix.
+Preparing to unpack .../04-publicsuffix_20211207.1025-1_all.deb ...
+Unpacking publicsuffix (20211207.1025-1) ...
+Selecting previously unselected package libbrotli1:amd64.
+Preparing to unpack .../05-libbrotli1_1.0.9-2build6_amd64.deb ...
+Unpacking libbrotli1:amd64 (1.0.9-2build6) ...
+Selecting previously unselected package libsasl2-modules-db:amd64.
+Preparing to unpack .../06-libsasl2-modules-db_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ...
+Unpacking libsasl2-modules-db:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Selecting previously unselected package libsasl2-2:amd64.
+Preparing to unpack .../07-libsasl2-2_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ...
+Unpacking libsasl2-2:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Selecting previously unselected package libldap-2.5-0:amd64.
+Preparing to unpack .../08-libldap-2.5-0_2.5.16+dfsg-0ubuntu0.22.04.1_amd64.deb ...
+Unpacking libldap-2.5-0:amd64 (2.5.16+dfsg-0ubuntu0.22.04.1) ...
+Selecting previously unselected package librtmp1:amd64.
+Preparing to unpack .../09-librtmp1_2.4+20151223.gitfa8646d.1-2build4_amd64.deb ...
+Unpacking librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
+Selecting previously unselected package libssh-4:amd64.
+Preparing to unpack .../10-libssh-4_0.9.6-2ubuntu0.22.04.3_amd64.deb ...
+Unpacking libssh-4:amd64 (0.9.6-2ubuntu0.22.04.3) ...
+Selecting previously unselected package libcurl4:amd64.
+Preparing to unpack .../11-libcurl4_7.81.0-1ubuntu1.15_amd64.deb ...
+Unpacking libcurl4:amd64 (7.81.0-1ubuntu1.15) ...
+Selecting previously unselected package curl.
+Preparing to unpack .../12-curl_7.81.0-1ubuntu1.15_amd64.deb ...
+Unpacking curl (7.81.0-1ubuntu1.15) ...
+Selecting previously unselected package libldap-common.
+Preparing to unpack .../13-libldap-common_2.5.16+dfsg-0ubuntu0.22.04.1_all.deb ...
+Unpacking libldap-common (2.5.16+dfsg-0ubuntu0.22.04.1) ...
+Selecting previously unselected package libsasl2-modules:amd64.
+Preparing to unpack .../14-libsasl2-modules_2.1.27+dfsg2-3ubuntu1.2_amd64.deb ...
+Unpacking libsasl2-modules:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Setting up libpsl5:amd64 (0.21.0-1.2build2) ...
+Setting up libbrotli1:amd64 (1.0.9-2build6) ...
+Setting up libsasl2-modules:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Setting up libnghttp2-14:amd64 (1.43.0-1ubuntu0.1) ...
+Setting up libldap-common (2.5.16+dfsg-0ubuntu0.22.04.1) ...
+Setting up libsasl2-modules-db:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Setting up librtmp1:amd64 (2.4+20151223.gitfa8646d.1-2build4) ...
+Setting up libsasl2-2:amd64 (2.1.27+dfsg2-3ubuntu1.2) ...
+Setting up libssh-4:amd64 (0.9.6-2ubuntu0.22.04.3) ...
+Setting up openssl (3.0.2-0ubuntu1.13) ...
+Setting up publicsuffix (20211207.1025-1) ...
+Setting up libldap-2.5-0:amd64 (2.5.16+dfsg-0ubuntu0.22.04.1) ...
+Setting up ca-certificates (20230311ubuntu0.22.04.1) ...
+debconf: unable to initialize frontend: Dialog
+debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78.)
+debconf: falling back to frontend: Readline
+debconf: unable to initialize frontend: Readline
+debconf: (Can't locate Term/ReadLine.pm in @INC (you may need to install the Term::ReadLine module) (@INC contains: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.34.0 /usr/local/share/perl/5.34.0 /usr/lib/x86_64-linux-gnu/perl5/5.34 /usr/share/perl5 /usr/lib/x86_64-l
+inux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.34 /usr/share/perl/5.34 /usr/local/lib/site_perl) at /usr/share/perl5/Debconf/FrontEnd/Readline.pm line 7.)
+debconf: falling back to frontend: Teletype
+Updating certificates in /etc/ssl/certs...
+137 added, 0 removed; done.
+Setting up libcurl4:amd64 (7.81.0-1ubuntu1.15) ...
+Setting up curl (7.81.0-1ubuntu1.15) ...
+Processing triggers for libc-bin (2.35-0ubuntu3.6) ...
+Processing triggers for ca-certificates (20230311ubuntu0.22.04.1) ...
+Updating certificates in /etc/ssl/certs...
+0 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d...
+done.
+```
diff --git a/README.md b/README.md
index 662973c..a9fd0ed 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,14 @@
# 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 .NET application memory dump
## Getting started
+### dotnet tool
+
+.NET 8 SDK is required
+
```shell
dotnet tool install --global Heartbeat
# optional
@@ -12,7 +16,9 @@ export PATH=$PATH:$HOME/.dotnet/tools
heartbeat --dump
```
Open `http://localhost:5000/` in web browser.
-See [UI screen](https://github.com/Ne4to/Heartbeat/tree/master/assets) for examples
+
+### Native AOT
+Heartbeat is also available in [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) version. You can download it from the [latest release](https://github.com/Ne4to/Heartbeat/releases/latest)
-## Usage
+### Using Heartbeat
```
heartbeat [options]
@@ -59,6 +65,9 @@ Options:
--version Show version information
-?, -h, --help Show help and usage information
```
-
+
+See [UI screen](https://github.com/Ne4to/Heartbeat/tree/master/assets) for examples.
+
+### Listening endpoint
+
+Use [ASPNETCORE_URLS](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-8.0) environment variable to change default endpoint (`export ASPNETCORE_URLS=http://0.0.0.0:5555` or `$env:ASPNETCORE_URLS='http://127.0.0.1:5555'`)
diff --git a/scripts/clean-artifacts.ps1 b/scripts/clean-artifacts.ps1
new file mode 100644
index 0000000..26cb73d
--- /dev/null
+++ b/scripts/clean-artifacts.ps1
@@ -0,0 +1,10 @@
+$RepositoryRoot = Split-Path $PSScriptRoot
+$ArtifactsRoot = Join-Path $RepositoryRoot 'artifacts'
+
+Remove-Item $ArtifactsRoot -Force -Recurse -ErrorAction SilentlyContinue
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter bin | Remove-Item -Force -Recurse
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter obj | Remove-Item -Force -Recurse
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter app | Remove-Item -Force -Recurse
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter nupkg | Remove-Item -Force -Recurse
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter build | Remove-Item -Force -Recurse
+Get-ChildItem $RepositoryRoot -Directory -Recurse -Filter node_modules | Remove-Item -Force -Recurse
diff --git a/scripts/publish-native-aot.ps1 b/scripts/publish-native-aot.ps1
new file mode 100644
index 0000000..6e079c5
--- /dev/null
+++ b/scripts/publish-native-aot.ps1
@@ -0,0 +1,76 @@
+$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
+$SpaRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp'
+$PublishProject = Join-Path $RepositoryRoot 'src/Heartbeat/Heartbeat.csproj'
+$ArtifactsRoot = Join-Path $RepositoryRoot 'artifacts'
+
+Push-Location
+try {
+ $SpaRoot
+ Set-Location $SpaRoot
+ npm install
+ npm run build
+
+ 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 native AOT version for $Runtime to $OutDir"
+ dotnet publish --configuration $Configuration --runtime $Runtime --output $OutDir $PublishProject
+ Assert-ExitCode
+
+ Write-Host "Files in $OutDir"
+ Get-ChildItem $OutDir | Select-Object -Property Length,Name
+ }
+
+ # 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 449f0b0..cb74db9 100644
--- a/scripts/reinstall-dev-tool.ps1
+++ b/scripts/reinstall-dev-tool.ps1
@@ -1,10 +1,23 @@
$ErrorActionPreference = "Stop"
+function Assert-ExitCode {
+ if (-not $?) {
+ throw 'Latest command failed'
+ }
+}
+
$RepositoryRoot = Split-Path $PSScriptRoot
+$SpaRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp'
+$PublishProject = Join-Path $RepositoryRoot 'src/Heartbeat/Heartbeat.csproj'
Push-Location
try
{
+ $SpaRoot
+ Set-Location $SpaRoot
+ npm install
+ npm run build
+
Set-Location $RepositoryRoot
[xml]$XmlConfig = Get-Content 'Directory.Build.props'
@@ -16,11 +29,14 @@ try
dotnet tool uninstall -g Heartbeat
dotnet clean --configuration Release
- Get-Date -Format ''
$VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')"
- dotnet pack --version-suffix $VersionSuffix
+ dotnet publish $PublishProject
+ Assert-ExitCode
+ dotnet pack --version-suffix $VersionSuffix $PublishProject
+ 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
@@ -28,4 +44,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 b77dc6e..f29ff4c 100644
--- a/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj
+++ b/src/Heartbeat.Runtime/Heartbeat.Runtime.csproj
@@ -3,6 +3,10 @@
+
+ true
+
+
@@ -11,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/Endpoints/EndpointJsonSerializerContext.cs b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs
new file mode 100644
index 0000000..2cae3ac
--- /dev/null
+++ b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+
+namespace Heartbeat.Host.Endpoints;
+
+[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 EndpointJsonSerializerContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..376877e
--- /dev/null
+++ b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs
@@ -0,0 +1,69 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Heartbeat.Host.Endpoints;
+
+internal static class EndpointRouteBuilderExtensions
+{
+ public static void MapDumpEndpoints(this IEndpointRouteBuilder app)
+ {
+ var dumpGroup = app.MapGroup("api/dump")
+ .CacheOutput()
+ .WithTags("Dump")
+ .WithOpenApi();
+
+ dumpGroup.MapGet("info", RouteHandlers.GetInfo)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetInfo");
+
+ dumpGroup.MapGet("modules", RouteHandlers.GetModules)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetModules");
+
+ dumpGroup.MapGet("segments", RouteHandlers.GetSegments)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetSegments");
+
+ dumpGroup.MapGet("roots", RouteHandlers.GetRoots)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetRoots");
+
+ dumpGroup.MapGet("heap-dump-statistics", RouteHandlers.GetHeapDumpStat)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetHeapDumpStat");
+
+ dumpGroup.MapGet("strings", RouteHandlers.GetStrings)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetStrings");
+
+ dumpGroup.MapGet("string-duplicates", RouteHandlers.GetStringDuplicates)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetStringDuplicates");
+
+ dumpGroup.MapGet("object-instances/{mt}", RouteHandlers.GetObjectInstances)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetObjectInstances");
+
+ dumpGroup.MapGet("arrays/sparse", RouteHandlers.GetSparseArrays)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetSparseArrays");
+
+ dumpGroup.MapGet("arrays/sparse/stat", RouteHandlers.GetSparseArraysStat)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetSparseArraysStat");
+
+ dumpGroup.MapGet("object/{address}", RouteHandlers.GetClrObject)
+ .Produces(StatusCodes.Status404NotFound)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetClrObject");
+
+ dumpGroup.MapGet("object/{address}/fields", RouteHandlers.GetClrObjectFields)
+ .Produces(StatusCodes.Status404NotFound)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetClrObjectFields");
+
+ dumpGroup.MapGet("object/{address}/roots", RouteHandlers.GetClrObjectRoots)
+ .Produces(StatusCodes.Status404NotFound)
+ .Produces(StatusCodes.Status500InternalServerError)
+ .WithName("GetClrObjectRoots");
+ }
+}
\ No newline at end of file
diff --git a/src/Heartbeat/Controllers/Models.cs b/src/Heartbeat/Endpoints/Models.cs
similarity index 97%
rename from src/Heartbeat/Controllers/Models.cs
rename to src/Heartbeat/Endpoints/Models.cs
index 06ccb75..e1dea58 100644
--- a/src/Heartbeat/Controllers/Models.cs
+++ b/src/Heartbeat/Endpoints/Models.cs
@@ -2,7 +2,7 @@
using System.Runtime.InteropServices;
-namespace Heartbeat.Host.Controllers;
+namespace Heartbeat.Host.Endpoints;
// ReSharper disable NotAccessedPositionalProperty.Global
public record DumpInfo(
@@ -40,7 +40,7 @@ public record ClrObjectField(
bool IsValueType,
ulong? ObjectAddress,
string Value,
- string Name);
+ string? Name);
public record Module(ulong Address, ulong Size, string? Name);
@@ -56,6 +56,7 @@ 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);
diff --git a/src/Heartbeat/Controllers/DumpController.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs
similarity index 63%
rename from src/Heartbeat/Controllers/DumpController.cs
rename to src/Heartbeat/Endpoints/RouteHandlers.cs
index fc96a50..e35eeca 100644
--- a/src/Heartbeat/Controllers/DumpController.cs
+++ b/src/Heartbeat/Endpoints/RouteHandlers.cs
@@ -5,62 +5,40 @@
using Heartbeat.Runtime.Extensions;
using Heartbeat.Runtime.Proxies;
+using Microsoft.AspNetCore.Http.HttpResults;
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;
- }
+namespace Heartbeat.Host.Endpoints;
- [HttpGet]
- [Route("info")]
- [ProducesResponseType(typeof(DumpInfo), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get dump info")]
- public DumpInfo GetInfo()
+internal static class RouteHandlers
+{
+ public static DumpInfo GetInfo([FromServices] RuntimeContext context)
{
- var clrHeap = _context.Heap;
- var clrInfo = _context.Runtime.ClrInfo;
+ var clrHeap = context.Heap;
+ var clrInfo = context.Runtime.ClrInfo;
var dataReader = clrInfo.DataTarget.DataReader;
+
- return new DumpInfo(
- _context.DumpPath,
+ var dumpInfo = new DumpInfo(
+ context.DumpPath,
clrHeap.CanWalkHeap,
clrHeap.IsServer,
clrInfo.ModuleInfo.FileName,
dataReader.Architecture,
dataReader.ProcessId,
dataReader.TargetPlatform.ToString(),
- clrInfo.Version.ToString(2)
+ clrInfo.Version.ToString()
);
+
+ return dumpInfo;
}
- [HttpGet]
- [Route("modules")]
- [ProducesResponseType(typeof(Module[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get modules")]
- public IEnumerable GetModules()
+ public static Module[] GetModules([FromServices] RuntimeContext context)
{
- var modules = _context.Runtime
+ var modules = context.Runtime
.EnumerateModules()
.Select(m => new Module(m.Address, m.Size, m.Name))
.ToArray();
@@ -68,30 +46,23 @@ public IEnumerable GetModules()
return modules;
}
- [HttpGet]
- [Route("segments")]
- [ProducesResponseType(typeof(HeapSegment[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get segments")]
- public IEnumerable GetSegments()
+ public static IEnumerable GetSegments([FromServices] RuntimeContext context)
{
var segments =
- from s in _context.Heap.Segments
+ from s in context.Heap.Segments
select new HeapSegment(
s.Start,
s.End,
- s.Kind);
+ s.Kind
+ );
return segments;
}
- [HttpGet]
- [Route("roots")]
- [ProducesResponseType(typeof(RootInfo[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get heap roots")]
- public IEnumerable GetRoots([FromQuery] ClrRootKind? kind = null)
+ public static IEnumerable GetRoots([FromServices] RuntimeContext context, [FromQuery] ClrRootKind? kind = null)
{
return
- from root in _context.Heap.EnumerateRoots()
+ from root in context.Heap.EnumerateRoots()
where kind == null || root.RootKind == kind
let objectType = root.Object.Type
select new RootInfo(
@@ -104,17 +75,14 @@ from root in _context.Heap.EnumerateRoots()
);
}
- [HttpGet]
- [Route("heap-dump-statistics")]
- [ProducesResponseType(typeof(ObjectTypeStatistics[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get heap dump statistics")]
- public IEnumerable GetHeapDumpStat(
+ 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 analyzer = new HeapDumpStatisticsAnalyzer(context) { ObjectGcStatus = gcStatus, Generation = generation };
var statistics = analyzer.GetObjectTypeStatistics()
.OrderByDescending(s => s.TotalSize)
@@ -124,17 +92,14 @@ public IEnumerable GetHeapDumpStat(
return statistics;
}
- [HttpGet]
- [Route("strings")]
- [ProducesResponseType(typeof(StringInfo[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get heap dump statistics")]
- public IEnumerable GetStrings(
+ 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)
+ 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);
@@ -143,26 +108,20 @@ public IEnumerable GetStrings(
return query;
}
- [HttpGet]
- [Route("string-duplicates")]
- [ProducesResponseType(typeof(StringDuplicate[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get string duplicates")]
- public IEnumerable GetStringDuplicates(
+ 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 };
+ 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(
+ public static GetObjectInstancesResult GetObjectInstances(
+ [FromServices] RuntimeContext context,
ulong mt,
[FromQuery] ObjectGCStatus? gcStatus = null,
[FromQuery] Generation? generation = null)
@@ -170,10 +129,10 @@ public GetObjectInstancesResult GetObjectInstances(
{
var methodTable = new MethodTable(mt);
- var clrType = _context.Heap.FindTypeByMethodTable(methodTable);
+ var clrType = context.Heap.FindTypeByMethodTable(methodTable);
var instances = (
- from obj in _context.EnumerateObjects(gcStatus, generation)
+ from obj in context.EnumerateObjects(gcStatus, generation)
where obj.Type != null
&& obj.Type.MethodTable == methodTable
orderby obj.Size descending
@@ -187,20 +146,17 @@ orderby obj.Size descending
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(
+ public static IEnumerable GetSparseArrays(
+ [FromServices] RuntimeContext context,
[FromQuery] ObjectGCStatus? gcStatus = null,
[FromQuery] Generation? generation = null)
{
- var query = from obj in _context.EnumerateObjects(gcStatus, generation)
+ var query = from obj in context.EnumerateObjects(gcStatus, generation)
where obj.IsArray
- let proxy = new ArrayProxy(_context, obj)
+ 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,
@@ -209,17 +165,14 @@ orderby proxy.Wasted descending
return query.Take(100);
}
- [HttpGet]
- [Route("arrays/sparse/stat")]
- [ProducesResponseType(typeof(SparseArrayStatistics[]), StatusCodes.Status200OK)]
- [SwaggerOperation(summary: "Get arrays")]
- public IEnumerable GetSparseArraysStat(
+ public static IEnumerable GetSparseArraysStat(
+ [FromServices] RuntimeContext context,
[FromQuery] ObjectGCStatus? gcStatus = null,
[FromQuery] Generation? generation = null)
{
- var query = from obj in _context.EnumerateObjects(gcStatus, generation)
+ var query = from obj in context.EnumerateObjects(gcStatus, generation)
where obj.IsArray
- let proxy = new ArrayProxy(_context, obj)
+ let proxy = new ArrayProxy(context, obj)
where proxy.UnusedItemsCount != 0
group proxy by obj.Type.MethodTable
into grp
@@ -234,17 +187,12 @@ into grp
return query;
}
- [HttpGet]
- [Route("object/{address}")]
- [ProducesResponseType(typeof(GetClrObjectResult), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [SwaggerOperation(summary: "Get object")]
- public IActionResult GetClrObject(ulong address)
+ public static Results, NotFound> GetClrObject([FromServices] RuntimeContext context, ulong address)
{
- var clrObject = _context.Heap.GetObject(address);
+ var clrObject = context.Heap.GetObject(address);
if (clrObject.Type == null)
{
- return NotFound();
+ return TypedResults.NotFound();
}
var result = new GetClrObjectResult(
@@ -253,23 +201,18 @@ public IActionResult GetClrObject(ulong address)
clrObject.Type.Name,
clrObject.Type.MethodTable,
clrObject.Size,
- _context.Heap.GetGeneration(clrObject.Address),
+ context.Heap.GetGeneration(clrObject.Address),
clrObject.Type.IsString ? clrObject.AsString() : null);
- return Ok(result);
+ return TypedResults.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)
+ public static Results, NotFound> GetClrObjectFields([FromServices] RuntimeContext context, ulong address)
{
- var clrObject = _context.Heap.GetObject(address);
+ var clrObject = context.Heap.GetObject(address);
if (clrObject.Type == null)
{
- return NotFound();
+ return TypedResults.NotFound();
}
var fields = (
@@ -287,24 +230,19 @@ from field in clrObject.Type.Fields
field.Name)
).ToArray();
- return Ok(fields);
+ return TypedResults.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)
+ public static Results>, NotFound> GetClrObjectRoots([FromServices] RuntimeContext context, ulong address, CancellationToken ct)
{
- var clrObject = _context.Heap.GetObject(address);
+ var clrObject = context.Heap.GetObject(address);
if (clrObject.Type == null)
{
- return NotFound();
+ return TypedResults.NotFound();
}
var result = new List();
- GCRoot gcRoot = new(_context.Heap, new[] { address });
+ GCRoot gcRoot = new(context.Heap, new[] { address });
foreach ((ClrRoot root, GCRoot.ChainLink path) in gcRoot.EnumerateRootPaths(ct))
{
var rootType = root.Object.Type!;
@@ -323,14 +261,14 @@ public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct)
GCRoot.ChainLink? current = path;
while (current != null)
{
- var obj = _context.Heap.GetObject(current.Object);
+ 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));
+ context.Heap.GetGeneration(obj.Address));
pathItems.Add(item);
@@ -342,7 +280,7 @@ public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct)
break;
}
- return Ok(result);
+ return TypedResults.Ok(result);
}
private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address)
@@ -386,12 +324,12 @@ string GetAddress()
if (field.Type?.IsEnum ?? false)
{
ClrEnum enumField = field.Type.AsEnum();
- // TODO handle other types
+ // TODO handle other enum base types
if (enumField.ElementType == ClrElementType.Int32)
{
var fieldValue = field.Read(address, false);
var name = enumField.EnumerateValues()
- .FirstOrDefault(v => (int)v.Value == fieldValue)
+ .FirstOrDefault(v => v.Value != null && (int)v.Value == fieldValue)
.Name;
return !string.IsNullOrEmpty(name)
@@ -410,7 +348,7 @@ string GetAddress()
if (clrObject.Type?.IsString ?? false)
{
- return clrObject.AsString(100);
+ return clrObject.AsString(100) ?? string.Empty;
}
if (clrObject is { IsNull: false, Type.Name: "System.Version" })
diff --git a/src/Heartbeat/Extensions/SwaggerExtensions.cs b/src/Heartbeat/Extensions/SwaggerExtensions.cs
index 70cbff7..95b4f4b 100644
--- a/src/Heartbeat/Extensions/SwaggerExtensions.cs
+++ b/src/Heartbeat/Extensions/SwaggerExtensions.cs
@@ -1,9 +1,11 @@
+#if OPENAPI
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Diagnostics;
using System.Reflection;
namespace Heartbeat.Host.Extensions;
@@ -88,6 +90,7 @@ public void Apply(OpenApiSchema model, SchemaFilterContext context)
}
}
+ [Conditional("DEBUG")]
private static void FixNullableProperties(OpenApiSchema schema, SchemaFilterContext context)
{
foreach (var property in schema.Properties)
@@ -115,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 8abf4e5..8071128 100644
--- a/src/Heartbeat/Heartbeat.csproj
+++ b/src/Heartbeat/Heartbeat.csproj
@@ -14,6 +14,21 @@
npm start
+
+ DEBUG;OPENAPI
+
+ true
+
+
+
+ true
+ true
+ RELEASE;AOT
+ true
+ false
+ true
+
+
true
true
@@ -24,62 +39,59 @@
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
-
+
wwwroot\%(RecursiveDir)%(FileName)%(Extension)
PreserveNewest
@@ -88,4 +100,4 @@
-
+
\ No newline at end of file
diff --git a/src/Heartbeat/Program.cs b/src/Heartbeat/Program.cs
index 11c08de..2b8c055 100644
--- a/src/Heartbeat/Program.cs
+++ b/src/Heartbeat/Program.cs
@@ -1,106 +1,108 @@
using Heartbeat.Host.CommandLine;
-using Heartbeat.Host.Extensions;
+using Heartbeat.Host.Endpoints;
using Heartbeat.Runtime;
-using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.Extensions.FileProviders;
using System.CommandLine;
-using System.Net.Mime;
+using System.Diagnostics;
+
+#if OPENAPI
+using Heartbeat.Host.Extensions;
using System.Text.Json.Serialization;
+#endif
-if (Environment.GetEnvironmentVariable("HEARTBEAT_GENERATE_CONTRACTS") == "true")
+#if OPENAPI
+Console.WriteLine("Generating OpenAPI contract");
+var builder = WebApplication.CreateSlimBuilder(args);
+
+// workaround for https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2550
+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);
- });
-
- builder.Services.AddSwagger();
- var app = builder.Build();
- app.UseSwagger();
- app.UseSwaggerUI(options =>
- {
- options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat");
- });
- app.MapControllers();
- app.Run();
- return;
-}
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, EndpointJsonSerializerContext.Default);
+});
+
+builder.Services.AddSwagger();
+
+var app = builder.Build();
+app.UseSwagger();
+app.MapDumpEndpoints();
+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);
- });
+ var builder = WebApplication.CreateSlimBuilder(args);
+
+ builder.Services.ConfigureHttpJsonOptions(options =>
+ {
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, EndpointJsonSerializerContext.Default);
+ });
builder.Services.AddProblemDetails();
- builder.Services.AddSwagger();
builder.Services.AddOutputCache();
// TODO support auth
-// TODO setup listening port
var runtimeContext = new RuntimeContext(options.Dump.FullName, options.DacPath?.FullName, options.IgnoreDacMismatch ?? false);
builder.Services.AddSingleton(runtimeContext);
var app = builder.Build();
- app.UseDefaultFiles();
- app.UseStaticFiles();
- app.UseSwagger();
- app.UseSwaggerUI(options =>
- {
- options.EnableTryItOutByDefault();
- options.SwaggerEndpoint("Heartbeat/swagger.yaml", "Heartbeat");
- });
- 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
- }
- });
- }
- });
+#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();
+#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();
- app.MapControllers();
+ app.MapDumpEndpoints();
app.Run();
-}
\ No newline at end of file
+}
diff --git a/src/Heartbeat/temp.props b/src/Heartbeat/temp.props
new file mode 100644
index 0000000..25eecce
--- /dev/null
+++ b/src/Heartbeat/temp.props
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wwwroot\%(RecursiveDir)%(FileName)%(Extension)
+ PreserveNewest
+ true
+
+
+
\ No newline at end of file