Skip to content

Commit c945a78

Browse files
CopilotadameatRaubzeug
authored
feat: add new tab to Node page with thread pool statistics (#2599)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> Co-authored-by: Elena Makarova <el-makarova@yandex-team.ru>
1 parent c170578 commit c945a78

File tree

18 files changed

+379
-25
lines changed

18 files changed

+379
-25
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Flex, HelpMark} from '@gravity-ui/uikit';
2+
3+
interface TitleWithHelpMarkProps {
4+
header: string;
5+
note: string;
6+
}
7+
8+
export function TitleWithHelpMark({header, note}: TitleWithHelpMarkProps) {
9+
return (
10+
<Flex gap={1} alignItems="center">
11+
{header}
12+
<HelpMark popoverProps={{placement: ['right', 'left']}}>{note}</HelpMark>
13+
</Flex>
14+
);
15+
}

src/containers/Node/Node.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {Tablets} from '../Tablets/Tablets';
2929
import type {NodeTab} from './NodePages';
3030
import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages';
3131
import NodeStructure from './NodeStructure/NodeStructure';
32+
import {Threads} from './Threads/Threads';
3233
import i18n from './i18n';
3334

3435
import './Node.scss';
@@ -247,6 +248,10 @@ function NodePageContent({
247248
return <NodeStructure nodeId={nodeId} />;
248249
}
249250

251+
case 'threads': {
252+
return <Threads nodeId={nodeId} />;
253+
}
254+
250255
default:
251256
return false;
252257
}

src/containers/Node/NodePages.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const NODE_TABS_IDS = {
1111
storage: 'storage',
1212
tablets: 'tablets',
1313
structure: 'structure',
14+
threads: 'threads',
1415
} as const;
1516

1617
export type NodeTab = ValueOf<typeof NODE_TABS_IDS>;
@@ -34,6 +35,12 @@ export const NODE_TABS = [
3435
return i18n('tabs.tablets');
3536
},
3637
},
38+
{
39+
id: NODE_TABS_IDS.threads,
40+
get title() {
41+
return i18n('tabs.threads');
42+
},
43+
},
3744
];
3845

3946
export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.cpu-usage-bar {
2+
&__progress {
3+
width: 60px;
4+
min-width: 60px;
5+
}
6+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {Flex, Text} from '@gravity-ui/uikit';
2+
3+
import {ProgressViewer} from '../../../../components/ProgressViewer/ProgressViewer';
4+
import {cn} from '../../../../utils/cn';
5+
6+
import './CpuUsageBar.scss';
7+
8+
const b = cn('cpu-usage-bar');
9+
10+
interface CpuUsageBarProps {
11+
systemUsage?: number;
12+
userUsage?: number;
13+
className?: string;
14+
}
15+
16+
export function CpuUsageBar({systemUsage = 0, userUsage = 0, className}: CpuUsageBarProps) {
17+
const totalUsage = systemUsage + userUsage;
18+
const systemPercent = Math.round(systemUsage * 100);
19+
const userPercent = Math.round(userUsage * 100);
20+
const totalPercent = Math.round(totalUsage * 100);
21+
22+
return (
23+
<Flex gap={2} className={b(null, className)}>
24+
<div className={b('progress')}>
25+
<ProgressViewer
26+
value={totalPercent}
27+
percents={true}
28+
colorizeProgress={true}
29+
capacity={100}
30+
className={b('progress')}
31+
/>
32+
</div>
33+
<Text color="secondary">
34+
(Sys: {systemPercent}%, U: {userPercent}%)
35+
</Text>
36+
</Flex>
37+
);
38+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.ydb-thread-states-bar {
2+
&__legend-color {
3+
flex-shrink: 0;
4+
5+
width: 8px;
6+
aspect-ratio: 1;
7+
8+
border-radius: var(--g-border-radius-xs);
9+
}
10+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {ProgressTheme} from '@gravity-ui/uikit';
2+
import {ActionTooltip, Flex, Progress, Text} from '@gravity-ui/uikit';
3+
4+
import {cn} from '../../../../utils/cn';
5+
import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants';
6+
7+
import './ThreadStatesBar.scss';
8+
9+
const b = cn('ydb-thread-states-bar');
10+
11+
interface ThreadStatesBarProps {
12+
states?: Record<string, number>;
13+
totalThreads?: number;
14+
className?: string;
15+
}
16+
17+
/**
18+
* Thread state colors based on the state type
19+
*/
20+
const getProgressBackgroundColor = (state: string): string => {
21+
switch (state.toUpperCase()) {
22+
case 'R': // Running
23+
return 'var(--g-color-base-positive-medium)';
24+
case 'S': // Sleeping
25+
return 'var(--g-color-base-info-medium)';
26+
case 'D': // Uninterruptible sleep
27+
return 'var(--g-color-base-warning-medium)';
28+
case 'Z': // Zombie
29+
case 'T': // Stopped
30+
case 'X': // Dead
31+
return 'var(--g-color-base-danger-medium)';
32+
default:
33+
return 'var(--g-color-base-misc-medium)';
34+
}
35+
};
36+
const getStackThemeColor = (state: string): ProgressTheme => {
37+
switch (state.toUpperCase()) {
38+
case 'R': // Running
39+
return 'success';
40+
case 'S': // Sleeping
41+
return 'info';
42+
case 'D': // Uninterruptible sleep
43+
return 'warning';
44+
case 'Z': // Zombie
45+
case 'T': // Stopped
46+
case 'X': // Dead
47+
return 'danger';
48+
default:
49+
return 'misc';
50+
}
51+
};
52+
const getStateTitle = (state: string): string => {
53+
switch (state.toUpperCase()) {
54+
case 'R': // Running
55+
return 'Running';
56+
case 'S': // Sleeping
57+
return 'Sleeping';
58+
case 'D': // Uninterruptible sleep
59+
return 'Uninterruptible sleep';
60+
case 'Z': // Zombie
61+
return 'Zombie';
62+
case 'T': // Stopped
63+
return 'Stopped';
64+
case 'X': // Dead
65+
return 'Dead';
66+
default:
67+
return 'Unknown';
68+
}
69+
};
70+
71+
export function ThreadStatesBar({states = {}, totalThreads, className}: ThreadStatesBarProps) {
72+
const total = totalThreads || Object.values(states).reduce((sum, count) => sum + count, 0);
73+
74+
if (total === 0) {
75+
return EMPTY_DATA_PLACEHOLDER;
76+
}
77+
78+
const stateEntries = Object.entries(states).filter(([, count]) => count > 0);
79+
80+
const stack = Object.entries(states).map(([state, count]) => ({
81+
theme: getStackThemeColor(state),
82+
value: (count / total) * 100,
83+
}));
84+
85+
return (
86+
<div className={b(null, className)}>
87+
<Progress stack={stack} size="s" />
88+
<Flex gap={2}>
89+
{stateEntries.map(([state, count]) => (
90+
<ActionTooltip key={state} title={getStateTitle(state)}>
91+
<Flex gap={1} alignItems="center">
92+
<div
93+
className={b('legend-color')}
94+
style={{backgroundColor: getProgressBackgroundColor(state)}}
95+
/>
96+
<Text color="secondary" variant="caption-2">
97+
{state}: {count}
98+
</Text>
99+
</Flex>
100+
</ActionTooltip>
101+
))}
102+
</Flex>
103+
</div>
104+
);
105+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {ResponseError} from '../../../components/Errors/ResponseError';
2+
import {LoaderWrapper} from '../../../components/LoaderWrapper/LoaderWrapper';
3+
import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable';
4+
import {nodeApi} from '../../../store/reducers/node/node';
5+
import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
6+
import {useAutoRefreshInterval} from '../../../utils/hooks';
7+
8+
import {columns} from './columns';
9+
import i18n from './i18n';
10+
11+
interface ThreadsProps {
12+
nodeId: string;
13+
className?: string;
14+
}
15+
16+
const THREADS_COLUMNS_WIDTH_LS_KEY = 'threadsTableColumnsWidth';
17+
18+
export function Threads({nodeId, className}: ThreadsProps) {
19+
const [autoRefreshInterval] = useAutoRefreshInterval();
20+
21+
const {
22+
currentData: nodeData,
23+
isLoading,
24+
error,
25+
} = nodeApi.useGetNodeInfoQuery({nodeId}, {pollingInterval: autoRefreshInterval});
26+
27+
const data = nodeData?.Threads || [];
28+
29+
return (
30+
<LoaderWrapper loading={isLoading} className={className}>
31+
{error ? <ResponseError error={error} /> : null}
32+
<ResizeableDataTable
33+
columnsWidthLSKey={THREADS_COLUMNS_WIDTH_LS_KEY}
34+
data={data}
35+
columns={columns}
36+
settings={DEFAULT_TABLE_SETTINGS}
37+
emptyDataMessage={i18n('alert_no-thread-data')}
38+
/>
39+
</LoaderWrapper>
40+
);
41+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type {Column} from '@gravity-ui/react-data-table';
2+
import DataTable from '@gravity-ui/react-data-table';
3+
4+
import {TitleWithHelpMark} from '../../../components/TitleWithHelpmark/TitleWithHelpmark';
5+
import type {TThreadPoolInfo} from '../../../types/api/threads';
6+
import {formatNumber} from '../../../utils/dataFormatters/dataFormatters';
7+
import {safeParseNumber} from '../../../utils/utils';
8+
9+
import {CpuUsageBar} from './CpuUsageBar/CpuUsageBar';
10+
import {ThreadStatesBar} from './ThreadStatesBar/ThreadStatesBar';
11+
import i18n from './i18n';
12+
13+
export const columns: Column<TThreadPoolInfo>[] = [
14+
{
15+
name: 'Name',
16+
header: i18n('field_pool-name'),
17+
render: ({row}) => row.Name || i18n('value_unknown'),
18+
width: 200,
19+
},
20+
{
21+
name: 'Threads',
22+
header: i18n('field_thread-count'),
23+
render: ({row}) => formatNumber(row.Threads),
24+
align: DataTable.RIGHT,
25+
width: 100,
26+
},
27+
{
28+
name: 'CpuUsage',
29+
header: (
30+
<TitleWithHelpMark
31+
header={i18n('field_cpu-usage')}
32+
note={i18n('description_cpu-usage')}
33+
/>
34+
),
35+
render: ({row}) => <CpuUsageBar systemUsage={row.SystemUsage} userUsage={row.UserUsage} />,
36+
sortAccessor: (row) => safeParseNumber(row.SystemUsage) + safeParseNumber(row.UserUsage),
37+
width: 200,
38+
},
39+
{
40+
name: 'MinorPageFaults',
41+
header: i18n('field_minor-page-faults'),
42+
render: ({row}) => formatNumber(row.MinorPageFaults),
43+
align: DataTable.RIGHT,
44+
width: 145,
45+
},
46+
{
47+
name: 'MajorPageFaults',
48+
header: i18n('field_major-page-faults'),
49+
render: ({row}) => formatNumber(row.MajorPageFaults),
50+
align: DataTable.RIGHT,
51+
width: 145,
52+
},
53+
{
54+
name: 'States',
55+
header: i18n('field_thread-states'),
56+
render: ({row}) => <ThreadStatesBar states={row.States} totalThreads={row.Threads} />,
57+
sortable: false,
58+
width: 250,
59+
},
60+
];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"field_pool-name": "Pool Name",
3+
"field_thread-count": "Threads",
4+
"field_cpu-usage": "CPU Usage",
5+
"field_minor-page-faults": "Minor Page Faults",
6+
"field_major-page-faults": "Major Page Faults",
7+
"field_thread-states": "Thread States",
8+
"value_unknown": "Unknown",
9+
"alert_no-thread-data": "No thread pool information available",
10+
"description_cpu-usage": "System usage + user usage"
11+
}

0 commit comments

Comments
 (0)