Skip to content

Commit

Permalink
feat: show array elements (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ne4to authored Jan 30, 2024
1 parent fcb457c commit aad2d26
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 53 deletions.
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.5.0</VersionPrefix>
<VersionPrefix>0.6.0</VersionPrefix>
<RepositoryUrl>https://github.com/Ne4to/Heartbeat</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
50 changes: 50 additions & 0 deletions src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.Diagnostics.Runtime;

using System.Globalization;

namespace Heartbeat.Runtime.Extensions
{
public static class ClrValueTypeExtensions
Expand Down Expand Up @@ -51,6 +53,32 @@ public static bool IsDefaultValue(this ClrValueType valueType)

return true;
}

public static string GetValueAsString(this ClrValueType valueType)
{
if (valueType.Type == null)
{
return "<unknown type>";
}

if (valueType.Type.IsObjectReference)
return "<object>";

if (valueType.Type.IsPrimitive)
{
if (valueType.Type.Fields.Length == 1)
{
return GetValueAsString(valueType.Address, valueType.Type.Fields[0]);
}

return $"primitive type with {valueType.Type.Fields.Length} fields";
}

if (valueType.Type.IsValueType)
return "<struct>";

return valueType.Type.Name ?? "<unknown type>";
}

private static bool IsValueDefault(ulong objRef, ClrInstanceField field)
{
Expand All @@ -73,6 +101,28 @@ private static bool IsValueDefault(ulong objRef, ClrInstanceField field)
_ => throw new ArgumentOutOfRangeException()
};
}

private static string GetValueAsString(ulong objRef, ClrInstanceField field)
{
return field.ElementType switch
{
ClrElementType.Boolean => field.Read<bool>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Char => field.Read<char>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Int8 => field.Read<sbyte>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.UInt8 => field.Read<byte>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Int16 => field.Read<short>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.UInt16 => field.Read<ushort>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Int32 => field.Read<int>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.UInt32 => field.Read<int>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Int64 => field.Read<long>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.UInt64 => field.Read<ulong>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Float => field.Read<float>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.Double => field.Read<double>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.NativeInt => field.Read<nint>(objRef, true).ToString(CultureInfo.InvariantCulture),
ClrElementType.NativeUInt => field.Read<nuint>(objRef, true).ToString(CultureInfo.InvariantCulture),
_ => throw new ArgumentOutOfRangeException($"Unable to get primitive value for {field.ElementType} field")
};
}

private static bool IsZeroPtr(ulong objRef, ClrInstanceField field)
{
Expand Down
59 changes: 58 additions & 1 deletion src/Heartbeat.Runtime/Proxies/ArrayProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

using Microsoft.Diagnostics.Runtime;

using System.Text;

namespace Heartbeat.Runtime.Proxies;

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

public ClrArray InnerArray => _clrArray;
public int Length => _clrArray.Length;

public int UnusedItemsCount => _unusedItemsCount.Value;
Expand Down Expand Up @@ -165,4 +168,58 @@ private int GetUnusedItemsCount()
return EnumerateObjectItems(_clrArray)
.Count(t => t.IsNull);
}
}

public IEnumerable<ArrayItem> EnumerateArrayElements()
{
// TODO set real index
int index = 0;

if (_clrArray.Type.ComponentType?.IsObjectReference ?? false)
{
foreach (var arrayElement in EnumerateObjectItems(_clrArray))
{
string? value = arrayElement.Type?.IsString ?? false
? arrayElement.AsString()
: "<object>";

yield return new ArrayItem(index++, new Address(arrayElement.Address), value);
}
}
else if (_clrArray.Type.ComponentType?.IsValueType ?? false)
{
// TODO use _clrArray.ReadValues for IsPrimitive == true

// TODO test and compare with WinDbg / dotnet dump
foreach (var arrayElement in EnumerateValueTypes(_clrArray))
{
// Support value type on UI, return MethodTable
// !DumpVC <MethodTable address> <Address>
// new Address(arrayElement.Address)
// Context.Heap.GetObject(arrayElement.Address, arrayElement.Type).Type.Fields.Single(f => f.Name == "runningValue").GetAddress(arrayElement.Address, true).ToString("x8")
yield return new ArrayItem(index++, Address.Null, arrayElement.GetValueAsString());
}
}
else
{
throw new NotSupportedException($"Enumerating array of {_clrArray.Type.ComponentType} type is not supported");
}
}

public string? AsStringValue()
{
if (_clrArray.Type.ComponentType?.ElementType == ClrElementType.UInt8)
{
var bytes = _clrArray.ReadValues<byte>(0, _clrArray.Length);
if (bytes != null)
{
return Encoding.UTF8.GetString(bytes);
}
}

// read char[] as string

return null;
}
}

public record struct ArrayItem(int Index, Address Address, string? Value);
54 changes: 53 additions & 1 deletion src/Heartbeat/ClientApp/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,42 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
'/api/dump/object/{address}/as-array':
get:
tags:
- Dump
operationId: GetClrObjectAsArray
parameters:
- name: address
in: path
required: true
style: simple
schema:
type: integer
format: int64
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ClrObjectArrayItem'
'204':
description: No Content
'404':
description: Not Found
content:
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:
Expand Down Expand Up @@ -429,11 +465,26 @@ components:
type: integer
format: int64
additionalProperties: false
ClrObjectArrayItem:
required:
- address
- index
- value
type: object
properties:
index:
type: integer
format: int32
address:
type: integer
format: int64
value:
type: string
additionalProperties: false
ClrObjectField:
required:
- isValueType
- methodTable
- name
- offset
- value
type: object
Expand All @@ -457,6 +508,7 @@ components:
type: string
name:
type: string
nullable: true
additionalProperties: false
ClrObjectRootPath:
required:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* tslint:disable */
/* eslint-disable */
// Generated by Microsoft Kiota
import { createClrObjectArrayItemFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, serializeProblemDetails, type ClrObjectArrayItem, type ProblemDetails } from '../../../../../models/';
import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions';

/**
* Builds and executes requests for operations under /api/dump/object/{address}/as-array
*/
export class AsArrayRequestBuilder extends BaseRequestBuilder<AsArrayRequestBuilder> {
/**
* Instantiates a new AsArrayRequestBuilder and sets the default values.
* @param pathParameters The raw url or the Url template parameters for the request.
* @param requestAdapter The request adapter to use to execute the requests.
*/
public constructor(pathParameters: Record<string, unknown> | string | undefined, requestAdapter: RequestAdapter) {
super(pathParameters, requestAdapter, "{+baseurl}/api/dump/object/{address}/as-array", (x, y) => new AsArrayRequestBuilder(x, y));
}
/**
* @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options.
* @returns a Promise of ClrObjectArrayItem
*/
public get(requestConfiguration?: RequestConfiguration<object> | undefined) : Promise<ClrObjectArrayItem[] | undefined> {
const requestInfo = this.toGetRequestInformation(
requestConfiguration
);
const errorMapping = {
"404": createProblemDetailsFromDiscriminatorValue,
"500": createProblemDetailsFromDiscriminatorValue,
} as Record<string, ParsableFactory<Parsable>>;
return this.requestAdapter.sendCollectionAsync<ClrObjectArrayItem>(requestInfo, createClrObjectArrayItemFromDiscriminatorValue, errorMapping);
}
/**
* @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options.
* @returns a RequestInformation
*/
public toGetRequestInformation(requestConfiguration?: RequestConfiguration<object> | undefined) : RequestInformation {
const requestInfo = new RequestInformation(HttpMethod.GET, this.urlTemplate, this.pathParameters);
requestInfo.configure(requestConfiguration);
requestInfo.headers.tryAdd("Accept", "application/json");
return requestInfo;
}
}
/* tslint:enable */
/* eslint-enable */
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable */
// Generated by Microsoft Kiota
import { createGetClrObjectResultFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, serializeProblemDetails, type GetClrObjectResult, type ProblemDetails } from '../../../../models/';
import { AsArrayRequestBuilder } from './asArray/';
import { FieldsRequestBuilder } from './fields/';
import { RootsRequestBuilder } from './roots/';
import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions';
Expand All @@ -10,6 +11,12 @@ import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type
* Builds and executes requests for operations under /api/dump/object/{address}
*/
export class WithAddressItemRequestBuilder extends BaseRequestBuilder<WithAddressItemRequestBuilder> {
/**
* The asArray property
*/
public get asArray(): AsArrayRequestBuilder {
return new AsArrayRequestBuilder(this.pathParameters, this.requestAdapter);
}
/**
* The fields property
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Heartbeat/ClientApp/src/client/kiota-lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"descriptionHash": "2680CD432C0BAF2EAD8335B87589C6613B8A32E7D90AAF6EC468A74F00CDC479E9320B872E8B6E0C6AE5CDD8A0F2251B0991E07026BB56A11771AE55760D7F36",
"descriptionHash": "C2E02AEF66804BDF43A6B9DAE16266E2193A8418F53798D9AD2BBD832A9C33A511C8C9D02638AB3C5C3D249C515A2D838D5564A4A22D99E716D918BFFAB95F77",
"descriptionLocation": "..\\..\\api.yml",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.10.1",
Expand Down
29 changes: 29 additions & 0 deletions src/Heartbeat/ClientApp/src/client/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ export interface ArrayInfo extends Parsable {
*/
wasted?: number;
}
export interface ClrObjectArrayItem extends Parsable {
/**
* The address property
*/
address?: number;
/**
* The index property
*/
index?: number;
/**
* The value property
*/
value?: string;
}
export interface ClrObjectField extends Parsable {
/**
* The isValueType property
Expand Down Expand Up @@ -78,6 +92,9 @@ export type ClrRootKind = (typeof ClrRootKindObject)[keyof typeof ClrRootKindObj
export function createArrayInfoFromDiscriminatorValue(parseNode: ParseNode | undefined) {
return deserializeIntoArrayInfo;
}
export function createClrObjectArrayItemFromDiscriminatorValue(parseNode: ParseNode | undefined) {
return deserializeIntoClrObjectArrayItem;
}
export function createClrObjectFieldFromDiscriminatorValue(parseNode: ParseNode | undefined) {
return deserializeIntoClrObjectField;
}
Expand Down Expand Up @@ -134,6 +151,13 @@ export function deserializeIntoArrayInfo(arrayInfo: ArrayInfo | undefined = {} a
"wasted": n => { arrayInfo.wasted = n.getNumberValue(); },
}
}
export function deserializeIntoClrObjectArrayItem(clrObjectArrayItem: ClrObjectArrayItem | undefined = {} as ClrObjectArrayItem) : Record<string, (node: ParseNode) => void> {
return {
"address": n => { clrObjectArrayItem.address = n.getNumberValue(); },
"index": n => { clrObjectArrayItem.index = n.getNumberValue(); },
"value": n => { clrObjectArrayItem.value = n.getStringValue(); },
}
}
export function deserializeIntoClrObjectField(clrObjectField: ClrObjectField | undefined = {} as ClrObjectField) : Record<string, (node: ParseNode) => void> {
return {
"isValueType": n => { clrObjectField.isValueType = n.getBooleanValue(); },
Expand Down Expand Up @@ -486,6 +510,11 @@ export function serializeArrayInfo(writer: SerializationWriter, arrayInfo: Array
writer.writeNumberValue("unusedPercent", arrayInfo.unusedPercent);
writer.writeNumberValue("wasted", arrayInfo.wasted);
}
export function serializeClrObjectArrayItem(writer: SerializationWriter, clrObjectArrayItem: ClrObjectArrayItem | undefined = {} as ClrObjectArrayItem) : void {
writer.writeNumberValue("address", clrObjectArrayItem.address);
writer.writeNumberValue("index", clrObjectArrayItem.index);
writer.writeStringValue("value", clrObjectArrayItem.value);
}
export function serializeClrObjectField(writer: SerializationWriter, clrObjectField: ClrObjectField | undefined = {} as ClrObjectField) : void {
writer.writeBooleanValue("isValueType", clrObjectField.isValueType);
writer.writeNumberValue("methodTable", clrObjectField.methodTable);
Expand Down
Loading

0 comments on commit aad2d26

Please sign in to comment.