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: show array elements #17

Merged
merged 1 commit into from
Jan 30, 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.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
Loading