Skip to content

Commit 3e4a04b

Browse files
authored
feat: render per component memory consumption (#1574)
1 parent 7e234a5 commit 3e4a04b

File tree

12 files changed

+443
-34
lines changed

12 files changed

+443
-34
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
@import '../../styles/mixins.scss';
2+
3+
$memory-type-colors: (
4+
'AllocatorCachesMemory': var(--g-color-base-utility-medium-hover),
5+
'SharedCacheConsumption': var(--g-color-base-info-medium-hover),
6+
'MemTableConsumption': var(--g-color-base-warning-medium-hover),
7+
'QueryExecutionConsumption': var(--g-color-base-positive-medium-hover),
8+
'Other': var(--g-color-base-neutral-light-hover),
9+
);
10+
11+
@mixin memory-type-color($type) {
12+
background-color: map-get($memory-type-colors, $type);
13+
}
14+
15+
.memory-viewer {
16+
$block: &;
17+
18+
position: relative;
19+
z-index: 0;
20+
21+
min-width: 150px;
22+
padding: 0 var(--g-spacing-1);
23+
24+
&__progress-container {
25+
position: relative;
26+
27+
overflow: hidden;
28+
29+
height: 20px;
30+
31+
border-radius: 2px;
32+
background: var(--g-color-base-generic);
33+
}
34+
35+
&__container {
36+
display: flex;
37+
38+
padding: 2px 0;
39+
}
40+
41+
&__legend {
42+
position: absolute;
43+
bottom: 2px;
44+
45+
width: 20px;
46+
height: 20px;
47+
48+
border-radius: 2px;
49+
50+
@each $type, $color in $memory-type-colors {
51+
&_type_#{$type} {
52+
@include memory-type-color($type);
53+
}
54+
}
55+
}
56+
57+
&__segment {
58+
position: absolute;
59+
60+
height: 100%;
61+
62+
@each $type, $color in $memory-type-colors {
63+
&_type_#{$type} {
64+
@include memory-type-color($type);
65+
}
66+
}
67+
}
68+
69+
&__name {
70+
padding-left: 28px;
71+
}
72+
73+
&_theme_dark {
74+
color: var(--g-color-text-light-primary);
75+
76+
#{$block}__segment {
77+
opacity: 0.75;
78+
}
79+
}
80+
81+
&_status {
82+
&_good {
83+
#{$block}__progress-container {
84+
background-color: var(--g-color-base-positive-light);
85+
}
86+
}
87+
&_warning {
88+
#{$block}__progress-container {
89+
background-color: var(--g-color-base-yellow-light);
90+
}
91+
}
92+
&_danger {
93+
#{$block}__progress-container {
94+
background-color: var(--g-color-base-danger-light);
95+
}
96+
}
97+
}
98+
99+
&__text {
100+
display: flex;
101+
justify-content: center;
102+
align-items: center;
103+
}
104+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import {DefinitionList, useTheme} from '@gravity-ui/uikit';
2+
3+
import type {TMemoryStats} from '../../types/api/nodes';
4+
import {formatBytes} from '../../utils/bytesParsers';
5+
import {cn} from '../../utils/cn';
6+
import {GIGABYTE} from '../../utils/constants';
7+
import {calculateProgressStatus} from '../../utils/progress';
8+
import {isNumeric} from '../../utils/utils';
9+
import {HoverPopup} from '../HoverPopup/HoverPopup';
10+
import type {FormatProgressViewerValues} from '../ProgressViewer/ProgressViewer';
11+
import {ProgressViewer} from '../ProgressViewer/ProgressViewer';
12+
13+
import {getMemorySegments} from './utils';
14+
15+
import './MemoryViewer.scss';
16+
17+
const MIN_VISIBLE_MEMORY_SHARE = 1;
18+
const MIN_VISIBLE_MEMORY_VALUE = 0.01 * GIGABYTE;
19+
20+
const b = cn('memory-viewer');
21+
22+
const formatDetailedValues: FormatProgressViewerValues = (value, total) => {
23+
return [
24+
formatBytes({
25+
value,
26+
size: 'gb',
27+
withSizeLabel: false,
28+
precision: 2,
29+
}),
30+
formatBytes({
31+
value: total,
32+
size: 'gb',
33+
withSizeLabel: true,
34+
precision: 1,
35+
}),
36+
];
37+
};
38+
39+
export interface MemoryProgressViewerProps {
40+
stats: TMemoryStats;
41+
className?: string;
42+
warningThreshold?: number;
43+
value?: number | string;
44+
capacity?: number | string;
45+
formatValues: FormatProgressViewerValues;
46+
percents?: boolean;
47+
dangerThreshold?: number;
48+
}
49+
50+
export function MemoryViewer({
51+
stats,
52+
value,
53+
capacity,
54+
percents,
55+
formatValues,
56+
className,
57+
warningThreshold = 60,
58+
dangerThreshold = 80,
59+
}: MemoryProgressViewerProps) {
60+
const theme = useTheme();
61+
let fillWidth =
62+
Math.round((parseFloat(String(value)) / parseFloat(String(capacity))) * 100) || 0;
63+
fillWidth = fillWidth > 100 ? 100 : fillWidth;
64+
let valueText: number | string | undefined = value,
65+
capacityText: number | string | undefined = capacity,
66+
divider = '/';
67+
if (percents) {
68+
valueText = fillWidth + '%';
69+
capacityText = '';
70+
divider = '';
71+
} else if (formatValues) {
72+
[valueText, capacityText] = formatValues(Number(value), Number(capacity));
73+
}
74+
75+
const renderContent = () => {
76+
if (isNumeric(capacity)) {
77+
return `${valueText} ${divider} ${capacityText}`;
78+
}
79+
80+
return valueText;
81+
};
82+
83+
const calculateMemoryShare = (segmentSize: number) => {
84+
if (!value) {
85+
return 0;
86+
}
87+
return (segmentSize / parseFloat(String(capacity))) * 100;
88+
};
89+
90+
const memorySegments = getMemorySegments(stats);
91+
92+
const status = calculateProgressStatus({
93+
fillWidth,
94+
warningThreshold,
95+
dangerThreshold,
96+
colorizeProgress: true,
97+
});
98+
99+
let currentPosition = 0;
100+
101+
return (
102+
<HoverPopup
103+
popupContent={
104+
<DefinitionList responsive>
105+
{memorySegments.map(
106+
({label, value: segmentSize, capacity: segmentCapacity, key}) => (
107+
<DefinitionList.Item
108+
key={label}
109+
name={
110+
<div className={b('container')}>
111+
<div className={b('legend', {type: key})}></div>
112+
<div className={b('name')}>{label}</div>
113+
</div>
114+
}
115+
>
116+
{segmentCapacity ? (
117+
<ProgressViewer
118+
value={segmentSize}
119+
capacity={segmentCapacity}
120+
formatValues={formatDetailedValues}
121+
colorizeProgress
122+
/>
123+
) : (
124+
formatBytes({
125+
value: segmentSize,
126+
size: 'gb',
127+
withSizeLabel: true,
128+
precision: 2,
129+
})
130+
)}
131+
</DefinitionList.Item>
132+
),
133+
)}
134+
</DefinitionList>
135+
}
136+
>
137+
<div className={b({theme, status}, className)}>
138+
<div className={b('progress-container')}>
139+
{memorySegments
140+
.filter(({isInfo}) => !isInfo)
141+
.map((segment) => {
142+
if (segment.value < MIN_VISIBLE_MEMORY_VALUE) {
143+
return null;
144+
}
145+
146+
const currentMemoryShare = Math.max(
147+
calculateMemoryShare(segment.value),
148+
MIN_VISIBLE_MEMORY_SHARE,
149+
);
150+
const position = currentPosition;
151+
currentPosition += currentMemoryShare;
152+
153+
return (
154+
<div
155+
key={segment.key}
156+
className={b('segment', {type: segment.key})}
157+
style={{
158+
width: `${currentMemoryShare}%`,
159+
left: `${position}%`,
160+
}}
161+
/>
162+
);
163+
})}
164+
<div className={b('text')}>{renderContent()}</div>
165+
</div>
166+
</div>
167+
</HoverPopup>
168+
);
169+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"text_external-consumption": "External Consumption",
3+
"text_allocator-caches": "Allocator Caches",
4+
"text_shared-cache": "Shared Cache",
5+
"text_memtable": "MemTable",
6+
"text_query-execution": "Query Execution",
7+
"text_usage": "Usage",
8+
"text_soft-limit": "Soft Limit",
9+
"text_hard-limit": "Hard Limit",
10+
"text_other": "Other"
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {registerKeysets} from '../../../utils/i18n';
2+
3+
import en from './en.json';
4+
5+
const COMPONENT = 'ydb-memory-viewer';
6+
7+
export default registerKeysets(COMPONENT, {en});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type {TMemoryStats} from '../../types/api/nodes';
2+
import {isNumeric} from '../../utils/utils';
3+
4+
import i18n from './i18n';
5+
6+
function getMaybeNumber(value: string | number | undefined): number | undefined {
7+
return isNumeric(value) ? parseFloat(String(value)) : undefined;
8+
}
9+
10+
interface MemorySegment {
11+
label: string;
12+
key: string;
13+
value: number;
14+
capacity?: number;
15+
isInfo?: boolean;
16+
}
17+
18+
export function getMemorySegments(stats: TMemoryStats): MemorySegment[] {
19+
const segments = [
20+
{
21+
label: i18n('text_shared-cache'),
22+
key: 'SharedCacheConsumption',
23+
value: getMaybeNumber(stats.SharedCacheConsumption),
24+
capacity: getMaybeNumber(stats.SharedCacheLimit),
25+
isInfo: false,
26+
},
27+
{
28+
label: i18n('text_query-execution'),
29+
key: 'QueryExecutionConsumption',
30+
value: getMaybeNumber(stats.QueryExecutionConsumption),
31+
capacity: getMaybeNumber(stats.QueryExecutionLimit),
32+
isInfo: false,
33+
},
34+
{
35+
label: i18n('text_memtable'),
36+
key: 'MemTableConsumption',
37+
value: getMaybeNumber(stats.MemTableConsumption),
38+
capacity: getMaybeNumber(stats.MemTableLimit),
39+
isInfo: false,
40+
},
41+
{
42+
label: i18n('text_allocator-caches'),
43+
key: 'AllocatorCachesMemory',
44+
value: getMaybeNumber(stats.AllocatorCachesMemory),
45+
isInfo: false,
46+
},
47+
];
48+
49+
const nonInfoSegments = segments.filter(
50+
(segment) => segment.value !== undefined,
51+
) as MemorySegment[];
52+
const sumNonInfoSegments = nonInfoSegments.reduce((acc, segment) => acc + segment.value, 0);
53+
54+
const totalMemory = getMaybeNumber(stats.AnonRss);
55+
56+
if (totalMemory) {
57+
const otherMemory = Math.max(0, totalMemory - sumNonInfoSegments);
58+
59+
segments.push({
60+
label: i18n('text_other'),
61+
key: 'Other',
62+
value: otherMemory,
63+
isInfo: false,
64+
});
65+
}
66+
67+
segments.push(
68+
{
69+
label: i18n('text_external-consumption'),
70+
key: 'ExternalConsumption',
71+
value: getMaybeNumber(stats.ExternalConsumption),
72+
isInfo: true,
73+
},
74+
{
75+
label: i18n('text_usage'),
76+
key: 'Usage',
77+
value: getMaybeNumber(stats.AnonRss),
78+
isInfo: true,
79+
},
80+
{
81+
label: i18n('text_soft-limit'),
82+
key: 'SoftLimit',
83+
value: getMaybeNumber(stats.SoftLimit),
84+
isInfo: true,
85+
},
86+
{
87+
label: i18n('text_hard-limit'),
88+
key: 'HardLimit',
89+
value: getMaybeNumber(stats.HardLimit),
90+
isInfo: true,
91+
},
92+
);
93+
94+
return segments.filter((segment) => segment.value !== undefined) as MemorySegment[];
95+
}

src/components/ProgressViewer/ProgressViewer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const b = cn('progress-viewer');
1111

1212
type ProgressViewerSize = 'xs' | 's' | 'ns' | 'm' | 'n' | 'l' | 'head';
1313

14-
type FormatProgressViewerValues = (
14+
export type FormatProgressViewerValues = (
1515
value?: number,
1616
capacity?: number,
1717
) => (string | number | undefined)[];

0 commit comments

Comments
 (0)