* -------------------------------------------------------------------------
* 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 { observer } from 'mobx-react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from '@emotion/styled';
import { Button, Select, FormItem, Tooltip } from '@insight/lib/components';
import {
getColumnSearchProps,
getDefaultColumData,
getPageData,
statsSystemViewItems,
expertSystemViewItems,
getVisibleStatsSystemViewItems,
type IQueryCondition,
SystemViewItem, queryTableDataNameList, type IndexedSystemViewItem, ftraceTypes,
} from './Common';
import { ResizeTable } from '@insight/lib/resize';
import { limitInput, StyledEmpty, GroupCardRankInfosByHost, getRankInfoLabel } from '@insight/lib/utils';
import type { CardMetaData } from '../../entity/data';
import { ChartErrorBoundary } from '../error/ChartErrorBoundary';
import { getTimeOffset } from '../../insight/units/utils';
import { getDetailTimeDisplay } from '../../insight/units/AscendUnit';
import { HelpIcon } from '@insight/lib/icon';
import { StatsSystemView } from './StatsSystemView';
import { ExpertSystemView, handleAdvisorSelected } from './ExpertSystemView';
import { EventView } from './EventsView';
import { TableDataView } from './TableDataView';
import { Session } from '../../entity/session';
import type { BaseSummaryRowItemType, CardRankInfo } from '../../api/interface';
import { ProjectType } from '../../entity/insight';
export const DETAIL_HEADER_HEIGHT_ETC_PX = 146;
const Container = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-flow: nowrap;
background-color: ${(p): string => p.theme.bgColorDark};
.ant-tree {
width: 280px;
height: 100%;
background-color: ${(p): string => p.theme.contentBackgroundColor};
overflow: auto;
}
.ant-tree-node-selected {
background-color: var(--grey50) !important;
}
.ant-divider-vertical {
height: 100%;
border-color: var(--grey80);
}
`;
const AsideSelectContainer = styled.div`
display: flex;
flex-direction: column;
flex: none;
padding: 8px 16px;
margin-right: 8px;
border-radius: 4px;
background-color: ${(p): string => p.theme.bgColor};
max-width: 400px;
.time-range-info{
margin-bottom: 8px;
flex: none;
}
.view-select{
margin-bottom: 8px;
flex: none;
}
.rank-filter{
margin-bottom: 8px;
flex: none;
color: ${(p): string => p.theme.textColorSecondary};
}
`;
const SelectContentContainer = styled.div`
flex: 1;
padding: 8px 16px;
height: 100%;
border-radius: 4px;
overflow: hidden;
background-color: ${(p): string => p.theme.bgColor};
.ant-table-wrapper {
height: 100%;
}
`;
const AsideSelectList = styled.div`
flex: 1;
overflow: auto;
& .aside-select-item {
display: flex;
align-items: center;
cursor: pointer;
color: ${(props): string => props.theme.textColorSecondary};
+ .aside-select-item {
margin-top: 8px;
}
&.selected{
color: ${(props): string => props.theme.primaryColor};
}
}
`;
export interface SelectedCardInfo {
cardId: string;
dbPath: string;
}
* 添加服务化视图描述字段接口
*/
export interface ServiceLayers {
name: string;
description: string;
}
interface ConditionType<T, K> {
options: T[];
value: K;
}
interface HostConditionType extends ConditionType<string, string> {
cardsMap?: Map<string, CardRankInfo[]>;
}
export interface SelectContentViewProps {
key: string | number;
card: SelectedCardInfo;
session: Session;
bottomHeight: number;
}
type SelectContentViewComponent<T extends SelectContentViewProps = SelectContentViewProps> = React.FC<T>;
export const DEFAULT_CARD_VALUE = { cardId: '', dbPath: '' };
const findRankCardInfo = (session: Session, card: SelectedCardInfo): CardRankInfo | undefined => {
return Array.from(session.rankCardInfoMap.values()).find((cardRankInfo) => {
return cardRankInfo.rankInfo.rankId === card.cardId && cardRankInfo.dbPath === card.dbPath;
});
};
const isRequestCardTypeMismatch = (session: Session, card: SelectedCardInfo, isFtrace: boolean): boolean => {
const rankCardInfo = findRankCardInfo(session, card);
if (isFtrace) {
return rankCardInfo?.isFtrace !== true;
}
return rankCardInfo?.isFtrace === true;
};
export const SystemView = observer((props: any) => {
const [viewOption, setViewOption] = useState(0);
const [key, setKey] = useState(0);
const isFtraceStatsItem = viewOption === 0 && ftraceTypes.includes(statsSystemViewItems[key]?.name ?? '');
const SelectContent = useMemo(() => {
if (viewOption === 0 && key >= statsSystemViewItems.length) {
return null;
}
return contentList[viewOption][key];
}, [viewOption, key]);
const [conditions, setConditions] = useState<SelectedCardInfo>(DEFAULT_CARD_VALUE);
const handleChange = (card: SelectedCardInfo): void => {
setConditions(card);
};
const handleViewChange = (value: number): void => {
setViewOption(value);
setKey(0);
};
useEffect(() => {
if (props.session.showEvent !== undefined) {
setViewOption(2);
setKey(0);
}
}, [props.session.showEvent]);
return (<Container>
<AsideSelectContainer>
{viewOption !== 3 && (<TimeRangeInfo session={props.session}></TimeRangeInfo>)}
<ViewSelect viewOption={viewOption} handleViewChange={handleViewChange}/>
{viewOption !== 2 && (
<RankFilter
session={props.session}
viewOption={viewOption}
handleChange={handleChange}
isFtraceRankOnly={isFtraceStatsItem}
excludeFtraceRank={viewOption === 0 && !isFtraceStatsItem}
/>
)}
<SelectList viewOption={viewOption} selectKey={key} setKey={setKey} card={conditions} session={props.session}></SelectList>
</AsideSelectContainer>
<ChartErrorBoundary>
<SelectContentContainer>
{viewOption === 0 && key >= statsSystemViewItems.length
? <TableDataView key={key - statsSystemViewItems.length} selectKey={key - statsSystemViewItems.length} card={conditions} session={props.session}
bottomHeight={props.bottomHeight}></TableDataView>
: SelectContent && (<SelectContent key={key} card={conditions} session={props.session}
bottomHeight={props.bottomHeight}></SelectContent>)}
</SelectContentContainer>
</ChartErrorBoundary>
</Container>);
});
const TimeRangeInfo = observer((props: { session: Session }) => {
const { session } = props;
const { t } = useTranslation('timeline');
return (
session.isTimeAnalysisMode && session.timeAnalysisRange
? <div className={'time-range-info'}>
{t('contextMenu.Time Filter')}
{': '}
<strong>{getDetailTimeDisplay(session.timeAnalysisRange[0])}</strong>
{' '}
{t('contextMenu.to')}
{' '}
<strong>{getDetailTimeDisplay(session.timeAnalysisRange[1])}</strong>
</div>
: <></>
);
});
const ViewSelect = observer((props: any) => {
const { viewOption, handleViewChange } = props;
const { t } = useTranslation('timeline', { keyPrefix: 'systemView' });
const options = [{ label: t('Stats System View'), value: 0 }, { label: t('Expert System View'), value: 1 }, { label: t('Events View'), value: 2 }];
return (
<div className={'view-select'}>
<Select id={'select-system-view'} width={'100%'} value={viewOption} onChange={handleViewChange} options={options}/>
</div>
);
});
export const RankFilter = observer((props: {
session: Session;
viewOption?: number;
handleChange: (v: SelectedCardInfo) => void;
defaultRankId?: string;
isFtraceRankOnly?: boolean;
excludeFtraceRank?: boolean;
}): JSX.Element => {
const [rankCondition, setRankCondition] = useState<ConditionType<CardRankInfo, number | undefined>>({ options: [], value: undefined });
const [hostCondition, setHostCondition] = useState<HostConditionType>({ options: [], value: '' });
const { t } = useTranslation('timeline');
const excludeFtraceRank = props.excludeFtraceRank ?? false;
useEffect(() => {
const cardList: CardRankInfo[] = [];
for (const v of props.session.rankCardInfoMap.values()) {
if (v.rankInfo.rankId.endsWith('Host')) {
continue;
}
if (props.isFtraceRankOnly && v.isFtrace !== true) {
continue;
}
if (excludeFtraceRank && v.isFtrace === true) {
continue;
}
cardList.push(v);
}
if (props.isFtraceRankOnly) {
setHostCondition({ options: [], value: '', cardsMap: new Map([['', cardList]]) });
return;
}
const { hosts, cardsMap }: { hosts: string[]; cardsMap: Map<string, CardRankInfo[]> } = GroupCardRankInfosByHost(cardList);
let initialHost = hosts[0] ?? '';
if (props.defaultRankId !== undefined && props.defaultRankId !== 'ALL') {
for (const [host, cards] of cardsMap.entries()) {
if (cards.some(card => card.rankInfo.rankId === props.defaultRankId)) {
initialHost = host;
break;
}
}
}
setHostCondition({ options: hosts, value: initialHost, cardsMap });
}, [props.session.rankCardInfoMap.size, props.defaultRankId, props.isFtraceRankOnly, excludeFtraceRank]);
useEffect(() => {
const rankOptions = hostCondition.cardsMap?.get(hostCondition.value) ?? [];
let initialIndex = rankOptions.length > 0 ? 0 : undefined;
if (props.defaultRankId !== undefined && props.defaultRankId !== 'ALL') {
const foundIndex = rankOptions.findIndex(card => card.rankInfo.rankId === props.defaultRankId);
if (foundIndex !== -1) {
initialIndex = foundIndex;
}
}
setRankCondition({ options: rankOptions, value: initialIndex });
}, [hostCondition, props.defaultRankId]);
useEffect(() => {
if (rankCondition.value === undefined) {
props.handleChange({ cardId: '', dbPath: '' });
return;
}
const cardRankInfo = rankCondition.options[rankCondition.value];
props.handleChange({ cardId: cardRankInfo.rankInfo.rankId, dbPath: cardRankInfo.dbPath });
}, [rankCondition]);
useEffect(() => {
limitInput();
}, []);
const onRankIdChanged = (value: number): void => {
setRankCondition({ ...rankCondition, value });
};
return (<div className={'rank-filter'} >
{!props.isFtraceRankOnly && hostCondition.options.length > 0
? <FormItem label={t('Host')} contentStyle={{ flex: 1, minWidth: 0 }}>
<Select
value={hostCondition.value}
width={'100%'}
onChange={(value: string): void => setHostCondition({ ...hostCondition, value })}
options={hostCondition.options.map((host) => ({ value: host, label: host }))}
/>
</FormItem>
: <></>
}
<FormItem label={t('Rank ID')} contentStyle={{ flex: 1, minWidth: 0 }}>
<Select
value={rankCondition.value}
width={'100%'}
onChange={onRankIdChanged}
options={rankCondition.options.map((card, index) => {
return {
value: index,
label: getRankInfoLabel(card.rankInfo),
};
})}
showSearch={true}
/>
</FormItem>
</div>);
});
* 国际化-中文
*/
const LANGUAGE_ZH = 'zhCN';
const SelectList = observer((props: { session: Session; viewOption: number; selectKey: number; setKey: (v: number) => void; card: SelectedCardInfo}) => {
const [selectedKey, setSelectedKey] = useState(0);
const [systemViewItems, setSystemViewItems] = useState<SystemViewItem[]>([]);
const { t } = useTranslation('timeline', { keyPrefix: 'systemView' });
const handleClick = (key: number): void => {
props.setKey(key);
setSelectedKey(key);
};
const params = useMemo(() => {
return { rankId: props.card.cardId, dbPath: props.card.dbPath, isZh: props.session.language === LANGUAGE_ZH };
}, [props.card, props.session.language]);
useEffect(() => {
setSelectedKey(props.selectKey);
}, [props.selectKey]);
const displayItems = useMemo(() => {
if (props.viewOption !== 0) {
return systemViewItems.map((item, index) => ({ ...item, originIndex: index }));
}
return getVisibleStatsSystemViewItems(
systemViewItems,
props.session.hasFtraceData,
props.session.hasNonFtraceData,
);
}, [props.viewOption, props.session.hasFtraceData, props.session.hasNonFtraceData, systemViewItems]);
useEffect(() => {
switch (props.viewOption) {
case 0: {
if (params.rankId !== undefined && params.rankId !== '') {
queryTableDataNameList(params).then((res) => {
const layers = res.layers as SystemViewItem[];
if (layers !== undefined && layers.length > 0) {
const merged = Array.from(
new Map([...statsSystemViewItems, ...layers].map(item => [item.name, item])).values(),
);
setSystemViewItems(merged);
} else {
setSystemViewItems(statsSystemViewItems);
}
}).catch(() => {
setSystemViewItems(statsSystemViewItems);
});
} else {
setSystemViewItems(statsSystemViewItems);
}
break;
}
case 1:
setSystemViewItems(expertSystemViewItems);
break;
case 2:
setSystemViewItems([]);
break;
default:
break;
}
}, [props.viewOption, params, props.session.language]);
useEffect(() => {
if (props.viewOption !== 0 || displayItems.length === 0) {
return;
}
if (!displayItems.some(item => item.originIndex === props.selectKey)) {
props.setKey(displayItems[0].originIndex);
setSelectedKey(displayItems[0].originIndex);
}
}, [props.viewOption, props.selectKey, props.setKey, displayItems]);
const renderItem = (item: IndexedSystemViewItem): JSX.Element => {
const isDynamicItem = item.originIndex >= statsSystemViewItems.length;
return (<div
className={`aside-select-item ${selectedKey === item.originIndex ? 'selected' : ''}`}
key={item.originIndex}
onClick={(): void => handleClick(item.originIndex)}
>
{isDynamicItem
? <Tooltip title={item.description}>
<div>{item.name}</div>
</Tooltip>
: <>
<div>{t(item.name)}</div>
{
item.tips !== undefined &&
<Tooltip title={t(item.tips)} placement={'topLeft'}>
<HelpIcon style={{ cursor: 'pointer', marginLeft: 4 }} height={20} width={20} />
</Tooltip>
}
</>}
</div>);
};
return (<AsideSelectList>
{
displayItems.map(renderItem)
}
</AsideSelectList>
);
});
export interface BaseSummaryProps extends SelectContentViewProps {
layerType?: string;
request: (...rest: any[]) => any;
isStats?: boolean;
isFtrace?: boolean;
isTrace?: boolean;
columns: any;
}
export const BaseSummary = observer((props: BaseSummaryProps) => {
const isStats = props.isStats as boolean;
const isFtrace = props.isFtrace as boolean;
const isTrace = props.isTrace as boolean;
const getDefaultSorter = (): {field: string;order: string} => {
if (isStats) {
return { field: 'totalTime', order: 'descend' };
}
if (isTrace) {
return { field: 'startTime', order: 'ascend' };
}
return { field: 'duration', order: 'descend' };
};
const defaultPage = { current: 1, pageSize: 10, total: 0 };
const defaultSorter = getDefaultSorter();
const [dataSource, setDataSource] = useState<any[]>([]);
const [page, setPage] = useState(defaultPage);
const [sorter, setSorter] = useState(defaultSorter);
const [isLoading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [searchedColumn, setSearchedColumn] = useState('');
const [rowData, setRowData] = useState<Partial<BaseSummaryRowItemType>>({});
const { t } = useTranslation('timeline', { keyPrefix: 'tableHead' });
const status = props.session.units.find((unit: any) => (unit.metadata as CardMetaData).cardId === props.card.cardId)?.phase;
let columns = props.columns?.map((col: any) => ({
...col,
title: t(col.title),
}));
if (isStats || isTrace) {
columns = [{
title: t('Name'),
dataIndex: 'name',
...getDefaultColumData('name'),
...getColumnSearchProps({ dataIndex: 'name', setSearchText, searchText, setSearchedColumn, searchedColumn }),
}, ...columns];
} else if (!isFtrace) {
columns = [...columns, {
title: t('Click To Timeline'),
dataIndex: 'click',
key: 'click',
ellipsis: true,
render: (_: any, record: any): JSX.Element => (<Button type="link"
onClick={(): void => {
setRowData({ name: record.name ?? record.originOptimizer, ...record });
}}>{t('Click')}</Button>),
}];
}
const updateData = async(searchName: string, pages: any, sorters: {field: string;order: string}, prop: BaseSummaryProps): Promise<void> => {
const _isStats = prop.isStats as boolean;
const _isFtrace = prop.isFtrace as boolean;
const _isTrace = prop.isTrace as boolean;
const targetInfo = props.session.units.find(unitItem => (unitItem.metadata as CardMetaData)?.cardId === props.card?.cardId);
if (props.card === undefined || props.card.cardId === '' || targetInfo?.projectType === ProjectType.IE) {
setDataSource([]);
setPage(defaultPage);
return;
}
if (isRequestCardTypeMismatch(prop.session, prop.card, _isFtrace)) {
setDataSource([]);
setPage(defaultPage);
return;
}
setLoading(true);
let startTime = prop.session.timeAnalysisRange?.[0] ?? 0;
startTime = startTime < 0 ? 0 : startTime;
let endTime = prop.session.timeAnalysisRange?.[1] ?? 0;
endTime = endTime < 0 ? 0 : endTime;
const timestampOffset = getTimeOffset(prop.session, { cardId: prop.card.cardId });
const sortedField = sorters.field === 'startTimeLabel' ? 'startTime' : sorters.field;
let params: IQueryCondition = {
rankId: prop.card.cardId,
dbPath: prop.card.dbPath,
pageSize: pages.pageSize,
current: pages.current,
orderBy: sorters.order ? sortedField : defaultSorter.field,
order: sorters.order ?? defaultSorter.order,
startTime: Math.floor(startTime + timestampOffset),
endTime: Math.ceil(endTime + timestampOffset),
};
if (_isStats || _isTrace) {
params = { isQueryTotal: true, layer: prop.layerType, searchName, ...params };
}
if (_isFtrace) {
params = { layer: prop.layerType, ...params };
}
try {
const res = await props.request(params);
if (_isStats) {
setDataSource(res.systemViewDetails);
} else if (_isTrace) {
const data = res.systemViewDetails.map((item: any) => ({
...item,
startTimeLabel: getDetailTimeDisplay(item.startTime - timestampOffset),
}));
setDataSource(data);
} else {
const timestampoffset = getTimeOffset(props.session, props.card);
const dbPath = res.dbPath;
const data = res.data.map((item: any) => {
item.startTimeLabel = getDetailTimeDisplay(item.startTime - timestampoffset);
item.dbPath = dbPath;
return item;
});
setDataSource(data);
}
setPage({ ...page, total: res.count });
} catch (err) {
setDataSource([]);
setPage(defaultPage);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (status === 'download') {
updateData(searchText, page, sorter, props);
}
}, [sorter, props.card.cardId, status, props.session.timeAnalysisRange]);
useEffect(() => {
if (rowData.name === null || rowData.name === undefined) {
return;
}
handleAdvisorSelected(rowData as BaseSummaryRowItemType, props);
}, [rowData]);
return (
(status === 'download' && props.card !== undefined && props.card.cardId !== '')
? <ResizeTable
onChange={(pagination: any, filters: any, nwSorter: any): void => {
setSorter(nwSorter);
}}
rowClassName={(record: any): string => {
return record.id !== undefined && record.id === rowData.id ? 'selected-row' : 'click-able';
}}
pagination={getPageData(page, setPage)}
dataSource={dataSource}
columns={columns}
size="small"
scroll={{ y: props.bottomHeight - DETAIL_HEADER_HEIGHT_ETC_PX }}
loading = {isLoading}/>
: <div style={{ display: 'flex', height: '100%' }}>
<StyledEmpty style={{ margin: 'auto' }}/>
</div>
);
});
const contentList: SelectContentViewComponent[][] = [StatsSystemView, ExpertSystemView, [EventView]];