Skip to content

Commit f868a93

Browse files
authored
feat: add new UI pages (#12)
* feat: filter GC heap stat by generation * feat: show error for http request * feat: add dump info * feat: add strings * feat: add roots * fix: address column style * feat: add object root path * feat: add UI screens
1 parent 501f8e8 commit f868a93

File tree

78 files changed

+2606
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2606
-440
lines changed

Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<PropertyGroup>
10-
<VersionPrefix>0.1.0</VersionPrefix>
10+
<VersionPrefix>0.2.0</VersionPrefix>
1111
<RepositoryUrl>https://github.com/Ne4to/Heartbeat</RepositoryUrl>
1212
<PublishRepositoryUrl>true</PublishRepositoryUrl>
1313
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Heartbeat.sln

+34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{677CC7ED-C15
2525
EndProject
2626
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugHost", "src\DebugHost\DebugHost.csproj", "{F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}"
2727
EndProject
28+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{88EB58BA-FB0F-4A15-B366-BB033820126B}"
29+
ProjectSection(SolutionItems) = preProject
30+
scripts\reinstall-dev-tool.ps1 = scripts\reinstall-dev-tool.ps1
31+
scripts\update-ts-client.ps1 = scripts\update-ts-client.ps1
32+
EndProjectSection
33+
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalRepo", "tests\MinimalRepo\MinimalRepo.csproj", "{3D11554D-E09C-4710-B071-D90BB2447F46}"
35+
EndProject
2836
Global
2937
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3038
Debug|Any CPU = Debug|Any CPU
@@ -161,6 +169,30 @@ Global
161169
{F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x64.Build.0 = Release|Any CPU
162170
{F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.ActiveCfg = Release|Any CPU
163171
{F1FF76E5-3DEE-4C64-8A62-8A645B981D1D}.Release|x86.Build.0 = Release|Any CPU
172+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
173+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|Any CPU.Build.0 = Debug|Any CPU
174+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|arm64.ActiveCfg = Debug|Any CPU
175+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|arm64.Build.0 = Debug|Any CPU
176+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|x64.ActiveCfg = Debug|Any CPU
177+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|x64.Build.0 = Debug|Any CPU
178+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|x86.ActiveCfg = Debug|Any CPU
179+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Debug|x86.Build.0 = Debug|Any CPU
180+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|Any CPU.ActiveCfg = Debug|Any CPU
181+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|Any CPU.Build.0 = Debug|Any CPU
182+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|arm64.ActiveCfg = Debug|Any CPU
183+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|arm64.Build.0 = Debug|Any CPU
184+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|x64.ActiveCfg = Debug|Any CPU
185+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|x64.Build.0 = Debug|Any CPU
186+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|x86.ActiveCfg = Debug|Any CPU
187+
{3D11554D-E09C-4710-B071-D90BB2447F46}.DebugLocal|x86.Build.0 = Debug|Any CPU
188+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|Any CPU.ActiveCfg = Release|Any CPU
189+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|Any CPU.Build.0 = Release|Any CPU
190+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|arm64.ActiveCfg = Release|Any CPU
191+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|arm64.Build.0 = Release|Any CPU
192+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x64.ActiveCfg = Release|Any CPU
193+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x64.Build.0 = Release|Any CPU
194+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.ActiveCfg = Release|Any CPU
195+
{3D11554D-E09C-4710-B071-D90BB2447F46}.Release|x86.Build.0 = Release|Any CPU
164196
EndGlobalSection
165197
GlobalSection(SolutionProperties) = preSolution
166198
HideSolutionNode = FALSE
@@ -171,6 +203,8 @@ Global
171203
{9E63F5A0-7695-474C-A946-64D75F8D9617} = {677CC7ED-C157-4885-884A-5C88B08A90C6}
172204
{D4060CFE-8141-49CE-99A5-559599D0E6B4} = {677CC7ED-C157-4885-884A-5C88B08A90C6}
173205
{F1FF76E5-3DEE-4C64-8A62-8A645B981D1D} = {E52617F0-FB17-4C0C-A70A-26A3C11A8647}
206+
{88EB58BA-FB0F-4A15-B366-BB033820126B} = {BE3A3B9E-B429-4D93-8E8D-9A23CCA4DA19}
207+
{3D11554D-E09C-4710-B071-D90BB2447F46} = {E52617F0-FB17-4C0C-A70A-26A3C11A8647}
174208
EndGlobalSection
175209
GlobalSection(ExtensibilityGlobals) = postSolution
176210
SolutionGuid = {9BC4B059-33F1-4B7C-B5D9-DA6D2F1E5076}

README.md

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# Heartbeat
22
Diagnostics utility to analyze memory dumps of a .NET application
3-
4-
## Installation
53
[![NuGet Badge](https://buildstats.info/nuget/heartbeat?includePreReleases=true&dWidth=0)](https://www.nuget.org/packages/Heartbeat/)
6-
```
4+
5+
## Getting started
6+
7+
```shell
78
dotnet tool install --global Heartbeat
9+
heartbeat --dump <path-to-dump-file>
810
```
11+
Open `http://localhost:5000/` in web browser.
12+
See [UI screen]([https://](https://github.com/Ne4to/Heartbeat/tree/master/assets)) for examples
913

14+
<!---
15+
TODO: update description
1016
## Summary
1117
1218
The purpose of the Heartbeat is finding runtime issues of .NET application in the production environment such as spontaneous high memory / CPU usage, high latency and so on.
@@ -36,11 +42,11 @@ Issue Finder example:
3642
- Find hot stack traces;
3743
- Find hung System.Threading.Tasks.Task objects;
3844
- Find System.Threading.Tasks.Task state.
39-
45+
-->
4046
## Usage
4147

4248
```
43-
Heartbeat [options]
49+
heartbeat [options]
4450
4551
Options:
4652
--dump <dump> (REQUIRED) Path to a dump file
@@ -49,4 +55,7 @@ Options:
4955
--ignore-dac-mismatch Ignore mismatches between DAC versions
5056
--version Show version information
5157
-?, -h, --help Show help and usage information
52-
```
58+
```
59+
<!---
60+
TODO: add screens
61+
-->

assets/01-dashboard.jpeg

103 KB
Loading

assets/02-heap-dump.jpeg

402 KB
Loading

assets/03-segments.jpeg

355 KB
Loading

assets/04-roots.jpeg

507 KB
Loading

assets/05-modules.jpeg

581 KB
Loading

assets/06-strings.jpeg

737 KB
Loading

assets/07-string-duplicates.jpeg

534 KB
Loading

assets/08-single-objest.jpeg

839 KB
Loading

assets/09-object-list.jpeg

285 KB
Loading

nuget.config

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
5+
<clear />
6+
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
7+
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
8+
</packageSources>
9+
</configuration>

scripts/reinstall-dev-tool.ps1

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ try
1212
Get-Date -Format ''
1313
$VersionSuffix = "rc.$(Get-Date -Format 'yyyy-MM-dd-HHmm')"
1414
dotnet pack --version-suffix $VersionSuffix
15-
$PackageVersion = "0.1.0-$VersionSuffix"
15+
# TODO get VersionPrefix from Directory.Build.props
16+
$PackageVersion = "0.2.0-$VersionSuffix"
1617
dotnet tool install --global --add-source ./src/Heartbeat/nupkg Heartbeat --version $PackageVersion
1718
}
1819
catch {

scripts/update-ts-client.ps1

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
$ErrorActionPreference = "Stop"
22

33
$RepositoryRoot = Split-Path $PSScriptRoot
4-
$FrontendRoot = Join-Path $RepositoryRoot 'src/Heartbeat.WebUI/ClientApp'
4+
$FrontendRoot = Join-Path $RepositoryRoot 'src/Heartbeat/ClientApp'
55
$ContractPath = Join-Path $FrontendRoot 'api.yml'
6-
$DllPath = Join-Path $RepositoryRoot 'src/Heartbeat.WebUI/bin/Debug/net8.0/Heartbeat.WebUI.dll'
6+
$DllPath = Join-Path $RepositoryRoot 'src/Heartbeat/bin/Debug/net8.0/Heartbeat.dll'
77

88
Push-Location
99
try
@@ -14,6 +14,7 @@ try
1414
dotnet build --configuration Debug
1515

1616
Set-Location $FrontendRoot
17+
$env:HEARTBEAT_GENERATE_CONTRACTS = 'true'
1718
dotnet swagger tofile --yaml --output $ContractPath $DllPath Heartbeat
1819
dotnet kiota generate -l typescript --openapi $ContractPath -c HeartbeatClient -o ./src/client
1920

@@ -25,4 +26,5 @@ catch {
2526
}
2627
finally {
2728
Pop-Location
29+
$env:HEARTBEAT_GENERATE_CONTRACTS = $null
2830
}

src/DebugHost/DebugHost.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.456101" />
8+
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.506101" />
99
</ItemGroup>
1010

1111
<ItemGroup>

src/Heartbeat.Runtime/Analyzers/ObjectTypeStatisticsAnalyzer.cs src/Heartbeat.Runtime/Analyzers/HeapDumpStatisticsAnalyzer.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
using Heartbeat.Runtime.Analyzers.Interfaces;
33
using Heartbeat.Runtime.Extensions;
44

5+
using Microsoft.Diagnostics.Runtime;
56
using Microsoft.Extensions.Logging;
67

78
namespace Heartbeat.Runtime.Analyzers;
89

9-
public sealed class ObjectTypeStatisticsAnalyzer : AnalyzerBase, ILoggerDump, IWithTraversingHeapMode
10+
public sealed class HeapDumpStatisticsAnalyzer : AnalyzerBase, ILoggerDump, IWithTraversingHeapMode
1011
{
1112
public TraversingHeapModes TraversingHeapMode { get; set; } = TraversingHeapModes.All;
13+
public Generation? Generation { get; set; }
1214

13-
public ObjectTypeStatisticsAnalyzer(RuntimeContext context) : base(context)
15+
public HeapDumpStatisticsAnalyzer(RuntimeContext context) : base(context)
1416
{
1517
}
1618

@@ -34,7 +36,7 @@ private void WriteLog(ILogger logger, int topTypeCount)
3436
public IReadOnlyCollection<ObjectTypeStatistics> GetObjectTypeStatistics()
3537
{
3638
return (
37-
from obj in Context.EnumerateObjects(TraversingHeapMode)
39+
from obj in Context.EnumerateObjects(TraversingHeapMode, Generation)
3840
let objSize = obj.Size
3941
//group new { size = objSize } by type.Name into g
4042
group objSize by obj.Type

src/Heartbeat.Runtime/Analyzers/LongStringAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private void WriteLog(ILogger logger, TraversingHeapModes traversingMode)
2727
private IEnumerable<ClrObject> GetLongestStrings(int count, TraversingHeapModes traversingMode)
2828
{
2929
var query =
30-
from clrObject in Context.EnumerateObjectsByTypeName("System.String", traversingMode)
30+
from clrObject in Context.EnumerateStrings(traversingMode)
3131
orderby clrObject.Size descending
3232
select clrObject;
3333

src/Heartbeat.Runtime/Analyzers/StringDuplicateAnalyzer.cs

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
using Heartbeat.Runtime.Analyzers.Interfaces;
22
using Heartbeat.Runtime.Extensions;
33

4+
using Microsoft.Diagnostics.Runtime;
45
using Microsoft.Extensions.Logging;
56

67
namespace Heartbeat.Runtime.Analyzers;
78

89
public sealed class StringDuplicateAnalyzer : AnalyzerBase, ILoggerDump, IWithTraversingHeapMode
910
{
1011
public TraversingHeapModes TraversingHeapMode { get; set; } = TraversingHeapModes.All;
12+
public Generation? Generation { get; set; }
1113

1214
public StringDuplicateAnalyzer(RuntimeContext context) : base(context)
1315
{
@@ -38,22 +40,37 @@ private void LogStringDuplicates(ILogger logger,
3840
}
3941
}
4042

43+
internal record struct StringDuplicateInfo(int Count, int FullLength)
44+
{
45+
public StringDuplicateInfo IncrementCount() => this with { Count = Count + 1 };
46+
};
47+
4148
public IReadOnlyList<StringDuplicate> GetStringDuplicates()
4249
{
43-
var stringCount = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
50+
var stringCount = new Dictionary<string, StringDuplicateInfo>(StringComparer.OrdinalIgnoreCase);
4451

4552
var query =
46-
from clrObject in Context.EnumerateObjectsByTypeName("System.String", TraversingHeapMode)
47-
select clrObject.AsString();
53+
from clrObject in Context.EnumerateStrings(TraversingHeapMode, Generation)
54+
select clrObject;
4855

4956
foreach (var stringInstance in query)
5057
{
51-
stringCount.IncrementValue(stringInstance);
58+
var stringValue = stringInstance.AsString()!;
59+
60+
if (stringCount.TryGetValue(stringValue, out var currentValue))
61+
{
62+
stringCount[stringValue] = currentValue.IncrementCount();
63+
}
64+
else
65+
{
66+
var fullLength = stringInstance.ReadField<int>("_stringLength");
67+
stringCount[stringValue] = new StringDuplicateInfo(1, fullLength);
68+
}
5269
}
53-
70+
5471
return stringCount
55-
.Where(kvp => kvp.Value > 1)
56-
.Select(kvp => new StringDuplicate(kvp.Key, kvp.Value, kvp.Key.Length))
72+
.Where(kvp => kvp.Value.Count > 1)
73+
.Select(kvp => new StringDuplicate(kvp.Key, kvp.Value.Count, kvp.Value.FullLength))
5774
.ToArray();
5875
}
5976

@@ -72,7 +89,7 @@ public IReadOnlyCollection<StringDuplicate> GetStringDuplicates(int minDuplicate
7289
continue;
7390
}
7491

75-
result.Add(new StringDuplicate(stringValue.Truncate(truncateLength), instanceCount, stringDuplicate.Length));
92+
result.Add(new StringDuplicate(stringValue.Truncate(truncateLength), instanceCount, stringDuplicate.FullLength));
7693
}
7794

7895
return result;

src/Heartbeat.Runtime/Domain/Analyzers.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
public record DumpInfo(string DumpFileName, string DacFileName, bool CanWalkHeap);
44
public record ObjectInfo(Address Address, TypeInfo Type);
55
public record HttpClientInfo(Address Address, TimeSpan Timeout);
6-
public record StringDuplicate(string Value, int Count, int Length);
6+
7+
public record StringDuplicate(string Value, int Count, int FullLength)
8+
{
9+
public Size WastedMemory { get; } = new((ulong)((Count - 1) * (
10+
FullLength * sizeof(char) // chars
11+
+ sizeof(int) // int _stringLength field
12+
+ sizeof(char) // char _firstChar field
13+
+ IntPtr.Size * 2 // object header (method table + syncblk)
14+
)));
15+
};
16+
717
public record ObjectTypeStatistics(MethodTable MethodTable, string TypeName, Size TotalSize, int InstanceCount);
818
public record CancellationTokenSourceInfo(bool CanBeCanceled, bool IsCancellationRequested, bool IsCancellationCompleted);
919
public record TimerQueueTimerInfo(Address Address, uint DueTime, uint Period, bool Cancelled, CancellationTokenSourceInfo? CancellationState);

src/Heartbeat.Runtime/Extensions/ClrHeapExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,9 @@ public static ClrModule GetModuleByFileName(this ClrHeap heap, string fileName)
7676

7777
return null;
7878
}
79+
80+
public static Generation GetGeneration(this ClrHeap heap, ulong address)
81+
{
82+
return heap.GetSegmentByAddress(address)?.GetGeneration(address) ?? Generation.Unknown;
83+
}
7984
}

src/Heartbeat.Runtime/HeapIndex.cs

+36-27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Heartbeat.Runtime.Proxies;
2+
13
using Microsoft.Diagnostics.Runtime;
24

35
namespace Heartbeat.Runtime;
@@ -26,13 +28,11 @@ public HeapIndex(ClrHeap heap)
2628
{
2729
// Pop an object, ignore it if we've seen it before.
2830
var address = eval.Pop();
29-
if (_walkableFromRoot.Contains(address))
31+
if (!_walkableFromRoot.Add(address))
3032
{
3133
continue;
3234
}
3335

34-
_walkableFromRoot.Add(address);
35-
3636
// Grab the type. We will only get null here in the case of heap corruption.
3737
ClrType? type = heap.GetObjectType(address);
3838
if (type == null)
@@ -54,41 +54,50 @@ public HeapIndex(ClrHeap heap)
5454

5555
void EnumerateArrayElements(ulong address)
5656
{
57-
//heap.Runtime.DacLibrary.OwningLibrary.
58-
5957
var obj = heap.GetObject(address);
6058
var array = obj.AsArray();
61-
throw new NotImplementedException();
62-
//var componentType = ((ClrmdArrayType)array.typ).ComponentType;
63-
64-
//if (componentType.IsObjectReference)
65-
//{
66-
// foreach (var arrayElement in ArrayProxy.EnumerateObjectItems(array))
67-
// {
68-
// if (!arrayElement.IsNull)
69-
// {
70-
// AddReference(address, arrayElement.Address);
71-
// eval.Push(arrayElement.Address);
72-
// }
73-
// }
74-
//}
75-
//else
76-
//{
77-
// // throw new NotSupportedException($"Enumerating array of {componentType} type is not supported");
78-
//}
59+
if (array.Type.ComponentType?.IsObjectReference ?? false)
60+
{
61+
foreach (var arrayElement in ArrayProxy.EnumerateObjectItems(array))
62+
{
63+
if (arrayElement is { IsNull: false, IsValid: true })
64+
{
65+
AddReference(address, arrayElement.Address);
66+
eval.Push(arrayElement.Address);
67+
}
68+
}
69+
}
70+
else if (array.Type.ComponentType?.IsValueType ?? false)
71+
{
72+
// TODO test and compare with WinDbg / dotnet dump
73+
foreach (var arrayElement in ArrayProxy.EnumerateValueTypes(array))
74+
{
75+
if (arrayElement.IsValid && arrayElement.Type != null)
76+
{
77+
EnumerateFields(arrayElement.Type, arrayElement.Address, address);
78+
}
79+
}
80+
}
81+
else
82+
{
83+
throw new NotSupportedException($"Enumerating array of {array.Type.ComponentType} type is not supported");
84+
}
7985
}
8086

81-
void EnumerateFields(ClrType? type, ulong address)
87+
void EnumerateFields(ClrType type, ulong objectAddress, ulong? parentAddress = null)
8288
{
83-
8489
foreach (var instanceField in type.Fields)
8590
{
8691
if (instanceField.IsObjectReference)
8792
{
88-
var fieldObject = instanceField.ReadObject(address, false);
93+
var fieldObject = instanceField.ReadObject(objectAddress, false);
8994
if (!fieldObject.IsNull)
9095
{
91-
AddReference(address, fieldObject.Address);
96+
AddReference(objectAddress, fieldObject.Address);
97+
if (parentAddress != null)
98+
{
99+
AddReference(parentAddress.Value, fieldObject.Address);
100+
}
92101
eval.Push(fieldObject.Address);
93102
}
94103
}

0 commit comments

Comments
 (0)