From 1b4a71fdb601c7c8feb8472700d068635c007407 Mon Sep 17 00:00:00 2001 From: Alexey Sosnin Date: Thu, 25 Jan 2024 22:32:01 +0300 Subject: [PATCH 01/14] feat: add sparse array stats grid fix: live object check in heap index --- src/Heartbeat.Runtime/HeapIndex.cs | 30 +++-- src/Heartbeat.Runtime/Proxies/ArrayProxy.cs | 8 +- src/Heartbeat/ClientApp/api.yml | 69 ++++++++--- src/Heartbeat/ClientApp/src/App.tsx | 2 + .../ClientApp/src/arraysGrid/ArraysGrid.tsx | 2 +- .../src/client/api/dump/arrays/index.ts | 41 ++----- .../client/api/dump/arrays/sparse/index.ts | 57 +++++++++ .../api/dump/arrays/sparse/stat/index.ts | 50 ++++++++ .../src/client/api/dump/segments/index.ts | 4 +- .../ClientApp/src/client/kiota-lock.json | 2 +- .../ClientApp/src/client/models/index.ts | 35 ++++++ .../ClientApp/src/clrObject/ClrObject.tsx | 3 +- src/Heartbeat/ClientApp/src/layout/Menu.tsx | 8 ++ .../src/sparseArraysStat/SparseArraysStat.tsx | 112 ++++++++++++++++++ .../ClientApp/src/sparseArraysStat/index.ts | 7 ++ src/Heartbeat/Controllers/DumpController.cs | 55 ++++++--- src/Heartbeat/Controllers/Models.cs | 4 +- 17 files changed, 408 insertions(+), 81 deletions(-) create mode 100644 src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts create mode 100644 src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts create mode 100644 src/Heartbeat/ClientApp/src/sparseArraysStat/SparseArraysStat.tsx create mode 100644 src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts diff --git a/src/Heartbeat.Runtime/HeapIndex.cs b/src/Heartbeat.Runtime/HeapIndex.cs index 23e5ee7..5f4cdfa 100644 --- a/src/Heartbeat.Runtime/HeapIndex.cs +++ b/src/Heartbeat.Runtime/HeapIndex.cs @@ -1,7 +1,6 @@ using Heartbeat.Runtime.Proxies; using Microsoft.Diagnostics.Runtime; - namespace Heartbeat.Runtime; public sealed class HeapIndex @@ -40,16 +39,23 @@ public HeapIndex(ClrHeap heap) continue; } - // Now enumerate all objects that this object points to, add them to the - // evaluation stack if we haven't seen them before. - if (type.IsArray) - { - EnumerateArrayElements(address); - } - else + var obj = heap.GetObject(address); + foreach (var reference in obj.EnumerateReferenceAddresses()) { - EnumerateFields(type, address); + eval.Push(reference); + AddReference(address, reference); } + + // // Now enumerate all objects that this object points to, add them to the + // // evaluation stack if we haven't seen them before. + // if (type.IsArray) + // { + // EnumerateArrayElements(address); + // } + // else + // { + // EnumerateFields(type, address); + // } } void EnumerateArrayElements(ulong address) @@ -80,7 +86,8 @@ void EnumerateArrayElements(ulong address) } else { - throw new NotSupportedException($"Enumerating array of {array.Type.ComponentType} type is not supported"); + throw new NotSupportedException( + $"Enumerating array of {array.Type.ComponentType} type is not supported"); } } @@ -90,7 +97,7 @@ void EnumerateFields(ClrType type, ulong objectAddress, ulong? parentAddress = n { if (instanceField.IsObjectReference) { - var fieldObject = instanceField.ReadObject(objectAddress, false); + var fieldObject = instanceField.ReadObject(objectAddress, !type.IsObjectReference); if (!fieldObject.IsNull) { AddReference(objectAddress, fieldObject.Address); @@ -98,6 +105,7 @@ void EnumerateFields(ClrType type, ulong objectAddress, ulong? parentAddress = n { AddReference(parentAddress.Value, fieldObject.Address); } + eval.Push(fieldObject.Address); } } diff --git a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs index 24751d3..9087733 100644 --- a/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs +++ b/src/Heartbeat.Runtime/Proxies/ArrayProxy.cs @@ -91,7 +91,7 @@ public static IEnumerable EnumerateObjectItems(ClrArray array) var lowerBound0 = array.GetLowerBound(0); var upperBound0 = array.GetUpperBound(0); - for (int index0 = lowerBound0; index0 < upperBound0; index0++) + for (int index0 = lowerBound0; index0 <= upperBound0; index0++) { if (array.Rank == 1) { @@ -101,7 +101,7 @@ public static IEnumerable EnumerateObjectItems(ClrArray array) { var lowerBound1 = array.GetLowerBound(1); var upperBound1 = array.GetUpperBound(1); - for (int index1 = lowerBound1; index1 < upperBound1; index1++) + for (int index1 = lowerBound1; index1 <= upperBound1; index1++) { if (array.Rank == 2) { @@ -129,7 +129,7 @@ public static IEnumerable EnumerateValueTypes(ClrArray array) var lowerBound0 = array.GetLowerBound(0); var upperBound0 = array.GetUpperBound(0); - for (int index0 = lowerBound0; index0 < upperBound0; index0++) + for (int index0 = lowerBound0; index0 <= upperBound0; index0++) { if (array.Rank == 1) { @@ -139,7 +139,7 @@ public static IEnumerable EnumerateValueTypes(ClrArray array) { var lowerBound1 = array.GetLowerBound(1); var upperBound1 = array.GetUpperBound(1); - for (int index1 = lowerBound1; index1 < upperBound1; index1++) + for (int index1 = lowerBound1; index1 <= upperBound1; index1++) { if (array.Rank == 2) { diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index e2000cf..7bdd1f0 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -13,7 +13,6 @@ paths: tags: - Dump summary: Get dump info - description: Get dump info operationId: GetInfo responses: '500': @@ -33,7 +32,6 @@ paths: tags: - Dump summary: Get modules - description: Get modules operationId: GetModules responses: '500': @@ -55,7 +53,6 @@ paths: tags: - Dump summary: Get segments - description: Get heap segments operationId: GetSegments responses: '500': @@ -77,7 +74,6 @@ paths: tags: - Dump summary: Get heap roots - description: Get heap roots operationId: GetRoots parameters: - name: kind @@ -105,7 +101,6 @@ paths: tags: - Dump summary: Get heap dump statistics - description: Get heap dump statistics operationId: GetHeapDumpStat parameters: - name: traversingMode @@ -138,7 +133,6 @@ paths: tags: - Dump summary: Get heap dump statistics - description: Get heap dump statistics operationId: GetStrings parameters: - name: traversingMode @@ -171,7 +165,6 @@ paths: tags: - Dump summary: Get string duplicates - description: Get string duplicates operationId: GetStringDuplicates parameters: - name: traversingMode @@ -204,7 +197,6 @@ paths: tags: - Dump summary: Get object instances - description: Get object instances operationId: GetObjectInstances parameters: - name: mt @@ -237,13 +229,12 @@ paths: application/json: schema: $ref: '#/components/schemas/GetObjectInstancesResult' - /api/dump/arrays: + /api/dump/arrays/sparse: get: tags: - Dump - summary: Get arrays - description: Get arrays - operationId: GetArrays + summary: Get sparse arrays + operationId: GetSparseArrays parameters: - name: traversingMode in: query @@ -270,12 +261,43 @@ paths: type: array items: $ref: '#/components/schemas/ArrayInfo' + /api/dump/arrays/sparse/stat: + get: + tags: + - Dump + summary: Get arrays + operationId: GetSparseArraysStat + 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/SparseArrayStatistics' '/api/dump/object/{address}': get: tags: - Dump summary: Get object - description: Get object operationId: GetClrObject parameters: - name: address @@ -309,7 +331,6 @@ paths: tags: - Dump summary: Get object roots - description: Get object roots operationId: GetClrObjectRoots parameters: - name: address @@ -679,6 +700,26 @@ components: generation: $ref: '#/components/schemas/Generation' additionalProperties: false + SparseArrayStatistics: + required: + - count + - methodTable + - totalWasted + type: object + properties: + methodTable: + type: integer + format: int64 + typeName: + type: string + nullable: true + count: + type: integer + format: int32 + totalWasted: + type: integer + format: int64 + additionalProperties: false StringDuplicate: required: - count diff --git a/src/Heartbeat/ClientApp/src/App.tsx b/src/Heartbeat/ClientApp/src/App.tsx index 99c0f59..184fbba 100644 --- a/src/Heartbeat/ClientApp/src/App.tsx +++ b/src/Heartbeat/ClientApp/src/App.tsx @@ -8,6 +8,7 @@ import clrObject from './clrObject' import roots from './roots' import modules from './modules' import arraysGrid from './arraysGrid' +import sparseArraysStat from './sparseArraysStat' import stringsGrid from './stringsGrid' import stringDuplicates from './stringDuplicates' import {AlertContext} from './contexts/alertContext'; @@ -59,6 +60,7 @@ const App = () => { + diff --git a/src/Heartbeat/ClientApp/src/arraysGrid/ArraysGrid.tsx b/src/Heartbeat/ClientApp/src/arraysGrid/ArraysGrid.tsx index 97f8a30..a4b3cad 100644 --- a/src/Heartbeat/ClientApp/src/arraysGrid/ArraysGrid.tsx +++ b/src/Heartbeat/ClientApp/src/arraysGrid/ArraysGrid.tsx @@ -51,7 +51,7 @@ export const ArraysGrid = () => { const loadData = async (mode: TraversingHeapModes, generation?: Generation) => { const client = getClient(); - const result = await client.api.dump.arrays.get( + const result = await client.api.dump.arrays.sparse.get( {queryParameters: {traversingMode: mode, generation: generation}} ) setArrays(result!) diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/index.ts index 0e15cd9..e049009 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/index.ts @@ -1,49 +1,26 @@ /* tslint:disable */ /* eslint-disable */ // Generated by Microsoft Kiota -import { createArrayInfoFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, Generation, serializeProblemDetails, TraversingHeapModes, type ArrayInfo, type ProblemDetails } from '../../../models/'; -import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions'; +import { SparseRequestBuilder } from './sparse/'; +import { BaseRequestBuilder, type RequestAdapter } from '@microsoft/kiota-abstractions'; -export interface ArraysRequestBuilderGetQueryParameters { - generation?: Generation; - traversingMode?: TraversingHeapModes; -} /** * Builds and executes requests for operations under /api/dump/arrays */ export class ArraysRequestBuilder extends BaseRequestBuilder { + /** + * The sparse property + */ + public get sparse(): SparseRequestBuilder { + return new SparseRequestBuilder(this.pathParameters, this.requestAdapter); + } /** * Instantiates a new ArraysRequestBuilder 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/arrays{?traversingMode*,generation*}", (x, y) => new ArraysRequestBuilder(x, y)); - } - /** - * Get arrays - * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. - * @returns a Promise of ArrayInfo - */ - public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { - const requestInfo = this.toGetRequestInformation( - requestConfiguration - ); - const errorMapping = { - "500": createProblemDetailsFromDiscriminatorValue, - } as Record>; - return this.requestAdapter.sendCollectionAsync(requestInfo, createArrayInfoFromDiscriminatorValue, errorMapping); - } - /** - * Get arrays - * @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; + super(pathParameters, requestAdapter, "{+baseurl}/api/dump/arrays", (x, y) => new ArraysRequestBuilder(x, y)); } } /* tslint:enable */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts new file mode 100644 index 0000000..1aecc1b --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/index.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +// Generated by Microsoft Kiota +import { createArrayInfoFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, Generation, serializeProblemDetails, TraversingHeapModes, type ArrayInfo, type ProblemDetails } from '../../../../models/'; +import { StatRequestBuilder } from './stat/'; +import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions'; + +export interface SparseRequestBuilderGetQueryParameters { + generation?: Generation; + traversingMode?: TraversingHeapModes; +} +/** + * Builds and executes requests for operations under /api/dump/arrays/sparse + */ +export class SparseRequestBuilder extends BaseRequestBuilder { + /** + * The stat property + */ + public get stat(): StatRequestBuilder { + return new StatRequestBuilder(this.pathParameters, this.requestAdapter); + } + /** + * Instantiates a new SparseRequestBuilder 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/arrays/sparse{?traversingMode*,generation*}", (x, y) => new SparseRequestBuilder(x, y)); + } + /** + * Get sparse arrays + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a Promise of ArrayInfo + */ + public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendCollectionAsync(requestInfo, createArrayInfoFromDiscriminatorValue, errorMapping); + } + /** + * Get sparse arrays + * @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/arrays/sparse/stat/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts new file mode 100644 index 0000000..06b7f49 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/arrays/sparse/stat/index.ts @@ -0,0 +1,50 @@ +/* tslint:disable */ +/* eslint-disable */ +// Generated by Microsoft Kiota +import { createProblemDetailsFromDiscriminatorValue, createSparseArrayStatisticsFromDiscriminatorValue, deserializeIntoProblemDetails, Generation, serializeProblemDetails, TraversingHeapModes, type ProblemDetails, type SparseArrayStatistics } from '../../../../../models/'; +import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions'; + +export interface StatRequestBuilderGetQueryParameters { + generation?: Generation; + traversingMode?: TraversingHeapModes; +} +/** + * Builds and executes requests for operations under /api/dump/arrays/sparse/stat + */ +export class StatRequestBuilder extends BaseRequestBuilder { + /** + * Instantiates a new StatRequestBuilder 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/arrays/sparse/stat{?traversingMode*,generation*}", (x, y) => new StatRequestBuilder(x, y)); + } + /** + * Get arrays + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a Promise of SparseArrayStatistics + */ + public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendCollectionAsync(requestInfo, createSparseArrayStatisticsFromDiscriminatorValue, errorMapping); + } + /** + * Get arrays + * @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/segments/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts index 6070cc8..3fcacc2 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/segments/index.ts @@ -17,7 +17,7 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder new SegmentsRequestBuilder(x, y)); } /** - * Get heap segments + * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a Promise of HeapSegment */ @@ -31,7 +31,7 @@ export class SegmentsRequestBuilder extends BaseRequestBuilder(requestInfo, createHeapSegmentFromDiscriminatorValue, errorMapping); } /** - * Get heap segments + * Get segments * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. * @returns a RequestInformation */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 8230cf1..6c54144 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "8DFECB5B7DD5C64D384E835EBEEC3B587F131D23B8EE3FC4BBA47A5F50CD5B073F1720F0F7976AA1F20C7A5ED244002FBF6BE4D59DDA91854956A1F35608B2F9", + "descriptionHash": "1B5889A413AAFEF0FD437A9EE3E61CC952CB089C6A7DE05080BFD48AEAA5951328679293160E1A0B15C265249DFCAAE34EBDDF885C85FC85BEB75EF3D265E0B9", "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 8fc8750..545f886 100644 --- a/src/Heartbeat/ClientApp/src/client/models/index.ts +++ b/src/Heartbeat/ClientApp/src/client/models/index.ts @@ -114,6 +114,9 @@ export function createRootInfoFromDiscriminatorValue(parseNode: ParseNode | unde export function createRootPathItemFromDiscriminatorValue(parseNode: ParseNode | undefined) { return deserializeIntoRootPathItem; } +export function createSparseArrayStatisticsFromDiscriminatorValue(parseNode: ParseNode | undefined) { + return deserializeIntoSparseArrayStatistics; +} export function createStringDuplicateFromDiscriminatorValue(parseNode: ParseNode | undefined) { return deserializeIntoStringDuplicate; } @@ -236,6 +239,14 @@ export function deserializeIntoRootPathItem(rootPathItem: RootPathItem | undefin "typeName": n => { rootPathItem.typeName = n.getStringValue(); }, } } +export function deserializeIntoSparseArrayStatistics(sparseArrayStatistics: SparseArrayStatistics | undefined = {} as SparseArrayStatistics) : Record void> { + return { + "count": n => { sparseArrayStatistics.count = n.getNumberValue(); }, + "methodTable": n => { sparseArrayStatistics.methodTable = n.getNumberValue(); }, + "totalWasted": n => { sparseArrayStatistics.totalWasted = n.getNumberValue(); }, + "typeName": n => { sparseArrayStatistics.typeName = n.getStringValue(); }, + } +} export function deserializeIntoStringDuplicate(stringDuplicate: StringDuplicate | undefined = {} as StringDuplicate) : Record void> { return { "count": n => { stringDuplicate.count = n.getNumberValue(); }, @@ -560,6 +571,12 @@ export function serializeRootPathItem(writer: SerializationWriter, rootPathItem: writer.writeNumberValue("size", rootPathItem.size); writer.writeStringValue("typeName", rootPathItem.typeName); } +export function serializeSparseArrayStatistics(writer: SerializationWriter, sparseArrayStatistics: SparseArrayStatistics | undefined = {} as SparseArrayStatistics) : void { + writer.writeNumberValue("count", sparseArrayStatistics.count); + writer.writeNumberValue("methodTable", sparseArrayStatistics.methodTable); + writer.writeNumberValue("totalWasted", sparseArrayStatistics.totalWasted); + writer.writeStringValue("typeName", sparseArrayStatistics.typeName); +} export function serializeStringDuplicate(writer: SerializationWriter, stringDuplicate: StringDuplicate | undefined = {} as StringDuplicate) : void { writer.writeNumberValue("count", stringDuplicate.count); writer.writeNumberValue("fullLength", stringDuplicate.fullLength); @@ -572,6 +589,24 @@ export function serializeStringInfo(writer: SerializationWriter, stringInfo: Str writer.writeNumberValue("size", stringInfo.size); writer.writeStringValue("value", stringInfo.value); } +export interface SparseArrayStatistics extends Parsable { + /** + * The count property + */ + count?: number; + /** + * The methodTable property + */ + methodTable?: number; + /** + * The totalWasted property + */ + totalWasted?: number; + /** + * The typeName property + */ + typeName?: string; +} export interface StringDuplicate extends Parsable { /** * The count property diff --git a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx index 9febcac..7a7d289 100644 --- a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx +++ b/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx @@ -70,7 +70,7 @@ export const ClrObject = () => { useEffect(() => { loadData().catch(console.error) loadRoots().catch(console.error) - }); + }, [address]); const loadData = async () => { const client = getClient(); @@ -117,6 +117,7 @@ export const ClrObject = () => { {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}, diff --git a/src/Heartbeat/ClientApp/src/layout/Menu.tsx b/src/Heartbeat/ClientApp/src/layout/Menu.tsx index 112c646..08bb149 100644 --- a/src/Heartbeat/ClientApp/src/layout/Menu.tsx +++ b/src/Heartbeat/ClientApp/src/layout/Menu.tsx @@ -15,6 +15,7 @@ import segments from '../segments'; import roots from '../roots'; import modules from '../modules'; import arraysGrid from '../arraysGrid'; +import sparseArraysStat from '../sparseArraysStat'; import stringsGrid from '../stringsGrid'; import stringDuplicates from '../stringDuplicates'; // import SubMenu from './SubMenu'; @@ -165,6 +166,13 @@ const Menu = ({ dense = false }: MenuProps) => { leftIcon={} dense={dense} /> + } + dense={dense} + /> { + const [loading, setLoading] = React.useState(true) + const [mode, setMode] = React.useState(TraversingHeapModesObject.All) + const [generation, setGeneration] = React.useState() + const [arrays, setArrays] = React.useState([]) + + useEffect(() => { + loadData(mode, generation).catch(console.error); + }, [mode, generation]); + + const loadData = async (mode: TraversingHeapModes, generation?: Generation) => { + const client = getClient(); + const result = await client.api.dump.arrays.sparse.stat.get( + {queryParameters: {traversingMode: mode, generation: generation}} + ) + setArrays(result!) + setLoading(false) + } + + const renderTable = (arrays: SparseArrayStatistics[]) => { + return ( +
+ + row.methodTable} + columns={columns} + rowHeight={25} + pageSizeOptions={[20, 50, 100]} + density='compact' + initialState={{ + sorting: { + sortModel: [{field: 'totalWasted', sort: 'desc'}], + }, + pagination: {paginationModel: {pageSize: 20}}, + }} + slots={{toolbar: GridToolbar}} + slotProps={{ + toolbar: { + showQuickFilter: true, + }, + }} + /> + +
+ ); + } + + let contents = loading + ? + + + : renderTable(arrays); + + const totalWasted = arrays.map(m => m.totalWasted!).reduce((sum, current) => sum + current, 0) + + const propertyRows: PropertyRow[] = [ + {title: 'Count', value: String(arrays.length)}, + {title: 'Total wasted', value: toSizeString(totalWasted)}, + ] + + return ( +
+
+ setMode(mode)}/> + setGeneration(generation)}/> +
+ + {contents} +
+ ); +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts b/src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts new file mode 100644 index 0000000..3fb556d --- /dev/null +++ b/src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts @@ -0,0 +1,7 @@ +import Icon from '@mui/icons-material/ViewModule'; +import { SparseArraysStat } from './SparseArraysStat'; + +export default { + icon: Icon, + list: SparseArraysStat, +}; diff --git a/src/Heartbeat/Controllers/DumpController.cs b/src/Heartbeat/Controllers/DumpController.cs index 7971df9..1a80060 100644 --- a/src/Heartbeat/Controllers/DumpController.cs +++ b/src/Heartbeat/Controllers/DumpController.cs @@ -31,7 +31,7 @@ public DumpController(RuntimeContext context) [HttpGet] [Route("info")] [ProducesResponseType(typeof(DumpInfo), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get dump info", description: "Get dump info")] + [SwaggerOperation(summary: "Get dump info")] public DumpInfo GetInfo() { var clrHeap = _context.Heap; @@ -53,7 +53,7 @@ public DumpInfo GetInfo() [HttpGet] [Route("modules")] [ProducesResponseType(typeof(Module[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get modules", description: "Get modules")] + [SwaggerOperation(summary: "Get modules")] public IEnumerable GetModules() { var modules = _context.Runtime @@ -67,7 +67,7 @@ public IEnumerable GetModules() [HttpGet] [Route("segments")] [ProducesResponseType(typeof(HeapSegment[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get segments", description: "Get heap segments")] + [SwaggerOperation(summary: "Get segments")] public IEnumerable GetSegments() { var segments = @@ -83,7 +83,7 @@ from s in _context.Heap.Segments [HttpGet] [Route("roots")] [ProducesResponseType(typeof(RootInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap roots", description: "Get heap roots")] + [SwaggerOperation(summary: "Get heap roots")] public IEnumerable GetRoots([FromQuery] ClrRootKind? kind = null) { return @@ -103,7 +103,7 @@ from root in _context.Heap.EnumerateRoots().ToArray() [HttpGet] [Route("heap-dump-statistics")] [ProducesResponseType(typeof(ObjectTypeStatistics[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics", description: "Get heap dump statistics")] + [SwaggerOperation(summary: "Get heap dump statistics")] public IEnumerable GetHeapDumpStat( [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, [FromQuery] Generation? generation = null) @@ -127,7 +127,7 @@ public IEnumerable GetHeapDumpStat( [HttpGet] [Route("strings")] [ProducesResponseType(typeof(StringInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get heap dump statistics", description: "Get heap dump statistics")] + [SwaggerOperation(summary: "Get heap dump statistics")] public IEnumerable GetStrings( [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, [FromQuery] Generation? generation = null) @@ -146,7 +146,7 @@ public IEnumerable GetStrings( [HttpGet] [Route("string-duplicates")] [ProducesResponseType(typeof(StringDuplicate[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get string duplicates", description: "Get string duplicates")] + [SwaggerOperation(summary: "Get string duplicates")] public IEnumerable GetStringDuplicates( [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, [FromQuery] Generation? generation = null) @@ -154,7 +154,8 @@ public IEnumerable GetStringDuplicates( { var analyzer = new StringDuplicateAnalyzer(_context) { - TraversingHeapMode = traversingMode, Generation = generation + TraversingHeapMode = traversingMode, + Generation = generation }; return analyzer.GetStringDuplicates() @@ -164,7 +165,7 @@ public IEnumerable GetStringDuplicates( [HttpGet] [Route("object-instances/{mt}")] [ProducesResponseType(typeof(GetObjectInstancesResult), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get object instances", description: "Get object instances")] + [SwaggerOperation(summary: "Get object instances")] public GetObjectInstancesResult GetObjectInstances( ulong mt, [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, @@ -191,12 +192,13 @@ orderby obj.Size descending } [HttpGet] - [Route("arrays")] + [Route("arrays/sparse")] [ProducesResponseType(typeof(ArrayInfo[]), StatusCodes.Status200OK)] - [SwaggerOperation(summary: "Get arrays", description: "Get arrays")] + [SwaggerOperation(summary: "Get sparse arrays")] + // TODO add arrays // TODO add arrays/sparse // TODO add arrays/sparse/stat - public IEnumerable GetArrays( + public IEnumerable GetSparseArrays( [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, [FromQuery] Generation? generation = null) { @@ -210,12 +212,37 @@ orderby proxy.Wasted descending return query.Take(100); } + + [HttpGet] + [Route("arrays/sparse/stat")] + [ProducesResponseType(typeof(SparseArrayStatistics[]), StatusCodes.Status200OK)] + [SwaggerOperation(summary: "Get arrays")] + public IEnumerable GetSparseArraysStat( + [FromQuery] TraversingHeapModes traversingMode = TraversingHeapModes.All, + [FromQuery] Generation? generation = null) + { + var query = from obj in _context.EnumerateObjects(traversingMode, generation) + where obj.IsArray + let proxy = new ArrayProxy(_context, obj) + where proxy.UnusedItemsCount != 0 + group proxy by obj.Type.MethodTable + into grp + select new SparseArrayStatistics + ( + grp.Key, + grp.First().TargetObject.Type.Name, + grp.Count(), + Size.Sum(grp.Select(t => t.Wasted)) + ); + + return query; + } [HttpGet] [Route("object/{address}")] [ProducesResponseType(typeof(GetClrObjectResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object", description: "Get object")] + [SwaggerOperation(summary: "Get object")] public IActionResult GetClrObject(ulong address) { var clrObject = _context.Heap.GetObject(address); @@ -258,7 +285,7 @@ from field in clrObject.Type.Fields [Route("object/{address}/roots")] [ProducesResponseType(typeof(ClrObjectRootPath[]), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [SwaggerOperation(summary: "Get object roots", description: "Get object roots")] + [SwaggerOperation(summary: "Get object roots")] public IActionResult GetClrObjectRoots(ulong address, CancellationToken ct) { var clrObject = _context.Heap.GetObject(address); diff --git a/src/Heartbeat/Controllers/Models.cs b/src/Heartbeat/Controllers/Models.cs index 70773cf..53ddace 100644 --- a/src/Heartbeat/Controllers/Models.cs +++ b/src/Heartbeat/Controllers/Models.cs @@ -59,4 +59,6 @@ public record RootInfo(ulong Address, ClrRootKind Kind, bool IsPinned, ulong Siz public record ClrObjectRootPath(RootInfo Root, IReadOnlyList PathItems); public record RootPathItem(ulong Address, ulong MethodTable, string? TypeName, ulong Size, Generation Generation); -public record ArrayInfo(ulong Address, ulong MethodTable, string? TypeName, int Length, int UnusedItemsCount, double UnusedPercent, ulong Wasted); \ No newline at end of file +public record ArrayInfo(ulong Address, ulong MethodTable, string? TypeName, int Length, int UnusedItemsCount, double UnusedPercent, ulong Wasted); + +public record SparseArrayStatistics(ulong MethodTable, string? TypeName, int Count, ulong TotalWasted); \ No newline at end of file From e5aea05995dc56dffe99fcef29b5ee6134db91de Mon Sep 17 00:00:00 2001 From: Alexey Sosnin Date: Fri, 26 Jan 2024 07:23:36 +0300 Subject: [PATCH 02/14] fix: navigation on single page with auto data refresh --- .../ClientApp/src/clrObject/ClrObject.tsx | 103 +++++++++--------- .../ClientApp/src/clrObject/index.ts | 2 +- .../src/lib/gridRenderCell/index.tsx | 13 ++- .../src/objectInstances/ObjectInstances.tsx | 8 +- .../ClientApp/src/objectInstances/index.ts | 2 +- src/Heartbeat/ClientApp/src/themes/themes.tsx | 16 ++- src/Heartbeat/Controllers/DumpController.cs | 1 - src/Heartbeat/Program.cs | 4 +- 8 files changed, 84 insertions(+), 65 deletions(-) diff --git a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx index 7a7d289..5fc952e 100644 --- a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx +++ b/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx @@ -1,5 +1,5 @@ import React, {useEffect} from 'react'; -import {useSearchParams} from 'react-router-dom'; +import {useParams} from 'react-router-dom'; import LinearProgress from '@mui/material/LinearProgress'; import {DataGrid, GridColDef, GridRenderCellParams, GridToolbar} from '@mui/x-data-grid'; import Box from '@mui/material/Box'; @@ -8,64 +8,65 @@ import getClient from '../lib/getClient' import toHexAddress from '../lib/toHexAddress' import {GetClrObjectResult, ClrObjectField, ClrObjectRootPath} from '../client/models'; import {PropertiesTable, PropertyRow} from '../components/PropertiesTable' -import {renderMethodTableLink} from "../lib/gridRenderCell"; +import {renderClrObjectLink, renderMethodTableLink} from "../lib/gridRenderCell"; import {ClrObjectRoot} from "../components/ClrObjectRoot"; import {methodTableColumn} from "../lib/gridColumns"; import toSizeString from "../lib/toSizeString"; - -const columns: GridColDef[] = [ - methodTableColumn, - { - field: 'offset', - headerName: 'Offset', - type: 'number', - width: 80 - }, - { - field: 'isValueType', - headerName: 'VT', - }, - { - field: 'typeName', - headerName: 'Type', - minWidth: 200, - flex: 0.5, - }, - { - field: 'name', - headerName: 'Name', - minWidth: 200, - flex: 0.5, - }, - { - field: 'value', - headerName: 'Value', - minWidth: 200, - flex: 1, - renderCell: (params: GridRenderCellParams) => { - if (params.value == null) { - return ''; - } - - const objectAddress = params.row.objectAddress; - - return objectAddress - ? ( - {params.value} - ) - : ( - params.value - ) - } - } -]; +import {Button, Link} from "react-admin"; export const ClrObject = () => { + const { id } = useParams(); const [loading, setLoading] = React.useState(true) const [objectResult, setObjectResult] = React.useState() const [roots, setRoots] = React.useState() - const [searchParams] = useSearchParams(); - const [address, setAddress] = React.useState(Number('0x' + searchParams.get('address'))) + const address = Number('0x' + id); + + const columns: GridColDef[] = [ + methodTableColumn, + { + field: 'offset', + headerName: 'Offset', + type: 'number', + width: 80 + }, + { + field: 'isValueType', + headerName: 'VT', + }, + { + field: 'typeName', + headerName: 'Type', + minWidth: 200, + flex: 0.5, + }, + { + field: 'name', + headerName: 'Name', + minWidth: 200, + flex: 0.5, + }, + { + field: 'value', + headerName: 'Value', + minWidth: 200, + flex: 1, + renderCell: (params: GridRenderCellParams) => { + if (params.value == null) { + return ''; + } + + const objectAddress = params.row.objectAddress; + + return objectAddress + ? + renderClrObjectLink(objectAddress) + // + : ( + params.value + ) + } + } + ]; useEffect(() => { loadData().catch(console.error) diff --git a/src/Heartbeat/ClientApp/src/clrObject/index.ts b/src/Heartbeat/ClientApp/src/clrObject/index.ts index b9dfb1b..c5a8379 100644 --- a/src/Heartbeat/ClientApp/src/clrObject/index.ts +++ b/src/Heartbeat/ClientApp/src/clrObject/index.ts @@ -3,5 +3,5 @@ import { ClrObject } from './ClrObject'; export default { icon: Icon, - list: ClrObject, + show: ClrObject, }; diff --git a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx index 48c54e5..7a541e2 100644 --- a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx +++ b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx @@ -1,6 +1,7 @@ import {GridRenderCellParams, GridValueFormatterParams} from "@mui/x-data-grid"; import toHexAddress from "../toHexAddress"; import React from "react"; +import {Button, Link} from "react-admin"; export function renderAddress(params: GridRenderCellParams): React.ReactNode { const address = toHexAddress(params.value) @@ -10,9 +11,13 @@ export function renderAddress(params: GridRenderCellParams): React.ReactNode { } export function renderClrObjectAddress(params: GridRenderCellParams): React.ReactNode { - const address = toHexAddress(params.value) + return renderClrObjectLink(params.value); +} + +export function renderClrObjectLink(address: number):React.ReactNode { + const hexAddress = toHexAddress(address) return ( - {address} + ); diff --git a/src/Heartbeat/ClientApp/src/layout/Layout.tsx b/src/Heartbeat/ClientApp/src/layout/Layout.tsx index 3de84c0..53de7f7 100644 --- a/src/Heartbeat/ClientApp/src/layout/Layout.tsx +++ b/src/Heartbeat/ClientApp/src/layout/Layout.tsx @@ -4,5 +4,6 @@ import AppBar from './AppBar'; import Menu from './Menu'; export default (props: LayoutProps) => ( - + ); diff --git a/src/Heartbeat/ClientApp/src/layout/Logo.tsx b/src/Heartbeat/ClientApp/src/layout/Logo.tsx index 842b792..7d5667d 100644 --- a/src/Heartbeat/ClientApp/src/layout/Logo.tsx +++ b/src/Heartbeat/ClientApp/src/layout/Logo.tsx @@ -1,11 +1,15 @@ import MonitorHeart from '@mui/icons-material/MonitorHeart' +import {Stack, Typography} from "@mui/material"; +import Box from "@mui/material/Box"; const Logo = () => { return ( - <> + - Heartbeat - + + Heartbeat + + ); }; diff --git a/src/Heartbeat/ClientApp/src/lib/dataProvider/index.ts b/src/Heartbeat/ClientApp/src/lib/dataProvider/index.ts new file mode 100644 index 0000000..a3978b4 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/lib/dataProvider/index.ts @@ -0,0 +1,56 @@ +import { + CreateParams, CreateResult, DeleteManyParams, DeleteManyResult, DeleteParams, DeleteResult, + GetListParams, + GetListResult, + GetManyParams, + GetManyReferenceParams, GetManyReferenceResult, GetManyResult, + GetOneParams, GetOneResult, HttpError, number, UpdateManyParams, UpdateManyResult, UpdateParams, UpdateResult +} from "react-admin"; +import getClient from "../getClient"; + +export const dataProvider = { + // get a list of records based on sort, filter, and pagination + getList: async (resource: string, params: GetListParams): Promise => { + if (resource === 'modules') + { + const client = getClient(); + const result = await client.api.dump.modules.get() || [] + const data = result.map((m) => ({ id: m.address, ...m })) + + return { data, total: undefined }; + } + throw new HttpError("getList not implemented", 500, null) + }, + // get a single record by id + getOne: async (resource: string, params: GetOneParams): Promise => { + throw new HttpError("getOne not implemented", 500, null) + }, + // get a list of records based on an array of ids + getMany: async (resource: string, params: GetManyParams): Promise => { + throw new HttpError("getMany not implemented", 500, null) + }, + // get the records referenced to another record, e.g. comments for a post + getManyReference: async (resource: string, params: GetManyReferenceParams): Promise => { + throw new HttpError("getManyReference not implemented", 500, null) + }, + // create a record + create: async (resource: string, params: CreateParams): Promise => { + throw new HttpError("create not implemented", 500, null) + }, + // update a record based on a patch + update: async (resource: string, params: UpdateParams): Promise => { + throw new HttpError("update not implemented", 500, null) + }, + // update a list of records based on an array of ids and a common patch + updateMany: async (resource: string, params: UpdateManyParams): Promise => { + throw new HttpError("updateMany not implemented", 500, null) + }, + // delete a record by id + delete: async (resource: string, params: DeleteParams): Promise => { + throw new HttpError("delete not implemented", 500, null) + }, + // delete a list of records based on an array of ids + deleteMany: async (resource: string, params: DeleteManyParams): Promise => { + throw new HttpError("deleteMany not implemented", 500, null) + }, +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx index 7a541e2..0e3c4a9 100644 --- a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx +++ b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx @@ -2,6 +2,7 @@ import {GridRenderCellParams, GridValueFormatterParams} from "@mui/x-data-grid"; import toHexAddress from "../toHexAddress"; import React from "react"; import {Button, Link} from "react-admin"; +import {NavLink} from "react-router-dom"; export function renderAddress(params: GridRenderCellParams): React.ReactNode { const address = toHexAddress(params.value) @@ -17,7 +18,7 @@ export function renderClrObjectAddress(params: GridRenderCellParams): React.Reac export function renderClrObjectLink(address: number):React.ReactNode { const hexAddress = toHexAddress(address) return ( - + renderClrObjectLink(objectAddress) + // : ( params.value ) @@ -86,7 +86,7 @@ export const ClrObject = () => { // TODO add Array view to a new tab // TODO add JWT decode tab (https://github.com/panva/jose) // TODO find other debugger visualizers - const loadRoots = async() => { + const loadRoots = async () => { const client = getClient(); const result = await client.api.dump.object.byAddress(address).roots.get() setRoots(result!) @@ -94,22 +94,18 @@ export const ClrObject = () => { const renderTable = (fields: ClrObjectField[]) => { return ( - - - row.name} - columns={columns} - rowHeight={25} - pageSizeOptions={[20, 50, 100]} - density='compact' - slots={{toolbar: GridToolbar}} - initialState={{ - pagination: {paginationModel: {pageSize: 20}}, - }} - /> - - + row.name} + columns={columns} + rowHeight={25} + pageSizeOptions={[20, 50, 100]} + density='compact' + slots={{toolbar: GridToolbar}} + initialState={{ + pagination: {paginationModel: {pageSize: 20}}, + }} + /> ); } @@ -136,11 +132,11 @@ export const ClrObject = () => { } const rootGrid = roots && roots.length !== 0 - ? + ? :
root path not found
; return ( -
+

Clr Object

{contents} diff --git a/src/Heartbeat/ClientApp/src/layout/Menu.tsx b/src/Heartbeat/ClientApp/src/layout/Menu.tsx index fbd0566..1d18fd8 100644 --- a/src/Heartbeat/ClientApp/src/layout/Menu.tsx +++ b/src/Heartbeat/ClientApp/src/layout/Menu.tsx @@ -13,7 +13,7 @@ import segments from '../pages/segments'; import roots from '../pages/roots'; import modules from '../pages/modules'; import arraysGrid from '../pages/arraysGrid'; -import sparseArraysStat from '../sparseArraysStat'; +import sparseArraysStat from '../pages/sparseArraysStat'; import stringsGrid from '../pages/stringsGrid'; import stringDuplicates from '../pages/stringDuplicates'; diff --git a/src/Heartbeat/ClientApp/src/sparseArraysStat/SparseArraysStat.tsx b/src/Heartbeat/ClientApp/src/pages/sparseArraysStat/SparseArraysStat.tsx similarity index 58% rename from src/Heartbeat/ClientApp/src/sparseArraysStat/SparseArraysStat.tsx rename to src/Heartbeat/ClientApp/src/pages/sparseArraysStat/SparseArraysStat.tsx index 6f176b3..aa658a3 100644 --- a/src/Heartbeat/ClientApp/src/sparseArraysStat/SparseArraysStat.tsx +++ b/src/Heartbeat/ClientApp/src/pages/sparseArraysStat/SparseArraysStat.tsx @@ -1,19 +1,19 @@ -import React, {useEffect} from 'react'; -import LinearProgress from '@mui/material/LinearProgress'; +import React from 'react'; import {DataGrid, GridColDef, GridToolbar} from '@mui/x-data-grid'; -import Box from '@mui/material/Box'; -import getClient from '../lib/getClient' +import getClient from '../../lib/getClient' import { Generation, ObjectGCStatus, SparseArrayStatistics, -} from '../client/models'; -import {PropertiesTable, PropertyRow} from "../components/PropertiesTable"; -import {ObjectGCStatusSelect} from "../components/ObjectGCStatusSelect"; -import {GenerationSelect} from "../components/GenerationSelect"; -import {methodTableColumn, sizeColumn} from "../lib/gridColumns"; -import toSizeString from "../lib/toSizeString"; +} from '../../client/models'; +import {PropertiesTable, PropertyRow} from "../../components/PropertiesTable"; +import {ObjectGCStatusSelect} from "../../components/ObjectGCStatusSelect"; +import {GenerationSelect} from "../../components/GenerationSelect"; +import {methodTableColumn, sizeColumn} from "../../lib/gridColumns"; +import toSizeString from "../../lib/toSizeString"; +import {Stack} from "@mui/material"; +import {ProgressContainer} from "../../components/ProgressContainer"; const columns: GridColDef[] = [ methodTableColumn, @@ -36,22 +36,15 @@ const columns: GridColDef[] = [ ]; export const SparseArraysStat = () => { - const [loading, setLoading] = React.useState(true) const [gcStatus, setGcStatus] = React.useState() const [generation, setGeneration] = React.useState() - const [arrays, setArrays] = React.useState([]) - useEffect(() => { - loadData(gcStatus, generation).catch(console.error); - }, [gcStatus, generation]); - - const loadData = async (gcStatus?: ObjectGCStatus, generation?: Generation) => { + const getData = async () => { const client = getClient(); const result = await client.api.dump.arrays.sparse.stat.get( {queryParameters: {gcStatus: gcStatus, generation: generation}} ) - setArrays(result!) - setLoading(false) + return result! } const renderTable = (arrays: SparseArrayStatistics[]) => { @@ -83,27 +76,27 @@ export const SparseArraysStat = () => { ); } - let contents = loading - ? - - - : renderTable(arrays); + const getChildrenContent = (arrays: SparseArrayStatistics[]) => { + const totalWasted = arrays.map(m => m.totalWasted!).reduce((sum, current) => sum + current, 0) - const totalWasted = arrays.map(m => m.totalWasted!).reduce((sum, current) => sum + current, 0) + const propertyRows: PropertyRow[] = [ + {title: 'Count', value: String(arrays.length)}, + {title: 'Total wasted', value: toSizeString(totalWasted)}, + ] - const propertyRows: PropertyRow[] = [ - {title: 'Count', value: String(arrays.length)}, - {title: 'Total wasted', value: toSizeString(totalWasted)}, - ] + return + + {renderTable(arrays)} + + } return ( -
-
+ + setGcStatus(status)}/> setGeneration(generation)}/> -
- - {contents} -
+ + + ); } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts b/src/Heartbeat/ClientApp/src/pages/sparseArraysStat/index.ts similarity index 100% rename from src/Heartbeat/ClientApp/src/sparseArraysStat/index.ts rename to src/Heartbeat/ClientApp/src/pages/sparseArraysStat/index.ts From a1e17b4b515865a78f5ea42fb6b7731342b6b4c3 Mon Sep 17 00:00:00 2001 From: Alexey Sosnin Date: Sat, 27 Jan 2024 23:58:12 +0300 Subject: [PATCH 14/14] fix: ProgressContainer infinitive data refresh --- Directory.Build.props | 2 +- scripts/reinstall-release-tool.ps1 | 11 ++ scripts/update-ts-client.ps1 | 2 +- .../Analyzers/HeapDumpStatisticsAnalyzer.cs | 4 +- .../Extensions/ClrValueTypeExtensions.cs | 20 +- src/Heartbeat/ClientApp/api.yml | 40 +++- src/Heartbeat/ClientApp/src/App.tsx | 2 +- .../api/dump/object/item/fields/index.ts | 47 +++++ .../src/client/api/dump/object/item/index.ts | 7 + .../ClientApp/src/client/kiota-lock.json | 2 +- .../ClientApp/src/client/models/index.ts | 6 - .../ClientApp/src/clrObject/ClrObject.tsx | 146 -------------- .../src/components/ClrObjectRoot.tsx | 6 +- .../src/components/ProgressContainer.tsx | 45 +---- .../src/components/PropertiesTable.tsx | 2 + .../ClientApp/src/hooks/useNotifyError.ts | 17 ++ .../ClientApp/src/hooks/useStateWithFetch.ts | 45 +++++ .../src/hooks/useStateWithLoading.ts | 13 ++ .../src/lib/gridRenderCell/index.tsx | 4 +- .../src/lib/handleFetchData/index.ts | 27 +++ .../src/pages/arraysGrid/ArraysGrid.tsx | 49 +++-- .../src/pages/clrObject/ClrObject.tsx | 180 ++++++++++++++++++ .../src/{ => pages}/clrObject/index.ts | 0 .../src/pages/heapDumpStat/HeapDumpStat.tsx | 51 +++-- .../ClientApp/src/pages/home/Home.tsx | 34 +++- .../ClientApp/src/pages/modules/Modules.tsx | 38 +++- .../pages/objectInstances/ObjectInstances.tsx | 61 +++--- .../ClientApp/src/pages/roots/RootsGrid.tsx | 46 +++-- .../src/pages/segments/SegmentsGrid.tsx | 49 +++-- .../sparseArraysStat/SparseArraysStat.tsx | 50 +++-- .../stringDuplicates/StringDuplicates.tsx | 49 +++-- .../src/pages/stringsGrid/StringsGrid.tsx | 45 +++-- src/Heartbeat/Controllers/DumpController.cs | 139 +++++++++----- src/Heartbeat/Controllers/Models.cs | 3 +- src/Heartbeat/Program.cs | 37 +++- 35 files changed, 846 insertions(+), 433 deletions(-) create mode 100644 scripts/reinstall-release-tool.ps1 create mode 100644 src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts delete mode 100644 src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx create mode 100644 src/Heartbeat/ClientApp/src/hooks/useNotifyError.ts create mode 100644 src/Heartbeat/ClientApp/src/hooks/useStateWithFetch.ts create mode 100644 src/Heartbeat/ClientApp/src/hooks/useStateWithLoading.ts create mode 100644 src/Heartbeat/ClientApp/src/lib/handleFetchData/index.ts create mode 100644 src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx rename src/Heartbeat/ClientApp/src/{ => pages}/clrObject/index.ts (100%) diff --git a/Directory.Build.props b/Directory.Build.props index 036f119..2ac07e6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ - 0.3.0 + 0.4.0 https://github.com/Ne4to/Heartbeat true MIT diff --git a/scripts/reinstall-release-tool.ps1 b/scripts/reinstall-release-tool.ps1 new file mode 100644 index 0000000..453c379 --- /dev/null +++ b/scripts/reinstall-release-tool.ps1 @@ -0,0 +1,11 @@ +$ErrorActionPreference = "Stop" + +try +{ + dotnet tool uninstall -g Heartbeat + dotnet tool install --global Heartbeat +} +catch { + Write-Host 'Install global tool - FAILED!' -ForegroundColor Red + throw +} \ No newline at end of file diff --git a/scripts/update-ts-client.ps1 b/scripts/update-ts-client.ps1 index 4fbdfee..718eaab 100644 --- a/scripts/update-ts-client.ps1 +++ b/scripts/update-ts-client.ps1 @@ -16,7 +16,7 @@ try Set-Location $FrontendRoot $env:HEARTBEAT_GENERATE_CONTRACTS = 'true' dotnet swagger tofile --yaml --output $ContractPath $DllPath Heartbeat - dotnet kiota generate -l typescript --openapi $ContractPath -c HeartbeatClient -o ./src/client + dotnet kiota generate -l typescript --openapi $ContractPath -c HeartbeatClient -o ./src/client --clean-output # TODO try --serializer Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory --deserializer Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory } diff --git a/src/Heartbeat.Runtime/Analyzers/HeapDumpStatisticsAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/HeapDumpStatisticsAnalyzer.cs index e5fc3ad..d4cee18 100644 --- a/src/Heartbeat.Runtime/Analyzers/HeapDumpStatisticsAnalyzer.cs +++ b/src/Heartbeat.Runtime/Analyzers/HeapDumpStatisticsAnalyzer.cs @@ -38,9 +38,7 @@ public IReadOnlyCollection GetObjectTypeStatistics() { return ( from obj in Context.EnumerateObjects(ObjectGcStatus, Generation) - let objSize = obj.Size - //group new { size = objSize } by type.Name into g - group objSize by obj.Type + group obj.Size by obj.Type into g let totalSize = (ulong)g.Sum(t => (long)t) let clrType = g.Key diff --git a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs index d0ad47d..0ee6aba 100644 --- a/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs +++ b/src/Heartbeat.Runtime/Extensions/ClrValueTypeExtensions.cs @@ -56,16 +56,16 @@ private static bool IsValueDefault(ulong objRef, ClrInstanceField field) { return field.ElementType switch { - ClrElementType.Boolean => field.Read(objRef, true) == false, - ClrElementType.Char => field.Read(objRef, true) == (char)0, - ClrElementType.Int8 => field.Read(objRef, true) == (sbyte)0, - ClrElementType.UInt8 => field.Read(objRef, true) == (byte)0, - ClrElementType.Int16 => field.Read(objRef, true) == (short)0, - ClrElementType.UInt16 => field.Read(objRef, true) == (ushort)0, - ClrElementType.Int32 => field.Read(objRef, true) == 0, - ClrElementType.UInt32 => field.Read(objRef, true) == (uint)0, - ClrElementType.Int64 => field.Read(objRef, true) == 0L, - ClrElementType.UInt64 => field.Read(objRef, true) == 0UL, + ClrElementType.Boolean => field.Read(objRef, true) == default, + ClrElementType.Char => field.Read(objRef, true) == default, + ClrElementType.Int8 => field.Read(objRef, true) == default, + ClrElementType.UInt8 => field.Read(objRef, true) == default, + ClrElementType.Int16 => field.Read(objRef, true) == default, + ClrElementType.UInt16 => field.Read(objRef, true) == default, + ClrElementType.Int32 => field.Read(objRef, true) == default, + ClrElementType.UInt32 => field.Read(objRef, true) == default, + ClrElementType.Int64 => field.Read(objRef, true) == default, + ClrElementType.UInt64 => field.Read(objRef, true) == default, ClrElementType.Float => field.Read(objRef, true) == 0f, ClrElementType.Double => field.Read(objRef, true) == 0d, ClrElementType.NativeInt => field.Read(objRef, true) == nint.Zero, diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index 90ecae4..1a3c09c 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -326,6 +326,41 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '/api/dump/object/{address}/fields': + get: + tags: + - Dump + summary: Get object fields + operationId: GetClrObjectFields + parameters: + - name: address + in: path + required: true + style: simple + schema: + type: integer + format: int64 + 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/ClrObjectField' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/roots': get: tags: @@ -513,7 +548,6 @@ components: GetClrObjectResult: required: - address - - fields - generation - methodTable - size @@ -539,10 +573,6 @@ components: value: type: string nullable: true - fields: - type: array - items: - $ref: '#/components/schemas/ClrObjectField' additionalProperties: false GetObjectInstancesResult: required: diff --git a/src/Heartbeat/ClientApp/src/App.tsx b/src/Heartbeat/ClientApp/src/App.tsx index c82f7b7..b4047f3 100644 --- a/src/Heartbeat/ClientApp/src/App.tsx +++ b/src/Heartbeat/ClientApp/src/App.tsx @@ -7,7 +7,7 @@ import {Home} from './pages/home' import headDump from './pages/heapDumpStat' import segments from './pages/segments' import objectInstances from './pages/objectInstances' -import clrObject from './clrObject' +import clrObject from './pages/clrObject' import roots from './pages/roots' import modules from './pages/modules' import arraysGrid from './pages/arraysGrid' diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts new file mode 100644 index 0000000..1d7c701 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/fields/index.ts @@ -0,0 +1,47 @@ +/* tslint:disable */ +/* eslint-disable */ +// Generated by Microsoft Kiota +import { createClrObjectFieldFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, serializeProblemDetails, type ClrObjectField, 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}/fields + */ +export class FieldsRequestBuilder extends BaseRequestBuilder { + /** + * Instantiates a new FieldsRequestBuilder 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}/fields", (x, y) => new FieldsRequestBuilder(x, y)); + } + /** + * Get object fields + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a Promise of ClrObjectField + */ + public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "404": createProblemDetailsFromDiscriminatorValue, + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendCollectionAsync(requestInfo, createClrObjectFieldFromDiscriminatorValue, errorMapping); + } + /** + * Get object fields + * @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 0500f90..b274c22 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 { 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'; @@ -9,6 +10,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 fields property + */ + public get fields(): FieldsRequestBuilder { + return new FieldsRequestBuilder(this.pathParameters, this.requestAdapter); + } /** * The roots property */ diff --git a/src/Heartbeat/ClientApp/src/client/kiota-lock.json b/src/Heartbeat/ClientApp/src/client/kiota-lock.json index 193fc4f..1671570 100644 --- a/src/Heartbeat/ClientApp/src/client/kiota-lock.json +++ b/src/Heartbeat/ClientApp/src/client/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "C8AA6F5F9033AED37E5F620BE4B5E0F3469B936452E641FF28F0BB7C12B59F405D50EF0E1696D76A34962B94903018F34C6678287699BE4980C7FFCC7BB4EBE4", + "descriptionHash": "E058DCE3BA746E408EADE32EA9E442AFC9D72743F2398CD19A5CCA613B92AB688674BFB60B624AA3AD06461E16CA4C2EBC6F9C5C62315B4F01C073DEF44C1C6C", "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 4c29b46..3413fd7 100644 --- a/src/Heartbeat/ClientApp/src/client/models/index.ts +++ b/src/Heartbeat/ClientApp/src/client/models/index.ts @@ -166,7 +166,6 @@ export function deserializeIntoDumpInfo(dumpInfo: DumpInfo | undefined = {} as D export function deserializeIntoGetClrObjectResult(getClrObjectResult: GetClrObjectResult | undefined = {} as GetClrObjectResult) : Record void> { return { "address": n => { getClrObjectResult.address = n.getNumberValue(); }, - "fields": n => { getClrObjectResult.fields = n.getCollectionOfObjectValues(createClrObjectFieldFromDiscriminatorValue); }, "generation": n => { getClrObjectResult.generation = n.getEnumValue(GenerationObject); }, "methodTable": n => { getClrObjectResult.methodTable = n.getNumberValue(); }, "moduleName": n => { getClrObjectResult.moduleName = n.getStringValue(); }, @@ -304,10 +303,6 @@ export interface GetClrObjectResult extends Parsable { * The address property */ address?: number; - /** - * The fields property - */ - fields?: ClrObjectField[]; /** * The generation property */ @@ -516,7 +511,6 @@ export function serializeDumpInfo(writer: SerializationWriter, dumpInfo: DumpInf } export function serializeGetClrObjectResult(writer: SerializationWriter, getClrObjectResult: GetClrObjectResult | undefined = {} as GetClrObjectResult) : void { writer.writeNumberValue("address", getClrObjectResult.address); - writer.writeCollectionOfObjectValues("fields", getClrObjectResult.fields, serializeClrObjectField); writer.writeEnumValue("generation", getClrObjectResult.generation); writer.writeNumberValue("methodTable", getClrObjectResult.methodTable); writer.writeStringValue("moduleName", getClrObjectResult.moduleName); diff --git a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx deleted file mode 100644 index e004c93..0000000 --- a/src/Heartbeat/ClientApp/src/clrObject/ClrObject.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, {useEffect} from 'react'; -import {useParams} from 'react-router-dom'; -import LinearProgress from '@mui/material/LinearProgress'; -import {DataGrid, GridColDef, GridRenderCellParams, GridToolbar} from '@mui/x-data-grid'; -import Box from '@mui/material/Box'; - -import getClient from '../lib/getClient' -import toHexAddress from '../lib/toHexAddress' -import {GetClrObjectResult, ClrObjectField, ClrObjectRootPath} from '../client/models'; -import {PropertiesTable, PropertyRow} from '../components/PropertiesTable' -import {renderClrObjectLink, renderMethodTableLink} from "../lib/gridRenderCell"; -import {ClrObjectRoot} from "../components/ClrObjectRoot"; -import {methodTableColumn} from "../lib/gridColumns"; -import toSizeString from "../lib/toSizeString"; -import {Button, Link} from "react-admin"; - -export const ClrObject = () => { - const {id} = useParams(); - const [loading, setLoading] = React.useState(true) - const [objectResult, setObjectResult] = React.useState() - const [roots, setRoots] = React.useState() - const address = Number('0x' + id); - - const columns: GridColDef[] = [ - methodTableColumn, - { - field: 'offset', - headerName: 'Offset', - type: 'number', - width: 80 - }, - { - field: 'isValueType', - headerName: 'VT', - }, - { - field: 'typeName', - headerName: 'Type', - minWidth: 200, - flex: 0.5, - }, - { - field: 'name', - headerName: 'Name', - minWidth: 200, - flex: 0.5, - }, - { - field: 'value', - headerName: 'Value', - minWidth: 200, - flex: 1, - renderCell: (params: GridRenderCellParams) => { - if (params.value == null) { - return ''; - } - - const objectAddress = params.row.objectAddress; - - return objectAddress - ? - renderClrObjectLink(objectAddress) - // - : ( - params.value - ) - } - } - ]; - - useEffect(() => { - loadData().catch(console.error) - loadRoots().catch(console.error) - }, [address]); - - const loadData = async () => { - const client = getClient(); - - const result = await client.api.dump.object.byAddress(address).get() - setObjectResult(result!) - setLoading(false) - } - - // TODO move root to a separate tab - // 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 find other debugger visualizers - const loadRoots = async () => { - const client = getClient(); - const result = await client.api.dump.object.byAddress(address).roots.get() - setRoots(result!) - } - - const renderTable = (fields: ClrObjectField[]) => { - return ( - row.name} - columns={columns} - rowHeight={25} - pageSizeOptions={[20, 50, 100]} - density='compact' - slots={{toolbar: GridToolbar}} - initialState={{ - pagination: {paginationModel: {pageSize: 20}}, - }} - /> - ); - } - - let contents = loading - ? - - - : renderTable(objectResult!.fields!); - - 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}, - ) - } - - const rootGrid = roots && roots.length !== 0 - ? - :
root path not found
; - - return ( -
-

Clr Object

- - {contents} - {rootGrid} -
- ); -} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/components/ClrObjectRoot.tsx b/src/Heartbeat/ClientApp/src/components/ClrObjectRoot.tsx index a37ad44..85387df 100644 --- a/src/Heartbeat/ClientApp/src/components/ClrObjectRoot.tsx +++ b/src/Heartbeat/ClientApp/src/components/ClrObjectRoot.tsx @@ -4,6 +4,7 @@ import {DataGrid, GridColDef, GridToolbar} from '@mui/x-data-grid'; import {ClrObjectRootPath, RootPathItem} from '../client/models'; import {PropertiesTable, PropertyRow} from './PropertiesTable' import {methodTableColumn, objectAddressColumn, sizeColumn} from "../lib/gridColumns"; +import {Stack} from "@mui/material"; const columns: GridColDef[] = [ methodTableColumn, @@ -57,10 +58,9 @@ export const ClrObjectRoot = (props: ClrObjectRootProps) => { ] return ( -
-
Clr Object root
+ {grid} -
+ ); } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx b/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx index 73f329b..19a99e1 100644 --- a/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx +++ b/src/Heartbeat/ClientApp/src/components/ProgressContainer.tsx @@ -1,42 +1,15 @@ -import React, {useEffect} from "react"; -import {useNotify} from "react-admin"; +import React from "react"; import Box from "@mui/material/Box"; import LinearProgress from "@mui/material/LinearProgress"; import ErrorIcon from "@mui/icons-material/ErrorOutlineOutlined"; -export type ProgressContainerProps = { - loadData: () => Promise, - getChildren: (data: T) => JSX.Element +export type ProgressContainerProps = { + isLoading: boolean, + children?: JSX.Element, } -export const ProgressContainer = (props: ProgressContainerProps) => { - const [loading, setLoading] = React.useState(false) - const [hasError, setHasError] = React.useState(false) - const [data, setData] = React.useState() - const notify = useNotify(); - - useEffect(() => { - setLoading(true) - setHasError(false) - - props.loadData() - .then((data) => { - setData(data) - }) - .catch((error) => { - setHasError(true) - notify('API call error', { - type: 'error', - anchorOrigin: {vertical: 'top', horizontal: 'right'} - }) - }) - .finally(() => { - setLoading(false) - }) - - }, [props, notify]); - - if (loading) +export const ProgressContainer = (props: ProgressContainerProps) => { + if (props.isLoading) return ( @@ -44,8 +17,8 @@ export const ProgressContainer = (props: ProgressContainerProps) => { ); // TODO add error message and remove notify - if (hasError || data === undefined) - return + // if (hasError || data === undefined) + // return - return props.getChildren(data); + return props.children ?? (

No data to display

); } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/components/PropertiesTable.tsx b/src/Heartbeat/ClientApp/src/components/PropertiesTable.tsx index cb836b7..0faa315 100644 --- a/src/Heartbeat/ClientApp/src/components/PropertiesTable.tsx +++ b/src/Heartbeat/ClientApp/src/components/PropertiesTable.tsx @@ -1,3 +1,5 @@ +import React from "react"; + export type PropertyRow = { title: string, value?: string | React.ReactNode diff --git a/src/Heartbeat/ClientApp/src/hooks/useNotifyError.ts b/src/Heartbeat/ClientApp/src/hooks/useNotifyError.ts new file mode 100644 index 0000000..5ca2c94 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/hooks/useNotifyError.ts @@ -0,0 +1,17 @@ +import {useNotify} from "react-admin"; + +export const useNotifyError = () => { + const notify = useNotify() + + const notifyError = (message: string) => { + notify(message, { + type: 'error', + anchorOrigin: {vertical: 'top', horizontal: 'right'} + }) + } + + return { + notify, + notifyError, + } +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/hooks/useStateWithFetch.ts b/src/Heartbeat/ClientApp/src/hooks/useStateWithFetch.ts new file mode 100644 index 0000000..3125037 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/hooks/useStateWithFetch.ts @@ -0,0 +1,45 @@ +// import {DependencyList, useState} from "react"; +// +// // export type StateWithFetchProps = { +// // loadData: () => Promise, +// // getChildren: (data: T) => JSX.Element, +// // deps?: DependencyList +// // } +// + +export const useStateWithFetch = () => {} + +// // export const useStateWithFetch = ({someProp}) => { +// export const useStateWithFetch = () => { +// const [itemData, setItemData] = useState(); +// const [isLoading, setIsLoading] = useState(false); +// +// const resolveSessionData = useCallback(() => { +// const data = database.getItemData(); // fetching data +// setItemData(data); // setting fetched data to state +// }, []); +// +// useEffect(() => { +// if (someProp) { +// resolveSessionData(); // or may be fetch data based on props +// } +// }, [someProp]); +// +// const addNewItem = useCallback(function (dataToAdd) { +// /** +// * write code to add new item to state +// */ +// }, []) +// +// const removeExistingItem = useCallback(function (dataOrIndexToRemove) { +// /** +// * write code to remove existing item from state +// */ +// }, []) +// +// return { +// itemData, +// addNewItem, +// removeExistingItem, +// } +// } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/hooks/useStateWithLoading.ts b/src/Heartbeat/ClientApp/src/hooks/useStateWithLoading.ts new file mode 100644 index 0000000..7d24481 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/hooks/useStateWithLoading.ts @@ -0,0 +1,13 @@ +import {Dispatch, SetStateAction, useState} from "react"; + +export const useStateWithLoading = () : [T | undefined, Dispatch>, boolean, Dispatch>] => { + const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(false); + + return [ + data, + setData, + isLoading, + setIsLoading + ] +} \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx index 0e3c4a9..0fe8c6b 100644 --- a/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx +++ b/src/Heartbeat/ClientApp/src/lib/gridRenderCell/index.tsx @@ -15,10 +15,10 @@ export function renderClrObjectAddress(params: GridRenderCellParams): React.Reac return renderClrObjectLink(params.value); } -export function renderClrObjectLink(address: number):React.ReactNode { +export function renderClrObjectLink(address: number, label: string|undefined=undefined):React.ReactNode { const hexAddress = toHexAddress(address) return ( -