diff --git a/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx new file mode 100644 index 000000000..c6d032c48 --- /dev/null +++ b/src/app/Diagnostics/Analysis/HeapDumps/HeapDumpAnalysis.tsx @@ -0,0 +1,946 @@ +/* + * 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, NotificationCategory, 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.getHeapDumpReport(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 + .getHeapDumpReport(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.getHeapDumpReport(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.notificationChannel.messages(NotificationCategory.HeapDumpAnalysisSuccess).subscribe((msg) => { + addSubscription( + context.api + .getHeapDumpReport(msg.message.jvmId, msg.message.heapDumpId) + .pipe(finalize(() => setIsAnalysisLoading(false))) + .subscribe({ + next: handleHeapDumpAnalysis, + }), + ); + return; + }), + ); + }, [addSubscription, context.notificationChannel]); + + 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/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, { 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',