* -------------------------------------------------------------------------
* 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 { runInAction } from 'mobx';
import { register } from './register';
import type { Session } from '../entity/session';
import { ThreadMetaData } from '../entity/data';
import { ThreadUnit } from '../insight/units/AscendUnit';
import { type ChartDesc, InsightUnit, UnitHeight } from '../entity/insight';
import { message } from 'antd';
import type { StackStatusConfig } from '../entity/chart';
import i18n from '@insight/lib/i18n';
import { checkIsSliceMode } from '../components/charts/ChartInteractor/draw';
import { isStreamUnit } from '../utils';
import { ThreadGroup } from '../entity/mergedThreadData';
const clearSelectedUnits = (session: Session): void => {
session.selectedUnits = [];
};
const findInsertIndex = (allUnits: InsightUnit[], selectedUnits: InsightUnit[]): number => {
let minIndex = Infinity;
for (const sUnit of selectedUnits) {
const sMetaData = sUnit.metadata as ThreadMetaData;
const indexInAllUnits = allUnits.findIndex(aUnit => {
const aMetaData = aUnit.metadata as ThreadMetaData;
return sMetaData.threadId !== '' ? aMetaData.threadId === sMetaData.threadId : aMetaData.threadIdList === sMetaData.threadIdList;
});
if (indexInAllUnits !== -1 && indexInAllUnits < minIndex) {
minIndex = indexInAllUnits;
}
}
return minIndex === Infinity ? 0 : minIndex;
};
const extractThreadIds = (units: InsightUnit[]): string[] => {
return units.flatMap(unit => {
const { threadIdList, threadId } = unit.metadata as ThreadMetaData;
return threadIdList ?? [threadId];
}) as string[];
};
const getThreadNameList = (threadIds: string[], firstUnit: InsightUnit): string[] => {
const childrenUnits = firstUnit.parent?.children ?? [];
const threadMap = new Map<string, ThreadMetaData>();
for (const unit of childrenUnits) {
const meta = unit.metadata as ThreadMetaData;
if (meta.threadId) {
threadMap.set(meta.threadId, meta);
}
}
return threadIds.sort().map(threadId => {
const meta = threadMap.get(threadId);
return meta?.threadName?.replace(/^Stream\s*/, '') ?? '';
});
};
const getMergedUnitMetaData = (selectedUnits: InsightUnit[]): ThreadMetaData => {
const [firstUnit] = selectedUnits;
const firstMeta = firstUnit.metadata as ThreadMetaData;
const threadIdList = extractThreadIds(selectedUnits);
const threadNameList = getThreadNameList(threadIdList, firstUnit);
return {
...firstMeta,
threadIdList,
threadId: '',
threadName: `Stream Merged (${threadNameList.join(', ')})`,
};
};
const markMergedUnits = (selectedUnits: InsightUnit[], allUnitList: InsightUnit[]): void => {
for (const unit of selectedUnits) {
const meta = unit.metadata as ThreadMetaData;
if (meta.threadIdList) {
const index = allUnitList.indexOf(unit);
if (index === -1) { continue; }
allUnitList.splice(index, 1);
} else {
unit.isMerged = true;
}
}
};
const createMergedUnit = (mergedMeta: ThreadMetaData): InsightUnit => {
const threadUnit = new ThreadUnit(mergedMeta);
const chart = threadUnit.chart as ChartDesc<'stackStatus'>;
const chartConfig = chart.config as StackStatusConfig;
chart.height = UnitHeight.STANDARD;
chart.renderTooltip = (data): Map<string, string> => new Map([
['Name', data.name],
['Stream', data.threadId ?? '-'],
]);
chartConfig.maxDepth = 0;
chartConfig.isCollapse = false;
threadUnit.collapsible = false;
threadUnit.notifications = [(): string => i18n.t('Merged unit', { ns: 'timeline' })];
return threadUnit;
};
* 合并泳道,用户操作情况,需要 addMergedGroup
* @param session
*/
const mergeUnits = (session: Session): void => {
const selectedUnits = session.selectedUnits;
if (selectedUnits.length === 0) { return; }
const [firstUnit] = selectedUnits;
const { cardId } = firstUnit.metadata as ThreadMetaData;
const allStreamUnits = selectedUnits.every(isStreamUnit);
if (!allStreamUnits) {
message.warning(i18n.t('timeline:MergeStreamOnly'));
return;
}
const allSameCard = selectedUnits.every(unit => {
const metaData = unit.metadata as ThreadMetaData;
return metaData.cardId === cardId;
});
if (!allSameCard) {
message.warning(i18n.t('timeline:MergeInSameCardOnly'));
return;
}
const unitParent = firstUnit.parent;
if (!unitParent?.children) { return; }
const mergedMeta = getMergedUnitMetaData(selectedUnits);
if (!mergedMeta) { return; }
const threadUnit = createMergedUnit(mergedMeta);
const insertIndex = findInsertIndex(unitParent.children, selectedUnits);
runInAction(() => {
if (!unitParent?.children) { return; }
unitParent.children.splice(insertIndex, 0, threadUnit);
markMergedUnits(selectedUnits, unitParent.children);
clearSelectedUnits(session);
const addedGroup: ThreadGroup = {
cardId: mergedMeta.cardId ?? '',
processId: mergedMeta.processId ?? '',
threadIds: mergedMeta.threadIdList ?? [],
};
session.mergedThreadData.addMergedGroup(addedGroup);
session.renderTrigger = !session.renderTrigger;
});
};
* 合并泳道,自动更新情况,根据 session.mergedThreadData 合并,不需要 addMergedGroup
* @param session
*/
export const mergeUnitsWhenLoadProject = (session: Session): void => {
const needMergeThreadLists = session.mergedThreadData.getNeedMergeThreadLists(session);
runInAction(() => {
needMergeThreadLists.forEach((needMergeThreadList) => {
if (needMergeThreadList.length <= 0) { return; }
const unitParent = needMergeThreadList[0].parent;
if (!unitParent?.children) { return; }
const mergedMeta = getMergedUnitMetaData(needMergeThreadList);
if (!mergedMeta) { return; }
const threadUnit = createMergedUnit(mergedMeta);
const insertIndex = findInsertIndex(unitParent.children, needMergeThreadList);
unitParent.children.splice(insertIndex, 0, threadUnit);
markMergedUnits(needMergeThreadList, unitParent.children);
});
session.renderTrigger = !session.renderTrigger;
});
};
const hasMergedUnit = (selectedUnits: InsightUnit[]): boolean => {
const mergedUnits = selectedUnits.filter(unit => {
const metaData = unit.metadata as ThreadMetaData;
return metaData.threadIdList;
});
return mergedUnits.length > 0;
};
const removeUnitFromParent = (unit: InsightUnit, parent?: { children?: InsightUnit[] }): void => {
const children = parent?.children;
if (!children) { return; }
const index = children.indexOf(unit);
if (index !== -1) {
children.splice(index, 1);
}
};
const getMergedThreadIdsAndRemoveMergedUnit = (selectedUnits: InsightUnit[], parent: InsightUnit): Set<string> => {
const threadIds = new Set<string>();
for (const unit of selectedUnits) {
const metadata = unit.metadata as ThreadMetaData;
if (Array.isArray(metadata.threadIdList)) {
removeUnitFromParent(unit, parent);
metadata.threadIdList.forEach(id => threadIds.add(id));
}
}
return threadIds;
};
const unmarkMergedUnits = (threadIds: Set<string>, children?: InsightUnit[]): void => {
if (!children) { return; }
for (const unit of children) {
const metadata = unit.metadata as ThreadMetaData;
if (threadIds.has(metadata.threadId as string)) {
unit.isMerged = false;
}
}
};
const unmergeUnits = (session: Session): void => {
const selectedUnits = session.selectedUnits;
if (!hasMergedUnit(selectedUnits)) {
return;
}
runInAction(() => {
const parent = selectedUnits[0].parent;
const children = parent?.children;
if (!children) { return; }
const mergedThreadIds = getMergedThreadIdsAndRemoveMergedUnit(selectedUnits, parent);
unmarkMergedUnits(mergedThreadIds, children);
clearSelectedUnits(session);
if (checkIsSliceMode(session)) {
session.selectedRange = undefined;
session.sliceSelection.targetUnit = null;
}
session.renderTrigger = !session.renderTrigger;
});
};
export const actionMergeUnits = register({
name: 'mergeUnits',
label: 'timeline:contextMenu.Merge Units',
visible: (session) => {
return session.selectedUnits.length > 1;
},
perform: (session): void => {
mergeUnits(session);
},
});
export const actionUnmergeUnits = register({
name: 'unmergeUnits',
label: 'timeline:contextMenu.Unmerge Units',
visible: (session) => {
return hasMergedUnit(session.selectedUnits);
},
perform: (session): void => {
unmergeUnits(session);
},
});