You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

378 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<back-header>
任务管理 > {{ detail.name }}
</back-header>
<block-box>
<div class="task-detail">
<div class="left">
<!-- <el-descriptions column="2" title="详细信息">-->
<!-- <el-descriptions-item-->
<!-- v-for="{ label, value, render } in columns"-->
<!-- :label="label"-->
<!-- >-->
<!-- <component v-if="render" :is="render(detail)" />-->
<!-- <span v-else class="value">{{ detail[value] || '&#45;&#45;' }}</span>-->
<!-- </el-descriptions-item>-->
<!-- </el-descriptions>-->
<div class="title">详细信息</div>
<ul class="node-detail-info">
<li v-for="{ label, value, render } in columns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
<li class="cp">
<span v-for="{ label, count } in cp" :key="label">
<span class="label">{{ label }}</span>
<span class="value">{{ count }} 倍</span>
</span>
</li>
</ul>
</div>
<div class="right">
<div v-for="item in gaugeConfig" :key="item.title">
<Gauge v-bind="item" />
</div>
</div>
</div>
</block-box>
<template v-for="({ title, data }, index) in lineConfig">
<block-box v-if="detail.deviceIds?.length || [2, 3].includes(index)" :key="title + index" :title="title">
<template #extra v-if="detail.type && detail.type.startsWith('NVIDIA')">
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<template v-if="detail.type && !detail.type.startsWith('NVIDIA')">
<el-empty description="该设备厂商暂不支持任务维度监控" :image-size="60" />
</template>
<template v-else>
<echarts-plus :options="getLineOptions({ data })" />
</template>
</div>
</block-box>
</template>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute, useRouter } from 'vue-router';
import { onMounted, ref, watch, watchEffect } from 'vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import cardApi from '~/vgpu/api/card';
import { QuestionFilled } from '@element-plus/icons-vue';
import { roundToDecimal, timeParse, calculateDuration, bytesToGB } from '@/utils';
import taskApi from '~/vgpu/api/task';
import BlockBox from '@/components/BlockBox.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import { getLineOptions } from '~/vgpu/components/config';
import EchartsPlus from '@/components/Echarts-plus.vue';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
const route = useRoute();
const router = useRouter();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const columns = [
{
label: '任务状态',
value: 'status',
render: ({ status }) => {
const enums = {
closed: { text: '已完成', color: '#999' },
success: { text: '运行中', color: '#2563eb' },
unknown: { text: '未知', color: '#FACC15' },
failed: { text: '错误', color: '#EF4444' },
};
const { text, color } = enums[status] || {};
return (
<div
style={{
color,
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
}}
>
<div
style={{
height: '7px',
width: '7px',
borderRadius: '50%',
backgroundColor: color,
display: 'inline-block',
}}
></div>{' '}
{text}
{(status === 'unknown' || status === 'failed') && (
<ElPopover placement="top" trigger="hover" popper-style={{ width: '180px' }}>
{{
reference: () => <el-icon color="#939EA9" size="14"><QuestionFilled /></el-icon>,
default: () => (
<span style={{ marginLeft: '5px', }}>
请跳转云平台查看详情
</span>
),
}}
</ElPopover>
)}
</div>
);
},
},
{
label: '所属显卡',
value: 'deviceIds',
render: ({ deviceIds }) => {
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return <span>--</span>;
}
const text = deviceIds.join(', ');
const maxLength = 25;
const isLongText = text.length > maxLength;
const displayText = isLongText ? `${text.slice(0, maxLength)}...` : text;
return isLongText ? (
<el-tooltip content={text} placement="top">
<span>{displayText}</span>
</el-tooltip>
) : (
<span>{displayText}</span>
);
},
},
{
label: '所属节点',
value: 'nodeName',
render: ({ nodeName }) => <text-plus text={nodeName} copy />,
},
{
label: '显卡类型',
value: 'type',
},
{
label: '分配CPU',
value: 'requestedCpuCores',
render: ({ requestedCpuCores }) => <span>{requestedCpuCores} 核</span>,
},
{
label: '分配内存',
value: 'requestedMemory',
render: ({ requestedMemory }) => <span>{bytesToGB(requestedMemory)} GiB</span>,
},
{
label: '分配算力',
value: 'allocatedCores',
},
{
label: '分配显存',
value: 'allocatedMem',
render: ({ allocatedMem }) =>
allocatedMem ? (
<span>{roundToDecimal(allocatedMem / 1024, 1)} GiB</span>
) : (
<span>--</span>
),
},
{
label: '应用名称',
value: 'appName',
},
{
label: '任务创建时间',
value: 'createTime',
render: ({ createTime }) => <span>{timeParse(createTime)}</span>,
},
// {
// label: '任务运行时长',
// value: 'createTime',
// render: ({ createTime, status }) =>
// status === 'success' ? <span>{calculateDuration(createTime)}</span> : null,
// },
];
const gaugeConfig = useInstantVector(
[
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_container_core_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
totalQuery: `avg(sum(hami_container_vcore_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
percentQuery: `avg(sum(hami_container_core_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance)) / avg(sum(hami_container_vcore_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance)) *100`,
total: 0,
used: 0,
unit: '%',
data: [],
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_container_memory_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/ 1024`,
totalQuery: `avg(sum(hami_container_vmemory_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_container_memory_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"})/ 1024)/(avg(sum(hami_container_vmemory_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/1024) *100)`,
total: 0,
used: 0,
unit: 'GiB',
data: [],
},
],
(query) =>
query
.replaceAll(`$container`, detail.value.name)
.replaceAll(`$namespace`, detail.value.namespace)
.replaceAll(`$pod`, detail.value.appName),
);
const lineConfig = ref([
{
title: '算力使用趋势(%',
query: `avg(sum(hami_container_core_util{container_name=~"$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
data: [],
},
{
title: '显存使用趋势(%',
query: `avg(sum(hami_container_memory_util{container_name=~"$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
data: [],
},
{
title: 'CPU使用趋势%',
query: `100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode="idle", instance=~"$node"}[1m])))`,
data: [],
},
{
title: '内存使用趋势(%',
query: `100 * (1 - node_memory_MemAvailable_bytes{instance=~"$node"} / node_memory_MemTotal_bytes{instance=~"$node"})`,
data: [],
},
]);
const fetchLineData = async () => {
lineConfig.value.map((item, index) =>
cardApi
.getRangeVector({
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
query: item.query
.replaceAll(`$container`, detail.value.name)
.replaceAll(`$namespace`, detail.value.namespace)
.replaceAll(`$pod`, detail.value.appName)
.replaceAll(`$node`, detail.value.nodeName),
})
.then((res) => {
lineConfig.value[index].data = res.data[0]?.values || [];
}),
);
};
watch(detail, async () => {
fetchLineData();
});
watch(times, () => {
fetchLineData();
});
onMounted(async () => {
const { name, podUid } = route.query;
detail.value = await taskApi.getTaskDetail({ name, podUid });
const cards = await cardApi.getCardListReq({ filters: {} });
const foundCard = cards.list.find((item) => item.uuid === detail.value.deviceIds[0]);
if (foundCard) {
detail.value.type = foundCard.type;
}
// if (!detail.value.deviceIds?.length) {
// lineConfig.value.splice(0, 2);
// }
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
// const lineReqs = gaugeConfig.value.map((item) =>
// cardApi.getRangeVector({
// range: {
// start: timeParse(start),
// end: timeParse(new Date()),
// step: '1m',
// },
// query: item.query
// .replaceAll(`$container`, detail.value.name)
// .replaceAll(`$namespace`, detail.value.namespace)
// .replaceAll(`$pod`, detail.value.appName),
// }),
// );
//
// const res = await Promise.all(lineReqs);
//
// gaugeConfig.value = gaugeConfig.value.map((item, index) => ({
// ...item,
// data: res[index].data[0]?.values || [],
// }));
});
</script>
<style lang="scss">
.task-detail {
display: grid;
grid-template-columns: repeat(2, 1fr);
.right {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
.node-detail-info {
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: 1fr 1fr;
.label {
display: inline-block;
width: 80px;
height: 20px;
color: #939ea9;
}
.cp {
display: flex;
gap: 25px;
}
}
}
</style>