提交 4975cb72 编写于 作者: M mindspore-ci-bot 提交者: Gitee

!259 UI supprot profiling feature (1st commit)

Merge pull request !259 from 潘慧/master_ph
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="96px" viewBox="0 0 32 96" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<title>编组 12</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-166.7%" y="-25.0%" width="433.3%" height="150.0%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<polygon id="path-2" points="0 0 8 0 4 5"></polygon>
</defs>
<g id="智能小助手" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="3" transform="translate(-461.000000, -525.000000)">
<g id="编组-12" filter="url(#filter-1)" transform="translate(471.000000, 533.000000)">
<path d="M-1.50990331e-14,0 L9.11107923,10.6295924 C10.9752941,12.8045097 12,15.5745537 12,18.4390889 L12,61.5609111 C12,64.4254463 10.9752941,67.1954903 9.11107923,69.3704076 L-1.50990331e-14,80 L-1.50990331e-14,80 L-1.50990331e-14,0 Z" id="矩形" fill="#FFFFFF"></path>
<g id="icon-下展" transform="translate(6.000000, 40.500000) rotate(90.000000) translate(-6.000000, -40.500000) translate(2.000000, 38.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<use id="蒙版" fill="#575D6C" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="96px" viewBox="0 0 32 96" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<title>编组 12</title>
<desc>Created with Sketch.</desc>
<defs>
<filter x="-166.7%" y="-25.0%" width="433.3%" height="150.0%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<polygon id="path-2" points="0 0 8 0 4 5"></polygon>
</defs>
<g id="智能小助手" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="3" transform="translate(-461.000000, -525.000000)">
<g id="编组-12" filter="url(#filter-1)" transform="translate(471.000000, 533.000000)">
<path d="M-1.50990331e-14,0 L9.11107923,10.6295924 C10.9752941,12.8045097 12,15.5745537 12,18.4390889 L12,61.5609111 C12,64.4254463 10.9752941,67.1954903 9.11107923,69.3704076 L-1.50990331e-14,80 L-1.50990331e-14,80 L-1.50990331e-14,0 Z" id="矩形" fill="#FFFFFF"></path>
<g id="icon-下展" transform="translate(6.000000, 40.500000) rotate(-90.000000) translate(-6.000000, -40.500000) translate(2.000000, 38.000000)">
<mask id="mask-3" fill="white">
<use xlink:href="#path-2"></use>
</mask>
<use id="蒙版" fill="#575D6C" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<template>
<div class="operator">
<div class="operator-title">{{$t('profiling.operatorDetail')}}</div>
<div class="cl-profiler">
<el-tabs v-model="apiType"
@tab-click="tabChange">
<el-tab-pane label="AI CORE"
name="core">
<div class="cl-profiler-top"
v-if="coreCharts.data.length">
<div>
<span class="profiler-title">
{{$t('operator.operatorTypeStatistics')}}
</span>
<el-radio-group class="chart-radio-group"
v-model="coreCharts.type"
@change="coreChartChange"
fill="#00A5A7"
text-color="#FFFFFF"
size="small">
<el-radio-button :label="0">
{{$t('operator.pie')}}
</el-radio-button>
<el-radio-button :label="1">
{{ $t('operator.bar')}}
</el-radio-button>
</el-radio-group>
</div>
<div class="cl-profiler-echarts">
<div id="core-echarts"></div>
</div>
</div>
<div class="cl-profiler-bottom"
v-if="coreCharts.data.length">
<span class="profiler-title">
{{ $t('operator.operatorStatistics') }}
</span>
<div>
<el-radio-group v-model="statisticType"
@change="coreTableChange"
fill="#00A5A7"
text-color="#FFFFFF"
size="small">
<el-radio-button :label="1">
{{$t('operator.allOperator')}}
</el-radio-button>
<el-radio-button :label="0">
{{$t('operator.ClassificationOperator')}}
</el-radio-button>
</el-radio-group>
<div class="cl-search-box">
<el-input v-model="searchByTypeInput"
v-if="!statisticType"
:placeholder="$t('operator.searchByType')"
clearable
@clear="searchOpCoreList()"
@keyup.enter.native="searchOpCoreList()"></el-input>
<el-input v-model="searchByNameInput"
v-if="statisticType"
:placeholder="$t('operator.searchByName')"
clearable
@clear="searchOpCoreList()"
@keyup.enter.native="searchOpCoreList()"></el-input>
</div>
</div>
<el-table v-show="!statisticType && opTypeCol && opTypeCol.length"
:data="opTypeList"
@expand-change="expandTypeItem"
stripe
height="calc(100% - 75px)"
width="100%">
<el-table-column type="expand">
<template slot-scope="props">
<div class="expand-table">
<el-table :data="props.row.opDetailList"
stripe
width="100%"
tooltip-effect="light"
@cell-click="showInfoDetail"
@sort-change="(...args)=>{coreDetailSortChange(props.row, ...args)}">
<el-table-column v-for="(ele, key) in props.row.opDetailCol"
:property="ele"
:key="key"
:sortable="ele === 'op_info' ? false : 'custom'"
:width="(ele==='execution_time'|| ele==='subgraph' ||
ele==='op_name'|| ele==='op_type')?'220':''"
show-overflow-tooltip
:label="ele">
</el-table-column>
</el-table>
<el-pagination :current-page="props.row.opDetailPage.offset + 1"
:page-size="props.row.opDetailPage.limit"
@current-change="(...args)=>{opDetailPageChange(props.row, ...args)}"
layout="total, prev, pager, next, jumper"
:total="props.row.pageTotal">
</el-pagination>
<div class="clear"></div>
</div>
</template>
</el-table-column>
<el-table-column v-for="(item, $index) in opTypeCol"
:property="item"
:key="$index"
sortable
:label="item">
</el-table-column>
</el-table>
<el-table v-show="statisticType && opAllTypeList.opDetailCol && opAllTypeList.opDetailCol.length"
:data="opAllTypeList.opDetailList"
stripe
width="100%"
height="calc(100% - 114px)"
@cell-click="showInfoDetail"
@sort-change="(...args)=>{coreDetailSortChange(opAllTypeList, ...args)}"
tooltip-effect="light">
<el-table-column v-for="(item, $index) in opAllTypeList.opDetailCol"
:property="item"
:key="$index"
:label="item"
:sortable="item === 'op_info' ? false : 'custom'"
:width="(item==='execution_time'|| item==='subgraph' ||
item==='op_name'|| item==='op_type')?'220':''"
show-overflow-tooltip>
</el-table-column>
</el-table>
<el-pagination v-show="statisticType"
v-if="opAllTypeList.opDetailList.length"
:current-page="opAllTypeList.opDetailPage.offset + 1"
:page-size="opAllTypeList.opDetailPage.limit"
@current-change="(...args)=>{opDetailPageChange(opAllTypeList, ...args)}"
layout="total, prev, pager, next, jumper"
:total="opAllTypeList.pageTotal">
</el-pagination>
</div>
<div class="image-noData"
v-if="initOver && coreCharts.data.length === 0">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{ $t("public.noData") }}</p>
</div>
</el-tab-pane>
<el-tab-pane label="AI CPU"
class="cpu-tab"
name="cpu"
v-if="false">
<div class="cl-profiler-top"
v-if="cpuCharts.data.length">
<div>
<span class="profiler-title">
{{ $t('operator.operatorTypeStatistics') }}
</span>
</div>
<div class="cl-profiler-echarts">
<div class
id="cpu-echarts"></div>
</div>
</div>
<div class="cl-profiler-bottom"
v-if="cpuCharts.data.length">
<span class="profiler-title">
{{ $t('operator.operatorStatistics') }}
</span>
<div class="cl-search-box">
<el-input v-model="searchByCPUNameInput"
:placeholder="$t('operator.searchByName')"
clearable
@clear="searchOpCpuList()"
@keyup.enter.native="searchOpCpuList()"></el-input>
</div>
<el-table v-show="opCpuList.opDetailCol && opCpuList.opDetailCol.length"
:data="opCpuList.opDetailList"
stripe
width="100%"
height="calc(100% - 82px)"
tooltip-effect="light"
@sort-change="(...args)=>{cpuDetailSortChange(opCpuList, ...args)}">
<el-table-column v-for="(item, $index) in opCpuList.opDetailCol"
:property="item"
:key="$index"
:label="item"
sortable="custom"
show-overflow-tooltip>
</el-table-column>
</el-table>
<el-pagination v-if="opCpuList.opDetailList.length"
:current-page="opCpuList.opDetailPage.offset + 1"
:page-size="opCpuList.opDetailPage.limit"
@current-change="(...args)=>{opCpuPageChange(opCpuList, ...args)}"
layout="total, prev, pager, next, jumper"
:total="opCpuList.pageTotal">
</el-pagination>
</div>
<div class="image-noData"
v-if="initOver && cpuCharts.data.length === 0">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
</el-tab-pane>
</el-tabs>
<el-dialog :title="rowName"
:visible.sync="detailsDialogVisible"
width="50%"
:close-on-click-modal="false"
class="details-data-list">
<el-table :data="detailsDataList"
row-key="id"
lazy
tooltip-effect="light"
:load="loadDataListChildren"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
<el-table-column width="50" />
<el-table-column prop="key"
width="180"
label="Key"> </el-table-column>
<el-table-column prop="value"
show-overflow-tooltip
label="Value">
<template slot-scope="scope">
{{ scope.row.value }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</div>
</template>
<script>
import echarts from 'echarts';
import requestService from '../../services/request-service';
import CommonProperty from '../../common/common-property';
export default {
data() {
return {
apiType: 'core',
currentCard: '',
cpuCharts: {
type: 1,
id: 'cpu-echarts',
chartDom: null,
data: [],
}, // ai cpu chart
coreCharts: {
type: 0,
id: 'core-echarts',
chartDom: null,
data: [],
}, // ai core chart
statisticType: 0, // ai core table statistic type
searchByTypeInput: '', // search by ai core type name
searchByNameInput: '', // search by ai core detail name
searchByCPUNameInput: '', // search by ai cpu name
opTypeCol: [], // table headers list of operator type
opTypeList: [], // table list of operator type
opCpuList: {
opDetailCol: [],
opDetailList: [],
pageTotal: 0,
opDetailPage: {
offset: 0,
limit: 20,
},
op_filter_condition: {},
op_sort_condition: {
name: 'total_time',
type: 'descending',
},
}, // table data of operator cpu
opAllTypeList: {
opDetailCol: [],
opDetailList: [],
pageTotal: 0,
opDetailPage: {
offset: 0,
limit: 8,
},
op_filter_condition: {},
op_sort_condition: {},
}, // table data of all operator details
rowName: this.$t('dataTraceback.details'), // dialog title
detailsDataList: [], // dialog table data
detailsDialogVisible: false, // show dialog
profile_dir: '', // profile directory
train_id: '', // train id
op_filter_condition: {}, // operator type filter
op_sort_condition: {
name: 'execution_time',
type: 'descending',
}, // operator type filter
initOver: false,
objectType: 'object',
};
},
watch: {
'$parent.curDashboardInfo': {
handler(newValue, oldValue) {
if (newValue.query.dir && newValue.query.id && newValue.curCardNum) {
this.profile_dir = newValue.query.dir;
this.train_id = newValue.query.id;
this.currentCard = newValue.curCardNum;
this.init();
}
},
deep: true,
immediate: true,
},
},
destroyed() {
// Remove the listener of window size change
window.removeEventListener('resize', this.resizeCallback);
},
methods: {
init() {
this.getCoreTypeList();
},
/**
* Current device change
*/
cardChange() {
if (this.apiType === 'core') {
this.statisticType = 0;
this.clearCoreData();
this.getCoreTypeList();
} else if (this.apiType === 'cpu') {
this.clearCpuData();
this.getCpuList(true);
}
},
/**
* clear cpu data
*/
clearCpuData() {
this.searchByCPUNameInput = '';
this.opCpuList = {
opDetailCol: [],
opDetailList: [],
pageTotal: 0,
opDetailPage: {
offset: 0,
limit: 20,
},
op_filter_condition: {},
op_sort_condition: {
name: 'total_time',
type: 'descending',
},
};
},
/**
* clear core data
*/
clearCoreData() {
this.searchByTypeInput = '';
this.searchByNameInput = '';
this.op_filter_condition = {};
this.opTypeCol = [];
this.opTypeList = [];
this.opAllTypeList = {
opDetailCol: [],
opDetailList: [],
pageTotal: 0,
opDetailPage: {
offset: 0,
limit: 8,
},
op_filter_condition: {},
op_sort_condition: {},
};
},
/**
* get core list
*/
getCoreTypeList() {
const params = {};
params.params = {
profile: this.profile_dir,
train_id: this.train_id,
};
params.body = {
op_type: 'aicore_type',
device_id: this.currentCard,
filter_condition: this.op_filter_condition,
sort_condition: this.op_sort_condition,
};
requestService
.getProfilerOpData(params)
.then((res) => {
this.initOver = true;
this.opTypeList = [];
if (res && res.data) {
this.opTypeCol = res.data.col_name;
if (res.data.object) {
res.data.object.forEach((k) => {
const object = {
isExpanded: false,
opDetailList: [],
opDetailCol: [],
opDetailPage: {
offset: 0,
limit: 8,
},
pageTotal: 0,
op_filter_condition: {
op_type: {
in: [k[0]],
},
},
op_sort_condition: {},
};
res.data.col_name.forEach((item, index) => {
object[item] = k[index];
});
this.opTypeList.push(object);
});
if (
!this.coreCharts.device_id ||
this.coreCharts.device_id !== this.currentCard
) {
this.coreCharts.device_id = this.currentCard;
this.coreCharts.data = [];
res.data.object.forEach((k) => {
if (
this.coreCharts.data &&
this.coreCharts.data.length < 19
) {
this.coreCharts.data.push({
name: k[0],
value: k[1],
percent: k[3],
});
} else {
if (!this.coreCharts.data[19]) {
this.coreCharts.data[19] = {
name: 'Other',
value: 0,
percent: 0,
};
}
this.coreCharts.data[19].value += k[1];
this.coreCharts.data[19].percent += k[3];
}
});
this.setOption(this.coreCharts);
}
}
}
})
.catch(() => {
this.opTypeList = [];
this.initOver = true;
});
},
/**
* get core detail list
* @param {Object} row type row
*/
getCoreDetailList(row) {
const params = {};
params.params = {
profile: this.profile_dir,
train_id: this.train_id,
};
params.body = {
op_type: 'aicore_detail',
device_id: this.currentCard,
filter_condition: row.op_filter_condition,
sort_condition: row.op_sort_condition,
group_condition: row.opDetailPage,
};
requestService
.getProfilerOpData(params)
.then((res) => {
if (res && res.data) {
this.formatterDetailData(row, res.data);
}
})
.catch(() => {});
},
/**
* get cpu list
*/
getCpuList() {
const params = {};
params.params = {
profile: this.profile_dir,
train_id: this.train_id,
};
params.body = {
op_type: 'aicpu',
device_id: this.currentCard,
filter_condition: this.opCpuList.op_filter_condition,
sort_condition: this.opCpuList.op_sort_condition,
group_condition: this.opCpuList.opDetailPage,
};
requestService
.getProfilerOpData(params)
.then((res) => {
this.initOver = true;
if (res && res.data) {
if (res.data.object) {
if (
!this.cpuCharts.device_id ||
this.cpuCharts.device_id !== this.currentCard
) {
this.cpuCharts.device_id = this.currentCard;
this.cpuCharts.data = [];
res.data.object.forEach((k) => {
this.cpuCharts.data.push({
name: k[0],
op_name: k[1],
value: k[2],
});
});
this.setOption(this.cpuCharts);
}
if (res.data.object.length > 8) {
this.opCpuList.opDetailPage.limit = 8;
res.data.object.splice(8);
}
this.formatterDetailData(this.opCpuList, res.data);
}
}
})
.catch(() => {
this.initOver = true;
});
},
/**
* operator detail list page change
* @param {Object} row table cell
* @param {Number} pageIndex current page
*/
opDetailPageChange(row, pageIndex) {
row.opDetailPage.offset = pageIndex - 1;
this.getCoreDetailList(row);
},
/**
* cpu list page change
* @param {Object} row table cell
* @param {Number} pageIndex current page
*/
opCpuPageChange(row, pageIndex) {
row.opDetailPage.offset = pageIndex - 1;
this.getCpuList();
},
/**
* get core list by search
*/
searchOpCoreList() {
if (this.statisticType) {
this.opAllTypeList.op_filter_condition = {};
if (this.searchByNameInput) {
this.opAllTypeList.op_filter_condition = {
op_name: {partial_match_str_in: [this.searchByNameInput]},
};
} else {
this.opAllTypeList.op_filter_condition = {};
}
this.getCoreDetailList(this.opAllTypeList);
} else {
this.op_filter_condition = {};
if (this.searchByTypeInput) {
this.op_filter_condition = {
op_type: {partial_match_str_in: [this.searchByTypeInput]},
};
} else {
this.op_filter_condition = {};
}
this.getCoreTypeList();
}
},
/**
* get cpu list by search
*/
searchOpCpuList() {
this.opCpuList.op_filter_condition = {};
if (this.searchByCPUNameInput) {
this.opCpuList.op_filter_condition = {
op_name: {partial_match_str_in: [this.searchByCPUNameInput]},
};
} else {
this.opCpuList.op_filter_condition = {};
}
this.getCpuList();
},
/**
* core detail sort
* @param {Object} row table cell
* @param {Object} column table cell
*/
coreDetailSortChange(row, column) {
row.op_sort_condition = {
name: column.prop,
type: column.order,
};
row.opDetailPage.offset = 0;
this.getCoreDetailList(row);
},
/**
* cpu detail sort
* @param {Object} row table cell
* @param {Object} column table cell
*/
cpuDetailSortChange(row, column) {
row.op_sort_condition = {
name: column.prop,
type: column.order,
};
row.opDetailPage.offset = 0;
this.getCpuList();
},
/**
* format detail data
* @param {Object} row table cell
* @param {Object} detailsDataList table detail
*/
formatterDetailData(row, detailsDataList) {
row.opDetailList = [];
row.opDetailCol = detailsDataList.col_name;
row.pageTotal = detailsDataList.size;
if (detailsDataList.object) {
detailsDataList.object.forEach((k) => {
const data = {};
detailsDataList.col_name.forEach((item, index) => {
if (item === 'op_info') {
data[item] = JSON.stringify(k[index]);
} else {
data[item] = k[index];
}
});
row.opDetailList.push(data);
});
}
},
/**
* expand core type table
* @param {Object} row table cell
*/
expandTypeItem(row) {
row.isExpanded = !row.isExpanded;
if (row.isExpanded) {
row.opDetailList = [];
row.opDetailCol = [];
row.opDetailPage.offset = 0;
row.pageTotal = 0;
row.op_sort_condition = {name: 'execution_time', type: 'descending'};
this.getCoreDetailList(row);
}
},
/**
* tab change
*/
tabChange() {
if (
this.apiType === 'cpu' &&
this.cpuCharts.device_id !== this.currentCard
) {
this.initOver = false;
this.clearCpuData();
this.getCpuList();
} else if (
this.apiType === 'core' &&
this.coreCharts.device_id !== this.currentCard
) {
this.initOver = false;
this.clearCoreData();
this.getCoreTypeList();
}
this.$nextTick(() => {
this.resizeCallback();
});
},
/**
* core table type change
*/
coreTableChange() {
if (this.statisticType && !this.opAllTypeList.opDetailCol.length) {
this.opAllTypeList.op_sort_condition = {
name: 'execution_time',
type: 'descending',
};
this.getCoreDetailList(this.opAllTypeList);
}
},
/**
* operator cpu chart change
*/
cpuChartChange() {
this.setOption(this.cpuCharts);
},
/**
* operator core chart change
*/
coreChartChange() {
this.setOption(this.coreCharts);
},
/**
* set chart option
* @param {Object} chart chart
*/
setOption(chart) {
const option = {};
if (!chart.type) {
option.legend = {
data: [],
orient: 'vertical',
icon: 'circle',
formatter: (params) => {
let legendStr = '';
for (let i = 0; i < chart.data.length; i++) {
if (chart.data[i].name === params) {
const name =
chart.data[i].name.length > 10
? `${chart.data[i].name.slice(0, 10)}...`
: chart.data[i].name;
legendStr = `{a|${i + 1}}{b|${name} ${chart.data[
i
].value.toFixed(3)}}\n{c|${chart.data[i].percent.toFixed(2)}%}`;
}
}
return legendStr;
},
itemWidth: 18,
itemHeight: 18,
padding: [0, 50, 0, 0],
top: '5%',
left: '37%',
textStyle: {
padding: [15, 0, 0, 0],
rich: {
a: {
width: 24,
align: 'center',
padding: [0, 10, 3, -26],
color: '#FFF',
},
b: {
padding: [0, 0, 3, 0],
},
c: {
width: '100%',
padding: [0, 0, 5, 10],
color: '#9EA4B3',
fontSize: 12,
},
},
},
};
option.tooltip = {
trigger: 'item',
formatter: (params) => {
return `${params.marker} ${params.data.name} ${params.percent}%`;
},
};
option.series = [
{
type: 'pie',
center: ['20%', '60%'],
data: chart.data,
radius: '60%',
lable: {
position: 'outer',
alignTo: 'none',
bleedMargin: 5,
},
itemStyle: {
normal: {
color: function(params) {
return CommonProperty.pieColorArr[params.dataIndex];
},
},
},
},
];
chart.data.forEach((item) => {
option.legend.data.push(item.name);
});
} else if (chart.type) {
option.color = ['#6C92FA'];
option.tooltip = {
trigger: 'axis',
};
option.series = [
{
type: 'bar',
barWidth: 30,
data: [],
},
];
option.xAxis = {
type: 'category',
axisLabel: {
interval: 0,
rotate: -30,
},
data: [],
};
option.grid = {
left: 50,
top: 20,
right: 0,
bottom: 50,
};
option.yAxis = {
type: 'value',
};
chart.data.forEach((item) => {
const name = this.apiType === 'cpu' ? item.op_name : item.name;
option.xAxis.data.push(name);
option.series[0].data.push(item.value);
});
if (this.apiType === 'cpu') {
option.xAxis.axisLabel.formatter = (params, dataIndex) => {
const xAxisValue = chart.data[dataIndex].op_name;
return xAxisValue.replace(/^.+\//g, '');
};
}
}
this.$nextTick(() => {
const cpuDom = document.getElementById(chart.id);
if (cpuDom) {
chart.chartDom = echarts.init(cpuDom, null);
} else {
if (chart.chartDom) {
chart.chartDom.clear();
}
return;
}
chart.chartDom.setOption(option, true);
chart.chartDom.resize();
}, 10);
},
/**
* show operator info deteail
* @param {Object} cellData cell data
* @param {Object} column column
*/
showInfoDetail(cellData, column) {
if (column.property !== 'op_info' || !cellData || !cellData.op_info) {
return;
}
this.showDialogData(cellData.op_info, column);
},
/**
* The detailed information is displayed in the dialog box.
* @param {String} val
* @param {Object} column
*/
showDialogData(val, column) {
this.detailsDataList = [];
if (typeof val !== 'string' || val == '{}') {
return;
} else {
const isJson = this.isJSON(val);
if (!isJson) {
return;
}
}
this.$nextTick(() => {
this.rowName = `${column.label}${this.$t('dataTraceback.details')}`;
this.detailsDialogVisible = true;
this.detailsDataList = this.formateJsonString(val);
});
},
/**
* Checks whether the value is a JSON character string.
* @param {String} val
* @return {Boolean}
*/
isJSON(val) {
try {
JSON.parse(val);
return true;
} catch (e) {
return false;
}
},
/**
* Converts JSON strings.
* @param {String} str
* @return {Array}
*/
formateJsonString(str) {
if (!str) {
return [];
}
const resultArr = [];
const dataObj = JSON.parse(str);
const keys = Object.keys(dataObj);
keys.forEach((key, index) => {
const tempData = {
id: index + 1,
hasChildren: false,
key: key,
value: '',
};
if (typeof dataObj[key] === this.objectType && dataObj[key] !== null) {
if (!(dataObj[key] instanceof Array)) {
tempData.hasChildren = true;
tempData.children = [];
Object.keys(dataObj[key]).forEach((k, j) => {
const item = {};
item.key = k;
item.value = dataObj[key][k];
item.id =
`${new Date().getTime()}` + `${this.$store.state.tableId}`;
this.$store.commit('increaseTableId');
tempData.children.push(item);
});
}
tempData.value = JSON.stringify(dataObj[key]);
} else {
tempData.value = dataObj[key];
}
resultArr.push(tempData);
});
return resultArr;
},
loadDataListChildren(tree, treeNode, resolve) {
setTimeout(() => {
resolve(tree.children);
});
},
/**
* window resize
*/
resizeCallback() {
if (this.coreCharts.chartDom && this.apiType === 'core') {
this.coreCharts.chartDom.resize();
}
if (this.cpuCharts.chartDom && this.apiType === 'cpu') {
this.cpuCharts.chartDom.resize();
}
},
},
mounted() {
if (this.train_id) {
document.title = `${decodeURIComponent(this.train_id)}-${this.$t('profiling.operatorDetail')}-MindInsight`;
} else {
document.title = `${this.$t('profiling.operatorDetail')}-MindInsight`;
}
window.addEventListener('resize', this.resizeCallback, false);
},
};
</script>
<style lang="scss">
.operator{
height: 100%;
}
.clear {
clear: both;
}
.el-tabs__item {
color: #6c7280;
font-size: 16px;
line-height: 36px;
height: 36px;
}
.el-tabs__item.is-active {
color: #00a5a7;
font-weight: bold;
}
.operator-title {
padding: 0 15px;
font-size: 16px;
font-weight: bold;
}
.cl-profiler {
height: calc(100% - 21px);
overflow-y: auto;
width: 100%;
background: #fff;
padding: 0 16px;
overflow: hidden;
.el-tabs {
height: 100%;
.el-tabs__header {
margin-bottom: 10px;
}
}
.el-tabs__content {
height: calc(100% - 46px);
}
.el-tab-pane {
height: 100%;
}
.cl-search-box {
float: right;
margin-bottom: 10px;
}
.cl-profiler-top {
height: 36%;
}
.cl-profiler-bottom {
height: 64%;
padding-top: 10px;
}
.cpu-tab {
.cl-profiler-top {
height: calc(36% + 32px);
}
.cl-profiler-bottom {
height: calc(64% - 32px);
}
}
.profiler-title {
font-size: 16px;
font-weight: bold;
line-height: 32px;
display: inline-block;
}
.cl-profiler-echarts {
width: 100%;
height: calc(100% - 32px);
display: inline-block;
position: relative;
overflow: auto;
#cpu-echarts,
#core-echarts {
width: 100%;
height: 100%;
min-width: 1300px;
min-height: 232px;
}
}
.chart-radio-group {
float: right;
}
.el-radio-group {
.el-radio-button--small .el-radio-button__inner {
height: 30px;
width: 70px;
font-size: 14px;
line-height: 10px;
}
}
.cl-profiler-bar {
display: inline-block;
width: calc(100% - 400px);
vertical-align: top;
height: 100%;
padding: 20px;
}
.cl-profiler-table-type {
display: inline-block;
width: calc(100% - 400px);
vertical-align: top;
height: 100%;
}
.el-pagination {
margin: 7px 0;
float: right;
}
.details-data-list {
.el-table {
th {
padding: 10px 0;
border-top: 1px solid #ebeef5;
.cell {
border-left: 1px solid #d9d8dd;
height: 14px;
line-height: 14px;
}
}
th:first-child {
.cell {
border-left: none;
}
}
th:nth-child(2),
td:nth-child(2) {
max-width: 30%;
}
td {
padding: 8px 0;
}
}
.el-table__row--level-0 td:first-child:after {
width: 20px;
height: 1px;
background: #ebeef5;
z-index: 11;
position: absolute;
left: 0;
bottom: -1px;
content: '';
display: block;
}
.el-table__row--level-1 {
td {
padding: 4px 0;
position: relative;
}
td:first-child::before {
width: 42px;
background: #f0fdfd;
border-right: 2px #00a5a7 solid;
z-index: 10;
position: absolute;
left: 0;
top: -1px;
bottom: 0px;
content: '';
display: block;
}
}
.el-table__row--level-1:first-child {
td:first-child::before {
bottom: 0;
}
}
.el-dialog__title {
font-weight: bold;
}
.el-dialog__body {
max-height: 500px;
padding-top: 10px;
overflow: auto;
.details-data-title {
margin-bottom: 20px;
}
}
}
.el-table__expanded-cell[class*='cell'] {
padding: 0;
}
.expand-table {
position: relative;
padding-left: 44px;
}
.expand-table::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
background: #f0fdfd;
width: 42px;
border-right: 2px #00a5a7 solid;
}
.el-radio-button:last-child .el-radio-button__inner,
.el-radio-button:first-child .el-radio-button__inner {
border-radius: 0;
}
.image-noData {
width: 100%;
height: 450px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
font-size: 16px;
padding-top: 10px;
}
}
}
</style>
<!--
Copyright 2020 Huawei Technologies Co., Ltd.All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="pro-router-wrap">
<div class="pro-router-left">
<div class="step-trace">
<div class="title-wrap">
<div class="title">{{ $t('profiling.stepTrace') }}</div>
<div class="view-detail">
<button @click="viewDetail('step-trace')"
:disabled="svg.noData && svg.data.length === 0"
:class="{disabled:svg.noData && svg.data.length === 0}">{{ $t('profiling.viewDetail') }}
<i class="el-icon-d-arrow-right"></i></button>
</div>
<div class="tip-icon"
v-show="false">
<el-tooltip content=""
placement="top"
effect="light">
<i class="el-icon-info"></i>
</el-tooltip>
</div>
</div>
<div class="trace-container">
<div id="trace"
class="training-trace">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
height="100%"
width="100%">
<defs>
<marker id="marker_end"
refX="5"
refY="4"
markerWidth="10"
markerHeight="8"
orient="auto">
<path d="M1,1 L1,7 L9,4 z"
fill="#E6EBF5"
stroke="#E6EBF5"></path>
</marker>
<marker id="marker_start"
refX="5"
refY="4"
markerWidth="10"
markerHeight="8"
orient="auto">
<path d="M9,1 L9,7 L1,4 z"
fill="#E6EBF5"
stroke="#E6EBF5"></path>
</marker>
</defs>
</svg>
</div>
<div class="image-noData"
v-if="svg.noData">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
</div>
</div>
<div class="minddata">
<div class="title-wrap">
<div class="title">{{ $t('profiling.mindData') }}</div>
<div class="view-detail"
v-if="false">
<button @click="viewDetail('minddata')">{{ $t('profiling.viewDetail') }}
<i class="el-icon-d-arrow-right"></i></button>
</div>
</div>
<div class="coming-soon-content">
<div class="coming-soon-container">
<img :src="require('@/assets/images/coming-soon.png')" />
<p class='coming-soon-text'>
{{$t("public.stayTuned")}}
</p>
</div>
</div>
</div>
</div>
<div class="pro-router-right">
<div class="op-time-consume">
<div class="title-wrap">
<div class="title">{{ $t('profiling.rankOfOperator') }}</div>
<div class="view-detail">
<button @click="viewDetail('operator')"
:disabled="pieChart.noData && pieChart.data.length === 0"
:class="{disabled:pieChart.noData && pieChart.data.length === 0}">{{ $t('profiling.viewDetail') }}
<i class="el-icon-d-arrow-right"></i></button>
</div>
</div>
<div class="image-noData"
v-if="pieChart.noData && pieChart.data.length === 0">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
<div class="op-time-content">
<div id="pieChart"
class="pie-chart"
v-if="pieChart.data.length"></div>
<div class="time-list"
v-if="pieChart.data.length">
<ul>
<li v-for="(item, index) in pieChart.topN"
:key="index"
class="item">
<span class="index"
:style="{'background-color': pieChart.colorList[index]}">{{index + 1}}</span>
<span class="name">{{item.name}}</span>
<span class="num">{{item.frequency + $t('profiling.times')}}</span>
<span class="time">
<span class="bar"
:style="{width: item.time / pieChart.topN[0].time * 100 + '%'}"></span>
<span class="value">{{item.time}}ms</span>
</span>
</li>
</ul>
</div>
</div>
</div>
<div class="time-line">
<div class="title-wrap">
<div class="title">{{ $t('profiling.timeLine') }}</div>
<div class="view-detail"
v-show="false">
<a @click="toPerfetto()">{{ $t('profiling.viewDetail') }} <i class="el-icon-d-arrow-right"></i></a>
</div>
</div>
<div class="coming-soon-content">
<div class="coming-soon-container">
<img :src="require('@/assets/images/coming-soon.png')" />
<p class='coming-soon-text'>
{{$t("public.stayTuned")}}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import echarts from 'echarts';
import RequestService from '../../services/request-service';
import CommonProperty from '../../common/common-property';
export default {
data() {
return {
svg: {
data: [],
svgPadding: 20,
totalWidth: 0,
totalTime: 0,
rowHeight: 60,
markerPadding: 4,
namespaceURI: 'http://www.w3.org/2000/svg',
resizeTimer: null,
colorList: [
['#A6DD82', '#edf8e6'],
['#6CBFFF', '#e2f2ff'],
['#fa8e5b', '#fff4de'],
['#01a5a7', '#cceded'],
],
colorIndex: 0,
noData: false,
},
trainingJobId: this.$route.query.id,
summaryPath: this.$route.query.dir,
relativePath: this.$route.query.path,
currentCard: '',
pieChart: {
chartDom: null,
data: [],
noData: false,
topN: [],
colorList: ['#6C92FA', '#6CBFFF', '#4EDED2', '#7ADFA0', '#A6DD82'],
},
};
},
mounted() {},
watch: {
'$parent.curDashboardInfo': {
handler(newValue, oldValue) {
if (newValue.query.dir && newValue.query.id && newValue.query.path) {
this.summaryPath = newValue.query.dir;
this.trainingJobId = newValue.query.id;
this.relativePath = newValue.query.path;
this.currentCard = newValue.curCardNum;
if (this.trainingJobId) {
document.title = `${decodeURIComponent(
this.trainingJobId,
)}-${this.$t('profiling.profilingDashboard')}
-MindInsight`;
} else {
document.title = `${this.$t(
'profiling.profilingDashboard',
)}-MindInsight`;
}
this.init();
}
},
deep: true,
immediate: true,
},
},
methods: {
init() {
this.queryTrainingTrace();
this.initPieChart();
window.addEventListener('resize', this.resizeTrace, false);
this.$bus.$on('resize', this.resizeTrace);
},
viewDetail(path) {
this.$router.push({
path,
query: {
id: this.trainingJobId,
dir: this.summaryPath,
path: this.relativePath,
},
});
},
setPieOption() {
const option = {};
option.tooltip = {
trigger: 'item',
formatter: (params) => {
return `${params.marker} ${params.data.name} ${params.percent}%`;
},
};
option.series = [
{
type: 'pie',
center: ['50%', '50%'],
data: this.pieChart.data,
radius: '50%',
lable: {
position: 'outer',
alignTo: 'none',
bleedMargin: 5,
},
itemStyle: {
normal: {
color: function(params) {
return CommonProperty.pieColorArr[params.dataIndex];
},
},
},
},
];
this.$nextTick(() => {
const dom = document.getElementById('pieChart');
if (dom) {
this.pieChart.chartDom = echarts.init(dom, null);
} else {
if (this.pieChart.chartDom) {
this.pieChart.chartDom.clear();
}
return;
}
this.pieChart.chartDom.setOption(option, true);
this.pieChart.chartDom.resize();
}, 10);
},
initPieChart() {
const params = {};
params.params = {
profile: this.summaryPath,
train_id: this.trainingJobId,
};
params.body = {
op_type: 'aicore_type',
device_id: this.currentCard,
filter_condition: {},
sort_condition: {
name: 'execution_time',
type: 'descending',
},
};
RequestService.getProfilerOpData(params)
.then((res) => {
if (res && res.data) {
if (res.data.object) {
this.pieChart.data = [];
res.data.object.forEach((item) => {
if (this.pieChart.data && this.pieChart.data.length < 19) {
this.pieChart.data.push({
name: item[0],
value: item[1],
frequency: item[2],
percent: item[3],
});
} else {
if (!this.pieChart.data[19]) {
this.pieChart.data[19] = {
name: 'Other',
value: 0,
percent: 0,
};
}
this.pieChart.data[19].value += item[1];
this.pieChart.data[19].percent += item[3];
}
});
this.setPieOption();
if (this.pieChart.data.length === 0) {
this.pieChart.noData = true;
}
this.pieChart.topN = this.pieChart.data
.slice(0, Math.min(this.pieChart.data.length, 5))
.map((i) => {
return {
name: i.name,
time: i.value,
frequency: i.frequency,
};
});
}
}
})
.catch(() => {
this.pieChart.noData = true;
});
},
queryTrainingTrace() {
const params = {
dir: this.relativePath,
type: 0,
device_id: this.currentCard,
};
RequestService.queryTrainingTrace(params).then(
(res) => {
if (
res.data &&
res.data.training_trace_graph &&
res.data.training_trace_graph.length
) {
this.svg.noData = false;
document.querySelector('#trace').style.height = `${res.data
.training_trace_graph.length * this.svg.rowHeight}px`;
this.svg.data = JSON.parse(
JSON.stringify(res.data.training_trace_graph),
);
this.removeTrace();
setTimeout(() => {
this.dealTraceData();
}, 100);
} else {
document.querySelector('#trace').style.height = '0px';
this.svg.noData = true;
this.svg.data = [];
this.removeTrace();
}
},
(error) => {
document.querySelector('#trace').style.height = '0px';
this.svg.noData = true;
this.svg.data = [];
this.removeTrace();
},
);
},
dealTraceData() {
const traceDom = document.querySelector('#trace');
if (traceDom) {
this.svg.totalWidth = traceDom.offsetWidth - this.svg.svgPadding * 2;
if (this.svg.data[0] && this.svg.data[0].length) {
const svg = document.querySelector('#trace svg');
this.svg.totalTime = this.svg.data[0][0].duration;
if (this.svg.totalTime) {
this.svg.data.forEach((row, index) => {
if (row && row.length) {
const dashedLine = this.addDashedLine(index);
svg.insertBefore(dashedLine, svg.querySelector('g'));
row.forEach((i) => {
if (i.duration) {
const tempDom = i.name
? this.createRect(i, index)
: this.createArrow(i, index);
svg.insertBefore(tempDom, svg.querySelector('g'));
}
});
}
});
}
} else {
this.removeTrace();
}
}
},
addDashedLine(index) {
const x1 = this.svg.svgPadding;
const x2 = this.svg.svgPadding + this.svg.totalWidth;
const y = index * this.svg.rowHeight;
const line = document.createElementNS(this.svg.namespaceURI, 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y);
line.setAttribute('x2', x2);
line.setAttribute('y2', y);
line.setAttribute('style', 'stroke:#E2E2E2;stroke-width:1');
line.setAttribute('stroke-dasharray', '5 5');
const g = document.createElementNS(this.svg.namespaceURI, 'g');
g.appendChild(line);
return g;
},
createRect(data, rowIndex) {
const color = this.svg.colorList[this.svg.colorIndex++ % 4];
const height = 40;
const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth;
const x1 =
(data.start / this.svg.totalTime) * this.svg.totalWidth +
this.svg.svgPadding;
const y1 =
rowIndex * this.svg.rowHeight + (this.svg.rowHeight - height) / 2;
const g = document.createElementNS(this.svg.namespaceURI, 'g');
const gChild = document.createElementNS(this.svg.namespaceURI, 'g');
const rect = document.createElementNS(this.svg.namespaceURI, 'rect');
rect.setAttribute('x', x1);
rect.setAttribute('y', y1);
rect.setAttribute('height', height);
rect.setAttribute('width', width);
rect.setAttribute('style', `fill:${color[1]};stroke:${color[1]};`);
const foreignObject = document.createElementNS(
this.svg.namespaceURI,
'foreignObject',
);
foreignObject.setAttribute('x', x1);
foreignObject.setAttribute('y', y1);
foreignObject.setAttribute('height', height);
foreignObject.setAttribute('width', width);
foreignObject.setAttribute(
'style',
`overflow:hidden;text-align:center;text-overflow:ellipsis;` +
`white-space:nowrap;font-size:12px;line-height:${height}px;color:${color[0]}`,
);
foreignObject.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`;
const title = document.createElementNS(this.svg.namespaceURI, 'title');
title.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`;
gChild.appendChild(rect);
gChild.appendChild(foreignObject);
gChild.appendChild(title);
g.appendChild(gChild);
return g;
},
createArrow(data, rowIndex) {
const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth;
const x1 =
(data.start / this.svg.totalTime) * this.svg.totalWidth +
this.svg.markerPadding +
this.svg.svgPadding;
const x2 = x1 + width - this.svg.markerPadding * 2;
const y = rowIndex * this.svg.rowHeight + this.svg.rowHeight / 2;
const g = document.createElementNS(this.svg.namespaceURI, 'g');
const line = document.createElementNS(this.svg.namespaceURI, 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y);
line.setAttribute('x2', x2);
line.setAttribute('y2', y);
line.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
line.setAttribute('marker-end', 'url(#marker_end)');
line.setAttribute('marker-start', 'url(#marker_start)');
const text = document.createElementNS(this.svg.namespaceURI, 'text');
text.textContent = `${data.duration.toFixed(4)}ms`;
const textWidth = this.getTextWidth(text.textContent);
text.setAttribute('x', Math.max(0, (x2 - x1) / 2 + x1 - textWidth / 2));
text.setAttribute('y', y - 6);
text.setAttribute('font-size', 12);
text.setAttribute('fill', '#6c7280');
const startLine = document.createElementNS(this.svg.namespaceURI, 'line');
startLine.setAttribute('x1', x1 - this.svg.markerPadding);
startLine.setAttribute('y1', y - this.svg.rowHeight / 4);
startLine.setAttribute('x2', x1 - this.svg.markerPadding);
startLine.setAttribute('y2', y + this.svg.rowHeight / 4);
startLine.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
g.appendChild(startLine);
const endLine = document.createElementNS(this.svg.namespaceURI, 'line');
endLine.setAttribute('x1', x1 + width - this.svg.markerPadding);
endLine.setAttribute('y1', y - this.svg.rowHeight / 4);
endLine.setAttribute('x2', x1 + width - this.svg.markerPadding);
endLine.setAttribute('y2', y + this.svg.rowHeight / 4);
endLine.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
g.appendChild(endLine);
g.appendChild(line);
g.appendChild(text);
return g;
},
getTextWidth(text) {
const body = document.querySelector('body');
const temp = document.createElement('span');
temp.style['font-size'] = '12px';
temp.textContent = text;
body.appendChild(temp);
const textWidth = temp.offsetWidth;
body.removeChild(temp);
return textWidth;
},
removeTrace() {
const svgDom = document.querySelector('#trace svg');
if (svgDom) {
const gDoms = svgDom.children;
if (gDoms) {
for (let i = 0; i < gDoms.length; i++) {
if (gDoms[i].nodeName === 'g') {
svgDom.removeChild(gDoms[i--]);
}
}
}
}
},
resizeTrace() {
if (this.svg.resizeTimer) {
clearTimeout(this.svg.resizeTimer);
}
this.svg.resizeTimer = setTimeout(() => {
this.removeTrace();
this.dealTraceData();
this.svg.resizeTimer = null;
}, 500);
},
stringToUint8Array(str) {
const arr = [];
for (let i = 0, strLen = str.length; i < strLen; i++) {
arr.push(str.charCodeAt(i));
}
return new Uint8Array(arr);
},
},
destroyed() {
window.removeEventListener('resize', this.resizeTrace, false);
this.$bus.$off('resize');
},
};
</script>
<style lang="scss">
.pro-router-wrap {
height: 100%;
& > div {
float: left;
height: 100%;
& > div {
border: 1px solid #ddd;
border-radius: 4px;
}
.title-wrap {
padding: 15px;
.title {
float: left;
font-weight: bold;
font-size: 16px;
}
.tip-icon {
float: right;
margin-right: 18px;
font-size: 20px;
.el-icon-warning {
cursor: pointer;
&:hover::before {
color: #00a5a7;
}
}
}
.view-detail {
float: right;
cursor: pointer;
color: #00a5a7;
font-size: 12px;
height: 24px;
line-height: 24px;
a {
color: #00a5a7 !important;
padding-right: 6px;
}
button {
color: #00a5a7;
border: none;
background-color: #fff;
cursor: pointer;
}
button.disabled {
cursor: not-allowed;
color: #c0c4cc;
}
}
&::after {
content: '';
clear: both;
display: block;
}
}
.coming-soon-content {
height: calc(100% - 50px);
position: relative;
.coming-soon-container {
text-align: center;
position: absolute;
top: 50%;
left: 50%;
border-radius: 5px;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.coming-soon-text {
font-size: 16px;
}
}
}
.pro-router-left {
width: calc(100% - 350px);
padding-right: 15px;
.step-trace {
height: 45%;
margin-bottom: 15px;
.trace-container {
width: 100%;
height: calc(100% - 50px);
overflow: auto;
.training-trace {
position: relative;
height: 0;
}
}
}
.minddata {
height: calc(55% - 15px);
}
}
.pro-router-right {
width: 350px;
.op-time-consume {
height: calc(60% - 15px);
margin-bottom: 15px;
.time-list {
height: calc(40% - 52px);
.item {
height: 25px;
line-height: 25px;
padding: 0 20px;
& > span {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.index {
color: white;
background-color: rgb(108, 146, 250);
width: 20px;
height: 20px;
border-radius: 20px;
text-align: center;
vertical-align: middle;
line-height: 20px;
}
.name {
margin-left: 10px;
width: calc(50% - 30px);
text-overflow: ellipsis;
overflow: hidden;
}
.num {
width: 20%;
}
.time {
width: 30%;
position: relative;
span {
display: inline-block;
position: absolute;
left: 0;
height: 20px;
}
.bar {
background-color: #cceded;
top: 2px;
}
.value {
line-height: 25px;
height: 25px;
}
}
}
}
}
.time-line {
height: 40%;
overflow: hidden;
}
}
.op-time-content {
height: calc(100% - 54px);
overflow: auto;
}
.pie-chart {
width: 100%;
height: 260px;
}
.image-noData {
width: 100%;
height: calc(100% - 52px);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
font-size: 16px;
padding-top: 10px;
}
}
}
</style>
<!--
Copyright 2020 Huawei Technologies Co., Ltd.All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="prof-wrap">
<div class="prof-content">
<div class="prof-content-left"
:class="{collapse:collapse}">
<div class="helper"
v-show="!collapse">
<div class="cur-card">
<label>{{$t('profiling.curCard')}}</label>
<el-select v-model="curDashboardInfo.curCardNum"
class="card-select"
:placeholder="$t('public.select')">
<el-option v-for="item in CardNumArr"
:key="item.value"
:label="item.value + $t('operator.card')"
:value="item.value">
</el-option>
</el-select>
</div>
<div class="helper-title">
{{$t("profiling.smartHelper")}}
</div>
</div>
<div class="collapse-btn" :class="{collapse:collapse}"
@click="collapseLeft()">
</div>
</div>
<div class="prof-content-right"
:class="{collapse:collapse}">
<router-view></router-view>
<div class="close"
@click="backToDdashboard"
v-if="$route.path !== '/profiling/profiling-dashboard'">
<img src="@/assets/images/close-page.png">
</div>
</div>
</div>
</div>
</template>
<script>
import RequestService from '../../services/request-service';
export default {
data() {
return {
CardNumArr: [],
collapse: false,
curDashboardInfo: {
curCardNum: '',
query: {},
},
};
},
watch: {},
mounted() {
this.$nextTick(() => {
this.init();
});
},
methods: {
init() {
if (this.$route.query && this.$route.query.id && this.$route.query.dir) {
this.curDashboardInfo.query.id = this.$route.query.id;
this.curDashboardInfo.query.dir = this.$route.query.dir;
this.curDashboardInfo.query.path = this.$route.query.path;
this.getDeviceList();
} else {
this.curDashboardInfo.query.trainingJobId = '';
this.curDashboardInfo.query.dir = '';
this.curDashboardInfo.query.path = '';
this.$message.error(this.$t('trainingDashboard.invalidId'));
}
},
getDeviceList() {
const params = {
profile: this.curDashboardInfo.query.dir,
train_id: this.curDashboardInfo.query.id,
};
RequestService.getProfilerDeviceData(params)
.then((res) => {
if (res && res.data) {
const deviceList = res.data;
if (deviceList.length) {
deviceList.forEach((item) => {
this.CardNumArr.push({
value: item,
});
});
this.curDashboardInfo.curCardNum = this.CardNumArr[0].value;
}
} else {
this.CardNumArr = [];
this.curDashboardInfo.curCardNum = '';
}
})
.catch(() => {});
},
backToDdashboard() {
this.$router.push({
path: '/profiling/profiling-dashboard',
query: {
dir: this.curDashboardInfo.query.dir,
id: this.curDashboardInfo.query.id,
path: this.curDashboardInfo.query.path,
},
});
},
collapseLeft() {
this.collapse = !this.collapse;
this.$bus.$emit('resize');
},
},
};
</script>
<style lang="scss">
.prof-wrap {
height: 100%;
background: #fff;
.prof-content {
height: 100%;
padding: 32px 32px 32px 0;
& > div {
float: left;
height: 100%;
}
.prof-content-left {
width: 25%;
transition: width 0.2s;
position: relative;
.el-input__inner {
padding: 0 10px;
}
.helper {
padding: 32px;
height: 100%;
margin-left: 32px;
background: #edf0f5;
.cur-card {
margin-bottom: 32px;
.card-select {
width: calc(100% - 70px);
}
& > label {
margin-right: 14px;
}
}
.helper-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 32px;
.el-icon-rank {
float: right;
cursor: pointer;
}
}
}
.collapse-btn {
position: absolute;
right: -21px;
width: 31px;
height: 100px;
top: 50%;
margin-top: -50px;
cursor: pointer;
line-height: 86px;
z-index: 1;
text-align: center;
background-image: url('../../assets/images/collapse-left.svg');
}
.collapse-btn.collapse{
background-image: url('../../assets/images/collapse-right.svg');
}
}
.prof-content-left.collapse {
width: 0;
}
.prof-content-right {
width: 75%;
padding-left: 20px;
transition: width 0.2s;
position: relative;
.close {
position: absolute;
right: 0;
top: -10px;
cursor: pointer;
}
}
.prof-content-right.collapse {
width: 100%;
}
}
}
</style>
<!--
Copyright 2020 Huawei Technologies Co., Ltd.All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="step-trace">
<div class="step-trace-title">{{$t('profiling.stepTraceDetail')}}
<div class="pf-content-right">
<div class="input-wrap">
<label>{{$t('profiling.stepSelect')}}</label>
<el-select v-model="selectedStep"
filterable
:placeholder="$t('profiling.selectStep')"
@change="changeStep">
<el-option v-for="item in steps"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
</div>
</div>
<div class="pf-content-middle"
v-show="!tabsArr[0].noData && !tabsArr[1].noData && !tabsArr[2].noData && !svg.noData">
<div id="trace-container">
<div id="trace"
class="training-trace">
<div :title="$t('graph.downloadPic')"
class="download-button"
@click="downloadSVG">
</div>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
height="100%"
width="100%">
<defs>
<marker id="marker_end"
refX="5"
refY="4"
markerWidth="10"
markerHeight="8"
orient="auto">
<path d="M1,1 L1,7 L9,4 z"
fill="#E6EBF5"
stroke="#E6EBF5"></path>
</marker>
<marker id="marker_start"
refX="5"
refY="4"
markerWidth="10"
markerHeight="8"
orient="auto">
<path d="M9,1 L9,7 L1,4 z"
fill="#E6EBF5"
stroke="#E6EBF5"></path>
</marker>
</defs>
</svg>
</div>
<div class="image-noData svg"
v-if="svg.data.length === 0">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
</div>
<div v-for="(item,key) in tabsArr"
:key="key"
class="chart-wrap">
<div class="title">{{ item.name }}</div>
<div class="rate-wrap">
<div v-if="item.timeSummary.total_time !== undefined">
<span>{{item.timeLabel}}:</span>{{item.timeSummary.total_time}}ms</div>
<div v-if="item.timeSummary[item.rate] !== undefined">
<span>{{item.rateLabel}}:</span>{{item.timeSummary[item.rate]}}</div>
<div v-if="item.timeSummary.total_steps !== undefined">
<span>{{$t('profiling.stepNum')}}:</span>{{item.timeSummary.total_steps}}</div>
</div>
<div class="chart"
:id="item.id"
v-show="!item.noData"></div>
<div class="image-noData"
v-if="item.noData">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
</div>
</div>
<div class="image-noData"
v-if="!(!tabsArr[0].noData && !tabsArr[1].noData && !tabsArr[2].noData && !svg.noData)">
<div>
<img :src="require('@/assets/images/nodata.png')"
alt="" />
</div>
<p>{{$t("public.noData")}}</p>
</div>
</div>
</template>
<script>
import echarts from 'echarts';
import RequestService from '../../services/request-service';
export default {
data() {
return {
dir: this.$route.query.dir,
train_id: this.$route.query.id,
relativePath: this.$route.query.path,
steps: [],
selectedStep: '',
charts: [],
svg: {
data: [],
svgPadding: 20,
totalWidth: 0,
totalTime: 0,
rowHeight: 60,
markerPadding: 4,
namespaceURI: 'http://www.w3.org/2000/svg',
resizeTimer: null,
colorList: [
['#A6DD82', '#edf8e6'],
['#6CBFFF', '#e2f2ff'],
['#fa8e5b', '#fff4de'],
['#01a5a7', '#cceded'],
],
colorIndex: 0,
noData: false,
},
deviceId: 0,
radio: this.$t('profiling.lterationGap'),
tabsArr: [
{
name: this.$t('profiling.lterationGap'),
id: 'iter-gap',
timeSummary: {},
rate: 'iteration_interval',
timeLabel: this.$t('profiling.iterGapTimeLabel'),
rateLabel: this.$t('profiling.iterGapRateLabel'),
noData: false,
},
{
name: 'Fp+bp',
id: 'fp-bp',
timeSummary: {},
rate: 'fp_and_bp',
timeLabel: this.$t('profiling.fpBpTimeLabel'),
rateLabel: this.$t('profiling.fpBpRateLabel'),
noData: false,
},
{
name: this.$t('profiling.lterationTail'),
id: 'tailing',
timeSummary: {},
rate: 'tail',
timeLabel: this.$t('profiling.tailTimeLabel'),
rateLabel: this.$t('profiling.tailRateLabel'),
noData: false,
},
],
};
},
watch: {
'$parent.curDashboardInfo': {
handler(newValue, oldValue) {
if (newValue.curCardNum || newValue.curCardNum === 0) {
this.dir = newValue.query.dir;
this.train_id = newValue.query.id;
this.deviceId = newValue.curCardNum;
this.relativePath = newValue.query.path;
if (this.train_id) {
document.title = `${decodeURIComponent(this.train_id)}-${this.$t(
'profiling.stepTrace',
)}-MindInsight`;
} else {
document.title = `${this.$t('profiling.stepTrace')}-MindInsight`;
}
this.svg.noData = false;
this.tabsArr.forEach((val) => {
val.noData = false;
});
this.init();
}
},
deep: true,
immediate: true,
},
},
computed: {},
mounted() {},
methods: {
init() {
window.addEventListener('resize', this.resizeTrace, false);
this.$bus.$on('resize', this.resizeTrace);
window.addEventListener('resize', this.resizeEchart, false);
this.$bus.$on('resize', this.resizeEchart);
if (this.charts.length) {
this.charts.forEach((val) => {
val.clear();
});
}
this.setStep();
this.getTimeInfo('fp-bp', 'fp_and_bp');
this.getTimeInfo('iter-gap', 'iteration_interval');
this.getTimeInfo('tailing', 'tail');
this.queryTrainingTrace(0);
},
changeStep(value) {
if (value === this.$t('profiling.showAverage')) {
value = 0;
}
this.queryTrainingTrace(value);
},
getTimeInfo(id, type) {
const params = {
dir: this.relativePath,
type,
device_id: this.deviceId,
};
RequestService.targetTimeInfo(params).then(
(res) => {
if (res.data && res.data.summary) {
const summary = res.data.summary;
Object.keys(summary).forEach((val) => {
summary[val] = summary[val];
});
this.tabsArr.forEach((val) => {
if (id === val.id) {
val.timeSummary = summary;
}
});
}
if (res.data && res.data.info) {
if (this.steps.length <= 1) {
this.setStep(res.data.size);
}
const timeInfo = [];
Object.keys(res.data.info).forEach((val) => {
timeInfo.push({
data: res.data.info[val],
name: val,
type: 'line',
});
});
if (timeInfo.length) {
const option = {
xAxis: {
type: 'category',
data: this.steps.map((val, index) => index + 1),
name: 'step',
},
yAxis: {
type: 'value',
name: '',
nameTextStyle: {
padding: [0, 0, 0, 30],
},
},
grid: {
left: 50,
top: 50,
right: 50,
bottom: 20,
},
series: timeInfo,
tooltip: {
trigger: 'axis',
},
};
if (type === 'iteration_interval') {
option.yAxis.name = `${this.$t(
'profiling.iterationGapTime',
)}(ms)`;
this.tabsArr[0].noData = this.steps.length ? false : true;
} else if (type === 'fp_and_bp') {
option.yAxis.name = `fp+bp${this.$t('profiling.time')}(ms)`;
this.tabsArr[1].noData = this.steps.length ? false : true;
} else if (type === 'tail') {
option.yAxis.name = `tail${this.$t('profiling.time')}(ms)`;
this.tabsArr[2].noData = this.steps.length ? false : true;
}
this.initChart(option, id);
} else {
this.steps = [];
this.selectedStep = '';
}
}
},
(error) => {
this.steps = [];
this.selectedStep = '';
if (type === 'iteration_interval') {
this.tabsArr[0].noData = true;
} else if (type === 'fp_and_bp') {
this.tabsArr[1].noData = true;
} else if (type === 'tail') {
this.tabsArr[2].noData = true;
}
},
);
},
setStep(step = 0) {
this.steps = [];
this.steps.push({
label: this.$t('profiling.showAverage'),
value: this.$t('profiling.showAverage'),
});
for (let i = 1; i <= step; i++) {
this.steps.push({
label: i,
value: i,
});
}
this.selectedStep = this.$t('profiling.showAverage');
},
initChart(option, id) {
this.$nextTick(() => {
const chart = echarts.init(document.getElementById(id));
chart.setOption(option, true);
this.charts.push(chart);
});
},
resizeEchart() {
setTimeout(() => {
this.charts.forEach((val)=>{
val.resize();
});
}, 300);
},
queryTrainingTrace(step) {
const params = {
dir: this.relativePath,
type: step,
device_id: this.deviceId,
};
RequestService.queryTrainingTrace(params).then(
(res) => {
if (
res.data &&
res.data.training_trace_graph &&
res.data.training_trace_graph.length
) {
this.svg.noData = false;
document.querySelector('#trace').style.height = `${res.data
.training_trace_graph.length * this.svg.rowHeight}px`;
this.svg.data = JSON.parse(
JSON.stringify(res.data.training_trace_graph),
);
this.removeTrace();
setTimeout(() => {
this.dealTraceData();
}, 100);
} else {
this.svg.data = [];
this.svg.noData = true;
this.removeTrace();
}
},
(error) => {
this.svg.data = [];
this.svg.noData = true;
this.removeTrace();
},
);
},
dealTraceData() {
this.svg.totalWidth =
document.querySelector('#trace').offsetWidth - this.svg.svgPadding * 2;
if (this.svg.data[0] && this.svg.data[0].length) {
const svg = document.querySelector('#trace svg');
this.svg.totalTime = this.svg.data[0][0].duration;
if (this.svg.totalTime) {
this.svg.data.forEach((row, index) => {
if (row && row.length) {
const dashedLine = this.addDashedLine(index);
svg.insertBefore(dashedLine, svg.querySelector('g'));
row.forEach((i) => {
if (i.duration) {
const tempDom = i.name
? this.createRect(i, index)
: this.createArrow(i, index);
svg.insertBefore(tempDom, svg.querySelector('g'));
}
});
}
});
}
} else {
this.removeTrace();
}
},
addDashedLine(index) {
const x1 = this.svg.svgPadding;
const x2 = this.svg.svgPadding + this.svg.totalWidth;
const y = index * this.svg.rowHeight;
const line = document.createElementNS(this.svg.namespaceURI, 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y);
line.setAttribute('x2', x2);
line.setAttribute('y2', y);
line.setAttribute('style', 'stroke:#E2E2E2;stroke-width:1');
line.setAttribute('stroke-dasharray', '5 5');
const g = document.createElementNS(this.svg.namespaceURI, 'g');
g.appendChild(line);
return g;
},
createRect(data, rowIndex) {
const color = this.svg.colorList[this.svg.colorIndex++ % 4];
const height = 40;
const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth;
const x1 =
(data.start / this.svg.totalTime) * this.svg.totalWidth +
this.svg.svgPadding;
const y1 =
rowIndex * this.svg.rowHeight + (this.svg.rowHeight - height) / 2;
const g = document.createElementNS(this.svg.namespaceURI, 'g');
const gChild = document.createElementNS(this.svg.namespaceURI, 'g');
const rect = document.createElementNS(this.svg.namespaceURI, 'rect');
rect.setAttribute('x', x1);
rect.setAttribute('y', y1);
rect.setAttribute('height', height);
rect.setAttribute('width', width);
rect.setAttribute('style', `fill:${color[1]};stroke:${color[1]};`);
const foreignObject = document.createElementNS(
this.svg.namespaceURI,
'foreignObject',
);
foreignObject.setAttribute('x', x1);
foreignObject.setAttribute('y', y1);
foreignObject.setAttribute('height', height);
foreignObject.setAttribute('width', width);
foreignObject.setAttribute(
'style',
`overflow:hidden;text-align:center;text-overflow:ellipsis;` +
`white-space:nowrap;font-size:12px;line-height:${height}px;color:${color[0]}`,
);
foreignObject.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`;
const title = document.createElementNS(this.svg.namespaceURI, 'title');
title.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`;
gChild.appendChild(rect);
gChild.appendChild(foreignObject);
gChild.appendChild(title);
g.appendChild(gChild);
return g;
},
createArrow(data, rowIndex) {
const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth;
const x1 =
(data.start / this.svg.totalTime) * this.svg.totalWidth +
this.svg.markerPadding +
this.svg.svgPadding;
const x2 = x1 + width - this.svg.markerPadding * 2;
const y = rowIndex * this.svg.rowHeight + this.svg.rowHeight / 2;
const g = document.createElementNS(this.svg.namespaceURI, 'g');
const line = document.createElementNS(this.svg.namespaceURI, 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y);
line.setAttribute('x2', x2);
line.setAttribute('y2', y);
line.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
line.setAttribute('marker-end', 'url(#marker_end)');
line.setAttribute('marker-start', 'url(#marker_start)');
const text = document.createElementNS(this.svg.namespaceURI, 'text');
text.textContent = `${data.duration.toFixed(4)}ms`;
const textWidth = this.getTextWidth(text.textContent);
text.setAttribute('x', Math.max(0, (x2 - x1) / 2 + x1 - textWidth / 2));
text.setAttribute('y', y - 6);
text.setAttribute('font-size', 12);
text.setAttribute('fill', '#6c7280');
const startLine = document.createElementNS(this.svg.namespaceURI, 'line');
startLine.setAttribute('x1', x1 - this.svg.markerPadding);
startLine.setAttribute('y1', y - this.svg.rowHeight / 4);
startLine.setAttribute('x2', x1 - this.svg.markerPadding);
startLine.setAttribute('y2', y + this.svg.rowHeight / 4);
startLine.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
g.appendChild(startLine);
const endLine = document.createElementNS(this.svg.namespaceURI, 'line');
endLine.setAttribute('x1', x1 + width - this.svg.markerPadding);
endLine.setAttribute('y1', y - this.svg.rowHeight / 4);
endLine.setAttribute('x2', x1 + width - this.svg.markerPadding);
endLine.setAttribute('y2', y + this.svg.rowHeight / 4);
endLine.setAttribute('style', 'stroke:#E6EBF5;stroke-width:1');
g.appendChild(endLine);
g.appendChild(line);
g.appendChild(text);
return g;
},
getTextWidth(text) {
const body = document.querySelector('body');
const temp = document.createElement('span');
temp.style['font-size'] = '12px';
temp.textContent = text;
body.appendChild(temp);
const textWidth = temp.offsetWidth;
body.removeChild(temp);
return textWidth;
},
removeTrace() {
const svgDom = document.querySelector('#trace svg');
if (svgDom) {
const gDoms = svgDom.children;
if (gDoms) {
for (let i = 0; i < gDoms.length; i++) {
if (gDoms[i].nodeName === 'g') {
svgDom.removeChild(gDoms[i--]);
}
}
}
}
},
resizeTrace() {
if (this.svg.resizeTimer) {
clearTimeout(this.svg.resizeTimer);
}
this.svg.resizeTimer = setTimeout(() => {
this.removeTrace();
this.dealTraceData();
this.svg.resizeTimer = null;
}, 500);
},
downloadSVG() {
const svgDom = document.querySelector('svg').outerHTML;
const src = `data:image/svg+xml;base64,
${window.btoa(unescape(encodeURIComponent(svgDom)))}`;
const a = document.createElement('a');
a.href = src;
a.download = new Date().valueOf();
a.click();
},
},
destroyed() {
window.removeEventListener('resize', this.resizeTrace, false);
window.removeEventListener('resize', this.resizeEchart, false);
this.$bus.$off('resize');
},
};
</script>
<style lang="scss">
.step-trace {
width: 100%;
height: 100%;
.step-trace-title {
padding: 0 15px;
font-size: 16px;
font-weight: bold;
.pf-content-right {
display: inline-block;
margin-left: 35px;
label {
margin-right: 10px;
}
}
.input-wrap {
font-weight: normal;
}
}
.pf-content-middle {
padding: 15px 15px 0;
height: calc(100% - 32px);
#trace-container {
width: 100%;
height: 50%;
border: 1px solid #ccc;
overflow: auto;
.training-trace {
position: relative;
height: 0;
.download-button {
display: none;
position: absolute;
width: 12px;
height: 12px;
right: 10px;
top: 10px;
cursor: pointer;
background-image: url('../../assets/images/download.png');
}
}
}
.chart-wrap {
float: left;
height: calc(50% - 20px);
margin-top: 20px;
margin-right: 15px;
width: calc(33.3% - 10px);
border: 1px solid #ccc;
padding: 30px;
border-radius: 4px;
overflow: auto;
&:last-child {
margin-right: 0;
}
.chart {
height: calc(100% - 85px);
min-height: 180px;
}
.title {
margin: 0 0 15px 20px;
font-weight: bold;
}
.rate-wrap {
font-size: 12px;
padding-left: 20px;
& > div {
display: inline-block;
margin: 0 15px 5px 0;
color: #464950;
span {
margin-right: 10px;
color: #6c7280;
}
}
}
}
}
.image-noData {
width: 100%;
height: calc(100% - 52px);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
font-size: 16px;
padding-top: 10px;
}
}
.image-noData.svg {
height: 100%;
}
}
</style>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册