refactor: summary

This commit is contained in:
jialin
2026-01-30 18:48:55 +08:00
parent 22e42e279c
commit 577996f507
14 changed files with 344 additions and 173 deletions
@@ -0,0 +1,57 @@
import React, { useEffect, useRef, useState } from 'react';
interface CollapseProps {
open: boolean;
children: React.ReactNode;
duration?: number;
minHeight?: number;
}
export default function Collapse({
open,
children,
minHeight = 0,
duration = 200
}: CollapseProps) {
const ref = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number | 'auto'>(minHeight);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (open) {
const h = el.scrollHeight;
setHeight(h);
const timer = setTimeout(() => {
setHeight('auto');
}, duration);
return () => clearTimeout(timer);
} else {
const h = el.scrollHeight;
setHeight(h);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setHeight(0);
});
});
}
return undefined;
}, [open, duration]);
return (
<div
ref={ref}
style={{
height,
overflow: 'hidden',
transition: `height ${duration}ms ease`
}}
>
{children}
</div>
);
}
@@ -1,23 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { useDetailContext } from '../../config/detail-context';
import Benchmark from './benchmark';
import Instance from './instance';
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const Configure: React.FC = () => {
const { detailData } = useDetailContext();
return (
<Container>
<Instance />
<Benchmark />
</Container>
);
};
export default Configure;
@@ -6,7 +6,6 @@ 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 Summary from './summary';
@@ -31,12 +30,6 @@ const Details: React.FC<{ currentData?: BenchmarkListItem }> = ({
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' }),
@@ -6,7 +6,6 @@ import _, { round } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { useDetailContext } from '../../config/detail-context';
import PercentileResult from './percentile-result';
import Section from './section';
const Container = styled.div`
@@ -239,7 +238,6 @@ const Summary: React.FC = () => {
></Descriptions>
</Section>
</Box>
<PercentileResult />
</Container>
);
};
@@ -1,7 +1,7 @@
import { Descriptions, DescriptionsProps } from 'antd';
import React from 'react';
import { useDetailContext } from '../../config/detail-context';
import Section from '../summary/section';
import Section from './section';
const Benchmark: React.FC = () => {
const { detailData } = useDetailContext();
@@ -19,7 +19,7 @@ const Benchmark: React.FC = () => {
},
{
key: '3',
label: 'Token Length (In/Out)',
label: 'Token Length (Prompt/Out)',
children: (
<span>
{detailData?.dataset_prompt_tokens || '-'} /{' '}
@@ -1,19 +1,23 @@
import React from 'react';
import styled from 'styled-components';
import BasicInfo from './basic';
import MetricsInfo from './metrics';
import BenchMark from './benchmark';
import Instance from './instance';
import MetricsResult from './metrics-result';
import PercentileResult from './percentile-result';
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
gap: 16px;
`;
const Summary: React.FC = () => {
return (
<Container>
<BasicInfo />
<MetricsInfo />
<Instance />
<BenchMark />
<MetricsResult />
<PercentileResult />
</Container>
);
};
@@ -1,11 +1,8 @@
import StatusTag from '@/components/status-tag';
import { InstanceStatusMapValue, status } from '@/pages/llmodels/config';
import { convertFileSize } from '@/utils';
import { Descriptions, Flex, Tag } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
import { useDetailContext } from '../../config/detail-context';
import Section from '../summary/section';
import Section from './section';
const calcTotalVram = (vram: Record<string, number>) => {
return _.sum(_.values(vram));
@@ -17,21 +14,16 @@ const Instance: React.FC = () => {
const items = useMemo(() => {
const { snapshot } = detailData;
const [instanceName, instanceData] =
Object.entries(snapshot.instances || {})[0] || [];
Object.entries(snapshot?.instances || {})[0] || [];
return [
{
key: '1',
label: 'Instance Name',
children: (
<div className="flex-center gap-8">
<span>{instanceName}</span>
<StatusTag
statusValue={{
status: status[instanceData.state],
text: InstanceStatusMapValue[instanceData.state],
message: detailData.state_message || undefined
}}
/>
<span>
{detailData?.model_name}/{detailData?.model_instance_name}
</span>
</div>
)
},
@@ -39,42 +31,18 @@ const Instance: React.FC = () => {
key: '2',
label: 'Worker',
children: instanceData?.worker_name || '-'
// children:
// instanceData?.ports?.length > 0
// ? `${instanceData.worker_ip}:${instanceData.ports[0]}`
// : instanceData.worker_ip || '-'
},
{
key: '3',
label: 'GPU Indexes',
children:
_.join(
instanceData.gpu_indexes?.sort?.((a, b) => a - b),
','
) || '-'
},
{
key: '6',
label: 'GPU Type',
children: instanceData?.gpu_type || '-'
},
{
key: '4',
label: 'Allocated VRAM',
children:
convertFileSize(
instanceData.computed_resource_claim?.vram
? calcTotalVram(instanceData.computed_resource_claim?.vram)
: 0,
1
) || '-'
},
{
key: '5',
label: 'Backend',
children: `${instanceData?.backend || '-'} ${
instanceData.backend_version
? `(${instanceData.backend_version})`
instanceData?.backend_version
? `(${instanceData?.backend_version})`
: ''
}`
}
@@ -84,14 +52,14 @@ const Instance: React.FC = () => {
const paramsItems = useMemo(() => {
const { snapshot } = detailData;
const [instanceName, instanceData] =
Object.entries(snapshot.instances || {})[0] || [];
Object.entries(snapshot?.instances || {})[0] || [];
return [
{
key: '1',
label: 'Backend Parameters',
children: (
<Flex gap={8} wrap="wrap">
{instanceData?.backend_parameters.map(
{instanceData?.backend_parameters?.map(
(param: string, index: number) => (
<Tag key={index} style={{ margin: 0 }}>
{param}
@@ -180,7 +148,7 @@ const Instance: React.FC = () => {
<Descriptions
items={items}
colon={false}
column={3}
column={4}
layout="vertical"
styles={{
content: {
@@ -0,0 +1,180 @@
import { Descriptions } from 'antd';
import _, { round } from 'lodash';
import React from 'react';
import { useDetailContext } from '../../config/detail-context';
import Section from './section';
const columns = [
{
title: 'Duration (s)',
dataIndex: 'duration',
path: ['raw_metrics', 'benchmarks', '0', 'duration'],
unit: 's',
render: (value: number) => round(value, 2)
},
{
title: 'Concurrency',
dataIndex: 'request_concurrency',
path: ['raw_metrics', 'benchmarks', '0'],
unit: '',
render: (value: number) => {
return round(
_.get(value, 'metrics.request_concurrency.successful.mean'),
0
);
}
},
{
title: 'Total Requests',
dataIndex: 'total_requests',
path: 'total_requests',
unit: '',
render: (value: number) => round(value, 0)
},
{
title: 'Success Requests',
dataIndex: 'successful_requests',
path: ['raw_metrics', 'benchmarks', '0'],
unit: '',
render: (value: number) => {
return (
<span style={{ color: 'var(--ant-color-success)' }}>
{_.get(value, 'metrics.request_totals.successful')}
</span>
);
}
},
{
title: 'Failed Requests',
dataIndex: 'failed_requests',
path: ['raw_metrics', 'benchmarks', '0'],
unit: '',
render: (value: number) => {
return (
<span style={{ color: 'var(--ant-color-error)' }}>
{_.get(value, 'metrics.request_totals.errored')}
</span>
);
}
},
{
title: 'Output token throughput (t/s)',
dataIndex: 'output_tokens_per_second_mean',
path: 'output_tokens_per_second_mean',
unit: 't/s',
render: (value: number) => round(value, 2)
}
];
const columnsSub = [
{
title: 'Total token throughput (t/s)',
dataIndex: 'tokens_per_second_mean',
path: 'tokens_per_second_mean',
unit: 't/s',
render: (value: number) => round(value, 2)
},
{
title: 'Request token throughput (t/s)',
dataIndex: 'prompt_tokens_per_second_mean',
path: 'prompt_tokens_per_second_mean',
unit: 't/s',
render: (value: number) => round(value, 2)
},
{
title: 'Average Request Latency (ms)',
dataIndex: 'request_latency_mean',
path: 'request_latency_mean',
unit: 'ms',
render: (value: number) => round(value, 2)
},
{
title: 'Average Time To First Token (ms)',
dataIndex: 'time_to_first_token_mean',
path: 'time_to_first_token_mean',
unit: 'ms',
render: (value: number) => round(value, 2)
},
{
title: 'Average Time Per Output Token (ms)',
dataIndex: 'time_per_output_token_mean',
path: 'time_per_output_token_mean',
unit: 'ms',
render: (value: number) => round(value, 2)
},
{
title: '',
dataIndex: '',
path: '',
unit: '',
render: (_: number) => ''
}
];
const PERCENTILES = [
{ key: 'metrics', label: 'Metrics', title: 'N/A' }
] as const;
const PercentileResult: React.FC = () => {
const { detailData } = useDetailContext();
const metrics = detailData?.raw_metrics?.benchmarks?.[0]?.metrics || {};
const buildPercentileTable = (metrics: any) => {
return PERCENTILES.map(({ key, title }) => {
const row: any = { metrics: title };
[...columns, ...columnsSub].forEach(({ dataIndex, path }) => {
row[dataIndex] = _.get(detailData, path) ?? 0;
});
return row;
});
};
const items = [...columns, ...columnsSub].map(
({ title, dataIndex, path, render, unit }) => ({
key: dataIndex,
label: title,
children: unit
? render(_.get(detailData, path) ?? 0) + ` (${unit})`
: render(_.get(detailData, path) ?? 0)
})
);
return (
<Section title="Metrics Result" minHeight={200}>
{/* <Table
size="small"
columns={[
{
title: 'Metrics',
dataIndex: 'metrics'
},
...columns
]}
dataSource={buildPercentileTable(metrics)}
rowKey="percentile"
pagination={false}
styles={{
body: {
cell: {
height: 54
}
}
}}
></Table> */}
<Descriptions
items={items}
colon={false}
column={2}
styles={{
content: {
justifyContent: 'flex-end'
}
}}
></Descriptions>
</Section>
);
};
export default PercentileResult;
@@ -1,20 +0,0 @@
import React from 'react';
import { useDetailContext } from '../../config/detail-context';
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 } = useDetailContext();
return <div></div>;
};
export default Summary;
@@ -1,107 +1,92 @@
import { Table } from 'antd';
import { round } from 'lodash';
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'
dataIndex: 'time_to_first_token_ms',
render: (value: number) => round(value, 2)
},
{
title: 'ITL (ms)',
dataIndex: 'itl',
key: 'itl'
dataIndex: 'inter_token_latency_ms',
render: (value: number) => round(value, 2)
},
{
title: 'TPOT (ms)',
dataIndex: 'tpot',
key: 'tpot'
dataIndex: 'time_per_output_token_ms',
render: (value: number) => round(value, 2)
},
{
title: 'Latency (ms)',
dataIndex: 'latency',
key: 'latency'
title: 'Latency (s)',
dataIndex: 'request_latency',
render: (value: number) => round(value, 2)
},
{
title: 'Input Tokens (t/s)',
dataIndex: 'inputTokens',
key: 'inputTokens'
title: 'Input tokens',
dataIndex: 'prompt_token_count',
render: (value: number) => round(value, 0)
},
{
title: 'Output Tokens (t/s)',
dataIndex: 'outputTokens',
key: 'outputTokens'
title: 'Output tokens',
dataIndex: 'output_token_count',
render: (value: number) => round(value, 0)
},
{
title: 'Input (t/s)',
dataIndex: 'prompt_tokens_per_second',
render: (value: number) => round(value, 2)
},
{
title: 'Output (t/s)',
dataIndex: 'output',
key: 'output'
dataIndex: 'output_tokens_per_second',
render: (value: number) => round(value, 2)
},
{
title: 'Total (t/s)',
dataIndex: 'total',
key: 'total'
dataIndex: 'tokens_per_second',
render: (value: number) => round(value, 2)
}
];
// 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 PERCENTILES = [
{ key: 'p50', label: '50%' },
{ key: 'p90', label: '90%' },
{ key: 'p99', label: '99%' }
] as const;
const PercentileResult: React.FC = () => {
const { detailData, id } = useDetailContext();
const { detailData } = useDetailContext();
const metrics = detailData?.raw_metrics?.benchmarks?.[0]?.metrics || {};
const buildPercentileTable = (metrics: any) => {
return PERCENTILES.map(({ key, label }) => {
const row: any = { percentile: label };
columns.forEach(({ dataIndex }) => {
row[dataIndex] =
metrics?.[dataIndex]?.successful?.percentiles?.[key] ?? 0;
});
return row;
});
};
return (
<Section title="Percentiles Result">
<Table
size="small"
columns={columns}
dataSource={data}
columns={[
{
title: 'Percentile',
dataIndex: 'percentile'
},
...columns
]}
dataSource={buildPercentileTable(metrics)}
rowKey="percentile"
pagination={false}
styles={{
@@ -1,3 +1,6 @@
import HeadlessCollapse from '@/components/collapse-container/headless-collapse';
import IconFont from '@/components/icon-font';
import { useState } from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
@@ -21,9 +24,10 @@ const Container = styled.div`
`;
const Title = styled.div`
justify-content: space-between;
display: flex;
cursor: pointer;
font-weight: 500;
// margin-bottom: 12px;
font-size: 14px;
gap: 8px;
padding: 12px 16px;
@@ -35,16 +39,31 @@ const Title = styled.div`
const DetailSection: React.FC<{
children: React.ReactNode;
title?: React.ReactNode;
minHeight?: number;
styles?: {
wraper?: React.CSSProperties;
container?: React.CSSProperties;
title?: React.CSSProperties;
};
}> = ({ children, title, styles }) => {
}> = ({ children, title, styles, minHeight = 100 }) => {
const [open, setOpen] = useState(true);
return (
<Wrapper style={styles?.wraper}>
{title && <Title style={styles?.title}>{title}</Title>}
<Container style={styles?.container}>{children}</Container>
{title && (
<Title style={styles?.title} onClick={() => setOpen(!open)}>
{title}
<IconFont
type={'icon-down'}
rotate={open ? 0 : -90}
style={{
transition: 'transform 0.2s'
}}
></IconFont>
</Title>
)}
<HeadlessCollapse open={open} minHeight={minHeight}>
<Container style={styles?.container}>{children}</Container>
</HeadlessCollapse>
</Wrapper>
);
};
@@ -67,6 +67,12 @@ export interface GPUData {
export interface BenchmarkDetail {
profile: string;
seed: number;
raw_metrics: {
benchmarks: Array<{
metrics: Record<string, any>;
}>;
[key: string]: any;
};
requests_per_second_mean: number;
request_latency_mean: number;
time_per_output_token_mean: number;
@@ -132,6 +132,7 @@ const ModelInstanceForm: React.FC = () => {
>
<SealCascader
required
showSearch
loading={modelLoading || instanceLoading}
changeOnSelect={false}
expandTrigger="hover"
+3
View File
@@ -13,6 +13,7 @@ import { useEffect } from 'react';
import NoResult from '../_components/no-result';
import PageBox from '../_components/page-box';
import {
BENCHMARKS_API,
createBenchmark,
deleteBenchmark,
queryBenchmarkList,
@@ -48,6 +49,8 @@ const Benchmark: React.FC = () => {
} = useTableFetch<ListItem>({
fetchAPI: queryBenchmarkList,
deleteAPI: deleteBenchmark,
API: BENCHMARKS_API,
watch: true,
contentForDelete: 'menu.models.benchmark'
});
const intl = useIntl();