chore: detail api

This commit is contained in:
jialin
2026-01-30 18:48:55 +08:00
parent 358df925e7
commit 8f2eea70f6
43 changed files with 1938 additions and 97 deletions
@@ -49,6 +49,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
<BenchmarkForm
ref={form}
action={action}
open={open}
currentData={currentData}
onFinish={handleOk}
/>
@@ -184,7 +184,7 @@ const CompareConditions: React.FC = () => {
}}
>
<Container>
<span className="holder">Compare Filter</span>
<span className="holder">Export filter</span>
</Container>
</Popover>
);
@@ -0,0 +1,89 @@
import IconFont from '@/components/icon-font';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { useIntl, useSearchParams } from '@umijs/max';
import { Tabs, TabsProps } from 'antd';
import React, { useEffect, useState } from 'react';
import DetailContext from '../config/detail-context';
import { BenchmarkListItem } from '../config/types';
import useQueryDetail from '../services/use-query-detail';
import Configure from './configure';
import Environment from './environment';
import Logs from './logs';
import Summary from './summary';
const Details: React.FC<{ currentData?: BenchmarkListItem }> = ({
currentData
}) => {
const intl = useIntl();
const [searchParams] = useSearchParams();
const id = searchParams.get('id');
const { loading, detailData, cancelRequest, fetchData } = useQueryDetail();
const {
clusterList,
fetchClusterList,
cancelRequest: cancelClusterRequest
} = useQueryClusterList();
const [activeKey, setActiveKey] = useState('summary');
const items: TabsProps['items'] = [
{
key: 'summary',
label: intl.formatMessage({ id: 'benchmark.detail.summary.title' }),
children: <Summary />,
icon: <IconFont type="icon-basic" />
},
{
key: 'configure',
label: intl.formatMessage({ id: 'benchmark.detail.configure.title' }),
children: <Configure />,
icon: <IconFont type="icon-settings-02" />
},
{
key: 'environment',
label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }),
children: <Environment />,
icon: <IconFont type="icon-server02" />
},
{
key: 'logs',
label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }),
children: <Logs />,
icon: <IconFont type="icon-logs" />
}
];
const handleChangeTab = (key: string) => {
setActiveKey(key);
};
useEffect(() => {
if (id) {
fetchData(id);
fetchClusterList({ page: -1 });
} else {
cancelRequest();
cancelClusterRequest();
}
}, [id]);
return (
<DetailContext.Provider
value={{
detailData: detailData || {},
clusterList: clusterList,
loading: loading,
id: Number(id)
}}
>
<Tabs
size="small"
activeKey={activeKey}
onChange={handleChangeTab}
items={items}
type="card"
/>
</DetailContext.Provider>
);
};
export default Details;
@@ -0,0 +1,29 @@
import FormDrawer from '@/pages/_components/form-drawer';
import { BenchmarkListItem } from '../config/types';
import DetailContent from './detail-content';
interface DetailModalProps {
open: boolean;
currentData?: BenchmarkListItem;
onClose: () => void;
}
const DetailModal: React.FC<DetailModalProps> = ({
open,
currentData,
onClose
}) => {
return (
<FormDrawer
title={currentData?.name}
open={open}
onCancel={onClose}
width={700}
footer={null}
>
<DetailContent currentData={currentData} />
</FormDrawer>
);
};
export default DetailModal;
@@ -0,0 +1,57 @@
import { convertFileSize } from '@/utils';
import { Descriptions, DescriptionsProps } from 'antd';
import React from 'react';
import { GPUData } from '../../config/detail-types';
import Section from '../summary/section';
const Environment: React.FC<GPUData> = (props) => {
const items: DescriptionsProps['items'] = [
{
key: '4',
label: 'Name',
children: props.name
},
{
key: '1',
label: 'VRAM',
children: convertFileSize(props.memory_total)
},
{
key: '3',
label: 'Driver Version',
children: props.driver_version
},
{
key: '2',
label: 'Runtime Version',
children: props.runtime_version
},
{
key: '6',
label: 'Core',
children: props.core_total
},
{
key: '5',
label: 'Vendor',
children: props.vendor
}
];
return (
<Section title={`GPU ${props.index}`}>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</Section>
);
};
export default Environment;
@@ -1,9 +1,82 @@
import React from 'react';
import { Tag } from 'antd';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useDetailContext } from '../../config/detail-context';
import WorkerData from './worker-data';
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const Environment: React.FC = () => {
const { detailData } = useDetailContext();
return <div>Environment Content</div>;
const { snapshot } = detailData;
const mainWorker = useMemo(() => {
// get workers data
const [[workerName, workerInfo]] = Object.entries(snapshot.workers);
const gpuData = Object.values(snapshot.gpus).filter(
(gpu) => gpu.worker_name === workerName
);
return {
workerData: workerInfo,
gpuData: [...gpuData, ...gpuData]
};
}, [snapshot]);
const subWorkers = useMemo(() => {
const [[mainWorkerName, mainWorkerInfo]] = Object.entries(snapshot.workers);
const subOrdinaryWorkers = Object.values(snapshot.instances).filter(
(instance) => instance.worker_name === mainWorkerName
);
return subOrdinaryWorkers.map((worker) => {
const gpuData = Object.values(snapshot.gpus).filter(
(gpu) => gpu.worker_name === worker.worker_name
);
return {
workerData: worker,
gpuData: gpuData
};
});
}, [snapshot, mainWorker]);
return (
<Container>
<WorkerData
workerData={mainWorker.workerData}
gpuData={mainWorker.gpuData}
title={
<div className="flex-center gap-8">
<Tag color="geekblue">Main</Tag>
<span>{mainWorker?.workerData?.name}</span>
</div>
}
></WorkerData>
<WorkerData
workerData={mainWorker.workerData}
gpuData={mainWorker.gpuData}
title={
<div className="flex-center gap-8">
<Tag color="geekblue">Sub</Tag>
<span>{mainWorker?.workerData?.name}</span>
</div>
}
></WorkerData>
{/* {subWorkers?.map?.((worker, index) => (
<WorkerData
key={index}
workerData={worker.workerData}
gpuData={worker.gpuData}
></WorkerData>
))} */}
</Container>
);
};
export default Environment;
@@ -0,0 +1,43 @@
import { convertFileSize } from '@/utils';
import { Descriptions, DescriptionsProps } from 'antd';
import React from 'react';
import { WorkerData } from '../../config/detail-types';
const Environment: React.FC<WorkerData> = (props) => {
const { os, cpu_total, memory_total } = props;
const items: DescriptionsProps['items'] = [
{
key: '4',
label: 'System',
children: os.name
},
{
key: '1',
label: 'CPU Count',
children: cpu_total
},
{
key: '5',
label: 'Memory Total',
children: convertFileSize(memory_total)
}
];
return (
<div>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</div>
);
};
export default Environment;
@@ -0,0 +1,57 @@
import { Col, Row } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { GPUData as GPUDataType, WorkerData } from '../../config/detail-types';
import Section from '../summary/section';
import GPUData from './gpu-data';
import Metadata from './metadata';
const Container = styled.div`
display: flex;
flex-direction: column;
border-radius: var(--ant-border-radius);
border: 1px solid var(--ant-color-border);
`;
const Content = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 16px;
`;
const Environment: React.FC<{
workerData: WorkerData;
gpuData: GPUDataType[];
title?: React.ReactNode;
}> = (props) => {
const { title } = props;
return (
<Container>
<Section
title={title}
styles={{
container: {
border: 'none',
paddingInline: 16,
borderRadius: 0,
borderBottom: '1px solid var(--ant-color-split)'
}
}}
>
<Metadata {...props.workerData} />
</Section>
<Content>
<Row gutter={16}>
{props.gpuData.map((gpu, index) => (
<Col span={12} key={index}>
<GPUData {...gpu} />
</Col>
))}
</Row>
</Content>
</Container>
);
};
export default Environment;
@@ -2,7 +2,6 @@ import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Space } from 'antd';
import React from 'react';
import CompareConditions from './compare-conditions';
export interface RightActionsProps {
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@ -30,7 +29,7 @@ const RightActions: React.FC<RightActionsProps> = ({
allowClear
onChange={handleInputChange}
></Input>
<CompareConditions></CompareConditions>
{/* <CompareConditions></CompareConditions> */}
<Button
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
+19 -2
View File
@@ -1,9 +1,26 @@
import React from 'react';
import LogsViewer from '@/components/logs-viewer/virtual-log-list';
import React, { useRef } from 'react';
import { BENCHMARKS_API } from '../../apis';
import { useDetailContext } from '../../config/detail-context';
const Logs: React.FC = () => {
const { id } = useDetailContext();
return <div>Logs Content</div>;
const logsViewerRef = useRef<any>(null);
return (
<div>
<LogsViewer
ref={logsViewerRef}
diffHeight={175}
url={`${BENCHMARKS_API}/${id}/logs`}
tail={undefined}
enableScorllLoad={true}
isDownloading={false}
params={{
follow: true
}}
></LogsViewer>
</div>
);
};
export default Logs;
@@ -1,7 +1,8 @@
import DropdownButtons from '@/components/drop-down-buttons';
import {
DeleteOutlined,
PlusOutlined,
SettingOutlined
DownloadOutlined,
PlusOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Space } from 'antd';
@@ -11,6 +12,7 @@ export interface RightActionsProps {
handleDeleteByBatch: () => void;
handleClickPrimary?: () => void;
handleSettingFields?: () => void;
settingButton?: React.ReactNode;
buttonText?: string;
rowSelection: {
selectedRowKeys: React.Key[];
@@ -21,14 +23,38 @@ const RightActions: React.FC<RightActionsProps> = ({
handleDeleteByBatch,
handleClickPrimary,
handleSettingFields,
settingButton,
buttonText,
rowSelection
}) => {
const intl = useIntl();
const ButtonList = [
{
label: 'common.button.export',
key: 'export',
icon: <DownloadOutlined />
},
{
label: 'common.button.delete',
key: 'start',
props: {
danger: true
},
icon: <DeleteOutlined />
}
];
const handleActionSelect = (val: string) => {};
return (
<Space size={16}>
<Button onClick={handleSettingFields} icon={<SettingOutlined />}></Button>
{/* <Tooltip title="Column Settings">
<Button
onClick={handleSettingFields}
icon={<SettingOutlined />}
></Button>
</Tooltip> */}
{settingButton}
<Button
icon={<PlusOutlined></PlusOutlined>}
type="primary"
@@ -36,7 +62,7 @@ const RightActions: React.FC<RightActionsProps> = ({
>
{buttonText}
</Button>
<Button
{/* <Button
icon={<DeleteOutlined />}
danger
onClick={handleDeleteByBatch}
@@ -48,7 +74,19 @@ const RightActions: React.FC<RightActionsProps> = ({
<span>({rowSelection?.selectedRowKeys?.length})</span>
)}
</span>
</Button>
</Button> */}
<DropdownButtons
items={ButtonList}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
size="large"
showText={true}
disabled={!rowSelection.selectedRowKeys.length}
onSelect={handleActionSelect}
/>
</Space>
);
};
@@ -0,0 +1,244 @@
import AutoTooltip from '@/components/auto-tooltip';
import StatusTag from '@/components/status-tag';
import { useIntl } from '@umijs/max';
import { Col, Descriptions, DescriptionsProps, Row, Statistic } from 'antd';
import dayjs from 'dayjs';
import { round } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { BenchmarkStatus, BenchmarkStatusLabelMap } from '../../config';
import { useDetailContext } from '../../config/detail-context';
import PercentileResult from './percentile-result';
import Section from './section';
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const Card = styled.div`
height: 78px;
padding: 12px 16px;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
background-color: var(--ant-color-bg-container);
`;
const Box = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
`;
const Summary: React.FC = () => {
const { detailData, clusterList } = useDetailContext();
const intl = useIntl();
const items: DescriptionsProps['items'] = [
{
key: '1',
label: 'Model',
children: <AutoTooltip ghost>{detailData.model_name}</AutoTooltip>
},
{
key: '2',
label: 'Cluster',
children: (
<AutoTooltip ghost>
{clusterList?.find((item) => item.value === detailData.cluster_id)
?.label || detailData.cluster_id}
</AutoTooltip>
)
},
{
key: '4',
label: 'Dataset',
children: detailData.dataset_name
},
{
key: '5',
label: 'Concurrency',
children: detailData.concurrency
},
{
key: '5',
label: 'Duration (s)',
children: detailData.duration
},
{
key: '3',
label: 'Update Time',
children: dayjs(detailData.updated_at).format('YYYY-MM-DD HH:mm:ss')
}
];
const cardFields = [
{
label: 'Total Requests',
key: 'total_requests',
value: detailData.total_requests,
unit: ''
},
{
label: 'Success Rate',
key: 'success_rate',
value: detailData.success_rate,
color: 'var(--ant-color-success)',
unit: '%'
},
{
label: 'TPS',
key: 'tps',
value: detailData.tokens_per_second_mean,
unit: 't/s'
},
{
label: 'RPS',
key: 'rps',
value: detailData.requests_per_second_mean,
unit: 'req/s'
},
// {
// label: 'ITL',
// key: 'itl',
// value: detailData.inter_token_latency_mean,
// unit: 'ms'
// },
{
label: 'TPOT',
key: 'time_per_output_token_mean',
value: detailData.time_per_output_token_mean,
unit: 'ms'
},
{
label: 'TTFT',
key: 'time_to_first_token_mean',
value: detailData.time_to_first_token_mean,
unit: 'ms'
}
];
const throughputFields = [
{
key: '3',
label: 'Total throughput',
children: `${round(detailData.total_through || 0, 2)} t/s`
},
{
key: '1',
label: 'Request throughput',
children: `${round(detailData.request_through || 0, 2)} t/s`
},
{
key: '2',
label: 'Output throughput',
children: `${round(detailData.output_through || 0, 2)} t/s`
}
];
const latencyFields = [
{
key: '1',
label: 'Request Latency',
children: `${round(detailData.request_latency_mean, 2)} t/s`
},
{
key: '2',
label: 'Avg Time to First Token',
children: `${round(detailData.time_to_first_token_mean, 2)} t/s`
},
{
key: '3',
label: 'Avg Time Per Output Token',
children: `${round(detailData.time_per_output_token_mean, 2)} t/s`
}
];
return (
<Container>
<Row gutter={16}>
{cardFields.map((field) => (
<Col span={8} key={field.key} style={{ padding: '8px' }}>
<Card>
<Statistic
title={field.label}
value={field.value}
precision={2}
styles={{
content: {
color: field.color,
fontSize: 16,
fontWeight: 500
},
header: {
paddingBottom: 4
}
}}
suffix={field.unit}
/>
</Card>
</Col>
))}
</Row>
<Section>
<div className="flex-center section-title gap-8">
<span>{detailData.model_instance_name}</span>
<StatusTag
statusValue={{
status: BenchmarkStatus[detailData.state],
text: BenchmarkStatusLabelMap[detailData.state],
message: detailData.state_message || undefined
}}
/>
</div>
<Descriptions
items={items}
colon={false}
column={3}
layout="vertical"
styles={{
content: {
justifyContent: 'flex-start'
}
}}
></Descriptions>
</Section>
<Box>
<Section>
<div className="flex-center section-title gap-8">
<span>Throughput</span>
</div>
<Descriptions
items={throughputFields}
colon={true}
column={1}
styles={{
content: {
justifyContent: 'flex-end'
}
}}
></Descriptions>
</Section>
<Section>
<div className="flex-center section-title gap-8">
<span>Latency</span>
</div>
<Descriptions
items={latencyFields}
colon={true}
column={1}
styles={{
content: {
justifyContent: 'flex-end'
}
}}
></Descriptions>
</Section>
</Box>
<PercentileResult />
</Container>
);
};
export default Summary;
@@ -1,9 +1,21 @@
import React from 'react';
import { useDetailContext } from '../../config/detail-context';
import styled from 'styled-components';
import BasicInfo from './basic';
import MetricsInfo from './metrics';
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
`;
const Summary: React.FC = () => {
const { detailData } = useDetailContext();
return <div>Summary Content</div>;
return (
<Container>
<BasicInfo />
<MetricsInfo />
</Container>
);
};
export default Summary;
@@ -0,0 +1,40 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useDetailContext } from '../../config/detail-context';
import useQueryMtrics from '../../services/use-query-metrics';
const fields = [
'requests_per_second_mean',
'request_latency_mean',
'time_per_output_token_mean',
'inter_token_latency_mean',
'time_to_first_token_mean',
'tokens_per_second_mean',
'output_tokens_per_second_mean',
'prompt_tokens_per_second_mean'
];
const Summary: React.FC = () => {
const { detailData, id } = useDetailContext();
const {
detailData: metricsData,
fetchData,
cancelRequest
} = useQueryMtrics();
useEffect(() => {
if (id) {
fetchData({
id: id,
data: {
..._.pick(detailData, fields)
}
});
} else {
cancelRequest();
}
}, [id]);
return <div></div>;
};
export default Summary;
@@ -0,0 +1,119 @@
import { Table } from 'antd';
import React from 'react';
import { useDetailContext } from '../../config/detail-context';
import Section from './section';
// Define table columns: Percentile、 TTFT (ms) 、 ITL (ms) 、TPOT (ms)、Latency (ms)、Input Tokens (t/s)、 Output Tokens (t/s)、 Output (t/s)、Total (t/s)
const columns = [
{
title: 'Percentile',
dataIndex: 'percentile',
key: 'percentile'
},
{
title: 'TTFT (ms)',
dataIndex: 'ttft',
key: 'ttft'
},
{
title: 'ITL (ms)',
dataIndex: 'itl',
key: 'itl'
},
{
title: 'TPOT (ms)',
dataIndex: 'tpot',
key: 'tpot'
},
{
title: 'Latency (ms)',
dataIndex: 'latency',
key: 'latency'
},
{
title: 'Input Tokens (t/s)',
dataIndex: 'inputTokens',
key: 'inputTokens'
},
{
title: 'Output Tokens (t/s)',
dataIndex: 'outputTokens',
key: 'outputTokens'
},
{
title: 'Output (t/s)',
dataIndex: 'output',
key: 'output'
},
{
title: 'Total (t/s)',
dataIndex: 'total',
key: 'total'
}
];
// mock data example: three rows of percentiles: 50th, 90th, 99th
const data = [
{
key: '1',
percentile: '50%',
ttft: 100,
itl: 50,
tpot: 20,
latency: 200,
inputTokens: 300,
outputTokens: 400,
output: 500,
total: 600
},
{
key: '2',
percentile: '90%',
ttft: 150,
itl: 70,
tpot: 30,
latency: 250,
inputTokens: 350,
outputTokens: 450,
output: 550,
total: 650
},
{
key: '3',
percentile: '99%',
ttft: 200,
itl: 90,
tpot: 40,
latency: 300,
inputTokens: 400,
outputTokens: 500,
output: 600,
total: 700
}
];
const PercentileResult: React.FC = () => {
const { detailData, id } = useDetailContext();
return (
<Section title="Percentiles Result">
<Table
size="small"
columns={columns}
dataSource={data}
rowKey="percentile"
pagination={false}
styles={{
body: {
cell: {
height: 54
}
}
}}
></Table>
</Section>
);
};
export default PercentileResult;
@@ -0,0 +1,42 @@
import styled from 'styled-components';
const Container = styled.div`
padding: 16px;
border: 1px solid var(--ant-color-border);
border-radius: 4px;
.section-title {
font-weight: 500;
margin-bottom: 12px;
font-size: 14px;
}
th.ant-descriptions-item {
padding-bottom: 0;
}
`;
const Title = styled.div`
display: flex;
font-weight: 500;
margin-bottom: 12px;
font-size: 14px;
gap: 8px;
align-items: center;
`;
const DetailSection: React.FC<{
children: React.ReactNode;
title?: React.ReactNode;
styles?: {
container?: React.CSSProperties;
title?: React.CSSProperties;
};
}> = ({ children, title, styles }) => {
return (
<Container style={styles?.container}>
{title && <Title style={styles?.title}>{title}</Title>}
{children}
</Container>
);
};
export default DetailSection;