diff --git a/Directory.Build.props b/Directory.Build.props index f538a75..a79bba2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ - 0.5.0 + 0.6.0 https://github.com/Ne4to/Heartbeat true MIT diff --git a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs index 0ee6aba..22cb0f0 100644 --- a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs +++ b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs @@ -1,5 +1,7 @@ using Microsoft.Diagnostics.Runtime; +using System.Globalization; + namespace Heartbeat.Runtime.Extensions { public static class ClrValueTypeExtensions @@ -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 ""; + } + + if (valueType.Type.IsObjectReference) + return ""; + + 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 ""; + + return valueType.Type.Name ?? ""; + } private static bool IsValueDefault(ulong objRef, ClrInstanceField field) { @@ -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(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Char => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Int8 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.UInt8 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Int16 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.UInt16 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Int32 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.UInt32 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Int64 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.UInt64 => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Float => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.Double => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.NativeInt => field.Read(objRef, true).ToString(CultureInfo.InvariantCulture), + ClrElementType.NativeUInt => field.Read(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) { diff --git a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs index 9087733..552afca 100644 --- a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs @@ -2,6 +2,8 @@ using Microsoft.Diagnostics.Runtime; +using System.Text; + namespace Heartbeat.Runtime.Proxies; public sealed class ArrayProxy : ProxyBase @@ -9,6 +11,7 @@ public sealed class ArrayProxy : ProxyBase private ClrArray _clrArray; private readonly Lazy _unusedItemsCount; + public ClrArray InnerArray => _clrArray; public int Length => _clrArray.Length; public int UnusedItemsCount => _unusedItemsCount.Value; @@ -165,4 +168,58 @@ private int GetUnusedItemsCount() return EnumerateObjectItems(_clrArray) .Count(t => t.IsNull); } -} \ No newline at end of file + + public IEnumerable 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() + : ""; + + 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
+ // 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(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); \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index 97c4ce8..ac166b9 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -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: @@ -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 @@ -457,6 +508,7 @@ components: type: string name: type: string + nullable: true additionalProperties: false ClrObjectRootPath: required: diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asArray/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asArray/index.ts new file mode 100644 index 0000000..769a2c9 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asArray/index.ts @@ -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 { + /** + * 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 | 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 | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "404": createProblemDetailsFromDiscriminatorValue, + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendCollectionAsync(requestInfo, createClrObjectArrayItemFromDiscriminatorValue, errorMapping); + } + /** + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a RequestInformation + */ + public toGetRequestInformation(requestConfiguration?: RequestConfiguration | 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 */ 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 547c321..ba41d41 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 @@ -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'; @@ -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 { + /** + * The asArray property + */ + public get asArray(): AsArrayRequestBuilder { + return new AsArrayRequestBuilder(this.pathParameters, this.requestAdapter); + } /** * The fields property */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 4104768..9fc2047 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "2680CD432C0BAF2EAD8335B87589C6613B8A32E7D90AAF6EC468A74F00CDC479E9320B872E8B6E0C6AE5CDD8A0F2251B0991E07026BB56A11771AE55760D7F36", + "descriptionHash": "C2E02AEF66804BDF43A6B9DAE16266E2193A8418F53798D9AD2BBD832A9C33A511C8C9D02638AB3C5C3D249C515A2D838D5564A4A22D99E716D918BFFAB95F77", "descriptionLocation": "..\\..\\api.yml", "lockFileVersion": "1.0.0", "kiotaVersion": "1.10.1", diff --git a/src/Heartbeat/ClientApp/src/client/models/index.ts b/src/Heartbeat/ClientApp/src/client/models/index.ts index 3413fd7..43afddc 100644 --- a/src/Heartbeat/ClientApp/src/client/models/index.ts +++ b/src/Heartbeat/ClientApp/src/client/models/index.ts @@ -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 @@ -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; } @@ -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 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 void> { return { "isValueType": n => { clrObjectField.isValueType = n.getBooleanValue(); }, @@ -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); diff --git a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx index e1212cf..8463260 100644 --- a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx +++ b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx @@ -4,7 +4,7 @@ import {DataGrid, GridColDef, GridRenderCellParams, GridToolbar} from '@mui/x-da import {Stack} from "@mui/material"; import {TabbedShowLayout} from "react-admin"; -import {ClrObjectField, ClrObjectRootPath, GetClrObjectResult} from '../../client/models'; +import {ClrObjectArrayItem, ClrObjectField, ClrObjectRootPath, GetClrObjectResult} from '../../client/models'; import {useStateWithLoading} from "../../hooks/useStateWithLoading"; import {useNotifyError} from "../../hooks/useNotifyError"; @@ -13,16 +13,17 @@ import fetchData from "../../lib/handleFetchData"; import getClient from '../../lib/getClient' import toHexAddress from '../../lib/toHexAddress' import {renderClrObjectLink, renderMethodTableLink} from "../../lib/gridRenderCell"; -import {methodTableColumn} from "../../lib/gridColumns"; +import {methodTableColumn, objectAddressColumn} from "../../lib/gridColumns"; import toSizeString from "../../lib/toSizeString"; import {PropertiesTable, PropertyRow} from '../../components/PropertiesTable' import {ClrObjectRoot} from "../../components/ClrObjectRoot"; import {ProgressContainer} from "../../components/ProgressContainer"; -// TODO add Dictionary view to a new tab -// TODO add Array view to a new tab -// TODO add JWT decode tab (https://github.com/panva/jose) +// TODO add Dictionary, Queue, Stack and other collections view to a new tab +// TODO add ConcurrentDictionary view to a new tab (dcd, dumpconcurrentdictionary
Displays concurrent dictionary content.) +// TODO add ConcurrentQueue view to a new tab (dcq, dumpconcurrentqueue
Displays concurrent queue content.) +// TODO add JWT decode tab (https://github.com/panva/jose) (System.IdentityModel.Tokens.Jwt) // TODO find other debugger visualizers export const ClrObject = () => { @@ -33,6 +34,7 @@ export const ClrObject = () => { const [clrObject, setClrObject, isClrObjectLoading, setIsClrObjectLoading] = useStateWithLoading() const [fields, setFields, isFieldsLoading, setIsFieldsLoading] = useStateWithLoading() const [roots, setRoots, isRootsLoading, setIsRootsLoading] = useStateWithLoading() + const [arrayItems, setArrayItems, isArrayItemsLoading, setArrayItemsLoading] = useStateWithLoading() useEffect(() => { const getObject = async () => { @@ -61,6 +63,61 @@ export const ClrObject = () => { fetchData(getRoots, setRoots, setIsRootsLoading, notifyError) }, [address, notify]); + useEffect(() => { + const fetchArrayItems = async () => { + const client = getClient(); + return await client.api.dump.object.byAddress(address).asArray.get() + } + + fetchData(fetchArrayItems, setArrayItems, setArrayItemsLoading, notifyError) + }, [address, notify]); + + const getChildrenContent = (objectResult?: GetClrObjectResult) => { + if (!objectResult) + return undefined; + + const propertyRows: PropertyRow[] = [ + {title: 'Address', value: toHexAddress(objectResult.address)}, + {title: 'Size', value: toSizeString(objectResult.size || 0)}, + {title: 'Generation', value: objectResult.generation}, + // TODO add Live / Dead + {title: 'MethodTable', value: renderMethodTableLink(objectResult.methodTable)}, + {title: 'Type', value: objectResult.typeName}, + {title: 'Module', value: objectResult.moduleName}, + ] + + if (objectResult.value) { + propertyRows.push( + {title: 'Value', value: objectResult.value}, + ) + } + + return + } + + return ( + + + {getChildrenContent(clrObject)} + + + {/* TODO move each tab to a separate component */} + + + + + + + + + + + + + ); +} + +const FieldsTabContent = (props: { isLoading: boolean, fields?: ClrObjectField[] }) => { const fieldsGridColumns: GridColDef[] = [ methodTableColumn, { @@ -107,29 +164,6 @@ export const ClrObject = () => { } ]; - const getChildrenContent = (objectResult?: GetClrObjectResult) => { - if (!objectResult) - return undefined; - - const propertyRows: PropertyRow[] = [ - {title: 'Address', value: toHexAddress(objectResult.address)}, - {title: 'Size', value: toSizeString(objectResult.size || 0)}, - {title: 'Generation', value: objectResult.generation}, - // TODO add Live / Dead - {title: 'MethodTable', value: renderMethodTableLink(objectResult.methodTable)}, - {title: 'Type', value: objectResult.typeName}, - {title: 'Module', value: objectResult.moduleName}, - ] - - if (objectResult.value) { - propertyRows.push( - {title: 'Value', value: objectResult.value}, - ) - } - - return - } - const getFieldsGrid = (fields?: ClrObjectField[]) => { if (!fields || fields.length === 0) return undefined; @@ -137,7 +171,7 @@ export const ClrObject = () => { return ( row.name} + getRowId={(row) => row.name!} columns={fieldsGridColumns} rowHeight={25} pageSizeOptions={[20, 50, 100]} @@ -150,6 +184,12 @@ export const ClrObject = () => { ); } + return ( + {getFieldsGrid(props.fields)} + ); +} + +const RootsTabContent = (props: { isLoading: boolean, roots?: ClrObjectRootPath[] }) => { const getRootsContent = (roots?: ClrObjectRootPath[]) => { if (!roots || roots.length === 0) return undefined; @@ -157,24 +197,51 @@ export const ClrObject = () => { return } - return ( - - - {getChildrenContent(clrObject)} - + return ( + {getRootsContent(props.roots)} + ) +} - - - - {getFieldsGrid(fields)} - - - - - {getRootsContent(roots)} - - - - - ); +const ArrayTabContent = (props: { isLoading: boolean, arrayItems?: ClrObjectArrayItem[] }) => { + + // TODO show char[] as string + // TODO show byte[] as utf8 string + const getArrayItemsContent = (arrayItems?: ClrObjectArrayItem[]) => { + if (!arrayItems || arrayItems.length === 0) + return undefined; + + const arrayItemsColumns: GridColDef[] = [ + { + field: 'index', + headerName: 'Index', + type: 'number' + }, + objectAddressColumn, + { + field: 'value', + headerName: 'Value', + minWidth: 200, + flex: 1 + } + ]; + + return ( + row.index} + columns={arrayItemsColumns} + rowHeight={25} + pageSizeOptions={[20, 50, 100]} + density='compact' + slots={{toolbar: GridToolbar}} + initialState={{ + pagination: {paginationModel: {pageSize: 20}}, + }} + /> + ); + } + + return ( + {getArrayItemsContent(props.arrayItems)} + ); } \ No newline at end of file diff --git a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs index 2cae3ac..412257a 100644 --- a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs +++ b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs @@ -16,4 +16,5 @@ namespace Heartbeat.Host.Endpoints; [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 index 376877e..3687ba3 100644 --- a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs +++ b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs @@ -55,6 +55,12 @@ public static void MapDumpEndpoints(this IEndpointRouteBuilder app) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError) .WithName("GetClrObject"); + + dumpGroup.MapGet("object/{address}/as-array", RouteHandlers.GetClrObjectAsArray) + .Produces(StatusCodes.Status204NoContent) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName("GetClrObjectAsArray"); dumpGroup.MapGet("object/{address}/fields", RouteHandlers.GetClrObjectFields) .Produces(StatusCodes.Status404NotFound) diff --git a/src/Heartbeat/Endpoints/Models.cs b/src/Heartbeat/Endpoints/Models.cs index e1dea58..50eee90 100644 --- a/src/Heartbeat/Endpoints/Models.cs +++ b/src/Heartbeat/Endpoints/Models.cs @@ -42,6 +42,8 @@ public record ClrObjectField( string Value, string? Name); +public record struct ClrObjectArrayItem(int Index, ulong Address, string? Value); + public record Module(ulong Address, ulong Size, string? Name); public record HeapSegment(ulong Start, ulong End, GCSegmentKind Kind) diff --git a/src/Heartbeat/Endpoints/RouteHandlers.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs index e35eeca..7811d59 100644 --- a/src/Heartbeat/Endpoints/RouteHandlers.cs +++ b/src/Heartbeat/Endpoints/RouteHandlers.cs @@ -20,7 +20,6 @@ public static DumpInfo GetInfo([FromServices] RuntimeContext context) var clrHeap = context.Heap; var clrInfo = context.Runtime.ClrInfo; var dataReader = clrInfo.DataTarget.DataReader; - var dumpInfo = new DumpInfo( context.DumpPath, @@ -282,6 +281,29 @@ public static Results>, NotFound> GetClrObjectRoots([ return TypedResults.Ok(result); } + + public static Results>, NoContent, NotFound> GetClrObjectAsArray([FromServices] RuntimeContext context, ulong address) + { + var clrObject = context.Heap.GetObject(address); + if (clrObject.Type == null) + { + return TypedResults.NotFound(); + } + + if (clrObject.IsArray) + { + ArrayProxy arrayProxy = new(context, clrObject); + + var str = arrayProxy.AsStringValue(); + + var items = arrayProxy.EnumerateArrayElements() + .Select(e => new ClrObjectArrayItem(e.Index, e.Address, e.Value)); + + return TypedResults.Ok(items); + } + + return TypedResults.NoContent(); + } private static Address? GetFieldObjectAddress(ClrInstanceField field, ulong address) { @@ -295,6 +317,7 @@ public static Results>, NotFound> GetClrObjectRoots([ private static string GetFieldValue(ClrInstanceField field, ulong address) { + // TODO merge with ClrValueTypeExtensions.GetValueAsString if (field.IsPrimitive) { return field.ElementType switch