Skip to content

feat: add new tab to Node page with thread pool statistics #2599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions THREADS_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Threads Tab Implementation Documentation

## Overview

This document describes the implementation of the Threads tab for the Node page in YDB Embedded UI, which displays detailed thread pool information as requested in issue #2051.

## Features Implemented

- **Complete UI Components**: Thread pools table with all required columns
- **CPU Usage Visualization**: Progress bars showing system + user CPU usage with color coding
- **Thread State Visualization**: Horizontal bar chart showing distribution of thread states (R, S, etc.)
- **Real API Integration**: Connected to RTK Query with auto-refresh and error handling
- **TypeScript Types**: Complete type definitions for thread pool information
- **Internationalization**: Full i18n support following project conventions

## Component Structure

```
src/containers/Node/Threads/
├── Threads.tsx # Main component
├── Threads.scss # Styling
├── CpuUsageBar/ # CPU usage visualization
│ ├── CpuUsageBar.tsx
│ └── CpuUsageBar.scss
├── ThreadStatesBar/ # Thread states visualization
│ ├── ThreadStatesBar.tsx
│ └── ThreadStatesBar.scss
└── i18n/ # Internationalization
├── en.json
└── index.ts
```

## Data Structure

The component expects thread pool information in the following format:

```typescript
interface TThreadPoolInfo {
Name?: string; // Thread pool name (e.g., "AwsEventLoop", "klktmr.IC")
Threads?: number; // Number of threads in the pool
SystemUsage?: number; // System CPU usage (0-1 range)
UserUsage?: number; // User CPU usage (0-1 range)
MinorPageFaults?: number; // Number of minor page faults
MajorPageFaults?: number; // Number of major page faults
States?: Record<string, number>; // Thread states with counts (e.g., {R: 2, S: 1})
}
```

## Backend Integration Required

Currently, the implementation uses mock data. To connect real data, the YDB backend needs to provide detailed thread information through one of these approaches:

### Option 1: New Dedicated Endpoint (Recommended)

```
GET /viewer/json/threads?node_id={nodeId}
```

Response format:

```json
{
"Threads": [
{
"Name": "AwsEventLoop",
"Threads": 64,
"SystemUsage": 0.0,
"UserUsage": 0.0,
"MinorPageFaults": 0,
"MajorPageFaults": 0,
"States": {
"S": 64
}
}
],
"ResponseTime": "1234567890",
"ResponseDuration": 123
}
```

### Option 2: Extend Existing Endpoint

Extend `/viewer/json/sysinfo` to include detailed thread information in addition to the current `PoolStats`.

## API Implementation

The frontend API integration is already implemented:

1. **Viewer API**: `getNodeThreads()` method in `src/services/api/viewer.ts`
2. **Node Store**: RTK Query endpoint in `src/store/reducers/node/node.ts`
3. **Component**: Connected with auto-refresh in `src/containers/Node/Threads/Threads.tsx`

## Data Mapping

The backend should provide:

- **Thread Pool Name**: From the actual thread pool name
- **Thread Count**: Number of threads in each pool
- **CPU Usage**: System and user CPU usage percentages (0-1 range)
- **Page Faults**: Minor and major page fault counts
- **Thread States**: Distribution of thread states using Linux process state codes:
- `R`: Running
- `S`: Sleeping (interruptible)
- `D`: Disk sleep (uninterruptible)
- `Z`: Zombie
- `T`: Stopped
- etc.

## Screenshots

The implementation provides a complete table view matching the requirements in the issue:

- Pool name column
- Thread count column
- CPU usage with visual progress bar
- Page fault counts
- Thread state distribution visualization

## Testing

To test the implementation:

1. Navigate to `/node/{nodeId}/threads` in the YDB Embedded UI
2. The tab will be available in the node page tabs
3. Currently shows mock data until backend integration is complete

## Next Steps

1. **Backend Development**: Implement the threads endpoint in YDB backend
2. **Real Data Integration**: Replace mock data with actual thread information
3. **Testing**: Verify with real YDB instances
4. **Performance**: Ensure efficient data collection for thread statistics

## Related Files

- Node page tabs: `src/containers/Node/NodePages.ts`
- Node page component: `src/containers/Node/Node.tsx`
- Thread types: `src/types/api/threads.ts`
- API viewer: `src/services/api/viewer.ts`
- Node store: `src/store/reducers/node/node.ts`
5 changes: 5 additions & 0 deletions src/containers/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -247,6 +248,10 @@ function NodePageContent({
return <NodeStructure nodeId={nodeId} />;
}

case 'threads': {
return <Threads nodeId={nodeId} />;
}

default:
return false;
}
Expand Down
7 changes: 7 additions & 0 deletions src/containers/Node/NodePages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const NODE_TABS_IDS = {
storage: 'storage',
tablets: 'tablets',
structure: 'structure',
threads: 'threads',
} as const;

export type NodeTab = ValueOf<typeof NODE_TABS_IDS>;
Expand All @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.cpu-usage-bar {
display: flex;
align-items: center;
gap: 8px;

min-width: 120px;

&__progress {
flex: 1;

min-width: 60px;
}

&__text {
font-size: 12px;
white-space: nowrap;
}

&__total {
font-weight: 500;
}

&__breakdown {
margin-left: 4px;

color: var(--g-color-text-secondary);
}
}
48 changes: 48 additions & 0 deletions src/containers/Node/Threads/CpuUsageBar/CpuUsageBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Progress} from '@gravity-ui/uikit';

import {cn} from '../../../../utils/cn';

import './CpuUsageBar.scss';

const b = cn('cpu-usage-bar');

interface CpuUsageBarProps {
systemUsage?: number;
userUsage?: number;
className?: string;
}

/**
* Component to display CPU usage as a progress bar showing both system and user usage
*/
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);

// Determine color based on total load
const getProgressTheme = (): 'success' | 'warning' | 'danger' => {
if (totalUsage >= 1.0) {
return 'danger';
} // 100% or more load
if (totalUsage >= 0.8) {
Copy link
Preview

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The magic numbers 1.0 and 0.8 should be extracted as named constants to improve maintainability and make the thresholds configurable.

Suggested change
if (totalUsage >= 1.0) {
return 'danger';
} // 100% or more load
if (totalUsage >= 0.8) {
if (totalUsage >= MAX_LOAD_THRESHOLD) {
return 'danger';
} // 100% or more load
if (totalUsage >= HIGH_LOAD_THRESHOLD) {

Copilot uses AI. Check for mistakes.

return 'warning';
} // 80% or more load
return 'success';
};

return (
<div className={b(null, className)}>
<div className={b('progress')}>
<Progress value={Math.min(totalPercent, 100)} theme={getProgressTheme()} size="s" />
</div>
<div className={b('text')}>
<span className={b('total')}>{totalPercent}%</span>
<span className={b('breakdown')}>
(S: {systemPercent}%, U: {userPercent}%)
</span>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.thread-states-bar {
&__bar {
display: flex;
overflow: hidden;

min-width: 80px;
height: 16px;
margin-bottom: 4px;

border: 1px solid var(--g-color-line-generic);
border-radius: 4px;
background-color: var(--g-color-base-generic);
}

&__segment {
transition: opacity 0.2s ease;

&:hover {
opacity: 0.8;
}
}

&__legend {
display: flex;
flex-wrap: wrap;
gap: 8px;

font-size: 11px;

color: var(--g-color-text-secondary);
}

&__legend-item {
display: flex;
align-items: center;
gap: 4px;

white-space: nowrap;
}

&__legend-color {
flex-shrink: 0;

width: 8px;
height: 8px;

border-radius: 2px;
}
}
76 changes: 76 additions & 0 deletions src/containers/Node/Threads/ThreadStatesBar/ThreadStatesBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {cn} from '../../../../utils/cn';

import './ThreadStatesBar.scss';

const b = cn('thread-states-bar');

interface ThreadStatesBarProps {
states?: Record<string, number>;
totalThreads?: number;
className?: string;
}

/**
* Thread state colors based on the state type
*/
const getStateColor = (state: string): string => {
switch (state.toUpperCase()) {
case 'R': // Running
return 'var(--g-color-text-positive)';
case 'S': // Sleeping
return 'var(--g-color-text-secondary)';
case 'D': // Uninterruptible sleep
return 'var(--g-color-text-warning)';
case 'Z': // Zombie
case 'T': // Stopped
case 'X': // Dead
return 'var(--g-color-text-danger)';
default:
return 'var(--g-color-text-misc)';
}
};

/**
* Component to display thread states as a horizontal bar chart
*/
export function ThreadStatesBar({states = {}, totalThreads, className}: ThreadStatesBarProps) {
const total = totalThreads || Object.values(states).reduce((sum, count) => sum + count, 0);

if (total === 0) {
return <div className={b(null, className)}>No threads</div>;
}

const stateEntries = Object.entries(states).filter(([, count]) => count > 0);

return (
<div className={b(null, className)}>
<div className={b('bar')}>
{stateEntries.map(([state, count]) => {
const percentage = (count / total) * 100;
return (
<div
key={state}
className={b('segment')}
style={{
width: `${percentage}%`,
backgroundColor: getStateColor(state),
}}
title={`${state}: ${count} threads (${Math.round(percentage)}%)`}
/>
);
})}
</div>
<div className={b('legend')}>
{stateEntries.map(([state, count]) => (
<span key={state} className={b('legend-item')}>
<span
className={b('legend-color')}
style={{backgroundColor: getStateColor(state)}}
/>
{state}: {count}
</span>
))}
</div>
</div>
);
}
Loading