From ee6a2bc8204211a3fa7928588b21bdb3b4399d1c Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Thu, 14 May 2026 11:58:57 -0400 Subject: [PATCH 1/9] Fix typo in trigger creation modal --- src/app/Triggers/SmartTriggers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/Triggers/SmartTriggers.tsx b/src/app/Triggers/SmartTriggers.tsx index 9b0e0e890..af3d2ab8e 100644 --- a/src/app/Triggers/SmartTriggers.tsx +++ b/src/app/Triggers/SmartTriggers.tsx @@ -708,8 +708,8 @@ export const CreateSmartTriggersModal: React.FC = { value: '>', label: 'Greater Than (>)' }, { value: '>=', label: 'Greater Than/Equal To (>=)' }, { value: '==', label: 'Equal To' }, - { value: '<=', label: 'Less Than/Equal To (>)' }, - { value: '<', label: 'Less Than (>)' }, + { value: '<=', label: 'Less Than/Equal To (<=)' }, + { value: '<', label: 'Less Than (<)' }, ]; const reset = React.useCallback(() => { From 5b73d22c2725b3fb0e76a3e07e744c0a45673971 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 16 Jun 2026 10:05:02 -0400 Subject: [PATCH 2/9] Add frontend for heap dump visualization --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 930 ++++++++++++++++++ .../Analysis/HeapDumps/HeapDumpSelector.tsx | 82 ++ .../Diagnostics/Analysis/HeapDumps/types.ts | 151 +++ src/app/Shared/Services/Api.service.tsx | 39 + src/app/Shared/Services/api.types.ts | 1 + src/app/routes.tsx | 10 + 6 files changed, 1213 insertions(+) create mode 100644 src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx create mode 100644 src/app/Diagnostics/Analysis/HeapDumps/HeapDumpSelector.tsx create mode 100644 src/app/Diagnostics/Analysis/HeapDumps/types.ts diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx new file mode 100644 index 000000000..98f72b6fc --- /dev/null +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -0,0 +1,930 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { modalPrefillClearIntent, RootState } from '@app/Shared/Redux/ReduxStore'; +import { HeapDump, NullableTarget, Target } from '@app/Shared/Services/api.types'; +import { ServiceContext } from '@app/Shared/Services/Services'; +import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; +import { + Card, + CardBody, + CardTitle, + Content, + ContentVariants, + EmptyState, + Grid, + GridItem, + Stack, + StackItem, +} from '@patternfly/react-core'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; +import { concatMap, EMPTY, finalize, first, of } from 'rxjs'; +import { ClassAndSizeCombo, HeapDumpAnalysisResult, HighSizeObjects, ProblemFieldsEntry } from './types'; +import { TopologyIcon } from '@patternfly/react-icons'; +import { HeapDumpSelector } from './HeapDumpSelector'; +import { TargetView } from '@app/TargetView/TargetView'; +import { LoadingView } from '@app/Shared/Components/LoadingView'; +import { + ExpandableRowContent, + SortByDirection, + Table, + TableVariant, + Tbody, + Td, + Th, + Thead, + Tr, +} from '@patternfly/react-table'; +import { hashCode, sortResources, TableColumn } from '@app/utils/utils'; +import { useSort } from '@app/utils/hooks/useSort'; +import { AggregateDataCard } from '../AggregateDataCard.tsx'; + +export interface HeapDumpAnalysisProps {} + +const isSameTarget = (a: NullableTarget, b: NullableTarget): boolean => + a?.connectUrl === b?.connectUrl && a?.jvmId === b?.jvmId; + +interface ProblemFieldRowData { + problemFieldsInfo: ProblemFieldsEntry; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +interface HighSizeObjsRowData { + highSizeObjsInfo: HighSizeObjects; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +const collectionsColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['clazz'], + sortable: true, + }, + { + title: 'Problem Kind', + keyPaths: ['problemKind'], + sortable: true, + }, + { + title: 'Instances', + keyPaths: ['instances'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, +]; + +const dupArraysColumns: TableColumn[] = [ + { + title: 'Element Type', + keyPaths: ['elementType'], + sortable: true, + }, + { + title: 'Size', + keyPaths: ['size'], + sortable: true, + }, +]; + +const highSizeObjectsColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['clazz'], + sortable: true, + }, + { + title: 'Instances', + keyPaths: ['instances'], + sortable: true, + }, + { + title: 'Size/Overhead', + keyPaths: ['sizeOrOvhd'], + sortable: true, + }, +]; + +const problemFieldColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['class'], + sortable: true, + }, + { + title: 'Instances', + keyPaths: ['numInstances'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['allProblemFieldsOvhd'], + sortable: true, + }, + { + title: 'Status', + keyPaths: ['status'], + sortable: true, + }, +]; + +const objectHistogramTableColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['class'], + sortable: true, + }, + { + title: 'Instances', + keyPaths: ['instances'], + sortable: true, + }, + { + title: 'Inclusive Size', + keyPaths: ['inclusiveSize'], + sortable: true, + }, + { + title: 'Shallow Size', + keyPaths: ['shallowSize'], + sortable: true, + }, +]; + +const problemFieldSubColumns: TableColumn[] = [ + { + title: 'Field Name', + keyPaths: ['problemFieldNames'], + sortable: true, + }, + { + title: 'Declaring Class', + keyPaths: ['problemFieldDeclaringClasses'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['perFieldOvhd'], + sortable: true, + }, +]; + +export const HeapDumpAnalysis: React.FC = ({ ...props }) => { + const context = React.useContext(ServiceContext); + const addSubscription = useSubscriptions(); + const location = useLocation(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const modalPrefill = useSelector((state: RootState) => state.modalPrefill); + + const [target, setTarget] = React.useState(undefined as NullableTarget); + const [selectedHeapDump, setSelectedHeapDump] = React.useState(''); + const [analysisResult, setAnalysisResult] = React.useState(); + const [isAnalysisLoading, setIsAnalysisLoading] = React.useState(false); + const [heapDumps, setHeapDumps] = React.useState([]); + const [sortBy, getSortParams] = useSort(); + const [openProblemFieldRows, setOpenProblemFieldRows] = React.useState([]); + const [openHighSizeObjRows, setOpenHighSizeObjRows] = React.useState([]); + + const selectedHeapDumpJvmIdRef = React.useRef(); + + const targetAsObs = React.useMemo(() => of(target), [target]); + + const handleHeapDumpAnalysis = React.useCallback( + (result: HeapDumpAnalysisResult) => { + setAnalysisResult(result); + }, + [setAnalysisResult], + ); + + React.useEffect(() => { + const stateData = location.state as Record | null; + const reduxData = modalPrefill.route === location.pathname ? (modalPrefill.data as Record) : null; + + const prefillJvmId = (stateData?.jvmId || reduxData?.jvmId) as string | undefined; + const prefillHeapDump = (stateData?.id || reduxData?.id) as string | undefined; + + var jvmId = prefillJvmId ? prefillJvmId : ''; + var heapDumpId = prefillHeapDump ? prefillHeapDump : ''; + + setSelectedHeapDump(heapDumpId); + if (jvmId != '' && heapDumpId != '') { + context.api.analyzeHeapDump(jvmId, heapDumpId).subscribe({ next: handleHeapDumpAnalysis }); + } + dispatch(modalPrefillClearIntent()); + }, [ + context.api, + location.state, + location.search, + location.hash, + location.pathname, + modalPrefill, + dispatch, + navigate, + handleHeapDumpAnalysis, + setSelectedHeapDump, + ]); + + const handleHeapDumps = React.useCallback( + (heapDumps: HeapDump[]) => { + setHeapDumps(heapDumps); + }, + [setHeapDumps], + ); + + const queryHeapDumpAnalysis = React.useCallback( + (heapDump: string, jvmId?: string) => { + const selectedJvmId = jvmId || heapDumps.find((t) => t.heapDumpId === heapDump)?.jvmId; + selectedHeapDumpJvmIdRef.current = selectedJvmId; + setIsAnalysisLoading(true); + if (selectedJvmId) { + addSubscription( + context.api + .analyzeHeapDump(selectedJvmId, heapDump) + .pipe(finalize(() => setIsAnalysisLoading(false))) + .subscribe({ + next: handleHeapDumpAnalysis, + }), + ); + return; + } + + addSubscription( + targetAsObs + .pipe( + first(), + concatMap((target: Target | undefined) => { + if (target) { + selectedHeapDumpJvmIdRef.current = target.jvmId; + return context.api.analyzeHeapDump(target.jvmId ? target.jvmId : '', heapDump); + } + return EMPTY; + }), + finalize(() => setIsAnalysisLoading(false)), + ) + .subscribe({ + next: handleHeapDumpAnalysis, + }), + ); + }, + [addSubscription, context.api, handleHeapDumpAnalysis, targetAsObs, heapDumps], + ); + + const handleHeapDumpChange = React.useCallback( + (heapDump?: string) => { + setSelectedHeapDump(heapDump || ''); + setAnalysisResult(undefined); + selectedHeapDumpJvmIdRef.current = heapDump + ? heapDumps.find((t) => t.heapDumpId === heapDump)?.jvmId || target?.jvmId + : undefined; + if (heapDump) { + queryHeapDumpAnalysis(heapDump, selectedHeapDumpJvmIdRef.current); + } + }, + [setSelectedHeapDump, setAnalysisResult, heapDumps, target, queryHeapDumpAnalysis], + ); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe((t) => { + setTarget((currentTarget) => { + if (currentTarget && !isSameTarget(currentTarget, t)) { + const selectedHeapDumpJvmId = selectedHeapDumpJvmIdRef.current; + if (!selectedHeapDumpJvmId || selectedHeapDumpJvmId !== t?.jvmId) { + selectedHeapDumpJvmIdRef.current = undefined; + setAnalysisResult(undefined); + setSelectedHeapDump(''); + } + } + return t; + }); + }), + ); + }, [addSubscription, context.target, setTarget]); + + React.useEffect(() => { + addSubscription( + context.target.target().subscribe((t) => { + setTarget(t); + setAnalysisResult(undefined); + setSelectedHeapDump(''); + }), + ); + }, [addSubscription, context.target, setTarget]); + + const queryTargetHeapDumps = React.useCallback( + (target: Target) => context.api.getTargetHeapDumps(target), + [context.api], + ); + + React.useEffect(() => { + addSubscription( + targetAsObs + .pipe( + first(), + concatMap((target: Target | undefined) => { + if (target) { + return queryTargetHeapDumps(target); + } else { + return of([]); + } + }), + ) + .subscribe({ + next: handleHeapDumps, + }), + ); + }, [addSubscription, handleHeapDumps, queryTargetHeapDumps, targetAsObs]); + + const selector = React.useMemo(() => { + return ; + }, [selectedHeapDump, heapDumps, handleHeapDumpChange]); + + const emptyTableState = React.useCallback((title: string) => { + return ; + }, []); + + const duplicateStringStatsCard = React.useMemo(() => { + return ( + + Duplicate String Stats + + + {' '} + Total Strings: {analysisResult?.duplicateStringStats.totalStrings} + + + {' '} + Unique Strings: {analysisResult?.duplicateStringStats.uniqueStrings} + + + {' '} + Duplicate Strings: {analysisResult?.duplicateStringStats.duplicateStrings} + + + {' '} + Memory Overhead: {analysisResult?.duplicateStringStats.overhead} + + + + ); + }, [analysisResult]); + + const histogramStatsCard = React.useMemo(() => { + return ( + + Object Histogram Stats + + + {' '} + Total Classes: {analysisResult?.histogramStats.totalClasses} + + + {' '} + Total Objects: {analysisResult?.histogramStats.totalObjects} + + + {' '} + Zero Instances: {analysisResult?.histogramStats.zeroInstances} + + + {' '} + Single Instances: {analysisResult?.histogramStats.singleInstances} + + + + ); + }, [analysisResult]); + + const compressibleStringStatsCard = React.useMemo(() => { + return ( + + Compressible String Stats + + + {' '} + String Objects: {analysisResult?.compressibleStringStats.stringObjects} + + + {' '} + Backing Array Bytes: {analysisResult?.compressibleStringStats.backingArrayBytes} + + + {' '} + Compressed Strings: {analysisResult?.compressibleStringStats.compressedStrings} + + + {' '} + Compressed String Bytes: {analysisResult?.compressibleStringStats.compressedStringBytes} + + + {' '} + ASCII Strings: {analysisResult?.compressibleStringStats.asciiStrings} + + + {' '} + ASCII String Bytes: {analysisResult?.compressibleStringStats.asciiStringBytes} + + + + ); + }, [analysisResult]); + + const fundamentalStatsCard = React.useMemo(() => { + return ( + + Fundamental Stats + + + {' '} + Pointer Size: {analysisResult?.fundamentalStats.pointerSize} + + + {' '} + Narrow Pointers: {analysisResult?.fundamentalStats.narrowPointers} + + + {' '} + Object Header Size: {analysisResult?.fundamentalStats.objectHeaderSize} + + + {' '} + Object Header Alignment: {analysisResult?.fundamentalStats.objectHeaderAlignment} + + Num Objects: {analysisResult?.fundamentalStats.numObjects} + + {' '} + Object Instances: {analysisResult?.fundamentalStats.objectInstances} + + + {' '} + Object Arrays: {analysisResult?.fundamentalStats.objectArrays} + + + {' '} + Primitive Arrays: {analysisResult?.fundamentalStats.primitiveArrays} + + Object Size: {analysisResult?.fundamentalStats.objectSize} + + {' '} + Instance Size: {analysisResult?.fundamentalStats.instanceSize} + + + {' '} + Object Array Size: {analysisResult?.fundamentalStats.objArraySize} + + + {' '} + Primitive Array Size: {analysisResult?.fundamentalStats.primitiveSize} + + + + ); + }, [analysisResult]); + + const problemFieldsSubTable = React.useCallback((fields: string[], classes: string[], overhead: number[]) => { + return ( + + Problem Fields + + + + {problemFieldSubColumns.map(({ title }) => ( + + ))} + + + + + {fields.map((s: string) => ( + + ))} + {classes.map((s: string) => ( + + ))} + {overhead.map((n: number) => ( + + ))} + + +
{title}
+ {s ? s : 'N/A'} + + {s ? s : 'N/A'} + + {n ? n : 'N/A'} +
+
+ ); + }, []); + + const highSizeObjsSubTable = React.useCallback((objs: ClassAndSizeCombo[]) => { + return ( + + Classes and Sizes + + + + {highSizeObjectsColumns.map(({ title }) => ( + + ))} + + + + {objs.map((o) => { + + + + + ; + })} + +
{title}
+ {o.clazz ? o.clazz : 'N/A'} + + {o.numInstances ? o.numInstances : 'N/A'} + + {o.sizeOrOvhd ? o.sizeOrOvhd : 'N/A'} +
+
+ ); + }, []); + + const objectHistogramTable = React.useMemo(() => { + return analysisResult?.objectHistogram.map((o) => { + + Object Histogram + + + + {objectHistogramTableColumns.map(({ title }) => ( + + ))} + + + + + + + + + + +
{title}
+ {o.class ? o.class : 'N/A'} + + {o.instances ? o.instances : 'N/A'} + + {o.inclusiveSize ? o.inclusiveSize : 'N/A'} + + {o.shallowSize ? o.shallowSize : 'N/A'} +
+
; + }); + }, [analysisResult]); + + const collectionsTable = React.useMemo(() => { + return ( + // 0 is full reference chains, 1 is nearest field + analysisResult?.collectionClusters[0].map((coll) => { + + Problem Collections + Good Collections: {coll.numGoodCollections} + + + + {collectionsColumns.map(({ title }) => ( + + ))} + + + + {coll.classAndOvhdList.map((o) => { + + + + + + ; + })} + +
{title}
+ {o.clazz ? o.clazz : 'N/A'} + + {o.problemKind ? o.problemKind : 'N/A'} + + {o.instances ? o.instances : 'N/A'} + + {o.overhead ? o.overhead : 'N/A'} +
+
; + }) + ); + }, [analysisResult]); + + const dupArraysTable = React.useMemo(() => { + return ( + // 0 is full reference chains, 1 is nearest field + analysisResult?.duplicateArrayClusters[0].map((cluster) => { + + Duplicate Arrays + + + + {dupArraysColumns.map(({ title }) => ( + + ))} + + + + {cluster.entries.map((e) => { + + + + ; + })} + +
{title}
+ {e.elementType ? e.elementType : 'N/A'} + + {e.size ? e.size : 'N/A'} +
+
; + }) + ); + }, [analysisResult]); + + const onProblemFieldRowToggle = React.useCallback( + (d: ProblemFieldsEntry) => { + setOpenProblemFieldRows((old) => { + const typeId = hashCode(d.class); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenProblemFieldRows], + ); + + const onHighSizeObjsRowToggle = React.useCallback( + (d: HighSizeObjects) => { + setOpenHighSizeObjRows((old) => { + const typeId = hashCode(d.clazz); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenHighSizeObjRows], + ); + + const displayedProblemFieldRowData = React.useMemo(() => { + const rows: ProblemFieldRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.nullProblemFields ? analysisResult.nullProblemFields : [], + problemFieldColumns, + ); + if (analysisResult) { + sorted.forEach((d: ProblemFieldsEntry) => { + rows.push({ + problemFieldsInfo: d, + cellContents: [d.class, d.numInstances, d.allProblemFieldsOvhd, d.status], + isExpanded: openProblemFieldRows.some((id) => id === hashCode(d.class)), + children: problemFieldsSubTable(d.problemFieldNames, d.problemFieldDeclaringClasses, d.perFieldOvhd), + }); + }); + } + return rows; + }, [openProblemFieldRows, sortBy, problemFieldsSubTable, analysisResult]); + + const displayedHighSizeObjsRowData = React.useMemo(() => { + const rows: HighSizeObjsRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.highSizeObjectClusters[0] ? analysisResult.highSizeObjectClusters[0] : [], + problemFieldColumns, + ); + if (analysisResult) { + sorted.forEach((d: HighSizeObjects) => { + rows.push({ + highSizeObjsInfo: d, + cellContents: [d.clazz, d.numInstances, d.sizeOrOvhd], + isExpanded: openHighSizeObjRows.some((id) => id === hashCode(d.clazz)), + children: highSizeObjsSubTable(d.classAndSizeList), + }); + }); + } + return rows; + }, [openHighSizeObjRows, sortBy, problemFieldsSubTable, analysisResult]); + + const problemFieldTable = React.useMemo(() => { + if (displayedProblemFieldRowData.length) { + return displayedProblemFieldRowData.map((d: ProblemFieldRowData, index) => ( + + + + + ))} + + + + + + + + + + + + + +
+ {problemFieldColumns.map(({ title, sortable }, index) => ( + + {title} +
onProblemFieldRowToggle(d.problemFieldsInfo), + }} + /> + + {d.problemFieldsInfo.class ? d.problemFieldsInfo.class : 'N/A'} + + {d.problemFieldsInfo.numInstances ? d.problemFieldsInfo.numInstances : 'N/A'} + + {d.problemFieldsInfo.allProblemFieldsOvhd ? d.problemFieldsInfo.allProblemFieldsOvhd : 'N/A'} + + {d.problemFieldsInfo.status ? d.problemFieldsInfo.status : 'N/A'} +
+ {d.children} +
+ )); + } else { + return emptyTableState('No Problem Fields Detected'); + } + }, [displayedProblemFieldRowData, getSortParams, emptyTableState, onProblemFieldRowToggle]); + + const highSizeObjsTable = React.useMemo(() => { + if (displayedHighSizeObjsRowData.length) { + return displayedHighSizeObjsRowData.map((d: HighSizeObjsRowData, index) => ( + + + + + ))} + + + + + + + + + + + + +
+ {highSizeObjectsColumns.map(({ title, sortable }, index) => ( + + {title} +
onHighSizeObjsRowToggle(d.highSizeObjsInfo), + }} + /> + + {d.highSizeObjsInfo.clazz ? d.highSizeObjsInfo.clazz : 'N/A'} + + {d.highSizeObjsInfo.numInstances ? d.highSizeObjsInfo.numInstances : 'N/A'} + + {d.highSizeObjsInfo.sizeOrOvhd ? d.highSizeObjsInfo.sizeOrOvhd : 'N/A'} +
+ {d.children} +
+ )); + } else { + return emptyTableState('No High Size Objects Detected'); + } + }, [displayedHighSizeObjsRowData, getSortParams, emptyTableState, onHighSizeObjsRowToggle]); + + var view; + if (isAnalysisLoading) { + view = ; + } else if (analysisResult == undefined) { + view = emptyTableState('Select a Heap Dump to Analyze'); + } else { + view = ( + + {fundamentalStatsCard} + {compressibleStringStatsCard} + {duplicateStringStatsCard} + {histogramStatsCard} + + + Class Loader Instances + + { + return { data: t.value, count: t.count }; + })} + title="Class Loader Instances" + description="Class Loader Instance Statistics" + /> + + + + + + Class Loader Classes + + { + return { data: t.value, count: t.count }; + })} + title="Class Loader Classes" + description="Class Loader Class Statistics" + /> + + + + + + Problem Fields + {problemFieldTable} + + + + + Object Histogram + {objectHistogramTable} + + + + + Collection Custers + {collectionsTable} + + + + + Duplicate Array Custers + {dupArraysTable} + + + + + High Size Object Custers + {dupArraysTable} + + + + ); + } + + return ( + + {selector} + {view} + + ); +}; diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpSelector.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpSelector.tsx new file mode 100644 index 000000000..3b6cbd1f8 --- /dev/null +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpSelector.tsx @@ -0,0 +1,82 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HeapDump } from '@app/Shared/Services/api.types'; +import { portalRoot } from '@app/utils/utils'; +import { Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement } from '@patternfly/react-core'; +import * as React from 'react'; + +export interface HeapDumpSelectorProps { + selected: string; + heapDumps: HeapDump[]; + onSelect: (heapDump?: string) => void; +} + +export const HeapDumpSelector: React.FC = ({ selected, heapDumps, onSelect }) => { + const [isOpen, setIsOpen] = React.useState(false); + + const handleHeapDumpSelect = React.useCallback( + (_, selected: string) => { + if (!selected.length) { + onSelect(undefined); + setIsOpen(false); + } else { + onSelect(selected); + setIsOpen(false); + } + }, + [onSelect], + ); + + const onToggle = React.useCallback(() => setIsOpen((isOpen) => !isOpen), [setIsOpen]); + + const toggle = React.useCallback( + (toggleRef: React.Ref) => ( + + {selected == '' ? 'Select a Heap Dump' : selected} + + ), + [onToggle, isOpen, selected], + ); + + return ( + + + {heapDumps.map((t: HeapDump) => ( + + {t.heapDumpId} + + ))} + + + ); +}; diff --git a/src/app/Diagnostics/Analysis/HeapDumps/types.ts b/src/app/Diagnostics/Analysis/HeapDumps/types.ts new file mode 100644 index 000000000..fa5135bd1 --- /dev/null +++ b/src/app/Diagnostics/Analysis/HeapDumps/types.ts @@ -0,0 +1,151 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ObjectHistogramEntry { + class: string; + instances: number; + inclusiveSize: number; + shallowSize: number; +} + +export interface ProblemFieldsEntry { + class: string; + numInstances: number; + problemFieldNames: string[]; + problemFieldDeclaringClasses: string[]; + perFieldOvhd: number[]; + allProblemFieldsOvhd: number; + status: string; // SOME_FIELDS_EMPTY, ALL_FIELDS_EMPTY, NO_FIELDS, SOME_FIELDS_UNUSED_HI_BYTES; +} + +export interface ClassAndSizeCombo { + clazz: string; + numInstances: number; + sizeOrOvhd: number; +} + +export interface ClassAndOvhdCombo { + clazz: string; + problemKind: string; + instances: number; + overhead: number; +} + +export interface PrimitiveArrayWrapper { + elementType: string; + size: number; +} + +export interface WeakHashMaps { + numInstances: number; + colClasses: string[]; + valueTypeAndFieldSamples: string[]; +} + +export interface HighSizeObjects { + classAndSizeList: ClassAndSizeCombo[]; + clazz: string; + numInstances: number; + sizeOrOvhd: number; +} + +export interface DupArrays { + numNonDupArrays: number; + entries: PrimitiveArrayWrapper[]; +} + +export interface DupStrings { + printLongStrings: boolean; + printAllStrings: boolean; + numDupBackingCharArrays: number; + numNonDupStrings: number; + entries: string[]; +} + +export interface Collections { + classAndOvhdList: ClassAndOvhdCombo[]; + numGoodCollections: number; +} + +export interface AggregateValue { + value: string; + count: number; +} + +export interface DuplicateStringStats { + totalStrings: number; + uniqueStrings: number; + duplicateStrings: number; + overhead: number; +} + +export interface HistogramStats { + totalClasses: number; + totalObjects: number; + zeroInstances: number; + singleInstances: number; +} + +export interface CompressibleStringStats { + stringObjects: number; + backingArrayBytes: number; + compressedStrings: number; + compressedStringBytes: number; + asciiStrings: number; + asciiStringBytes: number; +} + +export interface FundamentalStats { + pointerSize: number; + narrowPointers: boolean; + objectHeaderSize: number; + objectHeaderAlignment: number; + numObjects: number; + objectInstances: number; + objectArrays: number; + primitiveArrays: number; + objectSize: number; + instanceSize: number; + objArraySize: number; + primitiveSize: number; +} + +export interface HeapDumpAnalysisResult { + // Reference Chains + collectionClusters: Collections[][]; + duplicateArrayClusters: DupArrays[][]; + duplicateStringClusters: DupStrings[][]; + highSizeObjectClusters: HighSizeObjects[][]; + + // Object Histogram + objectHistogram: ObjectHistogramEntry[]; + + // Problem Fields + nullProblemFields: ProblemFieldsEntry[]; + nearNullProblemFields: ProblemFieldsEntry[]; + fullBytesFields: ProblemFieldsEntry[]; + highBytesFields: ProblemFieldsEntry[]; + + // Classloader Stats + classLoaderInstanceStats: AggregateValue[]; + classLoaderClassStats: AggregateValue[]; + + // General Stats + compressibleStringStats: CompressibleStringStats; + duplicateStringStats: DuplicateStringStats; + histogramStats: HistogramStats; + fundamentalStats: FundamentalStats; +} diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 80c329f93..6b1c975c5 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -91,6 +91,7 @@ import { import { NotificationService } from './Notifications.service'; import { CryostatContext } from './Services'; import { TargetService } from './Target.service'; +import { HeapDumpAnalysisResult } from '@app/Diagnostics/Analysis/HeapDumps/types'; export class ApiService { private readonly archiveEnabled = new BehaviorSubject(true); @@ -829,6 +830,44 @@ export class ApiService { ); } + analyzeHeapDump( + jvmId: string, + heapDumpId: string, + suppressNotifications = false, + ): Observable { + return this.sendRequest( + 'beta', + `diagnostics/targets/${jvmId}/heapdump/${heapDumpId}/analyze`, + { + method: 'POST', + }, + undefined, + suppressNotifications, + ).pipe( + concatMap((resp) => resp.json()), + first(), + ); + } + + getHeapDumpReport( + jvmId: string, + heapDumpId: string, + suppressNotifications = false, + ): Observable { + return this.sendRequest( + 'beta', + `diagnostics/targets/${jvmId}/heapdump/${heapDumpId}/analyze`, + { + method: 'GET', + }, + undefined, + suppressNotifications, + ).pipe( + concatMap((resp) => resp.json()), + first(), + ); + } + getHeapDumps(suppressNotifications = false): Observable { return this.target.target().pipe( filter((t) => !!t), diff --git a/src/app/Shared/Services/api.types.ts b/src/app/Shared/Services/api.types.ts index 3d2b4de22..7c26c98c8 100644 --- a/src/app/Shared/Services/api.types.ts +++ b/src/app/Shared/Services/api.types.ts @@ -733,6 +733,7 @@ export enum NotificationCategory { HeapDumpFailure = 'HeapDumpFailure', HeapDumpDeleted = 'HeapDumpDeleted', HeapDumpMetadataUpdated = 'HeapDumpMetadataUpdated', + HeapDumpAnalysisSuccess = 'HeapDumpAnalysisSuccess', ThreadDumpSuccess = 'ThreadDumpSuccess', ThreadDumpFailure = 'ThreadDumpFailure', ThreadDumpDeleted = 'ThreadDumpDeleted', diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 1be572f2f..7df9e4437 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -45,6 +45,7 @@ import CaptureSmartTriggers from './Triggers/CaptureSmartTriggers'; import { useDocumentTitle } from './utils/hooks/useDocumentTitle'; import { useFeatureLevel } from './utils/hooks/useFeatureLevel'; import { accessibleRouteChangeHandler, BASEPATH, toPath } from './utils/utils'; +import { HeapDumpAnalysis } from './Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis'; let routeFocusTimer: number; const OVERVIEW = 'Routes.NavGroups.OVERVIEW'; @@ -216,6 +217,15 @@ const diagnosticsRoutes: IAppRoute[] = [ navGroup: DIAGNOSTICS, navSubgroup: ANALYZE, }, + { + component: HeapDumpAnalysis, + label: 'Analyze Heap Dumps', + path: toPath('/analyze-heap-dumps'), + title: 'Analyze Heap Dumps', + description: 'Analyze Heap Dump Data', + navGroup: DIAGNOSTICS, + navSubgroup: ANALYZE, + }, { component: AnalyzeHeapDumps, label: 'Heap Dumps', From dd2b1f737530ff4d6c7e7c88a84dd228d93c7ce2 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 16 Jun 2026 11:50:59 -0400 Subject: [PATCH 3/9] Notification handler --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 10 +++++++++- src/app/Shared/Services/api.utils.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index 98f72b6fc..3a1ab34bf 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -15,7 +15,7 @@ */ import { modalPrefillClearIntent, RootState } from '@app/Shared/Redux/ReduxStore'; -import { HeapDump, NullableTarget, Target } from '@app/Shared/Services/api.types'; +import { HeapDump, NotificationCategory, NullableTarget, Target } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { @@ -306,6 +306,14 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) [setSelectedHeapDump, setAnalysisResult, heapDumps, target, queryHeapDumpAnalysis], ); + React.useEffect(() => { + addSubscription( + context.notificationChannel.messages(NotificationCategory.HeapDumpAnalysisSuccess).subscribe((msg) => { + queryHeapDumpAnalysis(msg.message.heapDumpId, msg.message.jvmId); + }), + ); + }, [addSubscription, context.notificationChannel]); + React.useEffect(() => { addSubscription( context.target.target().subscribe((t) => { diff --git a/src/app/Shared/Services/api.utils.ts b/src/app/Shared/Services/api.utils.ts index f63b9b257..9537dc6ab 100644 --- a/src/app/Shared/Services/api.utils.ts +++ b/src/app/Shared/Services/api.utils.ts @@ -430,6 +430,14 @@ export const messageKeys = new Map([ body: (evt) => `${evt.message.heapDump.heapDumpId} in target ${evt.message.jvmId} metadata was updated`, } as NotificationMessageMapper, ], + [ + NotificationCategory.HeapDumpAnalysisSuccess, + { + variant: AlertVariant.success, + title: 'Heap Dump Analysis Success', + body: (evt) => `Analysis Job ${evt.message.jobId} for ${evt.message.heapDumpId} in target ${evt.message.jvmId} completed successfully`, + } as NotificationMessageMapper, + ], [ NotificationCategory.ThreadDumpSuccess, { From d9fe2ff4eff69f3e17d916291a49768f0389a241 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Tue, 16 Jun 2026 12:07:49 -0400 Subject: [PATCH 4/9] Get report directly for testing --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index 3a1ab34bf..c6d032c48 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -231,7 +231,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) setSelectedHeapDump(heapDumpId); if (jvmId != '' && heapDumpId != '') { - context.api.analyzeHeapDump(jvmId, heapDumpId).subscribe({ next: handleHeapDumpAnalysis }); + context.api.getHeapDumpReport(jvmId, heapDumpId).subscribe({ next: handleHeapDumpAnalysis }); } dispatch(modalPrefillClearIntent()); }, [ @@ -262,7 +262,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) if (selectedJvmId) { addSubscription( context.api - .analyzeHeapDump(selectedJvmId, heapDump) + .getHeapDumpReport(selectedJvmId, heapDump) .pipe(finalize(() => setIsAnalysisLoading(false))) .subscribe({ next: handleHeapDumpAnalysis, @@ -278,7 +278,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) concatMap((target: Target | undefined) => { if (target) { selectedHeapDumpJvmIdRef.current = target.jvmId; - return context.api.analyzeHeapDump(target.jvmId ? target.jvmId : '', heapDump); + return context.api.getHeapDumpReport(target.jvmId ? target.jvmId : '', heapDump); } return EMPTY; }), @@ -309,7 +309,15 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) React.useEffect(() => { addSubscription( context.notificationChannel.messages(NotificationCategory.HeapDumpAnalysisSuccess).subscribe((msg) => { - queryHeapDumpAnalysis(msg.message.heapDumpId, msg.message.jvmId); + addSubscription( + context.api + .getHeapDumpReport(msg.message.jvmId, msg.message.heapDumpId) + .pipe(finalize(() => setIsAnalysisLoading(false))) + .subscribe({ + next: handleHeapDumpAnalysis, + }), + ); + return; }), ); }, [addSubscription, context.notificationChannel]); From 4e5190855bf62b11f0b141ec0d0a9bb0bf14c45a Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Wed, 24 Jun 2026 00:10:44 -0400 Subject: [PATCH 5/9] Page reorganization, adjust for new api return format --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 1246 +++++++++++++---- .../Diagnostics/Analysis/HeapDumps/types.ts | 111 +- src/app/Shared/Services/api.utils.ts | 3 +- 3 files changed, 1028 insertions(+), 332 deletions(-) diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index c6d032c48..7302ea5ca 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -27,14 +27,28 @@ import { EmptyState, Grid, GridItem, - Stack, - StackItem, + Tab, + Tabs, + TabTitleText, } from '@patternfly/react-core'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; import { concatMap, EMPTY, finalize, first, of } from 'rxjs'; -import { ClassAndSizeCombo, HeapDumpAnalysisResult, HighSizeObjects, ProblemFieldsEntry } from './types'; +import { + AggregateValue, + DuplicateArray, + DuplicateString, + Field, + HeapDumpAnalysisResult, + HighSizeObjects, + HistogramEntry, + ObjectEntry, + ProblemClass, + ProblemCollection, + ProblemField, + WeakHashMapEntry, +} from './types'; import { TopologyIcon } from '@patternfly/react-icons'; import { HeapDumpSelector } from './HeapDumpSelector'; import { TargetView } from '@app/TargetView/TargetView'; @@ -60,7 +74,7 @@ const isSameTarget = (a: NullableTarget, b: NullableTarget): boolean => a?.connectUrl === b?.connectUrl && a?.jvmId === b?.jvmId; interface ProblemFieldRowData { - problemFieldsInfo: ProblemFieldsEntry; + problemFieldsInfo: ProblemField; isExpanded: boolean; cellContents: React.ReactNode[]; children?: React.ReactNode; @@ -73,20 +87,99 @@ interface HighSizeObjsRowData { children?: React.ReactNode; } +interface CollectionRowData { + collectionInfo: ProblemCollection; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +interface DupArrayRowData { + dupArrayInfo: DuplicateArray; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +interface DupStringRowData { + dupStringInfo: DuplicateString; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +interface WeakHashMapRowData { + weakHashMapInfo: WeakHashMapEntry; + isExpanded: boolean; + cellContents: React.ReactNode[]; + children?: React.ReactNode; +} + +const weakHashMapColumns: TableColumn[] = [ + { + title: 'Class and Field', + keyPaths: ['classAndField'], + sortable: true, + }, + { + title: 'Defining Class', + keyPaths: ['definingClass'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, + { + title: 'Bad Objects', + keyPaths: ['badObjs'], + sortable: true, + }, +]; + const collectionsColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['classAndField'], + sortable: true, + }, + { + title: 'Defining Class', + keyPaths: ['definingClass'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, + { + title: 'Bad Objects', + keyPaths: ['badObjs'], + sortable: true, + }, + { + title: 'Good Collections', + keyPaths: ['goodCollections'], + sortable: true, + }, +]; + +const collectionsSubColumns: TableColumn[] = [ { title: 'Class', keyPaths: ['clazz'], sortable: true, }, { - title: 'Problem Kind', + title: 'Problem Type', keyPaths: ['problemKind'], sortable: true, }, { title: 'Instances', - keyPaths: ['instances'], + keyPaths: ['numInstances'], sortable: true, }, { @@ -98,31 +191,84 @@ const collectionsColumns: TableColumn[] = [ const dupArraysColumns: TableColumn[] = [ { - title: 'Element Type', - keyPaths: ['elementType'], + title: 'Class and Field', + keyPaths: ['classAndField'], sortable: true, }, { - title: 'Size', - keyPaths: ['size'], + title: 'Defining Class', + keyPaths: ['definingClass'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, + { + title: 'Bad Objects', + keyPaths: ['badObjs'], + sortable: true, + }, + { + title: 'Non Duplicate Arrays', + keyPaths: ['nonDupArrays'], + sortable: true, + }, +]; + +const dupStringsColumns: TableColumn[] = [ + { + title: 'Class and Field', + keyPaths: ['classAndField'], + sortable: true, + }, + { + title: 'Defining Class', + keyPaths: ['definingClass'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, + { + title: 'Bad Objects', + keyPaths: ['badObjs'], + sortable: true, + }, + { + title: 'Backing Char Array Memory', + keyPaths: ['dupBackingCharArrays'], + sortable: true, + }, + { + title: 'Non Duplicate Arrays', + keyPaths: ['nonDupArrays'], sortable: true, }, ]; const highSizeObjectsColumns: TableColumn[] = [ { - title: 'Class', - keyPaths: ['clazz'], + title: 'Class and Field', + keyPaths: ['classAndField'], sortable: true, }, { - title: 'Instances', - keyPaths: ['instances'], + title: 'Defining Class', + keyPaths: ['definingClass'], sortable: true, }, { - title: 'Size/Overhead', - keyPaths: ['sizeOrOvhd'], + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, + { + title: 'Bad Objects', + keyPaths: ['badObjs'], sortable: true, }, ]; @@ -130,7 +276,7 @@ const highSizeObjectsColumns: TableColumn[] = [ const problemFieldColumns: TableColumn[] = [ { title: 'Class', - keyPaths: ['class'], + keyPaths: ['clazz'], sortable: true, }, { @@ -140,12 +286,12 @@ const problemFieldColumns: TableColumn[] = [ }, { title: 'Overhead', - keyPaths: ['allProblemFieldsOvhd'], + keyPaths: ['overhead'], sortable: true, }, { - title: 'Status', - keyPaths: ['status'], + title: 'Problem Type', + keyPaths: ['problemKind'], sortable: true, }, ]; @@ -191,6 +337,37 @@ const problemFieldSubColumns: TableColumn[] = [ }, ]; +const dupArraysSubColumns: TableColumn[] = [ + { + title: 'Array Value', + keyPaths: ['value'], + sortable: true, + }, + { + title: 'Duplicate Array Count', + keyPaths: ['count'], + sortable: true, + }, +]; + +const highSizeObjectsSubColumns: TableColumn[] = [ + { + title: 'Class', + keyPaths: ['clazz'], + sortable: true, + }, + { + title: 'Instances', + keyPaths: ['numInstances'], + sortable: true, + }, + { + title: 'Overhead', + keyPaths: ['overhead'], + sortable: true, + }, +]; + export const HeapDumpAnalysis: React.FC = ({ ...props }) => { const context = React.useContext(ServiceContext); const addSubscription = useSubscriptions(); @@ -206,11 +383,21 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) const [heapDumps, setHeapDumps] = React.useState([]); const [sortBy, getSortParams] = useSort(); const [openProblemFieldRows, setOpenProblemFieldRows] = React.useState([]); + const [openCollectionRows, setOpenCollectionRows] = React.useState([]); + const [openDupArrayRows, setOpenDupArrayRows] = React.useState([]); + const [openDupStringRows, setOpenDupStringRows] = React.useState([]); + const [openWeakHashMapRows, setOpenWeakHashMapRows] = React.useState([]); const [openHighSizeObjRows, setOpenHighSizeObjRows] = React.useState([]); const selectedHeapDumpJvmIdRef = React.useRef(); const targetAsObs = React.useMemo(() => of(target), [target]); + const [activeTab, setActiveTab] = React.useState(0); + + const onTabSelect = React.useCallback( + (_evt: MouseEvent | React.MouseEvent, idx: string | number) => setActiveTab(Number(idx)), + [setActiveTab], + ); const handleHeapDumpAnalysis = React.useCallback( (result: HeapDumpAnalysisResult) => { @@ -520,182 +707,636 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) ); }, [analysisResult]); - const problemFieldsSubTable = React.useCallback((fields: string[], classes: string[], overhead: number[]) => { + const problemFieldsSubTable = React.useCallback( + (fields: Field[]) => { + if (fields.length) { + return ( + + Problem Fields + + + + {problemFieldSubColumns.map(({ title }) => ( + + ))} + + + + {fields.map((f: Field) => ( + + + + + + ))} + +
{title}
+ {f.field ? f.field : 'N/A'} + + {f.clazz ? f.clazz : 'N/A'} + + {f.overhead ? f.overhead : 'N/A'} +
+
+ ); + } else { + return emptyTableState('No Problem Field Details Found'); + } + }, + [problemFieldSubColumns], + ); + + const collectionsSubTable = React.useCallback((classAndOvhds: ProblemClass[]) => { return ( - Problem Fields - + Collection Overhead Details +
- {problemFieldSubColumns.map(({ title }) => ( + {collectionsSubColumns.map(({ title }) => ( ))} - - {fields.map((s: string) => ( - + - ))} - {classes.map((s: string) => ( - - ))} - {overhead.map((n: number) => ( - - ))} - + + + ))}
{title}
- {s ? s : 'N/A'} + {classAndOvhds.map((c: ProblemClass) => ( +
+ {c.clazz ? c.clazz : 'N/A'} - {s ? s : 'N/A'} + + {c.problemKind ? c.problemKind : 'N/A'} - {n ? n : 'N/A'} + + {c.numInstances ? c.numInstances : 'N/A'}
+ {c.overhead ? c.overhead : 'N/A'} +
); }, []); - const highSizeObjsSubTable = React.useCallback((objs: ClassAndSizeCombo[]) => { - return ( - - Classes and Sizes - + const highSizeObjectsSubTable = React.useCallback( + (classAndSizeCombos: ObjectEntry[]) => { + if (classAndSizeCombos.length) { + return ( + + High Size Object Details +
+ + + {highSizeObjectsSubColumns.map(({ title }) => ( + + ))} + + + + {classAndSizeCombos.map((c: ObjectEntry) => ( + + + + + + ))} + +
{title}
+ {c.clazz ? c.clazz : 'N/A'} + + {c.numInstances ? c.numInstances : 'N/A'} + + {c.overhead ? c.overhead : 'N/A'} +
+
+ ); + } else { + return emptyTableState('No Detailed Information Found'); + } + }, + [emptyTableState], + ); + + const weakHashMapSubTable = React.useCallback( + (classes: String[]) => { + if (classes.length) { + return ( + + Weak HashMap Classes + + + + + + + + {classes.map((c: String) => ( + + + + ))} + +
{'Classes'}
+ {c ? c : 'N/A'} +
+
+ ); + } else { + return emptyTableState('No Weak HashMap Classes Found'); + } + }, + [emptyTableState], + ); + + const dupArraysSubTable = React.useCallback( + (aggregates: AggregateValue[]) => { + if (aggregates.length) { + return ( + + Duplicate Array Overhead Details + + + + {dupArraysSubColumns.map(({ title }) => ( + + ))} + + + + {aggregates.map((c: AggregateValue) => ( + + + + + ))} + +
{title}
+ {c.value ? c.value : 'N/A'} + + {c.count ? c.count : 'N/A'} +
+
+ ); + } else { + return emptyTableState('No Duplicate Array Overhead Details Found'); + } + }, + [emptyTableState], + ); + + const dupStringsSubTable = React.useCallback( + (aggregates: AggregateValue[]) => { + if (aggregates.length) { + return ( + + Duplicate String Overhead Details + + + + {dupArraysSubColumns.map(({ title }) => ( + + ))} + + + + {aggregates.map((c: AggregateValue) => ( + + + + + ))} + +
{title}
+ {c.value ? c.value : 'N/A'} + + {c.count ? c.count : 'N/A'} +
+
+ ); + } else { + return emptyTableState('No Duplicate String Details Found'); + } + }, + [emptyTableState], + ); + + const displayedCollectionRowData = React.useMemo(() => { + const rows: CollectionRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.problemCollections ? analysisResult?.problemCollections : [], + collectionsColumns, + ); + if (analysisResult) { + sorted.forEach((p: ProblemCollection) => { + rows.push({ + collectionInfo: p, + cellContents: [p.classAndField, p.definingClass, p.overhead, p.badObjs, p.goodCollections], + isExpanded: openCollectionRows.some((id) => id === hashCode(p.classAndField)), + children: collectionsSubTable(p.classAndOvhds), + }); + }); + } + return rows; + }, [openCollectionRows, sortBy, collectionsSubTable, analysisResult]); + + const displayedWeakHashMapRowData = React.useMemo(() => { + const rows: WeakHashMapRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.weakHashMapClusters ? analysisResult?.weakHashMapClusters : [], + dupArraysColumns, + ); + if (analysisResult) { + sorted.forEach((d: WeakHashMapEntry) => { + rows.push({ + weakHashMapInfo: d, + cellContents: [d.classAndField, d.definingClass, d.overhead, d.badObjs], + isExpanded: openWeakHashMapRows.some((id) => id === hashCode(d.classAndField)), + children: weakHashMapSubTable(d.classes), + }); + }); + } + return rows; + }, [openWeakHashMapRows, sortBy, weakHashMapSubTable, analysisResult]); + + const displayedDupArrayRowData = React.useMemo(() => { + const rows: DupArrayRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.duplicateArrays ? analysisResult?.duplicateArrays : [], + dupArraysColumns, + ); + if (analysisResult) { + sorted.forEach((d: DuplicateArray) => { + rows.push({ + dupArrayInfo: d, + cellContents: [d.classAndField, d.definingClass, d.overhead, d.badObjs, d.nonDupArrays], + isExpanded: openDupArrayRows.some((id) => id === hashCode(d.classAndField)), + children: dupArraysSubTable(d.aggregates), + }); + }); + } + return rows; + }, [openDupArrayRows, sortBy, dupArraysSubTable, analysisResult]); + + const displayedDupStringRowData = React.useMemo(() => { + const rows: DupStringRowData[] = []; + const sorted = sortResources( + { + index: sortBy.index ?? 1, + direction: sortBy.direction ?? SortByDirection.asc, + }, + analysisResult?.duplicateStrings ? analysisResult?.duplicateStrings : [], + dupStringsColumns, + ); + if (analysisResult) { + sorted.forEach((d: DuplicateString) => { + rows.push({ + dupStringInfo: d, + cellContents: [ + d.classAndField, + d.definingClass, + d.overhead, + d.badObjs, + d.dupBackingCharArrays, + d.nonDupStrings, + ], + isExpanded: openDupStringRows.some((id) => id === hashCode(d.classAndField)), + children: dupStringsSubTable(d.aggregates), + }); + }); + } + return rows; + }, [openDupStringRows, sortBy, dupStringsSubTable, analysisResult]); + + const onDupArrayRowToggle = React.useCallback( + (d: DuplicateArray) => { + setOpenDupArrayRows((old) => { + const typeId = hashCode(d.classAndField); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenDupArrayRows], + ); + + const onDupStringRowToggle = React.useCallback( + (d: DuplicateString) => { + setOpenDupStringRows((old) => { + const typeId = hashCode(d.classAndField); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenDupStringRows], + ); + + const onWeakHashMapRowToggle = React.useCallback( + (d: WeakHashMapEntry) => { + setOpenWeakHashMapRows((old) => { + const typeId = hashCode(d.classAndField); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenWeakHashMapRows], + ); + + const dupArraysTable = React.useMemo(() => { + if (displayedDupArrayRowData.length) { + return ( + - {highSizeObjectsColumns.map(({ title }) => ( - + ))} - - {objs.map((o) => { - - + + - - - ; - })} - + + + + + + + + ))}
{title} + {dupArraysColumns.map(({ title }) => ( + {title}
- {o.clazz ? o.clazz : 'N/A'} + {displayedDupArrayRowData.map((c: DupArrayRowData, index) => ( +
onDupArrayRowToggle(c.dupArrayInfo), + }} + /> + + {c.dupArrayInfo.classAndField !== undefined ? c.dupArrayInfo.classAndField : 'N/A'} - {o.numInstances ? o.numInstances : 'N/A'} + + {c.dupArrayInfo.definingClass !== undefined ? c.dupArrayInfo.definingClass : 'N/A'} - {o.sizeOrOvhd ? o.sizeOrOvhd : 'N/A'} + + {c.dupArrayInfo.overhead !== undefined ? c.dupArrayInfo.overhead : 'N/A'}
+ {c.dupArrayInfo.badObjs != null ? c.dupArrayInfo.badObjs : 'N/A'} + + {c.dupArrayInfo.nonDupArrays !== undefined ? c.dupArrayInfo.nonDupArrays : 'N/A'} +
+ {c.children} +
- - ); - }, []); + ); + } else { + return emptyTableState('No Duplicate Arrays Found'); + } + }, [displayedDupArrayRowData, emptyTableState, onDupArrayRowToggle]); - const objectHistogramTable = React.useMemo(() => { - return analysisResult?.objectHistogram.map((o) => { - - Object Histogram - + const weakHashMapTable = React.useMemo(() => { + if (displayedWeakHashMapRowData.length) { + return ( +
- {objectHistogramTableColumns.map(({ title }) => ( - + ))} - - - - - - + {displayedWeakHashMapRowData.map((c: WeakHashMapRowData, index) => ( + + + + + + + + + + + + ))} +
{title} + {weakHashMapColumns.map(({ title }) => ( + {title}
- {o.class ? o.class : 'N/A'} - - {o.instances ? o.instances : 'N/A'} - - {o.inclusiveSize ? o.inclusiveSize : 'N/A'} - - {o.shallowSize ? o.shallowSize : 'N/A'} -
onWeakHashMapRowToggle(c.weakHashMapInfo), + }} + /> + + {c.weakHashMapInfo.classAndField !== undefined ? c.weakHashMapInfo.classAndField : 'N/A'} + + {c.weakHashMapInfo.definingClass !== undefined ? c.weakHashMapInfo.definingClass : 'N/A'} + + {c.weakHashMapInfo.overhead !== undefined ? c.weakHashMapInfo.overhead : 'N/A'} + + {c.weakHashMapInfo.badObjs != null ? c.weakHashMapInfo.badObjs : 'N/A'} +
+ {c.children} +
+ ); + } else { + return emptyTableState('No Weak HashMaps Found'); + } + }, [displayedWeakHashMapRowData, emptyTableState, onWeakHashMapRowToggle]); + + const dupStringsTable = React.useMemo(() => { + if (displayedDupStringRowData.length) { + return ( + + + + + ))} - + + {displayedDupStringRowData.map((c: DupStringRowData, index) => ( + + + + + + + + + + + + + + ))}
+ {dupStringsColumns.map(({ title }) => ( + {title}
onDupStringRowToggle(c.dupStringInfo), + }} + /> + + {c.dupStringInfo.classAndField !== undefined ? c.dupStringInfo.classAndField : 'N/A'} + + {c.dupStringInfo.definingClass !== undefined ? c.dupStringInfo.definingClass : 'N/A'} + + {c.dupStringInfo.overhead !== undefined ? c.dupStringInfo.overhead : 'N/A'} + + {c.dupStringInfo.badObjs != null ? c.dupStringInfo.badObjs : 'N/A'} + + {c.dupStringInfo.dupBackingCharArrays !== undefined ? c.dupStringInfo.dupBackingCharArrays : 'N/A'} + + {c.dupStringInfo.nonDupStrings !== undefined ? c.dupStringInfo.nonDupStrings : 'N/A'} +
+ {c.children} +
-
; - }); - }, [analysisResult]); + ); + } else { + return emptyTableState('No Duplicate Strings Found'); + } + }, [displayedDupStringRowData, emptyTableState, onDupStringRowToggle]); - const collectionsTable = React.useMemo(() => { + const onCollectionRowToggle = React.useCallback( + (d: ProblemCollection) => { + setOpenCollectionRows((old) => { + const typeId = hashCode(d.classAndField); + if (old.some((id) => id === typeId)) { + return old.filter((id) => id !== typeId); + } + return [...old, typeId]; + }); + }, + [setOpenCollectionRows], + ); + + const histogramRows = React.useMemo( + () => + analysisResult?.objectHistogram.map((h: HistogramEntry) => ( + + + {h.clazz ? h.clazz : 'N/A'} + + + {h.numInstances ? h.numInstances : 'N/A'} + + + {h.inclusiveSize ? h.inclusiveSize : 'N/A'} + + + {h.shallowSize ? h.shallowSize : 'N/A'} + + + )), + [analysisResult], + ); + + const objectHistogramTable = React.useMemo(() => { return ( - // 0 is full reference chains, 1 is nearest field - analysisResult?.collectionClusters[0].map((coll) => { - - Problem Collections - Good Collections: {coll.numGoodCollections} - + + Object Histogram + {histogramRows?.length ? ( +
- {collectionsColumns.map(({ title }) => ( - + {objectHistogramTableColumns.map(({ title }) => ( + ))} - - {coll.classAndOvhdList.map((o) => { - - - - - - ; - })} - + {histogramRows}
{title}{title}
- {o.clazz ? o.clazz : 'N/A'} - - {o.problemKind ? o.problemKind : 'N/A'} - - {o.instances ? o.instances : 'N/A'} - - {o.overhead ? o.overhead : 'N/A'} -
-
; - }) + ) : ( + emptyTableState('No Object Histogram') + )} + ); }, [analysisResult]); - const dupArraysTable = React.useMemo(() => { - return ( - // 0 is full reference chains, 1 is nearest field - analysisResult?.duplicateArrayClusters[0].map((cluster) => { - - Duplicate Arrays - - - - {dupArraysColumns.map(({ title }) => ( - - ))} + const collectionsTable = React.useMemo(() => { + if (displayedCollectionRowData) { + return ( +
{title}
+ + + + ))} + + + {displayedCollectionRowData.map((c: CollectionRowData, index) => ( + + + + + + + + + + - - - {cluster.entries.map((e) => { - - - - ; - })} -
+ {collectionsColumns.map(({ title }) => ( + {title}
onCollectionRowToggle(c.collectionInfo), + }} + /> + + {c.collectionInfo.classAndField !== undefined ? c.collectionInfo.classAndField : 'N/A'} + + {c.collectionInfo.definingClass !== undefined ? c.collectionInfo.definingClass : 'N/A'} + + {c.collectionInfo.overhead !== undefined ? c.collectionInfo.overhead : 'N/A'} + + {c.collectionInfo.badObjs != null ? c.collectionInfo.badObjs : 'N/A'} + + {c.collectionInfo.goodCollections !== undefined ? c.collectionInfo.goodCollections : 'N/A'} +
+ {c.children} +
- {e.elementType ? e.elementType : 'N/A'} - - {e.size ? e.size : 'N/A'} -
-
; - }) - ); - }, [analysisResult]); + ))} + + ); + } else { + return emptyTableState('No Problem Collections Found'); + } + }, [displayedCollectionRowData, emptyTableState, onCollectionRowToggle]); const onProblemFieldRowToggle = React.useCallback( - (d: ProblemFieldsEntry) => { + (d: ProblemField) => { setOpenProblemFieldRows((old) => { - const typeId = hashCode(d.class); + const typeId = hashCode(d.clazz); if (old.some((id) => id === typeId)) { return old.filter((id) => id !== typeId); } @@ -708,7 +1349,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) const onHighSizeObjsRowToggle = React.useCallback( (d: HighSizeObjects) => { setOpenHighSizeObjRows((old) => { - const typeId = hashCode(d.clazz); + const typeId = hashCode(d.classAndField); if (old.some((id) => id === typeId)) { return old.filter((id) => id !== typeId); } @@ -729,12 +1370,12 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) problemFieldColumns, ); if (analysisResult) { - sorted.forEach((d: ProblemFieldsEntry) => { + sorted.forEach((d: ProblemField) => { rows.push({ problemFieldsInfo: d, - cellContents: [d.class, d.numInstances, d.allProblemFieldsOvhd, d.status], - isExpanded: openProblemFieldRows.some((id) => id === hashCode(d.class)), - children: problemFieldsSubTable(d.problemFieldNames, d.problemFieldDeclaringClasses, d.perFieldOvhd), + cellContents: [d.clazz, d.numInstances, d.overhead, d.problemKind], + isExpanded: openProblemFieldRows.some((id) => id === hashCode(d.clazz)), + children: problemFieldsSubTable(d.fields), }); }); } @@ -748,29 +1389,29 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) index: sortBy.index ?? 1, direction: sortBy.direction ?? SortByDirection.asc, }, - analysisResult?.highSizeObjectClusters[0] ? analysisResult.highSizeObjectClusters[0] : [], - problemFieldColumns, + analysisResult?.highSizeObjects ? analysisResult.highSizeObjects : [], + highSizeObjectsColumns, ); if (analysisResult) { sorted.forEach((d: HighSizeObjects) => { rows.push({ highSizeObjsInfo: d, - cellContents: [d.clazz, d.numInstances, d.sizeOrOvhd], - isExpanded: openHighSizeObjRows.some((id) => id === hashCode(d.clazz)), - children: highSizeObjsSubTable(d.classAndSizeList), + cellContents: [d.classAndField, d.badObjs, d.overhead], + isExpanded: openHighSizeObjRows.some((id) => id === hashCode(d.classAndField)), + children: highSizeObjectsSubTable(d.classAndSizeCombos), }); }); } return rows; - }, [openHighSizeObjRows, sortBy, problemFieldsSubTable, analysisResult]); + }, [openHighSizeObjRows, sortBy, highSizeObjectsSubTable, analysisResult]); const problemFieldTable = React.useMemo(() => { if (displayedProblemFieldRowData.length) { - return displayedProblemFieldRowData.map((d: ProblemFieldRowData, index) => ( + return ( - - - - - - - - - - - - + {displayedProblemFieldRowData.map((d: ProblemFieldRowData, index) => ( + + + + + + + + + + + + ))}
+ {problemFieldColumns.map(({ title, sortable }, index) => ( {title} @@ -778,38 +1419,40 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) ))}
onProblemFieldRowToggle(d.problemFieldsInfo), - }} - /> - - {d.problemFieldsInfo.class ? d.problemFieldsInfo.class : 'N/A'} - - {d.problemFieldsInfo.numInstances ? d.problemFieldsInfo.numInstances : 'N/A'} - - {d.problemFieldsInfo.allProblemFieldsOvhd ? d.problemFieldsInfo.allProblemFieldsOvhd : 'N/A'} - - {d.problemFieldsInfo.status ? d.problemFieldsInfo.status : 'N/A'} -
- {d.children} -
onProblemFieldRowToggle(d.problemFieldsInfo), + }} + /> + + {d.problemFieldsInfo.clazz ? d.problemFieldsInfo.clazz : 'N/A'} + + {d.problemFieldsInfo.numInstances ? d.problemFieldsInfo.numInstances : 'N/A'} + + {d.problemFieldsInfo.overhead !== undefined ? d.problemFieldsInfo.overhead : 'N/A'} + + {d.problemFieldsInfo.problemKind != null ? d.problemFieldsInfo.problemKind : 'N/A'} +
+ {d.children} +
- )); + ); } else { return emptyTableState('No Problem Fields Detected'); } @@ -817,7 +1460,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) const highSizeObjsTable = React.useMemo(() => { if (displayedHighSizeObjsRowData.length) { - return displayedHighSizeObjsRowData.map((d: HighSizeObjsRowData, index) => ( + return ( @@ -829,35 +1472,40 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) ))} - - - - - - - - - - + {displayedHighSizeObjsRowData.map((d: HighSizeObjsRowData, index) => ( + + + + + + + + + + + + ))}
onHighSizeObjsRowToggle(d.highSizeObjsInfo), - }} - /> - - {d.highSizeObjsInfo.clazz ? d.highSizeObjsInfo.clazz : 'N/A'} - - {d.highSizeObjsInfo.numInstances ? d.highSizeObjsInfo.numInstances : 'N/A'} - - {d.highSizeObjsInfo.sizeOrOvhd ? d.highSizeObjsInfo.sizeOrOvhd : 'N/A'} -
- {d.children} -
onHighSizeObjsRowToggle(d.highSizeObjsInfo), + }} + /> + + {d.highSizeObjsInfo.classAndField ? d.highSizeObjsInfo.classAndField : 'N/A'} + + {d.highSizeObjsInfo.definingClass ? d.highSizeObjsInfo.definingClass : 'N/A'} + + {d.highSizeObjsInfo.overhead ? d.highSizeObjsInfo.overhead : 'N/A'} + + {d.highSizeObjsInfo.badObjs ? d.highSizeObjsInfo.badObjs : 'N/A'} +
+ {d.children} +
- )); + ); } else { return emptyTableState('No High Size Objects Detected'); } @@ -870,70 +1518,104 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) view = emptyTableState('Select a Heap Dump to Analyze'); } else { view = ( - - {fundamentalStatsCard} - {compressibleStringStatsCard} - {duplicateStringStatsCard} - {histogramStatsCard} - - - Class Loader Instances - - { - return { data: t.value, count: t.count }; - })} - title="Class Loader Instances" - description="Class Loader Instance Statistics" - /> - - - - - - Class Loader Classes - - { - return { data: t.value, count: t.count }; - })} - title="Class Loader Classes" - description="Class Loader Class Statistics" - /> - - - - - - Problem Fields - {problemFieldTable} - - - - - Object Histogram - {objectHistogramTable} - - - - - Collection Custers - {collectionsTable} - - - - - Duplicate Array Custers - {dupArraysTable} - - - - - High Size Object Custers - {dupArraysTable} - - - + + Basic Statistics}> + + {fundamentalStatsCard} + {compressibleStringStatsCard} + {duplicateStringStatsCard} + {histogramStatsCard} + + + Class Loader Instances + + { + return { data: t.value, count: t.count }; + })} + title="Class Loader Instances" + description="Class Loader Instance Statistics" + /> + + + + + + Class Loader Classes + + { + return { data: t.value, count: t.count }; + })} + title="Class Loader Classes" + description="Class Loader Class Statistics" + /> + + + + + + Problem Fields}> + + + + Problem Fields + {problemFieldTable} + + + + + Object Histogram}> + + {objectHistogramTable} + + + Collections}> + + {collectionsTable} + + + Duplicate Arrays}> + + + + Duplicate Arrays + {dupArraysTable} + + + + + High Size Objects}> + + + + High Size Objects + {highSizeObjsTable} + + + + + Duplicate Strings}> + + + + Duplicate Strings + {dupStringsTable} + + + + + Weak HashMaps}> + + + + Weak HashMaps + {weakHashMapTable} + + + + + ); } diff --git a/src/app/Diagnostics/Analysis/HeapDumps/types.ts b/src/app/Diagnostics/Analysis/HeapDumps/types.ts index fa5135bd1..04bb247cb 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/types.ts +++ b/src/app/Diagnostics/Analysis/HeapDumps/types.ts @@ -14,70 +14,82 @@ * limitations under the License. */ -export interface ObjectHistogramEntry { - class: string; - instances: number; +export interface HistogramEntry { + clazz: string; + numInstances: number; inclusiveSize: number; shallowSize: number; } -export interface ProblemFieldsEntry { - class: string; +export interface ProblemClass { + clazz: string; + problemKind: string; numInstances: number; - problemFieldNames: string[]; - problemFieldDeclaringClasses: string[]; - perFieldOvhd: number[]; - allProblemFieldsOvhd: number; - status: string; // SOME_FIELDS_EMPTY, ALL_FIELDS_EMPTY, NO_FIELDS, SOME_FIELDS_UNUSED_HI_BYTES; + overhead: number; } -export interface ClassAndSizeCombo { - clazz: string; - numInstances: number; - sizeOrOvhd: number; +export interface ProblemCollection { + classAndField: string; + definingClass: string; + overhead: number; + badObjs: number; + goodCollections: number; + classAndOvhds: ProblemClass[]; } -export interface ClassAndOvhdCombo { - clazz: string; - problemKind: string; - instances: number; +export interface DuplicateArray { + classAndField: string; + definingClass: string; overhead: number; + badObjs: number; + nonDupArrays: number; + aggregates: AggregateValue[]; } -export interface PrimitiveArrayWrapper { - elementType: string; - size: number; +export interface DuplicateString { + classAndField: string; + definingClass: string; + overhead: number; + badObjs: number; + dupBackingCharArrays: number; + nonDupStrings: number; + aggregates: AggregateValue[]; } -export interface WeakHashMaps { +export interface ObjectEntry { + clazz: string; numInstances: number; - colClasses: string[]; - valueTypeAndFieldSamples: string[]; + overhead: number; } export interface HighSizeObjects { - classAndSizeList: ClassAndSizeCombo[]; - clazz: string; - numInstances: number; - sizeOrOvhd: number; + classAndField: string; + definingClass: string; + overhead: number; + badObjs: number; + classAndSizeCombos: ObjectEntry[]; } -export interface DupArrays { - numNonDupArrays: number; - entries: PrimitiveArrayWrapper[]; +export interface WeakHashMapEntry { + classAndField: string; + definingClass: string; + overhead: number; + badObjs: number; + classes: string[]; } -export interface DupStrings { - printLongStrings: boolean; - printAllStrings: boolean; - numDupBackingCharArrays: number; - numNonDupStrings: number; - entries: string[]; +export interface Field { + clazz: string; + field: string; + overhead: number; } -export interface Collections { - classAndOvhdList: ClassAndOvhdCombo[]; - numGoodCollections: number; +export interface ProblemField { + clazz: string; + numInstances: number; + fields: Field[]; + overhead: number; + problemKind: string; // SOME_FIELDS_EMPTY, ALL_FIELDS_EMPTY, NO_FIELDS, SOME_FIELDS_UNUSED_HI_BYTES; } export interface AggregateValue { @@ -125,19 +137,20 @@ export interface FundamentalStats { export interface HeapDumpAnalysisResult { // Reference Chains - collectionClusters: Collections[][]; - duplicateArrayClusters: DupArrays[][]; - duplicateStringClusters: DupStrings[][]; - highSizeObjectClusters: HighSizeObjects[][]; + problemCollections: ProblemCollection[]; + duplicateArrays: DuplicateArray[]; + duplicateStrings: DuplicateString[]; + highSizeObjects: HighSizeObjects[]; + weakHashMapClusters: WeakHashMapEntry[]; // Object Histogram - objectHistogram: ObjectHistogramEntry[]; + objectHistogram: HistogramEntry[]; // Problem Fields - nullProblemFields: ProblemFieldsEntry[]; - nearNullProblemFields: ProblemFieldsEntry[]; - fullBytesFields: ProblemFieldsEntry[]; - highBytesFields: ProblemFieldsEntry[]; + nullProblemFields: ProblemField[]; + nearNullProblemFields: ProblemField[]; + fullBytesFields: ProblemField[]; + highBytesFields: ProblemField[]; // Classloader Stats classLoaderInstanceStats: AggregateValue[]; diff --git a/src/app/Shared/Services/api.utils.ts b/src/app/Shared/Services/api.utils.ts index 9537dc6ab..79b3d3a14 100644 --- a/src/app/Shared/Services/api.utils.ts +++ b/src/app/Shared/Services/api.utils.ts @@ -435,7 +435,8 @@ export const messageKeys = new Map([ { variant: AlertVariant.success, title: 'Heap Dump Analysis Success', - body: (evt) => `Analysis Job ${evt.message.jobId} for ${evt.message.heapDumpId} in target ${evt.message.jvmId} completed successfully`, + body: (evt) => + `Analysis Job ${evt.message.jobId} for ${evt.message.heapDumpId} in target ${evt.message.jvmId} completed successfully`, } as NotificationMessageMapper, ], [ From 7f0235e134f855088b2a03aad78ee3d9554f75cf Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Wed, 24 Jun 2026 00:28:06 -0400 Subject: [PATCH 6/9] Prefill --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 139 ++++++++++++++---- src/app/Diagnostics/HeapDumpsTable.tsx | 29 +++- 2 files changed, 136 insertions(+), 32 deletions(-) diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index 7302ea5ca..c79a25355 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -34,7 +34,7 @@ import { import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; -import { concatMap, EMPTY, finalize, first, of } from 'rxjs'; +import { concatMap, EMPTY, finalize, first, map, of } from 'rxjs'; import { AggregateValue, DuplicateArray, @@ -73,6 +73,46 @@ export interface HeapDumpAnalysisProps {} const isSameTarget = (a: NullableTarget, b: NullableTarget): boolean => a?.connectUrl === b?.connectUrl && a?.jvmId === b?.jvmId; +interface HeapDumpPrefillLocation { + state: unknown; + search: string; + pathname: string; +} + +interface HeapDumpPrefillStore { + route: string | null; + data: unknown; +} + +interface HeapDumpPrefill { + jvmId?: string; + heapDumpId?: string; +} + +const firstString = (...values: unknown[]): string | undefined => + values.find((value): value is string => typeof value === 'string' && value.length > 0); + +const readHeapDumpPrefill = ( + location: HeapDumpPrefillLocation, + modalPrefill: HeapDumpPrefillStore, +): HeapDumpPrefill => { + const stateData = location.state as Record | null; + const reduxData = modalPrefill.route === location.pathname ? (modalPrefill.data as Record) : null; + const params = new URLSearchParams(location.search); + + return { + jvmId: firstString(stateData?.jvmId, reduxData?.jvmId, params.get('jvmId')), + heapDumpId: firstString( + stateData?.id, + stateData?.heapDumpId, + reduxData?.id, + reduxData?.heapDumpId, + params.get('heapDumpId'), + params.get('id'), + ), + }; +}; + interface ProblemFieldRowData { problemFieldsInfo: ProblemField; isExpanded: boolean; @@ -394,6 +434,20 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) const targetAsObs = React.useMemo(() => of(target), [target]); const [activeTab, setActiveTab] = React.useState(0); + const prefill = React.useMemo( + () => + readHeapDumpPrefill( + { + pathname: location.pathname, + search: location.search, + state: location.state, + }, + modalPrefill, + ), + [location.pathname, location.search, location.state, modalPrefill], + ); + const hasPendingPrefill = !!prefill.jvmId && !!prefill.heapDumpId; + const onTabSelect = React.useCallback( (_evt: MouseEvent | React.MouseEvent, idx: string | number) => setActiveTab(Number(idx)), [setActiveTab], @@ -406,33 +460,23 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) [setAnalysisResult], ); - React.useEffect(() => { - const stateData = location.state as Record | null; - const reduxData = modalPrefill.route === location.pathname ? (modalPrefill.data as Record) : null; - - const prefillJvmId = (stateData?.jvmId || reduxData?.jvmId) as string | undefined; - const prefillHeapDump = (stateData?.id || reduxData?.id) as string | undefined; - - var jvmId = prefillJvmId ? prefillJvmId : ''; - var heapDumpId = prefillHeapDump ? prefillHeapDump : ''; - - setSelectedHeapDump(heapDumpId); - if (jvmId != '' && heapDumpId != '') { - context.api.getHeapDumpReport(jvmId, heapDumpId).subscribe({ next: handleHeapDumpAnalysis }); - } - dispatch(modalPrefillClearIntent()); - }, [ - context.api, - location.state, - location.search, - location.hash, - location.pathname, - modalPrefill, - dispatch, - navigate, - handleHeapDumpAnalysis, - setSelectedHeapDump, - ]); + const findTargetByJvmId = React.useCallback( + (jvmId: string) => + context.targets.targets().pipe( + first(), + concatMap((targets) => { + const matchedTarget = targets.find((target) => target.jvmId === jvmId); + if (matchedTarget) { + return of(matchedTarget); + } + return context.targets.queryForTargets().pipe( + concatMap(() => context.targets.targets().pipe(first())), + map((targets) => targets.find((target) => target.jvmId === jvmId)), + ); + }), + ), + [context.targets], + ); const handleHeapDumps = React.useCallback( (heapDumps: HeapDump[]) => { @@ -479,6 +523,43 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) [addSubscription, context.api, handleHeapDumpAnalysis, targetAsObs, heapDumps], ); + React.useEffect(() => { + if (!prefill.jvmId || !prefill.heapDumpId) { + return; + } + + addSubscription( + findTargetByJvmId(prefill.jvmId).subscribe((target) => { + setAnalysisResult(undefined); + selectedHeapDumpJvmIdRef.current = prefill.jvmId; + setSelectedHeapDump(prefill.heapDumpId!); + if (target) { + context.target.setTarget(target); + } + queryHeapDumpAnalysis(prefill.heapDumpId!, prefill.jvmId); + dispatch(modalPrefillClearIntent()); + if (location.state || location.search) { + navigate(`${location.pathname}${location.hash}`, { replace: true, state: null }); + } + }), + ); + }, [ + addSubscription, + context.target, + location.state, + location.search, + location.hash, + location.pathname, + prefill.jvmId, + prefill.heapDumpId, + dispatch, + findTargetByJvmId, + navigate, + queryHeapDumpAnalysis, + setAnalysisResult, + setSelectedHeapDump, + ]); + const handleHeapDumpChange = React.useCallback( (heapDump?: string) => { setSelectedHeapDump(heapDump || ''); @@ -1512,7 +1593,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) }, [displayedHighSizeObjsRowData, getSortParams, emptyTableState, onHighSizeObjsRowToggle]); var view; - if (isAnalysisLoading) { + if (isAnalysisLoading || (analysisResult == null && hasPendingPrefill)) { view = ; } else if (analysisResult == undefined) { view = emptyTableState('Select a Heap Dump to Analyze'); diff --git a/src/app/Diagnostics/HeapDumpsTable.tsx b/src/app/Diagnostics/HeapDumpsTable.tsx index c4a25dfc5..cde9782a2 100644 --- a/src/app/Diagnostics/HeapDumpsTable.tsx +++ b/src/app/Diagnostics/HeapDumpsTable.tsx @@ -27,7 +27,7 @@ import { HeapDumpDeleteFilterIntent, TargetHeapDumpFilters, } from '@app/Shared/Redux/Filters/HeapDumpFilterSlice'; -import { RootState, StateDispatch } from '@app/Shared/Redux/ReduxStore'; +import { modalPrefillSetIntent, RootState, StateDispatch, store } from '@app/Shared/Redux/ReduxStore'; import { NotificationCategory, HeapDump, @@ -38,7 +38,7 @@ import { import { ServiceContext } from '@app/Shared/Services/Services'; import useDayjs from '@app/utils/hooks/useDayjs'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { TableColumn, formatBytes, hashCode, portalRoot, sortResources } from '@app/utils/utils'; +import { TableColumn, formatBytes, hashCode, portalRoot, sortResources, toPath } from '@app/utils/utils'; import { useCryostatTranslation } from '@i18n/i18nextUtil'; import { Toolbar, @@ -74,6 +74,7 @@ import { combineLatest, concatMap, first, forkJoin, Observable, of } from 'rxjs' import { ColumnConfig, DiagnosticsTable } from './DiagnosticsTable'; import { filterHeapDumps, HeapDumpFilters, HeapDumpFiltersCategories } from './Filters/HeapDumpFilters'; import { HeapDumpLabelsPanel } from './HeapDumpLabelsPanel'; +import { useNavigate } from 'react-router-dom-v5-compat'; const tableColumns: TableColumn[] = [ { @@ -516,6 +517,23 @@ export interface HeapDumpActionProps { export const HeapDumpAction: React.FC = ({ heapDump, onDownload, ...props }) => { const { t } = useCryostatTranslation(); const [isOpen, setIsOpen] = React.useState(false); + const navigate = useNavigate(); + + const handleViewInAnalysis = React.useCallback( + (jvmId) => { + const id = heapDump.heapDumpId; + const analysisPath = toPath('/analyze-heap-dumps'); + const params = new URLSearchParams({ jvmId, heapDumpId: id }); + const state = { + jvmId, + id, + threadDumpId: id, + }; + store.dispatch(modalPrefillSetIntent(analysisPath, state as Record)); + navigate(`${analysisPath}?${params.toString()}`, { state }); + }, + [heapDump, navigate], + ); const actionItems = React.useMemo(() => { return [ @@ -524,8 +542,13 @@ export const HeapDumpAction: React.FC = ({ heapDump, onDown key: 'download-heapdump', onClick: () => onDownload(heapDump), }, + { + title: 'Analyze Heap Dump', + key: 'analyze-heapdump', + onClick: () => handleViewInAnalysis(heapDump.jvmId), + }, ] as RowAction[]; - }, [onDownload, heapDump]); + }, [onDownload, handleViewInAnalysis, heapDump]); const toggle = React.useCallback( (toggleRef: React.Ref) => ( From d7708461c7286551356d0889f5c6fcd19e796fa4 Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Wed, 24 Jun 2026 00:49:52 -0400 Subject: [PATCH 7/9] eslint --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 42 +++++++++---------- src/app/Diagnostics/HeapDumpsTable.tsx | 2 +- src/app/Shared/Services/Api.service.tsx | 2 +- src/app/routes.tsx | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index c79a25355..667ab32dd 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -14,10 +14,14 @@ * limitations under the License. */ +import { LoadingView } from '@app/Shared/Components/LoadingView'; import { modalPrefillClearIntent, RootState } from '@app/Shared/Redux/ReduxStore'; import { HeapDump, NotificationCategory, NullableTarget, Target } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; +import { TargetView } from '@app/TargetView/TargetView'; +import { useSort } from '@app/utils/hooks/useSort'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; +import { hashCode, sortResources, TableColumn } from '@app/utils/utils'; import { Card, CardBody, @@ -31,10 +35,24 @@ import { Tabs, TabTitleText, } from '@patternfly/react-core'; +import { TopologyIcon } from '@patternfly/react-icons'; +import { + ExpandableRowContent, + SortByDirection, + Table, + TableVariant, + Tbody, + Td, + Th, + Thead, + Tr, +} from '@patternfly/react-table'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; import { concatMap, EMPTY, finalize, first, map, of } from 'rxjs'; +import { AggregateDataCard } from '../AggregateDataCard.tsx'; +import { HeapDumpSelector } from './HeapDumpSelector'; import { AggregateValue, DuplicateArray, @@ -49,24 +67,6 @@ import { ProblemField, WeakHashMapEntry, } from './types'; -import { TopologyIcon } from '@patternfly/react-icons'; -import { HeapDumpSelector } from './HeapDumpSelector'; -import { TargetView } from '@app/TargetView/TargetView'; -import { LoadingView } from '@app/Shared/Components/LoadingView'; -import { - ExpandableRowContent, - SortByDirection, - Table, - TableVariant, - Tbody, - Td, - Th, - Thead, - Tr, -} from '@patternfly/react-table'; -import { hashCode, sortResources, TableColumn } from '@app/utils/utils'; -import { useSort } from '@app/utils/hooks/useSort'; -import { AggregateDataCard } from '../AggregateDataCard.tsx'; export interface HeapDumpAnalysisProps {} @@ -588,7 +588,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) return; }), ); - }, [addSubscription, context.notificationChannel]); + }, [addSubscription, handleHeapDumpAnalysis, setIsAnalysisLoading, context.api, context.notificationChannel]); React.useEffect(() => { addSubscription( @@ -824,7 +824,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) return emptyTableState('No Problem Field Details Found'); } }, - [problemFieldSubColumns], + [emptyTableState], ); const collectionsSubTable = React.useCallback((classAndOvhds: ProblemClass[]) => { @@ -1358,7 +1358,7 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) )} ); - }, [analysisResult]); + }, [emptyTableState, histogramRows]); const collectionsTable = React.useMemo(() => { if (displayedCollectionRowData) { diff --git a/src/app/Diagnostics/HeapDumpsTable.tsx b/src/app/Diagnostics/HeapDumpsTable.tsx index cde9782a2..dcdd7a1a0 100644 --- a/src/app/Diagnostics/HeapDumpsTable.tsx +++ b/src/app/Diagnostics/HeapDumpsTable.tsx @@ -70,11 +70,11 @@ import { ISortBy, SortByDirection, Table, Tbody, Td, ThProps, Tr } from '@patter import _ from 'lodash'; import * as React from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { combineLatest, concatMap, first, forkJoin, Observable, of } from 'rxjs'; import { ColumnConfig, DiagnosticsTable } from './DiagnosticsTable'; import { filterHeapDumps, HeapDumpFilters, HeapDumpFiltersCategories } from './Filters/HeapDumpFilters'; import { HeapDumpLabelsPanel } from './HeapDumpLabelsPanel'; -import { useNavigate } from 'react-router-dom-v5-compat'; const tableColumns: TableColumn[] = [ { diff --git a/src/app/Shared/Services/Api.service.tsx b/src/app/Shared/Services/Api.service.tsx index 6b1c975c5..240f10c5c 100644 --- a/src/app/Shared/Services/Api.service.tsx +++ b/src/app/Shared/Services/Api.service.tsx @@ -15,6 +15,7 @@ */ import { LayoutTemplate, SerialLayoutTemplate } from '@app/Dashboard/types'; +import { HeapDumpAnalysisResult } from '@app/Diagnostics/Analysis/HeapDumps/types'; import { createBlobURL } from '@app/utils/utils'; import { ValidatedOptions } from '@patternfly/react-core'; import _ from 'lodash'; @@ -91,7 +92,6 @@ import { import { NotificationService } from './Notifications.service'; import { CryostatContext } from './Services'; import { TargetService } from './Target.service'; -import { HeapDumpAnalysisResult } from '@app/Diagnostics/Analysis/HeapDumps/types'; export class ApiService { private readonly archiveEnabled = new BehaviorSubject(true); diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 7df9e4437..4c2516265 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -22,6 +22,7 @@ import AsyncProfiler from './AsyncProfiler/AsyncProfiler'; import CreateAsyncProfilerSession from './AsyncProfiler/CreateAsyncProfilerSession'; import Dashboard from './Dashboard/Dashboard'; import DashboardSolo from './Dashboard/DashboardSolo'; +import { HeapDumpAnalysis } from './Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis'; import ThreadDumpAnalysis from './Diagnostics/Analysis/ThreadDumpAnalysis'; import { AnalyzeHeapDumps } from './Diagnostics/AnalyzeHeapDumps'; import AnalyzeThreadDumps from './Diagnostics/AnalyzeThreadDumps'; @@ -45,7 +46,6 @@ import CaptureSmartTriggers from './Triggers/CaptureSmartTriggers'; import { useDocumentTitle } from './utils/hooks/useDocumentTitle'; import { useFeatureLevel } from './utils/hooks/useFeatureLevel'; import { accessibleRouteChangeHandler, BASEPATH, toPath } from './utils/utils'; -import { HeapDumpAnalysis } from './Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis'; let routeFocusTimer: number; const OVERVIEW = 'Routes.NavGroups.OVERVIEW'; From 2eb02afa08f2c8c4238ac5bc6ff4db260ddb3e0f Mon Sep 17 00:00:00 2001 From: jmatsuok Date: Wed, 24 Jun 2026 02:12:30 -0400 Subject: [PATCH 8/9] Tests --- .../Analysis/HeapDumps/HeapDumpAnalysis.tsx | 24 +- .../Diagnostics/AnalyzeHeapDumps.test.tsx | 562 ++++++++++++++++++ .../NotificationControl.test.tsx.snap | 62 +- 3 files changed, 615 insertions(+), 33 deletions(-) create mode 100644 src/test/Diagnostics/AnalyzeHeapDumps.test.tsx diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx index 667ab32dd..3c2354b60 100644 --- a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -284,8 +284,8 @@ const dupStringsColumns: TableColumn[] = [ sortable: true, }, { - title: 'Non Duplicate Arrays', - keyPaths: ['nonDupArrays'], + title: 'Non Duplicate Strings', + keyPaths: ['nonDupStrings'], sortable: true, }, ]; @@ -608,16 +608,6 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) ); }, [addSubscription, context.target, setTarget]); - React.useEffect(() => { - addSubscription( - context.target.target().subscribe((t) => { - setTarget(t); - setAnalysisResult(undefined); - setSelectedHeapDump(''); - }), - ); - }, [addSubscription, context.target, setTarget]); - const queryTargetHeapDumps = React.useCallback( (target: Target) => context.api.getTargetHeapDumps(target), [context.api], @@ -1614,8 +1604,8 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) data={analysisResult?.classLoaderInstanceStats.map((t) => { return { data: t.value, count: t.count }; })} - title="Class Loader Instances" - description="Class Loader Instance Statistics" + title="Class Loader Instance Statistics" + description="Class Loader Instance Memory Statistics" /> @@ -1628,15 +1618,15 @@ export const HeapDumpAnalysis: React.FC = ({ ...props }) data={analysisResult?.classLoaderClassStats.map((t) => { return { data: t.value, count: t.count }; })} - title="Class Loader Classes" - description="Class Loader Class Statistics" + title="Class Loader Class Statistics" + description="Class Loader Class Memory Statistics" /> - Problem Fields}> + Problem Fields}> diff --git a/src/test/Diagnostics/AnalyzeHeapDumps.test.tsx b/src/test/Diagnostics/AnalyzeHeapDumps.test.tsx new file mode 100644 index 000000000..d481b7211 --- /dev/null +++ b/src/test/Diagnostics/AnalyzeHeapDumps.test.tsx @@ -0,0 +1,562 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HeapDumpAnalysis } from '@app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis'; +import { HeapDumpAnalysisResult } from '@app/Diagnostics/Analysis/HeapDumps/types'; +import { ThemeSetting } from '@app/Settings/types'; +import { RootState } from '@app/Shared/Redux/ReduxStore'; +import { HeapDump, Target } from '@app/Shared/Services/api.types'; +import { defaultServices } from '@app/Shared/Services/Services'; +import { basePreloadedState, DEFAULT_DIMENSIONS, mockMediaQueryList, render, resize } from '@test/utils'; +import { act, screen, cleanup, waitFor, within } from '@testing-library/react'; +import { BehaviorSubject, of, Subject } from 'rxjs'; + +const mockNewConnectUrl = 'service:jmx:rmi://someNewUrl'; +const mockNewAlias = 'target'; +const mockTarget: Target = { + agent: false, + jvmId: 'target', + connectUrl: mockNewConnectUrl, + alias: mockNewAlias, + labels: [], + annotations: { + cryostat: [], + platform: [], + }, +}; + +const mockOtherTarget: Target = { + agent: false, + jvmId: 'otherTarget', + connectUrl: 'service:jmx:rmi://someOtherUrl', + alias: 'other target', + labels: [], + annotations: { + cryostat: [], + platform: [], + }, +}; + +const mockHeapDump: HeapDump = { + downloadUrl: 'someDownloadUrl', + heapDumpId: 'someUuid', + jvmId: mockTarget.jvmId, + size: 1, + metadata: { labels: [{ key: 'someLabel', value: 'someUpdatedValue' }] }, +}; + +const mockOtherHeapDump: HeapDump = { + downloadUrl: 'someOtherDownloadUrl', + heapDumpId: 'someOtherUuid', + jvmId: mockOtherTarget.jvmId, + size: 1, + metadata: { labels: [{ key: 'someLabel', value: 'someOtherValue' }] }, +}; + +const mockHeapDumpAnalysis: HeapDumpAnalysisResult = { + problemCollections: [ + { + classAndField: 'someClass-->someField', + definingClass: 'someClass', + overhead: 1, + badObjs: 2, + goodCollections: 3, + classAndOvhds: [ + { + clazz: 'someClass', + problemKind: 'PROBLEM', + numInstances: 1, + overhead: 1, + }, + ], + }, + ], + duplicateArrays: [ + { + classAndField: 'someOtherClass-->someOtherField', + definingClass: 'someOtherClass', + overhead: 4, + badObjs: 5, + nonDupArrays: 6, + aggregates: [ + { + value: 'someArray', + count: 7, + }, + ], + }, + ], + duplicateStrings: [ + { + classAndField: 'someDupStringClass-->someDupStringField', + definingClass: 'someDupStringClass', + overhead: 8, + badObjs: 9, + dupBackingCharArrays: 10, + nonDupStrings: 11, + aggregates: [ + { + value: 'someString', + count: 12, + }, + ], + }, + ], + highSizeObjects: [ + { + classAndField: 'someHighSizeObjClass-->someHighSizeObjField', + definingClass: 'someHighSizeObjClass', + overhead: 13, + badObjs: 14, + classAndSizeCombos: [ + { + clazz: 'someClazz', + numInstances: 1, + overhead: 15, + }, + ], + }, + ], + weakHashMapClusters: [ + { + classAndField: 'someWeakHashMapClass-->someWeakHashMapField', + definingClass: 'someWeakHashMapClass', + overhead: 16, + badObjs: 17, + classes: ['fooClass'], + }, + ], + objectHistogram: [ + { + clazz: 'histoClass1', + numInstances: 1, + inclusiveSize: 123, + shallowSize: 456, + }, + { + clazz: 'histoClass2', + numInstances: 2, + inclusiveSize: 321, + shallowSize: 654, + }, + ], + nullProblemFields: [ + { + clazz: 'NullProblemClass', + numInstances: 1, + fields: [ + { + clazz: 'NullClass', + field: 'NullField', + overhead: 1, + }, + ], + overhead: 1, + problemKind: 'SOME OTHER PROBLEM', + }, + ], + nearNullProblemFields: [], + fullBytesFields: [], + highBytesFields: [], + classLoaderInstanceStats: [], + classLoaderClassStats: [], + compressibleStringStats: { + stringObjects: 18, + backingArrayBytes: 19, + compressedStrings: 20, + compressedStringBytes: 21, + asciiStrings: 22, + asciiStringBytes: 23, + }, + duplicateStringStats: { + totalStrings: 24, + uniqueStrings: 25, + duplicateStrings: 26, + overhead: 27, + }, + histogramStats: { + totalClasses: 28, + totalObjects: 29, + zeroInstances: 30, + singleInstances: 31, + }, + fundamentalStats: { + pointerSize: 32, + narrowPointers: false, + objectHeaderSize: 33, + objectHeaderAlignment: 34, + numObjects: 35, + objectInstances: 36, + objectArrays: 37, + primitiveArrays: 38, + objectSize: 39, + instanceSize: 40, + objArraySize: 41, + primitiveSize: 42, + }, +}; + +jest.spyOn(defaultServices.api, 'getTargetHeapDumps').mockReturnValue(of([mockHeapDump])); + +jest.spyOn(defaultServices.api, 'getHeapDumps').mockReturnValue(of([mockHeapDump])); + +jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); + +jest.spyOn(defaultServices.targets, 'targets').mockReturnValue(of([mockTarget])); + +jest.spyOn(defaultServices.notificationChannel, 'messages').mockReturnValue(of()); + +jest.spyOn(defaultServices.target, 'authFailure').mockReturnValue(of()); + +jest.spyOn(defaultServices.settings, 'themeSetting').mockReturnValue(of(ThemeSetting.DARK)); +jest.spyOn(defaultServices.settings, 'media').mockReturnValue(of(mockMediaQueryList)); + +describe('', () => { + let preloadedState: RootState; + let setTargetSpy: jest.SpyInstance | undefined; + + beforeAll(async () => { + await act(async () => { + resize(2400, 1080); + }); + }); + + beforeEach(() => { + jest.spyOn(defaultServices.api, 'getTargetHeapDumps').mockReturnValue(of([mockHeapDump])); + jest.spyOn(defaultServices.api, 'getHeapDumps').mockReturnValue(of([mockHeapDump])); + jest.spyOn(defaultServices.target, 'target').mockReturnValue(of(mockTarget)); + jest.spyOn(defaultServices.targets, 'targets').mockReturnValue(of([mockTarget])); + preloadedState = { + ...basePreloadedState, + }; + }); + + afterEach(() => { + setTargetSpy?.mockRestore(); + setTargetSpy = undefined; + cleanup(); + }); + + afterAll(() => { + resize(DEFAULT_DIMENSIONS[0], DEFAULT_DIMENSIONS[1]); + }); + + it('renders empty state', async () => { + jest.spyOn(defaultServices.api, 'analyzeHeapDump').mockReturnValue(of()); + + render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + }, + preloadedState: preloadedState, + }); + + expect(screen.getByRole('heading', { name: 'Select a Heap Dump to Analyze' })).toBeInTheDocument(); + }); + + it('prefills the selected heap dump from location state', async () => { + const analyzeSpy = jest.spyOn(defaultServices.api, 'getHeapDumpReport').mockReturnValue(of(mockHeapDumpAnalysis)); + + render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + options: { + initialEntries: [ + { + pathname: '/analyze-heap-dumps', + state: { jvmId: mockTarget.jvmId, id: mockHeapDump.heapDumpId }, + }, + ], + }, + }, + preloadedState: preloadedState, + }); + + await waitFor(() => { + expect(analyzeSpy).toHaveBeenCalledWith(mockTarget.jvmId, mockHeapDump.heapDumpId); + expect(screen.getByRole('button', { name: 'heap dump selector toggle' })).toHaveTextContent( + mockHeapDump.heapDumpId, + ); + }); + expect(screen.getByText('Fundamental Stats')).toBeInTheDocument(); + }); + + it('prefills the selected heap dump from query params', async () => { + const analyzeSpy = jest.spyOn(defaultServices.api, 'getHeapDumpReport').mockReturnValue(of(mockHeapDumpAnalysis)); + + render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + options: { + initialEntries: [`/analyze-heap-dumps?jvmId=${mockTarget.jvmId}&heapDumpId=${mockHeapDump.heapDumpId}`], + }, + }, + preloadedState: preloadedState, + }); + + await waitFor(() => { + expect(analyzeSpy).toHaveBeenCalledWith(mockTarget.jvmId, mockHeapDump.heapDumpId); + expect(screen.getByRole('button', { name: 'heap dump selector toggle' })).toHaveTextContent( + mockHeapDump.heapDumpId, + ); + }); + expect(screen.getByText('Fundamental Stats')).toBeInTheDocument(); + }); + + it('updates the target context for a prefilled archived thread dump from another target', async () => { + const analyzeSpy = jest.spyOn(defaultServices.api, 'getHeapDumpReport').mockReturnValue(of(mockHeapDumpAnalysis)); + const target$ = new BehaviorSubject(mockTarget); + jest.spyOn(defaultServices.target, 'target').mockReturnValue(target$); + setTargetSpy = jest.spyOn(defaultServices.target, 'setTarget').mockImplementation((target) => target$.next(target)); + jest.spyOn(defaultServices.targets, 'targets').mockReturnValue(of([mockTarget, mockOtherTarget])); + jest.spyOn(defaultServices.api, 'getTargetHeapDumps').mockImplementation((target) => { + return of(target.connectUrl === mockOtherTarget.connectUrl ? [mockOtherHeapDump] : [mockHeapDump]); + }); + + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + options: { + initialEntries: [ + { + pathname: '/analyze-heap-dumps', + state: { jvmId: mockOtherTarget.jvmId, id: mockOtherHeapDump.heapDumpId }, + }, + ], + }, + }, + preloadedState: preloadedState, + }); + + await waitFor(() => { + expect(setTargetSpy).toHaveBeenCalledWith(mockOtherTarget); + expect(analyzeSpy).toHaveBeenCalledWith(mockOtherTarget.jvmId, mockOtherHeapDump.heapDumpId); + expect(screen.getByRole('button', { name: 'heap dump selector toggle' })).toHaveTextContent( + mockOtherHeapDump.heapDumpId, + ); + }); + + await act(async () => { + await user.click(screen.getByRole('button', { name: 'heap dump selector toggle' })); + }); + expect(within(await screen.findByRole('menu')).getByText(mockOtherHeapDump.heapDumpId)).toBeInTheDocument(); + }); + + it('shows a loading state while prefilled heap dump analysis is pending', async () => { + const analysis$ = new Subject(); + jest.spyOn(defaultServices.api, 'getHeapDumpReport').mockReturnValue(analysis$); + + render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + options: { + initialEntries: [ + { + pathname: '/analyze-heap-dumps', + state: { jvmId: mockTarget.jvmId, id: mockHeapDump.heapDumpId }, + }, + ], + }, + }, + preloadedState: preloadedState, + }); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Select a Heap Dump to Analyze' })).not.toBeInTheDocument(); + + await act(async () => { + analysis$.next(mockHeapDumpAnalysis); + analysis$.complete(); + }); + + await waitFor(() => { + expect(screen.getByText('Fundamental Stats')).toBeInTheDocument(); + }); + }); + + it('should render the page correctly', async () => { + jest.spyOn(defaultServices.api, 'getHeapDumpReport').mockReturnValue(of(mockHeapDumpAnalysis)); + const { user } = render({ + routerConfigs: { + routes: [ + { + path: '/analyze-heap-dumps', + element: , + }, + ], + }, + preloadedState: preloadedState, + }); + + const dropDownArrow = screen.getByRole('button', { name: 'heap dump selector toggle' }); + expect(dropDownArrow).toBeInTheDocument(); + expect(dropDownArrow).toBeVisible(); + + await act(async () => { + await user.click(dropDownArrow); + }); + + const selectMenu = await screen.findByRole('menu'); + expect(selectMenu).toBeInTheDocument(); + expect(selectMenu).toBeVisible(); + + const option = within(selectMenu).getByText(mockHeapDump.heapDumpId); + expect(option).toBeInTheDocument(); + expect(option).toBeVisible(); + + await act(async () => { + await user.click(screen.getByText(mockHeapDump.heapDumpId)); + }); + + // Tab 1 - Statistics + expect(screen.getByText('Fundamental Stats')).toBeInTheDocument(); + expect(screen.getByText('Duplicate String Stats')).toBeInTheDocument(); + expect(screen.getByText('Object Histogram Stats')).toBeInTheDocument(); + expect(screen.getByText('Compressible String Stats')).toBeInTheDocument(); + expect(screen.getByText('Class Loader Instances')).toBeInTheDocument(); + expect(screen.getByText('Class Loader Classes')).toBeInTheDocument(); + + // Change Tabs + const fieldsTab = screen.getByRole('tab', { name: 'Problem Fields' }); + expect(fieldsTab).toBeInTheDocument(); + expect(fieldsTab).toBeVisible(); + await act(async () => { + await user.click(fieldsTab); + }); + // Problem Fields + ['Class', 'Instances', 'Overhead', 'Problem Type'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const histogramTab = screen.getByRole('tab', { name: 'Object Histogram' }); + expect(histogramTab).toBeInTheDocument(); + expect(histogramTab).toBeVisible(); + await act(async () => { + await user.click(histogramTab); + }); + // Object Histogram + ['Class', 'Instances', 'Shallow Size', 'Inclusive Size'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const collectionsTab = screen.getByRole('tab', { name: 'Collections' }); + expect(collectionsTab).toBeInTheDocument(); + expect(collectionsTab).toBeVisible(); + await act(async () => { + await user.click(collectionsTab); + }); + // Collections + ['Class', 'Defining Class', 'Overhead', 'Bad Objects', 'Good Collections'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const arraysTab = screen.getByRole('tab', { name: 'Duplicate Arrays' }); + expect(arraysTab).toBeInTheDocument(); + expect(arraysTab).toBeVisible(); + await act(async () => { + await user.click(arraysTab); + }); + // Duplicate Arrays + ['Class and Field', 'Defining Class', 'Overhead', 'Bad Objects', 'Non Duplicate Arrays'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const objectsTab = screen.getByRole('tab', { name: 'High Size Objects' }); + expect(objectsTab).toBeInTheDocument(); + expect(objectsTab).toBeVisible(); + await act(async () => { + await user.click(objectsTab); + }); + // High Size Objects + ['Class and Field', 'Defining Class', 'Overhead', 'Bad Objects'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const stringsTab = screen.getByRole('tab', { name: 'Duplicate Strings' }); + expect(stringsTab).toBeInTheDocument(); + expect(stringsTab).toBeVisible(); + await act(async () => { + await user.click(stringsTab); + }); + // Duplicate Strings + [ + 'Class and Field', + 'Defining Class', + 'Overhead', + 'Bad Objects', + 'Backing Char Array Memory', + 'Non Duplicate Strings', + ].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + + // Change Tabs + const weakHashMapsTab = screen.getByRole('tab', { name: 'Weak HashMaps' }); + expect(weakHashMapsTab).toBeInTheDocument(); + expect(weakHashMapsTab).toBeVisible(); + await act(async () => { + await user.click(weakHashMapsTab); + }); + // Weak HashMaps + ['Class and Field', 'Defining Class', 'Overhead', 'Bad Objects'].map((text) => { + const header = screen.getByRole('columnheader', { name: text }); + expect(header).toBeInTheDocument(); + expect(header).toBeVisible(); + }); + }); +}); diff --git a/src/test/Settings/__snapshots__/NotificationControl.test.tsx.snap b/src/test/Settings/__snapshots__/NotificationControl.test.tsx.snap index ca2485127..495cb45b1 100644 --- a/src/test/Settings/__snapshots__/NotificationControl.test.tsx.snap +++ b/src/test/Settings/__snapshots__/NotificationControl.test.tsx.snap @@ -1107,6 +1107,36 @@ exports[` renders correctly 1`] = ` data-ouia-component-id="OUIA-Generated-Switch-31" data-ouia-component-type="PF6/Switch" data-ouia-safe="true" + for="HeapDumpAnalysisSuccess" + > + + + + + +
+