Skip to content

Commit 12711b5

Browse files
committed
Merge branch 'stage' into dev
2 parents edba07f + 6efea92 commit 12711b5

File tree

8 files changed

+246
-59
lines changed

8 files changed

+246
-59
lines changed

src/features/instance/operations/queries/getAnalytics.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';
22
import { queryOptions } from '@tanstack/react-query';
3+
import type { Units } from '@/lib/units';
34

5+
export type MetricDataKey = string | ((metric: Metric) => number);
6+
export type MetricUnits = Units | 'reads' | 'writes' | 'messages';
47
export interface MetricConfig {
58
id: string;
69
name: string;
7-
unit: string;
10+
label?: string;
11+
dataKey: MetricDataKey;
12+
units: MetricUnits;
813
path?: string;
914
}
1015

@@ -27,11 +32,17 @@ interface GetAnalyticsRequest {
2732
}[];
2833
}
2934

30-
type GetAnalyticsResponse = {
35+
export interface Metric {
3136
id: number;
3237
metric: string;
38+
count: number;
39+
mean: number;
40+
period: number;
41+
node: string;
3342
[key: string]: string|number|boolean|null;
34-
}[];
43+
}
44+
45+
type GetAnalyticsResponse = Metric[];
3546

3647
export function getAnalyticsQueryOptions({ metricConfig, startTime, endTime, instanceParams }: GetAnalyticsParams) {
3748
return queryOptions({

src/features/instance/status/components/Monitoring.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import { useMemo, useState } from 'react';
33
import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';
44
import { Label } from '@/components/ui/label.tsx';
55
import { useInterval } from '@/hooks/useInterval.ts';
6-
import type { MetricConfig } from '@/features/instance/operations/queries/getAnalytics.ts';
6+
import type { Metric, MetricConfig } from '@/features/instance/operations/queries/getAnalytics.ts';
77
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.tsx';
88

99
const metrics: MetricConfig[] = [
10-
{id: 'db-read', name: 'db-read', unit: ' reads'},
11-
{id: 'cpu-usage-user', name: 'cpu-usage', path: 'user', unit: ' s'},
10+
{id: 'db-read', name: 'db-read', dataKey: 'count', units: 'reads'},
11+
{id: 'db-read-bytes', label: 'db-read-bytes', name: 'db-read', dataKey: metricSum, units: 'bytes'},
12+
{id: 'db-write', name: 'db-write', dataKey: 'count', units: 'writes'},
13+
{id: 'db-write-bytes', label: 'db-write-bytes', name: 'db-write', dataKey: metricSum, units: 'bytes'},
14+
{id: 'db-message', name: 'db-message', dataKey: 'count', units: 'messages'},
15+
{id: 'db-message-bytes', label: 'db-message-bytes', name: 'db-message', dataKey: metricSum, units: 'bytes'},
16+
{id: 'cpu-usage-user', name: 'cpu-usage', path: 'user', dataKey: metricSum, units: 'secs'},
17+
{id: 'cpu-usage-harper', name: 'cpu-usage', path: 'harper', dataKey: metricSum, units: 'secs'},
1218
];
1319

20+
function metricSum(metric: Metric) {
21+
if (metric.mean && metric.count) {
22+
return metric.mean * metric.count;
23+
}
24+
return 0;
25+
}
26+
1427
interface TimeSelectOption {
1528
label: string;
1629
value: number;
@@ -66,7 +79,7 @@ export function Monitoring({instanceParams}: MonitoringParams) {
6679
<SelectContent>
6780
<SelectGroup>
6881
{metrics.map((m) => {
69-
let itemLabel = m.name;
82+
let itemLabel = m.label ?? m.name;
7083
if (m.path) {
7184
itemLabel += ` (${m.path})`;
7285
}
@@ -113,7 +126,6 @@ export function Monitoring({instanceParams}: MonitoringParams) {
113126
</div>
114127
<MetricVisualization
115128
metricConfig={selectedMetric}
116-
metricDataKey="count"
117129
startTime={startTime}
118130
endTime={endTime}
119131
instanceParams={instanceParams} />

src/features/instance/status/components/monitoring/MetricVisualization.tsx

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,43 @@
11
import { useQuery } from '@tanstack/react-query';
2-
import { getAnalyticsQueryOptions, MetricConfig } from '@/features/instance/operations/queries/getAnalytics.ts';
2+
import { getAnalyticsQueryOptions, type MetricConfig, type Metric, type MetricDataKey, type MetricUnits } from '@/features/instance/operations/queries/getAnalytics.ts';
33
import type { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceClientConfig.ts';
4-
import { useMemo } from 'react';
4+
import { useMemo, useState } from 'react';
55
import { Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
6+
import { scaleValueToUnits, determineUnits } from '@/lib/units';
7+
import { harperPalette } from '@/lib/colorPalette.ts';
68

79
type MetricValue = string | number | boolean;
810
type NullableMetricValue = MetricValue | null;
9-
type Metric = {node: string, id: number, period: number, [key: string]: MetricValue};
1011
type NullableMetric = {[key: string]: NullableMetricValue};
1112
type CoalescedMetrics = {[id: string]: {[node: string]: MetricValue}};
1213

1314
interface MetricVisualizationParams {
1415
metricConfig: MetricConfig;
15-
metricDataKey: keyof Metric;
1616
startTime: number;
1717
endTime: number;
1818
instanceParams: InstanceClientIdConfig & InstanceTypeConfig;
1919
}
2020

21-
const harperPalette = {
22-
'persistence-purple': '#403B8A',
23-
'b-tree-green': '#55C58F',
24-
'cyber-grape': '#7A3A87',
25-
'quantum-purple': '#312556',
26-
'cloud-white': '#F5F5F5',
27-
'acid-magenta': '#C63368',
28-
'edge-gray': '#383D40',
29-
};
30-
31-
export function MetricVisualization({ metricConfig, metricDataKey, startTime, endTime, instanceParams }: MetricVisualizationParams) {
21+
function resolveMetricDataKey(metric: Metric, dataKey: MetricDataKey, baseUnits: MetricUnits, conversionUnits?: string) {
22+
let baseValue;
23+
if (typeof dataKey === 'string') {
24+
baseValue = metric[dataKey] as number ?? 0;
25+
} else {
26+
baseValue = dataKey(metric);
27+
}
28+
29+
if (conversionUnits) {
30+
return scaleValueToUnits(baseValue, baseUnits, conversionUnits);
31+
}
32+
33+
return baseValue;
34+
}
35+
36+
export function MetricVisualization({ metricConfig, startTime, endTime, instanceParams }: MetricVisualizationParams) {
3237
const { data } = useQuery(getAnalyticsQueryOptions({instanceParams, metricConfig, startTime, endTime}));
3338
const metrics = useMemo(() => {
3439
return data?.reduce((ms: Metric[], m: NullableMetric) => {
35-
const newMetric: Metric = {node: '', id: 0, period: 0};
40+
const newMetric: Metric = {metric: '', node: '', id: 0, period: 0, count: 0, mean: 0};
3641
for (const k in m) {
3742
if (m[k] !== null) {
3843
newMetric[k] = m[k];
@@ -43,24 +48,34 @@ export function MetricVisualization({ metricConfig, metricDataKey, startTime, en
4348
}, [])
4449
}, [data]);
4550

51+
const [yAxisUnits, setYAxisUnits] = useState<string>(metricConfig.units);
52+
4653
const nodeMetrics = useMemo(() => {
4754
const coalescedMetrics: CoalescedMetrics = {};
55+
const { dataKey, units } = metricConfig;
56+
let conversionUnits = units as string;
57+
58+
if (metrics && metrics.length > 0) {
59+
const maxDataValue = Math.max(...metrics.map((m) => resolveMetricDataKey(m, dataKey, units)));
60+
conversionUnits = determineUnits(units, maxDataValue);
61+
setYAxisUnits(conversionUnits);
4862

49-
if (metrics) {
5063
for (const metric of metrics) {
5164
const coalescedTime = Math.floor(metric.id / metric.period) * metric.period;
65+
const resolvedMetric = resolveMetricDataKey(metric, dataKey, units, conversionUnits).toFixed(2);
66+
5267
if (coalescedMetrics[coalescedTime]) {
53-
coalescedMetrics[coalescedTime][metric.node] = metric[metricDataKey];
68+
coalescedMetrics[coalescedTime][metric.node] = resolvedMetric;
5469
} else {
55-
coalescedMetrics[coalescedTime] = { [metric.node]: metric[metricDataKey] };
70+
coalescedMetrics[coalescedTime] = { [metric.node]: resolvedMetric };
5671
}
5772
}
5873

5974
return Object.keys(coalescedMetrics).map((id: string) => {
6075
return { id: Number.parseInt(id), ...coalescedMetrics[id] };
6176
});
6277
}
63-
}, [metrics, metricDataKey]);
78+
}, [metrics, metricConfig]);
6479

6580
const nodes = useMemo(() => {
6681
return Array.from(new Set<string>(metrics?.map((m) => m.node)));
@@ -85,7 +100,7 @@ export function MetricVisualization({ metricConfig, metricDataKey, startTime, en
85100
})
86101
}
87102
<XAxis dataKey={(item) => formatTime(item.id)} />
88-
<YAxis unit={metricConfig.unit} width={80} />
103+
<YAxis unit={` ${yAxisUnits}`} width={80} />
89104
<Legend />
90105
<Tooltip />
91106
</LineChart>

src/lib/colorPalette.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const harperPalette = {
2+
'persistence-purple': '#403B8A',
3+
'b-tree-green': '#55C58F',
4+
'cyber-grape': '#7A3A87',
5+
'quantum-purple': '#312556',
6+
'cloud-white': '#F5F5F5',
7+
'acid-magenta': '#C63368',
8+
'edge-gray': '#383D40',
9+
};

src/lib/humanFileSize.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ import { describe, expect, it } from 'vitest';
22
import { humanFileSize } from './humanFileSize';
33

44
describe('humanFileSize', () => {
5-
it('should return bytes for values less than 1024', () => {
5+
it('should return bytes for values less than 1000', () => {
66
expect(humanFileSize(500)).toBe('500 B');
77
expect(humanFileSize(0)).toBe('0 B');
8-
expect(humanFileSize(1023)).toBe('1023 B');
8+
expect(humanFileSize(999)).toBe('999 B');
99
});
1010

11-
it('should convert to KB for values between 1024 and 1048575', () => {
12-
expect(humanFileSize(1024)).toBe('1 KB');
13-
expect(humanFileSize(2048)).toBe('2 KB');
14-
expect(humanFileSize(1000000)).toBe('977 KB');
11+
it('should convert to KiB for values between 1000 and 1000000', () => {
12+
expect(humanFileSize(1000)).toBe('1 KB');
13+
expect(humanFileSize(2000)).toBe('2 KB');
14+
expect(humanFileSize(900000)).toBe('900 KB');
1515
});
1616

17-
it('should convert to MB for appropriate values', () => {
18-
expect(humanFileSize(1048576)).toBe('1 MB');
19-
expect(humanFileSize(2097152)).toBe('2 MB');
17+
it('should convert to MiB for appropriate values', () => {
18+
expect(humanFileSize(1000000)).toBe('1 MB');
19+
expect(humanFileSize(2000000)).toBe('2 MB');
2020
});
2121

22-
it('should convert to GB for appropriate values', () => {
23-
expect(humanFileSize(1073741824)).toBe('1 GB');
22+
it('should convert to GiB for appropriate values', () => {
23+
expect(humanFileSize(1000000000)).toBe('1 GB');
2424
});
2525

2626
it('should apply the multiplier correctly', () => {
27-
expect(humanFileSize(2, 1024)).toBe('2 KB');
28-
expect(humanFileSize(2, 1048576)).toBe('2 MB');
27+
expect(humanFileSize(2, 1000)).toBe('2 KB');
28+
expect(humanFileSize(2, 1000000)).toBe('2 MB');
2929
});
3030
});

src/lib/humanFileSize.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
const units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
1+
import { scaleValueToUnits, determineUnits } from '@/lib/units.ts';
22

33
export function humanFileSize(input: number, multiplierFromBytes: number = 1) {
4-
const thresh = 1024;
5-
const bytes = input * multiplierFromBytes;
6-
let value = bytes;
4+
const initialValue = input * multiplierFromBytes;
5+
const units = determineUnits('bytes', initialValue);
6+
const scaled = scaleValueToUnits(initialValue, 'bytes', units);
77

8-
if (Math.abs(value) < thresh) {
9-
return value + ' B';
10-
}
11-
12-
let u = -1;
13-
const r = 10;
14-
15-
do {
16-
value /= thresh;
17-
++u;
18-
} while (Math.round(Math.abs(value) * r) / r >= thresh && u < units.length - 1);
19-
20-
return `${new Intl.NumberFormat().format(Math.round(value))} ${units[u]}`;
8+
const value = new Intl.NumberFormat().format(Math.round(scaled));
9+
return `${value} ${units}`;
2110
}

src/lib/units.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { determineUnits, scaleValueToUnits } from '@/lib/units.ts';
3+
4+
describe('determineUnits', () => {
5+
it('should return unsupported base units as-is', () => {
6+
expect(determineUnits('foos', 0)).toBe('foos');
7+
});
8+
9+
it("should return 'B' for bytes < 1,000", () => {
10+
expect(determineUnits('bytes', 0)).toBe('B');
11+
expect(determineUnits('bytes', 100)).toBe('B');
12+
expect(determineUnits('bytes', 999)).toBe('B');
13+
});
14+
15+
it("should return 'KiB' for bytes >= 1,000 && < 1,000,000", () => {
16+
expect(determineUnits('bytes', 1000)).toBe('KB');
17+
expect(determineUnits('bytes', 2000)).toBe('KB');
18+
expect(determineUnits('bytes', 900_000)).toBe('KB');
19+
});
20+
21+
it("should return 'MiB' for bytes >= 1,000,000 && < 1,000,000,000", () => {
22+
expect(determineUnits('bytes', 1_000_000)).toBe('MB');
23+
expect(determineUnits('bytes', 10_000_000)).toBe('MB');
24+
expect(determineUnits('bytes', 900_000_000)).toBe('MB');
25+
});
26+
27+
it("should return 'secs' for seconds < 60", () => {
28+
expect(determineUnits('secs', 0)).toBe('secs');
29+
expect(determineUnits('secs', 20)).toBe('secs');
30+
expect(determineUnits('secs', 59)).toBe('secs');
31+
});
32+
33+
it("should return 'mins' for seconds >= 60 && < 3,600", () => {
34+
expect(determineUnits('secs', 60)).toBe('mins');
35+
expect(determineUnits('secs', 2345)).toBe('mins');
36+
expect(determineUnits('secs', 3599)).toBe('mins');
37+
});
38+
39+
it("should return 'hrs' for seconds >= 3,600", () => {
40+
expect(determineUnits('secs', 3600)).toBe('hrs');
41+
expect(determineUnits('secs', 345679)).toBe('hrs');
42+
});
43+
});
44+
45+
describe('scaleValueToUnits', () => {
46+
it('should leave values with unsupported units as-is', () => {
47+
expect(scaleValueToUnits(12345, 'bytes', 'bazs')).toBe(12345);
48+
expect(scaleValueToUnits(6789, 'secs', 'quxes')).toBe(6789);
49+
});
50+
51+
it('should leave bytes scaled to bytes as-is', () => {
52+
expect(scaleValueToUnits(0, 'bytes', 'B')).toBe(0);
53+
expect(scaleValueToUnits(100, 'bytes', 'B')).toBe(100);
54+
expect(scaleValueToUnits(1024, 'bytes', 'B')).toBe(1024);
55+
expect(scaleValueToUnits(1000000, 'bytes', 'B')).toBe(1000000);
56+
});
57+
58+
it('should scale bytes to KB', () => {
59+
expect(scaleValueToUnits(0, 'bytes', 'KB')).toBe(0);
60+
expect(scaleValueToUnits(1000, 'bytes', 'KB')).toBe(1);
61+
expect(scaleValueToUnits(1000 * 2, 'bytes', 'KB')).toBe(2);
62+
expect(scaleValueToUnits(1000 * 10, 'bytes', 'KB')).toBe(10);
63+
});
64+
65+
it('should scale bytes to MB', () => {
66+
expect(scaleValueToUnits(0, 'bytes', 'MB')).toBe(0);
67+
expect(scaleValueToUnits(1000, 'bytes', 'MB')).toBeCloseTo(0.001);
68+
expect(scaleValueToUnits(1000 * 1000, 'bytes', 'MB')).toBe(1);
69+
expect(scaleValueToUnits(1000 * 1000 * 2, 'bytes', 'MB')).toBe(2);
70+
expect(scaleValueToUnits(1000 * 1000 * 10, 'bytes', 'MB')).toBe(10);
71+
});
72+
73+
it('should leave secs scaled to secs as-is', () => {
74+
expect(scaleValueToUnits(0, 'secs', 'secs')).toBe(0);
75+
expect(scaleValueToUnits(100, 'secs', 'secs')).toBe(100);
76+
expect(scaleValueToUnits(1024, 'secs', 'secs')).toBe(1024);
77+
expect(scaleValueToUnits(1000000, 'secs', 'secs')).toBe(1000000);
78+
});
79+
80+
it('should scale secs to mins', () => {
81+
expect(scaleValueToUnits(0, 'secs', 'mins')).toBe(0);
82+
expect(scaleValueToUnits(59, 'secs', 'mins')).toBeLessThan(1);
83+
expect(scaleValueToUnits(60, 'secs', 'mins')).toBe(1);
84+
expect(scaleValueToUnits(600, 'secs', 'mins')).toBe(10);
85+
expect(scaleValueToUnits(3600, 'secs', 'mins')).toBe(60);
86+
});
87+
88+
it('should scale secs to hrs', () => {
89+
expect(scaleValueToUnits(0, 'secs', 'hrs')).toBe(0);
90+
expect(scaleValueToUnits(60 * 59, 'secs', 'hrs')).toBeLessThan(1);
91+
expect(scaleValueToUnits(60 * 60, 'secs', 'hrs')).toBe(1);
92+
expect(scaleValueToUnits(60 * 600, 'secs', 'hrs')).toBe(10);
93+
expect(scaleValueToUnits(60 * 3600, 'secs', 'hrs')).toBe(60);
94+
});
95+
});

0 commit comments

Comments
 (0)