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',
+}