* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import * as echarts from 'echarts';
import type { Session } from '../../entity/session';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { getBaselineName, getCompareName, Loading } from '../Common';
import { colorPalette, hashToNumber } from '../../utils/colorUtil';
import { Dropdown } from '@insight/lib/components';
import { type MenuProps, Spin } from 'antd';
import connector from '../../connection';
import i18n from '@insight/lib/i18n';
import { themeInstance } from '@insight/lib/theme';
import { type Theme } from '@emotion/react';
import { disposeAdaptiveEchart, getAdaptiveEchart, getDefaultChartOptions, safeStr } from '@insight/lib/utils';
import { ChartZoomData, ClickOperatorItem, CompareData, FormatterParams } from '../../utils/interface';
import { queryTimelineUnitKernelDetail } from '../../utils/RequestUtils';
import { useEventBus } from '../../utils/eventBus';
import type { ECharts, InsideDataZoomComponentOption } from 'echarts';
interface OnClickSlowRankOpCallbackParams {
startValue: number;
endValue: number;
rankId: number;
name: string;
}
type RectItemValues = [number, number, number, number, number, string];
const DEFAULT_CHART_HEIGHT = 460;
const DEFAULT_INNER_CHART_HEIGHT = 300;
const DEFAULT_CHART_ZOOM_HEIGHT = 400;
const MIN_CHART_ITEM_HEIGHT = 30;
const MAX_CHART_HEIGHT = 800;
const NS_TO_MS_FACTOR = 0.000001;
const INITINAL_MAX_VISIBLE_RANK_NUMBER = 516;
const MAX_VISIBLE_OPERATOR_NUMBER = 10000;
const START_POSITION_AXIS_Y = 20;
function initDataZoom(totalNum: number, dataLength: number, communicationChartZoomData?: ChartZoomData): void {
if (dataLength <= 0 || totalNum <= 0 || option.dataZoom.length <= 1) {
return;
}
if (dataLength > INITINAL_MAX_VISIBLE_RANK_NUMBER) {
const yPercentage = Math.ceil(INITINAL_MAX_VISIBLE_RANK_NUMBER / dataLength * 100);
option.dataZoom[1].start = 100 - yPercentage;
option.dataZoom[1].end = 100;
} else {
option.dataZoom[1].start = 0;
option.dataZoom[1].end = 100;
}
if (totalNum > MAX_VISIBLE_OPERATOR_NUMBER) {
const xPercentage = Math.ceil(MAX_VISIBLE_OPERATOR_NUMBER / totalNum * 100);
option.dataZoom[0].start = communicationChartZoomData?.start ?? 0;
option.dataZoom[0].end = communicationChartZoomData?.end ?? xPercentage;
} else {
option.dataZoom[0].start = communicationChartZoomData?.start ?? 0;
option.dataZoom[0].end = communicationChartZoomData?.end ?? 100;
}
}
enum compareSource {
COMPARISON = 0,
BASELINE = 1,
}
const sourceIndex = 4;
function wrapData(dataSource: AnalysisChartData, isCompare: boolean, communicationChartZoomData?: ChartZoomData): any {
const data: any = [];
const yAxisData: string[] = [];
const dataLength = Math.max(dataSource?.data?.length, 0);
const theme = themeInstance.getThemeType();
let totalNumber = 0;
for (let i = dataLength - 1; i >= 0; --i) {
totalNumber += dataSource.data[i].lists.compare.length;
const rankId = dataSource.data[i].rankId;
yAxisData.push(rankId);
dataSource.data[i].lists?.compare.forEach((item, _) => {
data.push(getRenderData({ item, rankId, theme, source: compareSource.COMPARISON }));
});
if (isCompare) {
dataSource.data[i].lists?.baseline.forEach((item, _) => {
data.push(getRenderData({ item, rankId, theme, source: compareSource.BASELINE }));
});
}
}
option.yAxis.data = yAxisData;
option.xAxis.min = nsToMs(dataSource.minTime);
option.xAxis.max = nsToMs(dataSource.maxTime);
const dataHeight = calculateDataHeight(dataSource);
option.grid.height = dataHeight;
option.dataZoom[0].top = dataHeight - DEFAULT_INNER_CHART_HEIGHT + DEFAULT_CHART_ZOOM_HEIGHT;
initDataZoom(totalNumber, dataLength, communicationChartZoomData);
option.series = getSeries({ data, isCompare });
option.tooltip = getTooltip({ isCompare });
return option;
}
const getRenderData = ({ item, rankId, source, theme }: { item: OperatorTimeItem; rankId: string; source: compareSource; theme: Theme }): any => {
const startTime = nsToMs(item.startTime);
const duration = nsToMs(item.duration);
const endTime = startTime + duration;
return {
name: `${rankId}-${item.operatorName}`,
value: [rankId, startTime, endTime, duration, source, item.operatorName],
itemStyle: {
normal: {
color: theme.colorPalette[colorPalette[hashToNumber(item.operatorName, colorPalette.length)]],
},
},
};
};
const baseSeire = {
type: 'custom',
itemStyle: {
opacity: 1,
},
encode: {
x: [1, 2],
y: 0,
},
data: [],
};
function getSeries({ isCompare, data }: { isCompare: boolean; data: any[] }): any[] {
return [{ ...baseSeire, data, renderItem: getRenderItem(isCompare) }];
}
function getRenderItem(isCompare: boolean): any {
return (params: any, api: any): any => {
const categoryIndex = api.value(0);
const start = api.coord([api.value(1), categoryIndex]);
const end = api.coord([api.value(2), categoryIndex]);
const height = api.size([0, 1])[1] * 0.6 * (isCompare ? 0.5 : 1);
let y;
if (isCompare) {
const isComparison = api.value(4) === compareSource.COMPARISON;
y = isComparison ? start[1] - height : start[1] + (height / 3);
} else {
y = start[1] - (height / 2);
}
const rectShape = echarts.graphic.clipRectByRect(
{
x: start[0],
y,
width: end[0] - start[0],
height,
},
{
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height,
},
);
return (
{
type: 'rect',
transition: ['shape'],
shape: rectShape,
name: 'op',
style: api.style(),
emphasis: {
style: {
stroke: '#999999',
lineWidth: 1,
opacity: 0.6,
},
},
}
);
};
}
function getTooltip({ isCompare }: { isCompare: boolean }): any {
return {
formatter: (params: FormatterParams): string => {
let tooltipMarkup = `${params.marker} `;
let getName = (val: string): string => val;
if (isCompare) {
const isBaseline = params.value[sourceIndex] === compareSource.BASELINE;
getName = isBaseline ? getBaselineName : getCompareName;
}
tooltipMarkup += getTipLineStr('Rank ID', `${params.value[0]}`);
tooltipMarkup += getTipLineStr(getName('Operator Name'), `${params.value[5]}`);
tooltipMarkup += getTipLineStr(getName('Start Time'), `${numberToStr(params.value[1])}ms`);
tooltipMarkup += getTipLineStr(getName('Elapse Time'), `${numberToStr(params.value[3])}ms`);
return tooltipMarkup;
},
};
}
function numberToStr(value: number): string {
return `${value.toFixed(6).replace(/\.?0+$/, '')}`;
}
function nsToMs(value: number): number {
return value * NS_TO_MS_FACTOR;
}
function msToNs(value: number): number {
return Math.round(value / NS_TO_MS_FACTOR);
}
function getTipLineStr(name: string, value: string): string {
let html = `${i18n.t(`tableHead.${name}`, { ns: 'communication' })}: `;
html += `<strong style="color: black">${safeStr((`${value}`))}</strong><br/>`;
return html;
}
const option: any = {
textStyle: getDefaultChartOptions().textStyle,
dataZoom: [
{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: false,
top: DEFAULT_CHART_ZOOM_HEIGHT,
labelFormatter: '',
start: 0,
end: 100,
xAxisIndex: 0,
bottom: 10,
height: 20,
borderColor: '#d2dbee80',
},
{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: false,
labelFormatter: '',
start: 0,
end: 100,
yAxisIndex: 0,
right: 10,
width: 20,
borderColor: '#d2dbee80',
},
{
type: 'inside',
filterMode: 'weakFilter',
zoomOnMouseWheel: 'ctrl',
moveOnMouseWheel: 'shift',
},
],
grid: {
left: 100,
right: 120,
height: DEFAULT_INNER_CHART_HEIGHT,
},
xAxis: {
scale: true,
name: 'Time(ms)',
axisLabel: {
formatter: function (val: number) {
return numberToStr(Math.max(0, val));
},
},
},
yAxis: {
data: [],
name: 'Rank ID',
},
series: [],
};
export interface OperatorTimeItem {
operatorName: string;
startTime: number;
duration: number;
}
export interface OperatorTimeInfo {
rankId: string;
dbPath: string;
lists: CompareData<OperatorTimeItem[]>;
}
export interface AnalysisChartData {
minTime: number;
maxTime: number;
data: OperatorTimeInfo[];
}
interface OpDetail {
name: string;
rankId: number;
dbPath: string;
timestamp: number;
duration: number;
}
let selectedOpDetail: OpDetail | null;
interface ChartInstance {
chart: echarts.ECharts;
resizeObserver: ResizeObserver;
cleanup: () => void;
}
const chartInstanceMap: WeakMap<HTMLElement, ChartInstance> = new WeakMap<HTMLElement, ChartInstance>();
* 初始化图表
* @param chartDom 图表实例
* @param setDropDownVisible 设置下拉菜单可见性的函数
* @return 初始化的ECharts实例或 null
*/
function initChartInstance(chartDom: HTMLElement, setDropDownVisible: (_: boolean) => void): echarts.ECharts | null {
if (chartInstanceMap.has(chartDom)) {
return chartInstanceMap.get(chartDom)?.chart ?? null;
}
const chart = getAdaptiveEchart(chartDom, option);
if (!chart) {
return null;
}
let resizeObserverTimer: number | null = null;
const resizeObserver = new ResizeObserver((): void => {
if (resizeObserverTimer) {
clearTimeout(resizeObserverTimer);
}
resizeObserverTimer = window.setTimeout((): void => {
if (!chart.isDisposed() && chartDom.offsetHeight > 0 && chartDom.offsetWidth > 0) {
chart.resize();
}
resizeObserverTimer = null;
}, 100);
});
resizeObserver.observe(chartDom);
const contextMenuHandler = (e: echarts.ECElementEvent): void => {
setDropDownVisible(true);
const [rankId, timestamp, , duration, , operatorName] = e.value as RectItemValues;
const rankMap = (chart as any)._currentRankMap || new Map();
selectedOpDetail = {
name: operatorName,
rankId,
dbPath: rankMap.get(rankId.toString()) || '',
timestamp: msToNs(timestamp),
duration: msToNs(duration),
};
};
chart.on('contextmenu', { element: 'op' }, contextMenuHandler);
const cleanup = (): void => {
resizeObserver.disconnect();
chart.off('contextmenu', contextMenuHandler);
disposeAdaptiveEchart(chartDom);
chartInstanceMap.delete(chartDom);
};
chartInstanceMap.set(chartDom, { chart, resizeObserver, cleanup });
return chart;
}
* 更新图表数据,轻量级,高频调用
* @param chart 图表实例
* @param dataSource 数据源
* @param session 会话
*/
function updateChartData(chartDom: HTMLElement, dataSource: AnalysisChartData, session: Session): boolean {
const instance = chartInstanceMap.get(chartDom);
if (!instance || instance.chart.isDisposed()) {
return false;
}
const { chart } = instance;
const rankDbPathMap = new Map<string, string>();
dataSource?.data?.forEach(item => rankDbPathMap.set(item.rankId, item.dbPath));
(chart as any)._currentRankMap = rankDbPathMap;
if (dataSource !== undefined) {
chart.setOption(wrapData(dataSource, session.isCompare, session.communicationChartZoomData), { notMerge: true });
}
return true;
}
* 清理资源,组件卸载时调用
* @param chartDom 图表实例
*/
function disposeChartInstance(chartDom: HTMLElement): void {
const instance = chartInstanceMap.get(chartDom);
if (instance) {
instance.cleanup();
}
}
* 获取图表高度
* @param dataSource - 分析图表数据
* @returns 图表高度
*/
function calculateDataHeight(dataSource: AnalysisChartData): number {
let calculateHeight: number;
if (dataSource?.data?.length !== undefined) {
calculateHeight = Math.max(dataSource.data.length * MIN_CHART_ITEM_HEIGHT, DEFAULT_INNER_CHART_HEIGHT);
} else {
calculateHeight = DEFAULT_INNER_CHART_HEIGHT;
}
return Math.min(MAX_CHART_HEIGHT, calculateHeight);
}
function getChartHeight(dataSource: AnalysisChartData): number {
return calculateDataHeight(dataSource) + DEFAULT_CHART_HEIGHT - DEFAULT_INNER_CHART_HEIGHT;
}
* 重定向到时间线
* @param setDropDownVisible - 设置下拉菜单可见性的函数
* @returns 无返回值
*/
async function redirectToTimeline(setDropDownVisible: (_: boolean) => void): Promise<void> {
if (selectedOpDetail === null) {
return;
}
const { name, rankId, dbPath, duration } = selectedOpDetail;
const params = {
name,
rankId: rankId.toString(),
dbPath,
};
try {
const res = await queryTimelineUnitKernelDetail(params);
setDropDownVisible(false);
const resObj = res ?? {};
connector.send({
event: 'switchModule',
body: {
switchTo: 'timeline',
toModuleEvent: 'locateUnit',
params: {
...resObj,
...params,
processId: resObj.pid,
startTime: resObj.startTime,
rankId: resObj.rankId,
duration,
showSelectedData: true,
},
},
});
} catch (e) {
setDropDownVisible(false);
}
}
* 更新时间线加载状态
* @param isLoading - 是否正在加载
*/
const findInTimelineLoad = (isLoading: boolean): void => {
const element = document?.getElementById('findInTimeline');
if (!element) {
return;
}
if (isLoading) {
element.classList.add('find-in-time-line-load');
} else {
element.classList.remove('find-in-time-line-load');
}
};
* 获取图表缩放数据
* @param chartInstance - 图表实例
* @returns 缩放数据
*/
const getZoomData = (chartInstance: ECharts | null): ChartZoomData => {
const currentOption = chartInstance?.getOption();
const { start = 0, end = 100 } = (currentOption?.dataZoom as InsideDataZoomComponentOption[])?.[0] || [];
return {
start,
end,
};
};
* 生成菜单项
* @param session - 会话对象
* @param setDropDownVisible - 设置下拉菜单可见性的函数
* @param chartInstance - 图表实例
* @returns 菜单项数组
*/
const useMenuItems = (session: Session, setDropDownVisible: (_: boolean) => void, chartInstance: ECharts | null): MenuProps['items'] => {
const { t } = useTranslation('communication');
const findInTimeline = {
label: t('Find in Timeline'),
key: 'findInTimeline',
id: 'findInTimeline',
disabled: false,
onClick: () => {
findInTimelineLoad(true);
setTimeout(() => {
redirectToTimeline(setDropDownVisible);
});
},
};
const alignOperator = {
label: t('Align according to selected operator'),
key: 'alignAccordingToSelectedOperator',
disabled: false,
onClick: (): void => {
setDropDownVisible(false);
if (selectedOpDetail === null) {
return;
}
session.communicationChartZoomData = getZoomData(chartInstance);
session.targetOperator = selectedOpDetail as ClickOperatorItem;
},
};
const restoredefault = {
label: t('Restore default state'),
key: 'restoreDefaultState',
disabled: false,
onClick: (): void => {
session.communicationChartZoomData = getZoomData(chartInstance);
session.targetOperator = undefined;
},
};
if (session.unitcount === 0) {
findInTimeline.disabled = true;
}
if (session.targetOperator === undefined) {
restoredefault.disabled = true;
}
return [
findInTimeline,
alignOperator,
restoredefault,
];
};
* Y轴的接口定义。
*
* @interface YAxis
* @property {string[]} data - Y轴的数据,表示为字符串数组。
* @property {string} [name] - Y轴的名称,可选属性。
* @property {boolean} [show] - 是否显示Y轴,可选属性。
*/
export interface YAxis {
data: string[];
name?: string;
show?: boolean;
}
* CommunicationTimeAnalysisChart组件,用于展示通信时间分析图表。
* @param dataSource - 分析图表的数据源。
* @param session - 当前会话对象。
* @param loading - 是否正在加载数据。
* @returns 返回一个React组件,用于展示通信时间分析图表。
*/
const CommunicationTimeAnalysisChart = observer(({ dataSource, session, loading }: { dataSource: AnalysisChartData; session: Session; loading: boolean }) => {
const [chartHeight, setChartHeight] = useState(DEFAULT_CHART_HEIGHT);
const [dropDownVisible, setDropDownVisible] = useState(false);
const chartRef = useRef<HTMLDivElement>(null);
const chartInst = useRef<echarts.ECharts | null>(null);
const menuItems = useMenuItems(session, setDropDownVisible, chartInst.current);
* 同步滚动事件处理函数,用于处理鼠标滚轮缩放时的页面滚动问题。
* @param e - 鼠标滚轮事件对象。
* @returns 无返回值。
*/
const syncScroll = (e: WheelEvent): void => {
if ((e.target as HTMLElement).tagName !== 'CANVAS') {
return;
}
if (!e.ctrlKey && !e.shiftKey) {
const scrollContainer = document.querySelector('.mi-page-content');
scrollContainer?.scrollBy(0, e.deltaY);
}
};
const updateData = React.useCallback((dataSource: AnalysisChartData): void => {
const chartDom = chartRef.current;
if (chartDom && chartInst.current && dataSource) {
runInAction(() => {
session.communicationChartZoomData = undefined;
updateChartData(chartDom, dataSource, session);
});
setTimeout(() => {
setChartHeight(getChartHeight(dataSource));
});
}
}, [chartRef.current, chartInst.current, session]);
* 使用useEffect更新图表数据
* @param
* @returns void
*/
useEffect(() => {
updateData(dataSource);
}, [updateData, dataSource]);
* 使用useEffect初始化图表
* @param
* @returns void
*/
useEffect(() => {
const dom = chartRef.current;
if (!dom) {
return;
}
const firstInitChart = (): void => {
chartInst.current = initChartInstance(dom, setDropDownVisible);
chartRef.current?.addEventListener('wheel', syncScroll, true);
updateData(dataSource);
};
let resizeObserverTimer: number | null = null;
const resizeObserver = new ResizeObserver((): void => {
if (resizeObserverTimer) {
clearTimeout(resizeObserverTimer);
}
resizeObserverTimer = window.setTimeout((): void => {
if (dom.clientHeight > 0 && dom.clientWidth > 0) {
resizeObserver.disconnect();
firstInitChart();
}
resizeObserverTimer = null;
}, 20);
});
resizeObserver.observe(dom);
if (dom.clientHeight > 0 && dom.clientWidth > 0) {
resizeObserver.disconnect();
firstInitChart();
}
return (): void => {
resizeObserver.disconnect();
disposeChartInstance(dom);
chartRef.current?.removeEventListener('wheel', syncScroll, true);
};
}, [updateData]);
* 监听并处理慢算子点击事件的函数。
* @param res - 包含慢算子事件参数的对象,包括起始值、结束值、算子名称和算子ID。
* @returns 无返回值。
*/
useEventBus('onClickSlowRankOp', (res): void => {
const { startValue, endValue, name, rankId } = res as OnClickSlowRankOpCallbackParams;
const dataSourceAxisY = chartInst.current?.getOption().yAxis as YAxis[];
const dataSourceLength = dataSourceAxisY[0]?.data.length || 0;
let startAxisY = 0;
let endAxisY = 100;
if (dataSourceLength >= START_POSITION_AXIS_Y) {
const tempSourceIndex = Number.isNaN(rankId) ? 0 : Number(rankId);
const rankIdNumber = dataSourceAxisY[0].data.findIndex(item => Number(item) === tempSourceIndex);
startAxisY = Math.floor(Math.max(0, rankIdNumber - 10) / dataSourceLength * 100);
endAxisY = Math.floor(Math.min(dataSourceLength, rankIdNumber + 10) / dataSourceLength * 100);
}
chartInst.current?.dispatchAction({
type: 'dataZoom',
startValue,
endValue,
});
chartInst.current?.dispatchAction({
type: 'dataZoom',
dataZoomIndex: 1,
start: startAxisY,
end: endAxisY,
});
chartInst.current?.dispatchAction({
type: 'downplay',
seriesIndex: 0,
});
chartInst.current?.dispatchAction({
type: 'highlight',
seriesIndex: 0,
name: `${rankId}-${name}`,
});
setTimeout(() => {
chartInst.current?.dispatchAction({
type: 'downplay',
seriesIndex: 0,
name: `${rankId}-${name}`,
});
}, 5000);
chartRef.current?.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
});
return session.durationFileCompleted
? <Dropdown
menu={{
items: menuItems,
onBlur: (e: React.FocusEvent<HTMLUListElement, Element>): void => {
const hasItem = menuItems?.findIndex(item =>
(e.relatedTarget as HTMLElement)?.dataset?.menuId?.includes(item?.key as string)) !== -1;
if (!hasItem) {
setDropDownVisible(false);
}
},
}}
trigger={['contextMenu']}
open={dropDownVisible}
autoFocus
>
<Spin spinning={loading} delay={400}>
<div ref={chartRef} id={'hccl'} style={{ width: 'calc(100vw - 80px)', height: chartHeight }}></div>
</Spin>
</Dropdown>
: <div style={{ height: '400px' }}><Loading style={{ margin: '200px auto 0' }}/></div>;
});
export default CommunicationTimeAnalysisChart;