* -------------------------------------------------------------------------
* 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 { makeAutoObservable, when } from 'mobx';
import type { FC } from 'react';
import type { Theme } from '@emotion/react';
import { debounce, omit } from 'lodash';
import i18n from '@insight/lib/i18n';
import { type Caches } from '../cache/cache';
import { toLocalTimeString } from '../utils/humanReadable';
import { type TimeStamp } from './common';
import { Domain, DomainRange, MAX_ZOOM_DURATION } from './domain';
import type { InsightUnit, UnitMatcher, LinkLines } from './insight';
import { type TimeLineMaker, TIME_MAKER_DEFAULT } from './timeMaker';
import { platform } from '../platforms';
import { type Phase, stateTexts } from '../utils/constant';
import { SimpleCache } from '../cache/simplecache';
import { InsightUnitSet } from '../utils/PageSetting';
import { getTimeOffsetKey, setTimeOffsetForUnitTree, visitUnitTree } from '../insight/units/utils';
import { CardMetaData, SliceData, SliceMeta, ThreadMetaData, ThreadTrace } from './data';
import { CardRankInfo } from '../api/interface';
import { getRootUnit } from '../utils';
import { getAutoKey } from '../utils/dataAutoKey';
import type { FlowPoint } from '../insight/units/AscendUnit';
import { MergedThreadData } from './mergedThreadData';
export const MAX_ZOOM_COUNT = 10000;
export interface SelectedParams {
baseRawId?: number;
curRawId?: number;
}
export type SelectedData = Record<string, unknown> & {
sourceUnit?: InsightUnit;
};
export type ValidSession = Session & { startRecordTime: TimeStamp; phase: Exclude<Phase, 'configuring'> };
export function isValidSession(session?: Session): session is ValidSession {
return !(session === undefined || session.phase === 'configuring' || session.startRecordTime === undefined);
}
export type LinkDataType<T extends Record<string, unknown> = Record<string, unknown>> = T & { startTime: number; height: number; duration: number };
export type DataMatcher = (unit: InsightUnit) => boolean;
export interface LinkData {
target: {
data: LinkDataType;
matcher: DataMatcher;
};
sources: Array<{ data: LinkDataType; matcher: DataMatcher }>;
}
export interface ContextMenu {
isVisible: boolean;
zoomHistory: DomainRange[];
activeMenuKey: string;
}
interface UnitsConfig {
jsAllocationUsage: {
isRecordStackTraces: boolean;
};
nativeConfig: {
filterSize: number;
maxStackDepth: number;
};
offsetConfig: {
timestampOffset: Record<string, number>;
};
filterConfig: {
pythonFunction: Record<string, boolean>;
};
}
export interface SelectedDataType extends Pick<ThreadTrace, 'duration' | 'startTime' | 'name'> {
id?: SliceData['id'];
type?: string;
depth?: ThreadTrace['depth'];
threadId: SliceMeta['threadId'] | SliceData['tid'];
processId: SliceMeta['processId'];
cardId?: SliceMeta['cardId'];
dbPath?: string;
metaType?: ThreadMetaData['metaType'];
rawStartTime?: string;
color?: keyof Theme['colorPalette'] | Array<[ number, keyof Theme['colorPalette'] ]>;
startRecordTime?: number;
showSelectedData?: boolean;
showDetail?: boolean;
timestamp?: number;
}
export type TimelineScale = (x: number) => number;
interface ScaleBag {
timelineMarkerTimeScale: TimelineScale | null;
timelineMarkerXScale: TimelineScale | null;
}
export interface MapValueOfLinkLines {
cat: string;
from: FlowPoint[];
to: FlowPoint[];
current: FlowPoint;
}
export class Session {
language: 'zhCN' | 'enUS' = 'enUS';
id = '';
remoteAttrs: Map<string, Record<string, unknown>> = new Map();
singleLinkLine: LinkLines = {};
linkLineCategories: string[] = [];
isPending: boolean = false;
isParserLoading: boolean = false;
isSimulation: boolean = false;
asyncDataLoadingList: { [key: string]: any } = {};
isIE: boolean = false;
areFlagEventsHidden: boolean = false;
alignRender: boolean = false;
isCluster: boolean = false;
isMultiCluster: boolean = false;
viewedExpandedCardIdSet: Set<string> = new Set<string>();
selectedMultiSlice: string = '';
isMultiDevice: boolean = false;
isFullDb: boolean = false;
isIpynb: boolean = false;
ipynbUrl: string = '';
hasFtraceData: boolean = false;
hasNonFtraceData: boolean = false;
contextMenu: ContextMenu = {
isVisible: false,
zoomHistory: [],
activeMenuKey: '',
};
isOverflowMaxSafeNumber: boolean = false;
isNeedResetRankId: boolean = true;
pinnedUnits: InsightUnit[] = [];
icon?: JSX.Element;
caches: Caches | null = null;
simpleCache: SimpleCache;
startRecordTime: TimeStamp;
maxDuration = Number.MAX_SAFE_INTEGER;
isNsMode: boolean = false;
startTime: string;
selectedParams: SelectedParams = { baseRawId: undefined, curRawId: undefined };
scrollTop: number = 0;
scrollArea: string = '';
unitsConfig: UnitsConfig = {
jsAllocationUsage: {
isRecordStackTraces: false,
},
nativeConfig: {
filterSize: 4096,
maxStackDepth: 10,
},
offsetConfig: {
timestampOffset: {},
},
filterConfig: {
pythonFunction: {},
},
};
searchData?: { content: string; isMatchCase: boolean; isMatchExact: boolean; rankId?: string };
doContextSearch?: boolean;
showEvent?: boolean;
linkLines: LinkLines = {};
drawLineMode: 'all' | 'single' = 'all';
mapOfLinkLines: Map<string, MapValueOfLinkLines> = new Map();
shouldRefetchLines: boolean = false;
threadsToFetch: Map<string, InsightUnit> = new Map();
totalHeight: number = 0;
renderTrigger: boolean = true;
isTimeAnalysisMode: boolean = false;
timeAnalysisRange?: [ TimeStamp, TimeStamp ];
* 页面是否有m快捷键的遮罩m
*/
mKeyRender: boolean = false;
* m快捷键遮罩范围
*/
mMaskRange: number[] = [];
* 框选的范围是否已经被锁定
*/
selectedRangeIsLock: boolean = false;
* 锁定的框选范围
*/
lockRange?: [ TimeStamp, TimeStamp ];
* 锁定的泳道个数
*/
lockUnitCount: number = 0;
* 锁定的泳道
*/
lockUnit: InsightUnit[] = [];
* 表格点击的连线类型
*/
ridLineType: string = '';
buttons: Array<FC<{ session: Session }>>;
locateUnit?: UnitMatcher;
timer: ReturnType<typeof setInterval> | undefined;
sharedState: Record<string, unknown> = {};
timelineMaker: TimeLineMaker = TIME_MAKER_DEFAULT;
zoom?: {zoomCount: number; zoomPoint?: number };
doReset: boolean = false;
eventUnits: InsightUnit[] = [];
projectName?: string;
mergedThreadData: MergedThreadData;
pageSetting: Record<string, {
domainRange: DomainRange;
units: InsightUnitSet[];
pinnedUnits: InsightUnit[];
} | undefined> = {};
autoAdjustUnitHeight: boolean = false;
panMode: boolean = false;
panModePressed: boolean = false;
showBottomPanel: boolean | null = null;
isDragging: boolean = false;
debouncedSetZoomingHistory;
hoverMouseX: number | null = null;
showCreateFlagMarkKey: boolean = false;
scaleBag: ScaleBag = {
timelineMarkerTimeScale: null,
timelineMarkerXScale: null,
};
modeOfParse: 'auto_parse' | 'global_parse' = 'auto_parse';
parseQueue: Array<() => void> = [];
sliceSelection = {
active: false,
selecting: false,
startPos: [] as number[],
rangeOfLevels: [] as number[],
targetUnit: null as (InsightUnit | null),
activeIsChanged: false,
searchOfSlice: false,
};
private _selectedRange?: [ TimeStamp, TimeStamp ];
private readonly _domain: Domain;
private _initEndTimeAll: TimeStamp | undefined;
private _endTimeAll: TimeStamp | undefined;
private _name: string | null;
private _phase: Phase = 'configuring';
private _units: InsightUnit[] = [];
private readonly _rankCardInfoMap: Map<string, CardRankInfo> = new Map();
private _availableUnits: InsightUnit[] = [];
private _selectedData?: SelectedDataType;
private _benchMarkData?: Record<string, unknown>;
private _alignSliceData: SliceData[] = [];
private _selectedRangeData?: Array<Record<string, unknown>>;
private _interval: number;
private _selectedUnitKeys: string[] = [];
private _selectedUnits: InsightUnit[] = [];
private _disableZoomingHistory: boolean = false;
constructor(conf?: Partial<Session>) {
makeAutoObservable(this, {
timer: false,
selectedUnits: false,
caches: false,
isNsMode: false,
printSessionInfo: false,
linkLines: false,
mapOfLinkLines: false,
singleLinkLine: false,
resetOfSliceSelection: false,
threadsToFetch: false,
});
this._name = conf?.name ?? this.id;
this._interval = 100;
this.startRecordTime = 0;
this.startTime = '';
if (conf) {
Object.assign(this, conf);
}
this.debouncedSetZoomingHistory = debounce(this.setZoomingHistory.bind(this), 300);
this._domain = new Domain(this.isNsMode, this.endTimeAll, this.debouncedSetZoomingHistory);
this.mergedThreadData = new MergedThreadData();
this.buttons = conf?.buttons ?? [];
this.simpleCache = new SimpleCache();
const MAXTIME = this.isNsMode ? 5 * 60 * 1e9 : 5 * 60 * 1e3;
when(
() => this._endTimeAll !== undefined && this._endTimeAll >= MAXTIME,
() => {
const showTimeoutTip = window.localStorage.getItem('showTimeoutTip');
if (showTimeoutTip === null) {
platform.notify(i18n.t('notify:5012'));
window.localStorage.setItem('showTimeoutTip', 'false');
}
},
);
}
get endTimeAll(): TimeStamp | undefined {
return this._endTimeAll;
}
get name(): string | null {
return this._name;
}
get phase(): Phase {
return this._phase;
}
get statusInfo(): string {
if (this.phase === 'configuring') {
return 'Idle';
} else if (this.phase === 'download' && this.startRecordTime !== undefined) {
return `Recorded at: ${toLocalTimeString(this.startRecordTime)}`;
} else {
return stateTexts[this.phase];
}
}
get selectedUnitKeys(): (string[]) {
return this._selectedUnitKeys;
}
get domainRange(): DomainRange {
const { domainStart, domainEnd } = this._domain.domainRange;
return { domainStart, domainEnd };
}
get realTimeUpdate(): boolean {
return this._domain.realTimeUpdate && this.phase === 'recording';
}
get domain(): Domain {
return this._domain;
}
get interval(): number {
return this._interval;
}
get units(): InsightUnit[] {
return this._units;
}
get rankCardInfoMap(): Map<string, CardRankInfo> {
return this._rankCardInfoMap;
}
get availableUnits(): InsightUnit[] {
return this._availableUnits;
}
get selectedData(): SelectedDataType | undefined {
return this._selectedData;
}
get benchMarkData(): Record<string, unknown> | undefined {
return this._benchMarkData;
}
get alignSliceData(): SliceData[] {
return this._alignSliceData;
}
get selectedRangeData(): Array<Record<string, unknown>> | undefined {
return this._selectedRangeData;
}
get selectedUnits(): InsightUnit[] {
return this._selectedUnits;
}
get selectedRange(): [TimeStamp, TimeStamp] | undefined {
return this._selectedRange;
}
get haveCreateFlagMarkPosition(): boolean {
return this._selectedRange !== undefined || this.hoverMouseX !== null;
}
set endTimeAll(endTimeAll: TimeStamp | undefined) {
this._initEndTimeAll = endTimeAll;
this.updateEndTimeAll();
}
set name(value: string | null) {
this._name = value;
}
set phase(value: Phase) {
this._phase = value;
}
set selectedUnitKeys(value: string[]) {
if (this.selectedRangeIsLock) {
return;
}
this._selectedUnitKeys = value;
}
set domainRange(domainRange: DomainRange) {
const { domainStart, domainEnd } = domainRange;
let range = domainRange;
if (domainEnd - domainStart > MAX_ZOOM_DURATION) {
range = { domainStart, domainEnd: domainStart + MAX_ZOOM_DURATION };
}
this._domain.domainRange = range;
if (!this._disableZoomingHistory) {
this.debouncedSetZoomingHistory(range);
}
}
set realTimeUpdate(realTime: boolean) {
this._domain.realTimeUpdate = realTime && this.phase === 'recording';
}
set interval(value: number) {
this._interval = value;
}
set units(units: InsightUnit[]) {
const rootUnits = getRootUnit(this._units);
if (rootUnits.length > 0) {
const map = units.reduce((map, unit) => {
for (const root of rootUnits) {
if (root !== unit.parent) { continue; }
if (!map.has(root)) { map.set(root, []); }
map.get(root)?.push(unit);
break;
}
return map;
}, new Map<InsightUnit, InsightUnit[]>());
map.forEach((v, k) => { k.children = v; });
}
this._units = units;
}
set availableUnits(availableUnits: InsightUnit[]) {
this._availableUnits = availableUnits;
}
set selectedData(data: SelectedDataType | undefined) {
this._selectedData = data;
}
set benchMarkData(data: Record<string, unknown> | undefined) {
this._benchMarkData = data;
}
set alignSliceData(data: SliceData[]) {
this._alignSliceData = data;
}
set selectedRangeData(data: Array<Record<string, unknown>> | undefined) {
this._selectedRangeData = data;
}
set selectedUnits(data: InsightUnit[]) {
if (this.selectedRangeIsLock) {
return;
}
this._selectedUnits = data;
this._selectedUnitKeys = this._selectedUnits.map(getAutoKey);
}
set selectedRange(data: [TimeStamp, TimeStamp] | undefined) {
if (this.selectedRangeIsLock) {
console.warn('[WARN] selectedRange is locked, cannot set value.');
return;
}
this._selectedRange = data;
}
updateEndTimeAll(): void {
this._endTimeAll = this._initEndTimeAll === undefined ? undefined : (this._initEndTimeAll + this.getMaxRelativeOffset());
this._domain !== undefined && (this._domain.endTimeAll = this._endTimeAll ?? this.domain.maxDuration);
}
setTimestampOffset(key: string, value: number): void {
const prevObj = this.unitsConfig.offsetConfig.timestampOffset;
this.unitsConfig.offsetConfig.timestampOffset = { ...prevObj, [key]: (value) };
this.updateEndTimeAll();
}
setTimestampOffsetAll(offsetConfig: Record<string, number>): void {
this.unitsConfig.offsetConfig.timestampOffset = { ...offsetConfig };
this.updateEndTimeAll();
}
setTimestampOffsetByUnit(unit: InsightUnit, value: number, shouldUpdate: boolean = true): void {
const prevObj = { ...this.unitsConfig.offsetConfig.timestampOffset };
setTimeOffsetForUnitTree(this, unit, value, prevObj);
this.unitsConfig.offsetConfig.timestampOffset = {
...prevObj,
[(unit.metadata as unknown as CardMetaData).cardId]: (value),
};
if (shouldUpdate) { this.updateEndTimeAll(); }
}
printSessionInfo(): string {
return `${JSON.stringify({ ...omit(this, ['caches', 'sharedState', '_units']) })}`;
}
updateUnitsForMultiDevice(): void {
const rootUnits = getRootUnit(this._units);
if (!this.isMultiDevice) {
return;
}
const len = rootUnits.length;
for (let i = 1; i < len; ++i) {
this.setUnitMultiDeviceHidden(rootUnits[i], true);
}
}
setDomainWithoutHistory(domainRange: DomainRange): void {
this._disableZoomingHistory = true;
this.domainRange = domainRange;
this._disableZoomingHistory = false;
}
setZoomingHistory(domainRange: DomainRange): void {
this.contextMenu.zoomHistory.push(domainRange);
if (this.contextMenu.zoomHistory.length > MAX_ZOOM_COUNT) {
this.contextMenu.zoomHistory = this.contextMenu.zoomHistory.slice(-MAX_ZOOM_COUNT);
}
}
resetOfSliceSelection(isSelecting = true): void {
this.sliceSelection.startPos = [];
this.sliceSelection.rangeOfLevels = [];
this.sliceSelection.selecting = isSelecting;
this.sliceSelection.targetUnit = null;
}
deleteRankCardInfoMapItemByDbPath(dbPath: string): boolean {
for (const [k, v] of this._rankCardInfoMap) {
if (v.dbPath === dbPath) {
this._rankCardInfoMap.delete(k);
return true;
}
}
return false;
}
private setUnitMultiDeviceHidden(unit: InsightUnit, hidden: boolean): void {
const visited = new Set<InsightUnit>();
const setUnitDisplayable = (child: InsightUnit): void => {
if (visited.has(child)) {
return;
}
child.isMultiDeviceHidden = hidden;
visited.add(child);
if (child.children) {
for (const item of child.children) {
setUnitDisplayable(item);
}
}
};
setUnitDisplayable(unit);
}
private getMaxRelativeOffset(): number {
if (!Array.isArray(this._units) ||
this.unitsConfig.offsetConfig.timestampOffset === undefined ||
Object.keys(this.unitsConfig.offsetConfig.timestampOffset).length === 0) {
return 0;
}
const getRelativeOffset = (unit: InsightUnit): number => {
const initTimeOffset = unit.alignStartTimestamp ?? 0;
const timeOffsetKey = getTimeOffsetKey(this, unit.metadata as { cardId?: string; processId?: string });
const currentTimeOffset = this.unitsConfig.offsetConfig.timestampOffset[timeOffsetKey] ?? 0;
return Math.max(0, initTimeOffset - currentTimeOffset);
};
const result = this._units.flatMap((unit: InsightUnit): number[] => {
const relativeOffsets: number[] = [];
visitUnitTree(unit, (item) => {
relativeOffsets.push(getRelativeOffset(item as InsightUnit));
});
return relativeOffsets;
});
return Math.max(...result);
}
}