Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add arrays grid #13

Merged
merged 4 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>0.2.0</VersionPrefix>
<VersionPrefix>0.3.0</VersionPrefix>
<RepositoryUrl>https://github.com/Ne4to/Heartbeat</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Heartbeat
Diagnostics utility to analyze memory dumps of a .NET application
[![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

## Getting started

```shell
dotnet tool install --global Heartbeat
# optional
export PATH=$PATH:$HOME/.dotnet/tools
heartbeat --dump <path-to-dump-file>
```
Open `http://localhost:5000/` in web browser.
See [UI screen]([https://](https://github.com/Ne4to/Heartbeat/tree/master/assets)) for examples
See [UI screen](https://github.com/Ne4to/Heartbeat/tree/master/assets) for examples

<!---
TODO: update description
Expand Down
9 changes: 9 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<a href="01-dashboard.jpeg"><img src="01-dashboard.jpeg" alt="Dashboard" width="30%"/><a>
<a href="02-heap-dump.jpeg"><img src="02-heap-dump.jpeg" alt="Heap dump" width="30%"/><a>
<a href="03-segments.jpeg"><img src="03-segments.jpeg" alt="Segments" width="30%"/><a>
<a href="04-roots.jpeg"><img src="04-roots.jpeg" alt="Roots" width="30%"/><a>
<a href="05-modules.jpeg"><img src="05-modules.jpeg" alt="Modules" width="30%"/><a>
<a href="06-strings.jpeg"><img src="06-strings.jpeg" alt="Strings" width="30%"/><a>
<a href="07-string-duplicates.jpeg"><img src="07-string-duplicates.jpeg" alt="String duplicates" width="30%"/><a>
<a href="08-single-objest.jpeg"><img src="08-single-objest.jpeg" alt="Single object" width="30%"/><a>
<a href="09-object-list.jpeg"><img src="09-object-list.jpeg" alt="Object list" width="30%"/><a>
10 changes: 8 additions & 2 deletions scripts/reinstall-dev-tool.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ 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

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
# TODO get VersionPrefix from Directory.Build.props
$PackageVersion = "0.2.0-$VersionSuffix"
$PackageVersion = "$VersionPrefix-$VersionSuffix"
dotnet tool install --global --add-source ./src/Heartbeat/nupkg Heartbeat --version $PackageVersion
}
catch {
Expand Down
87 changes: 87 additions & 0 deletions src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.Diagnostics.Runtime;

namespace Heartbeat.Runtime.Extensions
{
public static class ClrValueTypeExtensions
{
public static bool IsDefaultValue(this ClrValueType valueType)
{
if (valueType.Type == null)
{
return true;
}

foreach (var field in valueType.Type.Fields)
{
if (field.IsObjectReference)
{
if (!field.ReadObject(valueType.Address, true).IsNull)
{
return false;
}
}
else if (field.IsPrimitive)
{
if (!IsValueDefault(valueType.Address, field))
{
return false;
}
}
else if (field.ElementType == ClrElementType.Struct)
{
var fieldValue = field.ReadStruct(valueType.Address, true);
if (!fieldValue.IsDefaultValue())
{
return false;
}
}
else if (field.ElementType == ClrElementType.Pointer)
{
if (!IsZeroPtr(valueType.Address, field))
{
return false;
}
}
else
{
throw new InvalidOperationException(
"Unexpected field, it non of IsObjectReference | IsValueType | IsPrimitive");
}
}

return true;
}

private static bool IsValueDefault(ulong objRef, ClrInstanceField field)
{
return field.ElementType switch
{
ClrElementType.Boolean => field.Read<bool>(objRef, true) == false,
ClrElementType.Char => field.Read<char>(objRef, true) == (char)0,
ClrElementType.Int8 => field.Read<sbyte>(objRef, true) == (sbyte)0,
ClrElementType.UInt8 => field.Read<byte>(objRef, true) == (byte)0,
ClrElementType.Int16 => field.Read<short>(objRef, true) == (short)0,
ClrElementType.UInt16 => field.Read<ushort>(objRef, true) == (ushort)0,
ClrElementType.Int32 => field.Read<int>(objRef, true) == 0,
ClrElementType.UInt32 => field.Read<int>(objRef, true) == (uint)0,
ClrElementType.Int64 => field.Read<long>(objRef, true) == 0L,
ClrElementType.UInt64 => field.Read<ulong>(objRef, true) == 0UL,
ClrElementType.Float => field.Read<float>(objRef, true) == 0f,
ClrElementType.Double => field.Read<double>(objRef, true) == 0d,
ClrElementType.NativeInt => field.Read<nint>(objRef, true) == nint.Zero,
ClrElementType.NativeUInt => field.Read<nuint>(objRef, true) == nuint.Zero,
_ => throw new ArgumentOutOfRangeException()
};
}

private static bool IsZeroPtr(ulong objRef, ClrInstanceField field)
{
return field.Type.Name switch
{
"System.UIntPtr" => field.Read<UIntPtr>(objRef, true) == UIntPtr.Zero,
"System.IntPtr" => field.Read<IntPtr>(objRef, true) == IntPtr.Zero,
_ => throw new ArgumentException($"Unknown Pointer type: {field.Type.Name}")
};
}
}
}
24 changes: 23 additions & 1 deletion src/Heartbeat.Runtime/Proxies/ArrayProxy.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
using Heartbeat.Runtime.Extensions;

using Microsoft.Diagnostics.Runtime;

namespace Heartbeat.Runtime.Proxies;

public sealed class ArrayProxy : ProxyBase
{
private ClrArray _clrArray;
private readonly Lazy<int> _unusedItemsCount;

public int Length => _clrArray.Length;

public int UnusedItemsCount => _unusedItemsCount.Value;
public double UnusedItemsPercent => (double)UnusedItemsCount / Length;
public Size Wasted => new Size((ulong)(_clrArray.Type.ComponentSize * UnusedItemsCount));

public ArrayProxy(RuntimeContext context, ClrObject targetObject)
: base(context, targetObject)
{
_clrArray = TargetObject.AsArray();
_unusedItemsCount = new Lazy<int>(GetUnusedItemsCount);
}

public ArrayProxy(RuntimeContext context, ulong address)
: base(context, address)
{
_clrArray = TargetObject.AsArray();
_unusedItemsCount = new Lazy<int>(GetUnusedItemsCount);
}

public string?[] GetStringArray()
Expand Down Expand Up @@ -105,7 +115,7 @@ public static IEnumerable<ClrObject> EnumerateObjectItems(ClrArray array)
}
}
}

public static IEnumerable<ClrValueType> EnumerateValueTypes(ClrArray array)
{
var length = array.Length;
Expand Down Expand Up @@ -143,4 +153,16 @@ public static IEnumerable<ClrValueType> EnumerateValueTypes(ClrArray array)
}
}
}

private int GetUnusedItemsCount()
{
if (_clrArray.Type.ComponentType?.IsValueType ?? false)
{
return EnumerateValueTypes(_clrArray)
.Count(t => t.IsDefaultValue());
}

return EnumerateObjectItems(_clrArray)
.Count(t => t.IsNull);
}
}
65 changes: 65 additions & 0 deletions src/Heartbeat/ClientApp/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,39 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GetObjectInstancesResult'
/api/dump/arrays:
get:
tags:
- Dump
summary: Get arrays
description: Get arrays
operationId: GetArrays
parameters:
- name: traversingMode
in: query
style: form
schema:
$ref: '#/components/schemas/TraversingHeapModes'
- name: generation
in: query
style: form
schema:
$ref: '#/components/schemas/Generation'
responses:
'500':
description: Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ArrayInfo'
'/api/dump/object/{address}':
get:
tags:
Expand Down Expand Up @@ -321,6 +354,38 @@ components:
- Armv6
- Ppc64le
type: string
ArrayInfo:
required:
- address
- length
- methodTable
- unusedItemsCount
- unusedPercent
- wasted
type: object
properties:
address:
type: integer
format: int64
methodTable:
type: integer
format: int64
typeName:
type: string
nullable: true
length:
type: integer
format: int32
unusedItemsCount:
type: integer
format: int32
unusedPercent:
type: number
format: double
wasted:
type: integer
format: int64
additionalProperties: false
ClrObjectField:
required:
- isValueType
Expand Down
2 changes: 2 additions & 0 deletions src/Heartbeat/ClientApp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import objectInstances from './objectInstances'
import clrObject from './clrObject'
import roots from './roots'
import modules from './modules'
import arraysGrid from './arraysGrid'
import stringsGrid from './stringsGrid'
import stringDuplicates from './stringDuplicates'
import {AlertContext} from './contexts/alertContext';
Expand Down Expand Up @@ -58,6 +59,7 @@ const App = () => {
<Resource name='roots' {...roots} />
<Resource name='modules' {...modules} />
<Resource name='clr-object' {...clrObject} />
<Resource name='arrays' {...arraysGrid} />
<Resource name='strings' {...stringsGrid} />
<Resource name='string-duplicates' {...stringDuplicates} />
</Admin>
Expand Down
Loading
Loading