diff --git a/src/components/TitleWithHelpmark/TitleWithHelpmark.tsx b/src/components/TitleWithHelpmark/TitleWithHelpmark.tsx new file mode 100644 index 0000000000..583abd1de6 --- /dev/null +++ b/src/components/TitleWithHelpmark/TitleWithHelpmark.tsx @@ -0,0 +1,15 @@ +import {Flex, HelpMark} from '@gravity-ui/uikit'; + +interface TitleWithHelpMarkProps { + header: string; + note: string; +} + +export function TitleWithHelpMark({header, note}: TitleWithHelpMarkProps) { + return ( + + {header} + {note} + + ); +} diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 491c005d05..861d4e566c 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -29,6 +29,7 @@ import {Tablets} from '../Tablets/Tablets'; import type {NodeTab} from './NodePages'; import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages'; import NodeStructure from './NodeStructure/NodeStructure'; +import {Threads} from './Threads/Threads'; import i18n from './i18n'; import './Node.scss'; @@ -247,6 +248,10 @@ function NodePageContent({ return ; } + case 'threads': { + return ; + } + default: return false; } diff --git a/src/containers/Node/NodePages.ts b/src/containers/Node/NodePages.ts index 49ade751f1..3f4c23ac13 100644 --- a/src/containers/Node/NodePages.ts +++ b/src/containers/Node/NodePages.ts @@ -11,6 +11,7 @@ const NODE_TABS_IDS = { storage: 'storage', tablets: 'tablets', structure: 'structure', + threads: 'threads', } as const; export type NodeTab = ValueOf; @@ -34,6 +35,12 @@ export const NODE_TABS = [ return i18n('tabs.tablets'); }, }, + { + id: NODE_TABS_IDS.threads, + get title() { + return i18n('tabs.threads'); + }, + }, ]; export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets); diff --git a/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.scss b/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.scss new file mode 100644 index 0000000000..724e167354 --- /dev/null +++ b/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.scss @@ -0,0 +1,6 @@ +.cpu-usage-bar { + &__progress { + width: 60px; + min-width: 60px; + } +} diff --git a/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.tsx b/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.tsx new file mode 100644 index 0000000000..15755ef585 --- /dev/null +++ b/src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.tsx @@ -0,0 +1,38 @@ +import {Flex, Text} from '@gravity-ui/uikit'; + +import {ProgressViewer} from '../../../../components/ProgressViewer/ProgressViewer'; +import {cn} from '../../../../utils/cn'; + +import './CpuUsageBar.scss'; + +const b = cn('cpu-usage-bar'); + +interface CpuUsageBarProps { + systemUsage?: number; + userUsage?: number; + className?: string; +} + +export function CpuUsageBar({systemUsage = 0, userUsage = 0, className}: CpuUsageBarProps) { + const totalUsage = systemUsage + userUsage; + const systemPercent = Math.round(systemUsage * 100); + const userPercent = Math.round(userUsage * 100); + const totalPercent = Math.round(totalUsage * 100); + + return ( + +
+ +
+ + (Sys: {systemPercent}%, U: {userPercent}%) + +
+ ); +} diff --git a/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.scss b/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.scss new file mode 100644 index 0000000000..249de46045 --- /dev/null +++ b/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.scss @@ -0,0 +1,10 @@ +.ydb-thread-states-bar { + &__legend-color { + flex-shrink: 0; + + width: 8px; + aspect-ratio: 1; + + border-radius: var(--g-border-radius-xs); + } +} diff --git a/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.tsx b/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.tsx new file mode 100644 index 0000000000..3f9792319b --- /dev/null +++ b/src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.tsx @@ -0,0 +1,105 @@ +import type {ProgressTheme} from '@gravity-ui/uikit'; +import {ActionTooltip, Flex, Progress, Text} from '@gravity-ui/uikit'; + +import {cn} from '../../../../utils/cn'; +import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants'; + +import './ThreadStatesBar.scss'; + +const b = cn('ydb-thread-states-bar'); + +interface ThreadStatesBarProps { + states?: Record; + totalThreads?: number; + className?: string; +} + +/** + * Thread state colors based on the state type + */ +const getProgressBackgroundColor = (state: string): string => { + switch (state.toUpperCase()) { + case 'R': // Running + return 'var(--g-color-base-positive-medium)'; + case 'S': // Sleeping + return 'var(--g-color-base-info-medium)'; + case 'D': // Uninterruptible sleep + return 'var(--g-color-base-warning-medium)'; + case 'Z': // Zombie + case 'T': // Stopped + case 'X': // Dead + return 'var(--g-color-base-danger-medium)'; + default: + return 'var(--g-color-base-misc-medium)'; + } +}; +const getStackThemeColor = (state: string): ProgressTheme => { + switch (state.toUpperCase()) { + case 'R': // Running + return 'success'; + case 'S': // Sleeping + return 'info'; + case 'D': // Uninterruptible sleep + return 'warning'; + case 'Z': // Zombie + case 'T': // Stopped + case 'X': // Dead + return 'danger'; + default: + return 'misc'; + } +}; +const getStateTitle = (state: string): string => { + switch (state.toUpperCase()) { + case 'R': // Running + return 'Running'; + case 'S': // Sleeping + return 'Sleeping'; + case 'D': // Uninterruptible sleep + return 'Uninterruptible sleep'; + case 'Z': // Zombie + return 'Zombie'; + case 'T': // Stopped + return 'Stopped'; + case 'X': // Dead + return 'Dead'; + default: + return 'Unknown'; + } +}; + +export function ThreadStatesBar({states = {}, totalThreads, className}: ThreadStatesBarProps) { + const total = totalThreads || Object.values(states).reduce((sum, count) => sum + count, 0); + + if (total === 0) { + return EMPTY_DATA_PLACEHOLDER; + } + + const stateEntries = Object.entries(states).filter(([, count]) => count > 0); + + const stack = Object.entries(states).map(([state, count]) => ({ + theme: getStackThemeColor(state), + value: (count / total) * 100, + })); + + return ( +
+ + + {stateEntries.map(([state, count]) => ( + + +
+ + {state}: {count} + + + + ))} + +
+ ); +} diff --git a/src/containers/Node/Threads/Threads.tsx b/src/containers/Node/Threads/Threads.tsx new file mode 100644 index 0000000000..2328ada020 --- /dev/null +++ b/src/containers/Node/Threads/Threads.tsx @@ -0,0 +1,41 @@ +import {ResponseError} from '../../../components/Errors/ResponseError'; +import {LoaderWrapper} from '../../../components/LoaderWrapper/LoaderWrapper'; +import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable'; +import {nodeApi} from '../../../store/reducers/node/node'; +import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants'; +import {useAutoRefreshInterval} from '../../../utils/hooks'; + +import {columns} from './columns'; +import i18n from './i18n'; + +interface ThreadsProps { + nodeId: string; + className?: string; +} + +const THREADS_COLUMNS_WIDTH_LS_KEY = 'threadsTableColumnsWidth'; + +export function Threads({nodeId, className}: ThreadsProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + + const { + currentData: nodeData, + isLoading, + error, + } = nodeApi.useGetNodeInfoQuery({nodeId}, {pollingInterval: autoRefreshInterval}); + + const data = nodeData?.Threads || []; + + return ( + + {error ? : null} + + + ); +} diff --git a/src/containers/Node/Threads/columns.tsx b/src/containers/Node/Threads/columns.tsx new file mode 100644 index 0000000000..1646f40b12 --- /dev/null +++ b/src/containers/Node/Threads/columns.tsx @@ -0,0 +1,60 @@ +import type {Column} from '@gravity-ui/react-data-table'; +import DataTable from '@gravity-ui/react-data-table'; + +import {TitleWithHelpMark} from '../../../components/TitleWithHelpmark/TitleWithHelpmark'; +import type {TThreadPoolInfo} from '../../../types/api/threads'; +import {formatNumber} from '../../../utils/dataFormatters/dataFormatters'; +import {safeParseNumber} from '../../../utils/utils'; + +import {CpuUsageBar} from './CpuUsageBar/CpuUsageBar'; +import {ThreadStatesBar} from './ThreadStatesBar/ThreadStatesBar'; +import i18n from './i18n'; + +export const columns: Column[] = [ + { + name: 'Name', + header: i18n('field_pool-name'), + render: ({row}) => row.Name || i18n('value_unknown'), + width: 200, + }, + { + name: 'Threads', + header: i18n('field_thread-count'), + render: ({row}) => formatNumber(row.Threads), + align: DataTable.RIGHT, + width: 100, + }, + { + name: 'CpuUsage', + header: ( + + ), + render: ({row}) => , + sortAccessor: (row) => safeParseNumber(row.SystemUsage) + safeParseNumber(row.UserUsage), + width: 200, + }, + { + name: 'MinorPageFaults', + header: i18n('field_minor-page-faults'), + render: ({row}) => formatNumber(row.MinorPageFaults), + align: DataTable.RIGHT, + width: 145, + }, + { + name: 'MajorPageFaults', + header: i18n('field_major-page-faults'), + render: ({row}) => formatNumber(row.MajorPageFaults), + align: DataTable.RIGHT, + width: 145, + }, + { + name: 'States', + header: i18n('field_thread-states'), + render: ({row}) => , + sortable: false, + width: 250, + }, +]; diff --git a/src/containers/Node/Threads/i18n/en.json b/src/containers/Node/Threads/i18n/en.json new file mode 100644 index 0000000000..a589c515fd --- /dev/null +++ b/src/containers/Node/Threads/i18n/en.json @@ -0,0 +1,11 @@ +{ + "field_pool-name": "Pool Name", + "field_thread-count": "Threads", + "field_cpu-usage": "CPU Usage", + "field_minor-page-faults": "Minor Page Faults", + "field_major-page-faults": "Major Page Faults", + "field_thread-states": "Thread States", + "value_unknown": "Unknown", + "alert_no-thread-data": "No thread pool information available", + "description_cpu-usage": "System usage + user usage" +} diff --git a/src/containers/Node/Threads/i18n/index.ts b/src/containers/Node/Threads/i18n/index.ts new file mode 100644 index 0000000000..6bf98d2542 --- /dev/null +++ b/src/containers/Node/Threads/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-threads'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Node/i18n/en.json b/src/containers/Node/i18n/en.json index 6fe9179f88..0762ce7af3 100644 --- a/src/containers/Node/i18n/en.json +++ b/src/containers/Node/i18n/en.json @@ -5,6 +5,7 @@ "tabs.storage": "Storage", "tabs.structure": "Structure", "tabs.tablets": "Tablets", + "tabs.threads": "Threads", "node": "Node", "fqdn": "FQDN", diff --git a/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.scss b/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.scss index f8c86111ae..74e4179a75 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.scss +++ b/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.scss @@ -47,12 +47,6 @@ text-overflow: ellipsis; } - &__note { - display: flex; - .g-help-mark__button { - display: flex; - } - } &__rights-wrapper { position: relative; diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/columns.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/columns.tsx index 84c4f38197..2462b3d6fb 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/columns.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/columns.tsx @@ -1,7 +1,8 @@ import type {Column} from '@gravity-ui/react-data-table'; -import {Flex, HelpMark, Label} from '@gravity-ui/uikit'; +import {Flex, Label} from '@gravity-ui/uikit'; import {SubjectWithAvatar} from '../../../../../../components/SubjectWithAvatar/SubjectWithAvatar'; +import {TitleWithHelpMark} from '../../../../../../components/TitleWithHelpmark/TitleWithHelpmark'; import type {PreparedAccessRights} from '../../../../../../types/api/acl'; import i18n from '../../i18n'; import {block} from '../../shared'; @@ -24,7 +25,7 @@ export const columns: Column[] = [ width: 400, get header() { return ( - @@ -55,7 +56,7 @@ export const columns: Column[] = [ width: 400, get header() { return ( - @@ -75,19 +76,3 @@ export const columns: Column[] = [ sortable: false, }, ]; - -interface HeaderWithHelpMarkProps { - header: string; - note: string; -} - -function HeaderWithHelpMark({header, note}: HeaderWithHelpMarkProps) { - return ( - - {header} - - {note} - - - ); -} diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 45c2c0745a..c9ac1e1e06 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -66,6 +66,7 @@ export class ViewerAPI extends BaseYdbAPI { this.getPath('/viewer/json/sysinfo?enums=true'), { node_id: id, + fields_required: -1, }, {concurrentId, requestConfig: {signal}, timeout}, ); diff --git a/src/types/api/nodes.ts b/src/types/api/nodes.ts index 2afee29df1..b42b7a31d3 100644 --- a/src/types/api/nodes.ts +++ b/src/types/api/nodes.ts @@ -2,6 +2,7 @@ import type {BackendSortParam} from './common'; import type {EFlag} from './enums'; import type {TPDiskStateInfo} from './pdisk'; import type {TTabletStateInfo} from './tablet'; +import type {TThreadPoolInfo} from './threads'; import type {TVDiskStateInfo} from './vdisk'; /** @@ -152,6 +153,8 @@ export interface TSystemStateInfo { SharedCacheStats?: TNodeSharedCache; TotalSessions?: number; NodeName?: string; + /** Detailed thread information when fields_required=-1 is used */ + Threads?: TThreadPoolInfo[]; } interface TNodeStateInfo { diff --git a/src/types/api/systemState.ts b/src/types/api/systemState.ts index c15b172944..9053e1e3c4 100644 --- a/src/types/api/systemState.ts +++ b/src/types/api/systemState.ts @@ -1,4 +1,5 @@ import type {TSystemStateInfo} from './nodes'; +import type {TThreadPoolInfo} from './threads'; /** * endpoint: /viewer/json/sysinfo @@ -7,6 +8,8 @@ import type {TSystemStateInfo} from './nodes'; */ export interface TEvSystemStateResponse { SystemStateInfo?: TSystemStateInfo[]; + /** Detailed thread information when fields_required=-1 is used */ + Threads?: TThreadPoolInfo[]; /** uint64 */ ResponseTime?: string; ResponseDuration?: number; diff --git a/src/types/api/threads.ts b/src/types/api/threads.ts new file mode 100644 index 0000000000..5513030917 --- /dev/null +++ b/src/types/api/threads.ts @@ -0,0 +1,62 @@ +/** + * Thread pool information with detailed statistics + * Based on the thread information shown in the node page + */ +export interface TThreadPoolInfo { + /** Thread pool name (e.g., AwsEventLoop, klktmr.IC) */ + Name?: string; + /** Number of threads in the pool */ + Threads?: number; + /** System CPU usage (0-1 range) */ + SystemUsage?: number; + /** User CPU usage (0-1 range) */ + UserUsage?: number; + /** Number of minor page faults */ + MinorPageFaults?: number; + /** Number of major page faults */ + MajorPageFaults?: number; + /** Thread states with counts */ + States?: Record; +} + +/** + * Response containing thread pool information for a node + */ +export interface TThreadPoolsResponse { + /** Array of thread pools */ + Threads?: TThreadPoolInfo[]; + /** Response time */ + ResponseTime?: string; + ResponseDuration?: number; +} + +/** + * Thread states enum based on Linux process states + * Reference: https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html + */ +export enum ThreadState { + /** Running */ + R = 'R', + /** Sleeping in an interruptible wait */ + S = 'S', + /** Waiting in uninterruptible disk sleep */ + D = 'D', + /** Zombie */ + Z = 'Z', + /** Stopped (on a signal) */ + T = 'T', + /** Tracing stop */ + t = 't', + /** Paging (not valid since Linux 2.6.0) */ + W = 'W', + /** Dead (should never be seen) */ + X = 'X', + /** Dead (Linux 2.6.0 and later) */ + x = 'x', + /** Wakekill (Linux 2.6.33 and later) */ + K = 'K', + /** Waking (Linux 2.6.33 and later) */ + W_WAKE = 'W', + /** Parked (Linux 3.9 and later) */ + P = 'P', +}