diff --git a/react/src/components/BAIProgressWithLabel.tsx b/react/src/components/BAIProgressWithLabel.tsx index 0f466f0e9..79024152a 100644 --- a/react/src/components/BAIProgressWithLabel.tsx +++ b/react/src/components/BAIProgressWithLabel.tsx @@ -1,15 +1,17 @@ import Flex from './Flex'; -import { Typography, theme } from 'antd'; +import { ProgressProps, Typography, theme } from 'antd'; import _ from 'lodash'; import React from 'react'; -export interface BAIProgressWithLabelProps { +export interface BAIProgressWithLabelProps + extends Omit { title?: React.ReactNode; valueLabel?: React.ReactNode; percent?: number; width?: React.CSSProperties['width']; strokeColor?: string; labelStyle?: React.CSSProperties; + progressStyle?: React.CSSProperties; size?: 'small' | 'middle' | 'large'; } const BAIProgressWithLabel: React.FC = ({ @@ -19,6 +21,7 @@ const BAIProgressWithLabel: React.FC = ({ width, strokeColor, labelStyle, + progressStyle, size = 'small', }) => { const { token } = theme.useToken(); @@ -39,6 +42,7 @@ const BAIProgressWithLabel: React.FC = ({ ...(_.isNumber(width) || _.isString(width) ? { width: width } : { flex: 1 }), + ...progressStyle, }} direction="column" align="stretch" diff --git a/react/src/components/SessionDetailContent.tsx b/react/src/components/SessionDetailContent.tsx index a57287260..f2da9038a 100644 --- a/react/src/components/SessionDetailContent.tsx +++ b/react/src/components/SessionDetailContent.tsx @@ -10,6 +10,7 @@ import SessionStatusTag from './ComputeSessionNodeItems/SessionStatusTag'; import SessionTypeTag from './ComputeSessionNodeItems/SessionTypeTag'; import Flex from './Flex'; import ImageMetaIcon from './ImageMetaIcon'; +import SessionUsageMonitor from './SessionUsageMonitor'; import { SessionDetailContentLegacyQuery } from './__generated__/SessionDetailContentLegacyQuery.graphql'; import { SessionDetailContentQuery } from './__generated__/SessionDetailContentQuery.graphql'; import { @@ -100,6 +101,7 @@ const SessionDetailContent: React.FC<{ # fix: This fragment is not used in this component, but it is required by the SessionActionButtonsFragment. # It might be a bug in relay ...ContainerLogModalFragment + ...SessionUsageMonitorFragment } legacy_session: compute_session(id: $uuid) { image @@ -201,11 +203,14 @@ const SessionDetailContent: React.FC<{ {session.agent_ids || '-'} - + + + + ) : ( diff --git a/react/src/components/SessionUsageMonitor.tsx b/react/src/components/SessionUsageMonitor.tsx new file mode 100644 index 000000000..ca91b2466 --- /dev/null +++ b/react/src/components/SessionUsageMonitor.tsx @@ -0,0 +1,311 @@ +import { + convertBinarySizeUnit, + convertDecimalSizeUnit, + toFixedFloorWithoutTrailingZeros, +} from '../helper'; +import { useResourceSlotsDetails } from '../hooks/backendai'; +import BAIProgressWithLabel from './BAIProgressWithLabel'; +import Flex from './Flex'; +import { SessionUsageMonitorFragment$key } from './__generated__/SessionUsageMonitorFragment.graphql'; +import { + Progress, + ProgressProps, + Tooltip, + Typography, + theme, + Row, + Col, +} from 'antd'; +import graphql from 'babel-plugin-relay/macro'; +import _ from 'lodash'; +import { useMemo } from 'react'; +import { useFragment } from 'react-relay'; + +interface SessionUsageMonitorProps extends ProgressProps { + sessionFrgmt: SessionUsageMonitorFragment$key | null; + size?: 'small' | 'default'; +} + +const SessionUsageMonitor: React.FC = ({ + sessionFrgmt, + size = 'default', +}) => { + const { token } = theme.useToken(); + const { mergedResourceSlots } = useResourceSlotsDetails(); + + const sessionNode = useFragment( + graphql` + fragment SessionUsageMonitorFragment on ComputeSessionNode { + kernel_nodes { + edges { + node { + live_stat + occupied_slots + } + } + } + } + `, + sessionFrgmt, + ); + + const firstKernelNode = _.get(sessionNode, 'kernel_nodes.edges[0].node'); + const occupiedSlots = useMemo( + () => JSON.parse(firstKernelNode?.occupied_slots ?? '{}'), + [firstKernelNode?.occupied_slots], + ); + const resourceSlotNames = _.keysIn(occupiedSlots); + const liveStat = JSON.parse( + _.get(sessionNode, 'kernel_nodes.edges[0].node.live_stat') ?? '{}', + ); + + // to display util first, mem second + const sortedLiveStat = useMemo( + () => + Object.keys(liveStat) + .sort((a, b) => { + const aUtil = a.includes('_util'); + const bUtil = b.includes('_util'); + const aMem = a.includes('_mem'); + const bMem = b.includes('_mem'); + + if (aUtil && !bUtil) return -1; + if (!aUtil && bUtil) return 1; + if (aMem && !bMem) return -1; + if (!aMem && bMem) return 1; + + return 0; + }) + .reduce((acc: { [key: string]: any }, key) => { + acc[key] = liveStat[key]; + return acc; + }, {}), + [liveStat], + ); + + const displayMemoryUsage = ( + current: string, + capacity: string, + decimalSize: number = 2, + ) => { + return `${convertBinarySizeUnit(current, 'g', decimalSize)?.numberFixed ?? '-'} / ${ + convertBinarySizeUnit(capacity, 'g', decimalSize)?.numberFixed ?? '-' + } GiB`; + }; + + return ( + + {sortedLiveStat?.cpu_util ? ( + + + {size === 'default' ? ( + <> + + {mergedResourceSlots?.['cpu']?.human_readable_name} + + + + ) : ( + + ( + + {percent + '%'} + + )} + percent={ + _.toNumber( + toFixedFloorWithoutTrailingZeros( + sortedLiveStat?.cpu_util?.pct, + 1, + ), + ) || 0 + } + strokeColor="#BFBFBF" + strokeLinecap="butt" + /> + + )} + + + ) : null} + + {sortedLiveStat?.mem ? ( + + + {size === 'default' ? ( + <> + + + {mergedResourceSlots?.['mem']?.human_readable_name} + + + {displayMemoryUsage( + sortedLiveStat?.mem?.current, + // mem.capacity does not report total amount of memory allocated to + // the container, so, we just replace with the value of occupied slot. + // NOTE: this assumes every containers in a session have the same + // amount of memory. + occupiedSlots?.mem, + )} + + + + + ) : ( + + {mergedResourceSlots?.['mem']?.human_readable_name} +
+ {displayMemoryUsage( + sortedLiveStat?.mem?.current, + // mem.capacity does not report total amount of memory allocated to + // the container, so, we just replace with the value of occupied slot. + // NOTE: this assumes every containers in a session have the same + // amount of memory. + occupiedSlots?.mem, + )} +
+ } + > + ( + + {percent + '%'} + + )} + percent={ + _.toNumber( + toFixedFloorWithoutTrailingZeros( + sortedLiveStat?.mem?.pct, + 1, + ), + ) || 0 + } + strokeColor="#BFBFBF" + strokeLinecap="butt" + /> + + )} + + + ) : null} + + {_.map( + _.omit(sortedLiveStat, 'cpu_util', 'cpu_used', 'mem'), + (value, key) => { + const deviceName = _.split(key, '_')[0]; + const deviceKey = _.find(resourceSlotNames, (name) => + _.includes(name, deviceName), + ); + + return deviceKey ? ( + + + {size === 'default' ? ( + <> + + + {mergedResourceSlots?.[deviceKey]?.human_readable_name} + + {_.includes(key, 'util') && ' (util)'} + {_.includes(key, 'mem') && ' (mem)'} + + + {_.includes(key, 'mem') ? ( + + {displayMemoryUsage(value?.current, value?.capacity)} + + ) : null} + + + + + ) : ( + + {mergedResourceSlots?.[deviceKey]?.human_readable_name} + {_.includes(key, 'mem') && ( + <> + (mem) +
+ {displayMemoryUsage( + value?.current, + value?.capacity, + )} + + )} +
+ } + > + ( + + {percent + '%'} + + )} + percent={ + _.toNumber( + toFixedFloorWithoutTrailingZeros(value?.pct, 1), + ) || 0 + } + strokeColor="#BFBFBF" + strokeLinecap="butt" + /> + + )} + + + ) : null; + }, + )} + + + + {`I/O Read: ${convertDecimalSizeUnit(sortedLiveStat?.io_read?.current, 'm')?.numberUnit ?? '-'}B / Write: ${convertDecimalSizeUnit(sortedLiveStat?.io_write?.current, 'm')?.numberUnit ?? '-'}B`} + + + +
+ ); +}; + +export default SessionUsageMonitor;