diff --git a/ide/src/base-ui/tabs/lit-tabs.ts b/ide/src/base-ui/tabs/lit-tabs.ts index 1187d8bc7242b6a7f8f1b7b11c954536fb57b104..e0f8eb54022db63ec2c8dd1eccf449ca294a0c53 100644 --- a/ide/src/base-ui/tabs/lit-tabs.ts +++ b/ide/src/base-ui/tabs/lit-tabs.ts @@ -417,6 +417,7 @@ export class LitTabs extends HTMLElement {
+
diff --git a/ide/src/trace/SpApplication.ts b/ide/src/trace/SpApplication.ts index d5ec88d636cee2bec7d5c9879f7e253feeb76104..b9ab3c5bc6852764c89610fc79d372714a4a68c9 100644 --- a/ide/src/trace/SpApplication.ts +++ b/ide/src/trace/SpApplication.ts @@ -59,6 +59,8 @@ import { type SpKeyboard } from './component/SpKeyboard'; import './component/SpKeyboard'; import { parseKeyPathJson } from './component/Utils'; import { Utils } from './component/trace/base/Utils'; +import { SpThirdParty } from './component/SpThirdParty'; +import './component/SpThirdParty' @element('sp-application') export class SpApplication extends BaseElement { @@ -567,6 +569,8 @@ export class SpApplication extends BaseElement { +
@@ -603,6 +607,7 @@ export class SpApplication extends BaseElement { let chartFilter = this.shadowRoot?.querySelector('.chart-filter') as TraceRowConfig; let cutTraceFile = this.shadowRoot?.querySelector('.cut-trace-file') as HTMLImageElement; let longTracePage = that.shadowRoot!.querySelector('.long_trace_page') as HTMLDivElement; + let spThirdParty = this.shadowRoot!.querySelector('#sp-third-party') as SpThirdParty; cutTraceFile.addEventListener('click', () => { this.croppingFile(progressEL, litSearch); }); @@ -624,7 +629,17 @@ export class SpApplication extends BaseElement { spRecordTemplate, spFlags, SpKeyboard, + spThirdParty, ]; + + document.addEventListener('file-error', () => { + litSearch.setPercent('This File is Error!', -1); + }); + + document.addEventListener('file-correct', () => { + litSearch.setPercent('', 101); + }); + document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') { if (window.localStorage.getItem('Theme') == 'dark') { @@ -1421,6 +1436,16 @@ export class SpApplication extends BaseElement { function handleWasmMode(ev: any, showFileName: string, fileSize: string, fileName: string) { litSearch.setPercent('', 1); + if (fileName.endsWith('.json')) { + progressEL.loading = true; + spSystemTrace!.loadSample(ev).then(() => { + showContent(spSystemTrace!); + litSearch.setPercent('', 101); + that.freshMenuDisable(false); + chartFilter!.setAttribute('mode', ''); + progressEL.loading = false; + }) + } else { threadPool.init('wasm').then((res) => { let reader: FileReader | null = new FileReader(); reader.readAsArrayBuffer(ev as any); @@ -1545,6 +1570,7 @@ export class SpApplication extends BaseElement { }; }); } + } let openFileInit = () => { this.clearTraceFileCache(); @@ -1947,6 +1973,14 @@ export class SpApplication extends BaseElement { }); }, }, + { + title: '第三方文件', + icon: 'file-fill', + clickHandler: function (item: MenuItem) { + that.search = false; + showContent(spThirdParty!); + }, + }, ], }, ]; diff --git a/ide/src/trace/bean/BoxSelection.ts b/ide/src/trace/bean/BoxSelection.ts index 0744b5623b34e3201343deb6ef5c2a792aa63925..1f87c5b129e70af8cfba4d68b1c77e12c73e1fe8 100644 --- a/ide/src/trace/bean/BoxSelection.ts +++ b/ide/src/trace/bean/BoxSelection.ts @@ -114,6 +114,7 @@ export class SelectionParam { gpuMemoryTrackerData: Array = []; hiLogs: Array = []; hiSysEvents: Array = []; + sampleData: Array = []; } export class BoxJumpParam { diff --git a/ide/src/trace/component/SpSystemTrace.ts b/ide/src/trace/component/SpSystemTrace.ts index 6834c59f069e8bd4183819c49f92db070f011994..366cb73d4f10c39cefca38b5ffb25a1e983a9a00 100644 --- a/ide/src/trace/component/SpSystemTrace.ts +++ b/ide/src/trace/component/SpSystemTrace.ts @@ -111,6 +111,7 @@ import { type SpKeyboard } from '../component/SpKeyboard'; import { drawVSync, enableVSync, setVSyncDisable } from './chart/VSync'; import { LtpoStruct } from '../database/ui-worker/ProcedureWorkerLTPO'; import { HitchTimeStruct } from '../database/ui-worker/ProcedureWorkerHitchTime' +import { SampleStruct } from '../database/ui-worker/ProcedureWorkerSample'; function dpr() { return window.devicePixelRatio || 1; @@ -682,7 +683,19 @@ export class SpSystemTrace extends BaseElement { selection.rightNs = TraceRow.rangeSelectObject?.endNS || 0; selection.recordStartNs = (window as any).recordStartNS; rows.forEach((it) => { - if (it.rowType == TraceRow.ROW_TYPE_CPU) { + if (it.rowType == TraceRow.ROW_TYPE_SAMPLE) { + let dataList: SampleStruct[] = JSON.parse(JSON.stringify(it.dataList)); + dataList.forEach( + SampleStruct => { + SampleStruct.property = SampleStruct.property!.filter((i : any) => + ((i.begin! - i.startTs!) ?? 0) >= TraceRow.rangeSelectObject!.startNS! && + ((i.end! - i.startTs!) ?? 0) <= TraceRow.rangeSelectObject!.endNS!) + } + ) + if (dataList[0].property!.length !== 0) { + selection.sampleData.push(...dataList); + } + } else if (it.rowType == TraceRow.ROW_TYPE_CPU) { selection.cpus.push(parseInt(it.rowId!)); info('load CPU traceRow id is : ', it.rowId); } else if (it.rowType == TraceRow.ROW_TYPE_CPU_STATE) { @@ -2464,6 +2477,7 @@ export class SpSystemTrace extends BaseElement { JsCpuProfilerStruct.hoverJsCpuProfilerStruct = undefined; SnapshotStruct.hoverSnapshotStruct = undefined; HiPerfCallChartStruct.hoverPerfCallCutStruct = undefined; + SampleStruct.hoverSampleStruct = undefined; this.tipEL!.style.display = 'none'; } @@ -2492,6 +2506,7 @@ export class SpSystemTrace extends BaseElement { AllAppStartupStruct.selectStartupStruct = undefined; LtpoStruct.selectLtpoStruct = undefined; HitchTimeStruct.selectHitchTimeStruct = undefined; + SampleStruct.selectSampleStruct = undefined; } isWASDKeyPress() { @@ -2682,6 +2697,7 @@ export class SpSystemTrace extends BaseElement { () => SnapshotStruct.hoverSnapshotStruct !== null && SnapshotStruct.hoverSnapshotStruct !== undefined, ], [TraceRow.ROW_TYPE_LOGS, () => LogStruct.hoverLogStruct !== null && LogStruct.hoverLogStruct !== undefined], + [TraceRow.ROW_TYPE_SAMPLE, () => SampleStruct.hoverSampleStruct !== null && SampleStruct.hoverSampleStruct !== undefined], ]); onClickHandler(clickRowType: string, row?: TraceRow) { @@ -3255,6 +3271,10 @@ export class SpSystemTrace extends BaseElement { } else if (clickRowType === TraceRow.ROW_TYPE_GPU_RESOURCE_VMTRACKER && SnapshotStruct.hoverSnapshotStruct) { SnapshotStruct.selectSnapshotStruct = SnapshotStruct.hoverSnapshotStruct; this.traceSheetEL?.displayGpuResourceVmTracker(SnapshotStruct.selectSnapshotStruct.startNs); + } else if (clickRowType === TraceRow.ROW_TYPE_SAMPLE && SampleStruct.hoverSampleStruct) { + SampleStruct.selectSampleStruct = SampleStruct.hoverSampleStruct; + this.traceSheetEL?.displaySampleData(SampleStruct.selectSampleStruct, SampleStruct.reqProperty); + this.timerShaftEL?.modifyFlagList(undefined); } else { if (!JankStruct.hoverJankStruct && JankStruct.delJankLineFlag) { this.removeLinkLinesByBusinessType('janks'); @@ -4075,6 +4095,28 @@ export class SpSystemTrace extends BaseElement { window.unsubscribe(window.SmartEvent.UI.SliceMark, this.sliceMarkEventHandler.bind(this)); } + loadSample = async (ev: File) => { + this.observerScrollHeightEnable = false; + await this.initSample(ev); + this.rowsEL?.querySelectorAll('trace-row').forEach((it: any) => this.observer.observe(it)); + window.publish(window.SmartEvent.UI.MouseEventEnable, { + mouseEnable: true, + }); + } + + initSample = async (ev: File) => { + this.rowsPaneEL!.scroll({ + top: 0, + left: 0, + }); + this.chartManager?.initSample(ev).then(() => { + this.loadTraceCompleted = true; + this.rowsEL!.querySelectorAll>('trace-row').forEach((it) => { + this.intersectionObserver?.observe(it); + }) + }) + } + sliceMarkEventHandler(ev: any) { SpSystemTrace.sliceRangeMark = ev; let startNS = ev.timestamp - (window as any).recordStartNS; @@ -4281,17 +4323,13 @@ export class SpSystemTrace extends BaseElement { } } } else { - if (currentIndex == -1) { - findIndex = 0; - } else { - findIndex = structs.findIndex((it, idx) => { - return ( - idx > currentIndex && - it.startTime! >= TraceRow.range!.startNS && - it.startTime! + it.dur! <= TraceRow.range!.endNS - ); - }); - } + findIndex = structs.findIndex((it, idx) => { + return ( + idx > currentIndex && + it.startTime! >= TraceRow.range!.startNS && + it.startTime! + it.dur! <= TraceRow.range!.endNS + ); + }); } let findEntry: any; if (findIndex >= 0) { @@ -4315,8 +4353,8 @@ export class SpSystemTrace extends BaseElement { } } findEntry = structs[findIndex]; + this.moveRangeToCenter(findEntry.startTime!, findEntry.dur!); } - this.moveRangeToCenter(findEntry.startTime!, findEntry.dur!); this.queryAllTraceRow().forEach((item) => { item.highlight = false; }); diff --git a/ide/src/trace/component/SpThirdParty.ts b/ide/src/trace/component/SpThirdParty.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac645e564bf7927632c24085db17eb6314a0cf42 --- /dev/null +++ b/ide/src/trace/component/SpThirdParty.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { BaseElement, element } from '../../base-ui/BaseElement'; +import { LitMainMenu, MenuItem } from '../../base-ui/menu/LitMainMenu'; +import { SpApplication } from '../SpApplication'; +@element('sp-third-party') +export class SpThirdParty extends BaseElement { + private bodyEl: HTMLElement | undefined | null; + private uploadEl: HTMLElement | undefined | null; + private inputEl: HTMLInputElement | undefined | null; + private sp: SpApplication | undefined; + + initElements(): void { + let parentElement = this.parentNode as HTMLElement; + parentElement.style.overflow = 'hidden'; + this.bodyEl = this.shadowRoot?.querySelector('.body'); + this.uploadEl = this.shadowRoot?.querySelector('.upload-btn')?.shadowRoot?.querySelector('#custom-button'); + this.inputEl = this.shadowRoot?.querySelector('#file'); + this.uploadEl?.addEventListener('click', () => { + this.inputEl?.click(); + }) + this.inputEl!.addEventListener('change', () => { + let files = this.inputEl!.files; + if (files && files.length > 0) { + let main = this.parentNode!.parentNode!.querySelector('lit-main-menu') as LitMainMenu; + let children = main.menus!; + let child = children[0].children as Array; + let fileHandler = child[0].fileHandler!; + fileHandler({ + detail: files[0] + }) + } + if (this.inputEl) this.inputEl.value = ''; + }) + } + + initHtml(): string { + return ` + ${this.initHtmlStyle()} +
+
+ + + Open bpftrace file + +
+
+ `; + } + + private initHtmlStyle(): string { + return ` + + `; + } + +} + + diff --git a/ide/src/trace/component/Utils.ts b/ide/src/trace/component/Utils.ts index 83f9faf1a1dfbac549af8fc6d69456148c10db59..588b95c21675aa1eea8a5415c409e22e732050d2 100644 --- a/ide/src/trace/component/Utils.ts +++ b/ide/src/trace/component/Utils.ts @@ -92,3 +92,13 @@ export function parseKeyPathJson(content: string): Array { } return parseResult; } + +export function debounce(func: any, delay: number) { + let timer: any = null; + return function() { + clearTimeout(timer); + timer = setTimeout(() => { + func(); + }, delay) + } +} diff --git a/ide/src/trace/component/chart/SpChartManager.ts b/ide/src/trace/component/chart/SpChartManager.ts index 8d9084bf227269faaa52dd4024c5c95208222453..1272a7f0662ed71dd2413afcfc229322b3ba7d13 100644 --- a/ide/src/trace/component/chart/SpChartManager.ts +++ b/ide/src/trace/component/chart/SpChartManager.ts @@ -52,6 +52,7 @@ import { SpHiSysEventChart } from './SpHiSysEventChart'; import { SpAllAppStartupsChart } from './SpAllAppStartups'; import {setVSyncData} from './VSync'; import { SpLtpoChart } from './SpLTPO'; +import { SpSampleChart } from './SpSampleChart'; export class SpChartManager { static APP_STARTUP_PID_ARR: Array = []; @@ -78,6 +79,7 @@ export class SpChartManager { private logChart: SpLogChart; private spHiSysEvent: SpHiSysEventChart; SegMenTaTion: any; + private sampleChart: SpSampleChart; constructor(trace: SpSystemTrace) { this.trace = trace; @@ -101,6 +103,12 @@ export class SpChartManager { this.spHiSysEvent = new SpHiSysEventChart(trace); this.SpAllAppStartupsChart = new SpAllAppStartupsChart(trace); this.SpLtpoChart = new SpLtpoChart(trace) + this.sampleChart = new SpSampleChart(trace); + } + + async initSample(ev: File) { + await this.initSampleTime(); + await this.sampleChart.init(ev); } async init(progress: Function) { @@ -125,6 +133,7 @@ export class SpChartManager { progress('cpu', 70); await this.cpu.init(); info('cpu Data initialized'); + await this.sampleChart.init(null); progress('process/thread state', 73); await this.cpu.initProcessThreadStateData(progress); if (FlagsConfig.getFlagsConfigEnableStatus('SchedulingAnalysis')) { @@ -226,6 +235,21 @@ export class SpChartManager { } }; + initSampleTime = async () => { + if (this.trace.timerShaftEL) { + let total = 30_000_000_000; + let startNS = 0; + let endNS = 30_000_000_000; + this.trace.timerShaftEL.totalNS = total; + this.trace.timerShaftEL.getRangeRuler()!.drawMark = true; + this.trace.timerShaftEL.setRangeNS(0, total); + (window as any).recordStartNS = startNS; + (window as any).recordEndNS = endNS; + (window as any).totalNS = total; + this.trace.timerShaftEL.loadComplete = true; + } + }; + initCpuRate = async () => { let rates = await getCpuUtilizationRate(0, this.trace.timerShaftEL?.totalNS || 0); if (this.trace.timerShaftEL) this.trace.timerShaftEL.cpuUsage = rates; diff --git a/ide/src/trace/component/chart/SpSampleChart.ts b/ide/src/trace/component/chart/SpSampleChart.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2461425dce799236ea37f8bb3377ca6857c03a4 --- /dev/null +++ b/ide/src/trace/component/chart/SpSampleChart.ts @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { SpSystemTrace } from '../SpSystemTrace'; +import { TraceRow } from '../trace/base/TraceRow'; +import { renders } from '../../database/ui-worker/ProcedureWorker'; +import { SampleStruct, SampleRender } from '../../database/ui-worker/ProcedureWorkerSample'; +import { queryStartTime } from '../../database/SqlLite'; + +export class SpSampleChart { + private trace: SpSystemTrace; + + constructor(trace: SpSystemTrace) { + this.trace = trace; + } + + async init(file: File | null) { + if (!file) { + let startTime = await queryStartTime(); + let folder = await this.initSample(startTime[0].start_ts, file); + this.trace.rowsEL?.appendChild(folder); + } else { + let folder = await this.initSample(-1, file); + this.trace.rowsEL?.appendChild(folder); + } + } + + async initSample(start_ts: number, file: any): Promise> { + let traceRow = TraceRow.skeleton(); + traceRow.rowId = 'sample'; + traceRow.rowType = TraceRow.ROW_TYPE_SAMPLE; + traceRow.rowParentId = ''; + traceRow.style.height = '40px'; + traceRow.folder = false; + traceRow.index = 0; + traceRow.name = 'Sample'; + traceRow.selectChangeHandler = this.trace.selectChangeHandler; + if (!file) { + //添加上传图标 + traceRow.addRowSampleUpload(); + //获取所上传的文件内容 + traceRow.uploadEl?.addEventListener('file-change', (e: any) => { + this.getJsonData(e).then((res: any) => { + const propertyData = res.data; + const treeNodes = res.relation.children || [res.relation.RS.children[0]]; + const uniqueProperty = this.removeDuplicates(propertyData); + const flattenTreeArray = this.getFlattenTreeData(treeNodes); + const copyFlattenTreeArray = JSON.parse(JSON.stringify(flattenTreeArray)); + const height = (Math.max(...flattenTreeArray.map((obj: any) => obj.depth)) + 1) * 20; + this.setRelationDataProperty(flattenTreeArray, uniqueProperty); + traceRow.supplier = () => + new Promise((resolve): void => { + resolve(flattenTreeArray) + }) + traceRow.onThreadHandler = (useCache) => { + let context = this.trace.canvasPanelCtx!; + traceRow.canvasSave(context); + (renders.sample as SampleRender).renderMainThread( + { + context: context, + useCache: useCache, + type: 'sample', + start_ts: start_ts, + uniqueProperty: uniqueProperty, + flattenTreeArray: copyFlattenTreeArray + }, + traceRow + ); + traceRow.canvasRestore(context) + }; + traceRow.style.height = `${ height }px`; + }) + }) + } else { + this.getJsonData(file).then((res: any) => { + const propertyData = res.data; + const treeNodes = res.relation.children || [res.relation.RS.children[0]]; + const uniqueProperty = this.removeDuplicates(propertyData); + const flattenTreeArray = this.getFlattenTreeData(treeNodes); + const copyFlattenTreeArray = JSON.parse(JSON.stringify(flattenTreeArray)); + const height = (Math.max(...flattenTreeArray.map((obj: any) => obj.depth)) + 1) * 20; + this.setRelationDataProperty(flattenTreeArray, uniqueProperty); + const startTS = flattenTreeArray[0].property[0].begin; + traceRow.supplier = () => + new Promise((resolve): void => { + resolve(flattenTreeArray) + }) + traceRow.onThreadHandler = (useCache) => { + let context = this.trace.canvasPanelCtx!; + traceRow.canvasSave(context); + (renders.sample as SampleRender).renderMainThread( + { + context: context, + useCache: useCache, + type: 'sample', + start_ts: startTS, + uniqueProperty: uniqueProperty, + flattenTreeArray: copyFlattenTreeArray + }, + traceRow + ); + traceRow.canvasRestore(context) + }; + traceRow.style.height = `${ height }px`; + }) + } + return traceRow; + } + + /** + * 获取上传的文件内容 转为json格式 + * @param file + * @returns + */ + getJsonData(file: any): Promise { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.readAsText(file.detail || file); + reader.onloadend = (e: any) => { + const fileContent = e.target?.result; + try { + resolve(JSON.parse(fileContent)); + document.dispatchEvent( + new CustomEvent('file-correct') + ) + } catch (error) { + document.dispatchEvent( + new CustomEvent('file-error') + ) + } + } + }) + } + + /** + * 树结构扁平化 + * @param treeData + * @param depth + * @param parentName + * @returns + */ + getFlattenTreeData(treeData: Array, depth: number = 0, parentName: string = ''): Array { + let result: Array = []; + treeData.forEach(node => { + const name: string = node['function_name']; + const newNode: any = {}; + if (name.indexOf('unknown') > -1) { + newNode['children'] = this.getUnknownAllChildrenNames(node); + } + newNode['detail'] = node['detail']; + newNode['depth'] = depth; + newNode['name'] = name; + newNode['parentName'] = parentName; + newNode['property'] = []; + result.push(newNode); + if (node.children) { + result = result.concat(this.getFlattenTreeData(node.children, depth + 1, node['function_name'])) + } + }) + return result + } + + /** + * 查找重复项 + * @param propertyData + * @returns + */ + removeDuplicates(propertyData: Array): Array { + const result: Array = []; + propertyData.forEach(propertyGroup => { + const groups: Array = []; + propertyGroup.forEach((property: any) => { + const duplicateObj = groups.find(group => group['func_name'] === property['func_name']); + if (duplicateObj) { + duplicateObj['begin'] = Math.min(duplicateObj['begin'], property['begin']); + duplicateObj['end'] = Math.max(duplicateObj['end'], property['end']); + } else { + groups.push(property) + } + }) + result.push(groups); + }) + return result + } + + /** + * 关系树赋值 + * @param relationData + * @param propertyData + */ + setRelationDataProperty(relationData: Array, propertyData: Array): void { + //数组每一项进行比对 + propertyData.forEach(propertyGroup => { + propertyGroup.forEach((property: any) => { + const relation = relationData.find(relation => relation['name'] === property['func_name']); + //property属性存储每帧数据 + relation?.property.push({ + name: property['func_name'], + detail: relation['detail'], + end: property['end'], + begin: property['begin'], + depth: relation['depth'], + instructions: property['instructions'], + cycles: property['cycles'] + }) + }) + }) + + //获取所有名字为unknown的数据 + const unknownRelation = relationData.filter(relation => relation['name'].indexOf('unknown') > -1); + //二维数组 用于存放unknown下所有子节点的数据 + let twoDimensionalArray: Array = []; + let result: Array = []; + unknownRelation.forEach(unknownItem => { + result = []; + twoDimensionalArray = []; + const children = unknownItem['children']; + //先获取到unknwon节点下每个子节点的property + Object.keys(children).forEach(key => { + unknownItem.children[key] = (relationData.find(relation => relation['name'] === key)).property; + }) + //将每个子节点的property加到二维数组中 + Object.values(children).forEach((value: any) => { + if (value.length > 0) { + twoDimensionalArray.push(value) + } + }) + if (twoDimensionalArray.length > 0) { + //取每列的最大值和最小值 + for (let i = 0; i < twoDimensionalArray[0].length; i++) { + const data = { + name: unknownItem['name'], + detail: unknownItem['detail'], + begin: (twoDimensionalArray[0][i]).begin, + end: 0, + depth: unknownItem['depth'] + } + for (let j = 0; j < twoDimensionalArray.length; j++) { + data['end'] = Math.max((twoDimensionalArray[j][i])['end'], data['end']); + data['begin'] = Math.min((twoDimensionalArray[j][i])['begin'], data['begin']); + } + result.push(data); + } + unknownItem.property = result; + } + }) + } + + /** + * 获取unknown节点下所有孩子节点的名称 + * @param node + * @param names + */ + getUnknownAllChildrenNames(node: any, names: any = {}): object { + if (node['children']) { + node['children'].forEach((child: any) => { + if (child['function_name'].indexOf('unknown') < 0) { + names[child['function_name']] = [] + } else { + this.getUnknownAllChildrenNames(child, names) + } + }) + } + return names + } +} diff --git a/ide/src/trace/component/trace/base/TraceRow.ts b/ide/src/trace/component/trace/base/TraceRow.ts index f1d9bbc78724ae2acba15d0f4bda9e92bba8b196..e9c44ce00313805611eebec185ece8fc04b3b7cd 100644 --- a/ide/src/trace/component/trace/base/TraceRow.ts +++ b/ide/src/trace/component/trace/base/TraceRow.ts @@ -119,7 +119,8 @@ export class TraceRow extends HTMLElement { static ROW_TYPE_LOGS = 'logs'; static ROW_TYPE_ALL_APPSTARTUPS = 'all-appstartups'; static ROW_TYPE_LTPO = 'ltpo'; - static ROW_TYPE_HITCH_TIME = 'hitch-time'; + static ROW_TYPE_HITCH_TIME = 'hitch-time';; + static ROW_TYPE_SAMPLE = 'sample'; static FRAME_WIDTH: number = 0; static range: TimeRange | undefined | null; static rangeSelectObject: RangeSelectStruct | undefined; @@ -167,6 +168,8 @@ export class TraceRow extends HTMLElement { private _drawType: number = 0; private folderIconEL: LitIcon | null | undefined; private _enableCollapseChart: boolean = false; + private sampleUploadEl: HTMLDivElement | null | undefined; + public fileEl: HTMLInputElement | null | undefined; online: boolean = false; static isUserInteraction: boolean; asyncFuncName: string | undefined | null; @@ -238,6 +241,11 @@ export class TraceRow extends HTMLElement { 'row-setting-popover-direction', ]; } + + get uploadEl() { + return this.sampleUploadEl; + } + get docompositionList(): Array | undefined { return this._docompositionList; } @@ -710,6 +718,34 @@ export class TraceRow extends HTMLElement { this.checkType = '-1'; } + addRowSampleUpload(): void { + this.sampleUploadEl = document.createElement('div'); + this.sampleUploadEl!.className = 'upload'; + this.sampleUploadEl!.innerHTML = ` + + + ` + this.fileEl = this.sampleUploadEl!.querySelector('.file') as HTMLInputElement; + this.collectEL!.style.display = 'none'; + this.sampleUploadEl!.addEventListener('change', () => { + let files = this.fileEl!.files; + if (files && files.length > 0) { + this.sampleUploadEl!.dispatchEvent( + new CustomEvent('file-change', { + detail: files[0] + }) + ) + if (this.fileEl) this.fileEl.value = ''; + } + }) + this.sampleUploadEl!.addEventListener('click', (e) => { + e.stopPropagation(); + }) + this.describeEl?.appendChild(this.sampleUploadEl!); + } + addRowSettingPop(): void { this.rowSettingPop = document.createElement('lit-popover') as LitPopover; this.rowSettingPop.innerHTML = `
@@ -1442,6 +1478,11 @@ export class TraceRow extends HTMLElement { } :host([row-setting='enable']:not([check-type='-1'])) .collect{ margin-right: 5px; + } + .upload { + color: var(--dark-icon,#333333); + margin-right: 5px; + margin-top: 4px; }
diff --git a/ide/src/trace/component/trace/base/TraceSheet.ts b/ide/src/trace/component/trace/base/TraceSheet.ts index e7615b0dab3c2afa469a5fa6f74b66c31b7b0d84..2f07886c1985e9a37b3a928271b3f76b1dee7d4c 100644 --- a/ide/src/trace/component/trace/base/TraceSheet.ts +++ b/ide/src/trace/component/trace/base/TraceSheet.ts @@ -82,6 +82,8 @@ import { type LitPageTable } from '../../../../base-ui/table/LitPageTable'; import '../../../../base-ui/popover/LitPopoverV'; import { LitPopover } from '../../../../base-ui/popover/LitPopoverV'; import { LitTree, TreeItemData } from '../../../../base-ui/tree/LitTree'; +import { SampleStruct } from '../../../database/ui-worker/ProcedureWorkerSample'; +import { TabPaneSampleInstruction } from '../sheet/sample/TabPaneSampleInstruction'; @element('trace-sheet') export class TraceSheet extends BaseElement { @@ -97,6 +99,8 @@ export class TraceSheet extends BaseElement { private fragment: DocumentFragment | undefined; private lastSelectIPid: number = -1; private lastProcessSet: Set = new Set(); + private optionsDiv: LitPopover | undefined | null; + private optionsSettingTree: LitTree | undefined | null; static get observedAttributes(): string[] { return ['mode']; @@ -122,6 +126,7 @@ export class TraceSheet extends BaseElement { displayTab(...names: string[]): T { this.setAttribute('mode', 'max'); this.showUploadSoBt(null); + this.showOptionsBt(null); this.showSwitchProcessBt(null); this.shadowRoot ?.querySelectorAll('#tabs lit-tabpane') @@ -152,6 +157,20 @@ export class TraceSheet extends BaseElement { this.importDiv = this.shadowRoot?.querySelector('#import_div'); this.switchDiv = this.shadowRoot?.querySelector('#select-process'); this.processTree = this.shadowRoot?.querySelector('#processTree'); + this.optionsDiv = this.shadowRoot?.querySelector('#options'); + this.optionsSettingTree = this.shadowRoot?.querySelector('#optionsSettingTree'); + + this.optionsSettingTree!.onChange = (e: any): void => { + const select = this.optionsSettingTree!.getCheckdKeys(); + document.dispatchEvent( + new CustomEvent('sample-popver-change', { + detail: { + select: select[0] + } + }) + ) + } + this.processTree!.onChange = (e: any): void => { const select = this.processTree!.getCheckdKeys(); const selectIPid = Number(select[0]); @@ -448,9 +467,22 @@ export class TraceSheet extends BaseElement { margin-right: 10px; z-index: 2; } + .option { + display: flex; + margin-right: 10px; + cursor: pointer; + }
+
+ +
+ +
+ +
+
@@ -706,11 +738,18 @@ export class TraceSheet extends BaseElement { } }; + displaySampleData = (data: SampleStruct, reqProperty: any): void => { + this.displayTab('box-sample-instruction').setSampleInstructionData(data, reqProperty); + this.optionsDiv!.style.display = "flex"; + this.optionsSettingTree!.treeData = [{key: '0', title: 'instruction', checked: true}, {key: '1', title: 'cycles'}]; + } + rangeSelect(selection: SelectionParam, restore = false): boolean { this.selection = selection; this.exportBt!.style.display = 'flex'; this.showUploadSoBt(selection); this.showSwitchProcessBt(selection); + this.showOptionsBt(selection); Reflect.ownKeys(tabConfig) .reverse() .forEach((id) => { @@ -772,6 +811,16 @@ export class TraceSheet extends BaseElement { } } + showOptionsBt(selection: SelectionParam | null | undefined): void { + if ( selection && selection.sampleData.length > 0) { + this.optionsDiv!.style.display = 'flex'; + const select = this.optionsSettingTree!.getCheckdKeys().length === 0 ? ['0'] : this.optionsSettingTree!.getCheckdKeys(); + this.optionsSettingTree!.treeData = [{key: '0', title: 'instruction', checked: select[0] === '0'}, {key: '1', title: 'cycles', checked: select[0] === '1'}]; + } else { + this.optionsDiv!.style.display = 'none'; + } + } + showUploadSoBt(selection: SelectionParam | null | undefined): void { if ( selection && diff --git a/ide/src/trace/component/trace/base/TraceSheetConfig.ts b/ide/src/trace/component/trace/base/TraceSheetConfig.ts index 52a5a2becd3abcf2ae99123b6b4142d615f3de6e..bcd0795a66d618de6d278fafe93b1c27a4d0557e 100644 --- a/ide/src/trace/component/trace/base/TraceSheetConfig.ts +++ b/ide/src/trace/component/trace/base/TraceSheetConfig.ts @@ -122,6 +122,11 @@ import { TabPaneFreqUsage } from '../sheet/frequsage/TabPaneFreqUsage'; import { TabPaneFreqDataCut } from '../sheet/frequsage/TabPaneFreqDataCut'; import { TabPaneHisysEvents } from '../sheet/hisysevent/TabPaneHisysEvents'; import { TabPaneHiSysEventSummary } from '../sheet/hisysevent/TabPaneHiSysEventSummary'; +import { TabPaneSampleInstruction } from '../sheet/sample/TabPaneSampleInstruction'; +import { TabPaneSampleInstructionDistributions } from '../sheet/sample/TabPaneSampleInstructionDistributions'; +import { TabPaneSampleInstructionTotalTime } from '../sheet/sample/TabPaneSampleInstructionSelectionTotalTime'; +import { TabPaneSampleInstructionSelection } from '../sheet/sample/TabPaneSampleInstructionSelection'; + export let tabConfig: any = { 'current-selection': { @@ -650,4 +655,23 @@ export let tabConfig: any = { type: TabPaneHiSysEventSummary, require: (param: SelectionParam) => param.hiSysEvents.length > 0, }, + 'box-sample-instruction-selection': { + title: 'Instruction Selection', + type: TabPaneSampleInstructionSelection, + require: (param: SelectionParam) => param.sampleData.length > 0 + }, + 'box-sample-instruction-distribution-selection': { + title: 'Instruction Distribution', + type: TabPaneSampleInstructionDistributions, + require: (param: SelectionParam) => param.sampleData.length > 0 + }, + 'box-sample-instruction-totaltime-selection': { + title: 'Total Duration', + type: TabPaneSampleInstructionTotalTime, + require: (param: SelectionParam) => param.sampleData.length > 0 + }, + 'box-sample-instruction': { + title: 'Instruction Flow', + type: TabPaneSampleInstruction, + }, }; diff --git a/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstruction.ts b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstruction.ts new file mode 100644 index 0000000000000000000000000000000000000000..e290057ff33d8189602aaeab473152699cff07d6 --- /dev/null +++ b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstruction.ts @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { BaseElement, element } from '../../../../../base-ui/BaseElement'; +import { Rect, drawString } from '../../../../database/ui-worker/ProcedureWorkerCommon'; +import { ColorUtils } from '../../base/ColorUtils'; +import { SampleStruct } from '../../../../database/ui-worker/ProcedureWorkerSample'; +import { SpApplication } from '../../../../SpApplication'; + +const SAMPLE_STRUCT_HEIGHT = 30; +const Y_PADDING = 4; + +@element('tab-sample-instruction') +export class TabPaneSampleInstruction extends BaseElement { + private instructionEle: HTMLCanvasElement | undefined | null; + private ctx: CanvasRenderingContext2D| undefined | null; + private textEle: HTMLSpanElement | undefined | null; + private instructionArray: Array = []; + private instructionData: Array = []; + private flattenTreeData: Array = []; + private isUpdateCanvas = false; + private canvasX = -1; // 鼠标当前所在画布x坐标 + private canvasY = -1; // 鼠标当前所在画布y坐标 + private startX = 0; // 画布相对于整个界面的x坐标 + private startY = 0; // 画布相对于整个界面的y坐标 + private hintContent = ""; //悬浮框内容 + private floatHint!: HTMLDivElement | undefined | null; //悬浮框 + private canvasScrollTop = 0; // tab页上下滚动位置 + private hoverSampleStruct: SampleStruct | undefined; + private isChecked: boolean = false; + private maxDepth = 0; + + initHtml(): string { + return ` + +
+ +
+
+ 指令数数据流 +
+
+ ` + } + + initElements(): void { + this.instructionEle = this.shadowRoot?.querySelector('#instruct-canvas'); + this.textEle = this.shadowRoot?.querySelector('.headline'); + this.ctx = this.instructionEle?.getContext('2d'); + this.floatHint = this.shadowRoot?.querySelector('#float_hint'); + } + + connectedCallback(): void { + super.connectedCallback(); + this.parentElement!.onscroll = () => { + this.canvasScrollTop = this.parentElement!.scrollTop; + this.hideTip(); + } + this.instructionEle!.onmousemove = (e): void => { + if (!this.isUpdateCanvas) { + this.updateCanvasCoord(); + } + this.canvasX = e.clientX - this.startX; + this.canvasY = e.clientY - this.startY + this.canvasScrollTop; + this.onMouseMove(); + } + this.instructionEle!.onmouseleave = () => { + this.hideTip(); + } + document.addEventListener('sample-popver-change', (e: any) => { + const select = Number(e.detail.select); + this.isChecked = Boolean(select); + this.drawInstructionData(this.isChecked); + }) + this.listenerResize(); + } + /** + * 初始化窗口大小 + * @param newWidth + */ + updateCanvas(newWidth?: number): void { + if (this.instructionEle instanceof HTMLCanvasElement) { + this.instructionEle.style.width = `${100}%`; + this.instructionEle.style.height = `${(this.maxDepth + 1) * SAMPLE_STRUCT_HEIGHT}px`; + if (this.instructionEle.clientWidth === 0 && newWidth) { + this.instructionEle.width = newWidth; + } else { + this.instructionEle.width = this.instructionEle.clientWidth; + } + this.instructionEle.height = (this.maxDepth + 1) * SAMPLE_STRUCT_HEIGHT; + this.updateCanvasCoord(); + } + } + + setSampleInstructionData(clickData: SampleStruct, reqProperty: any): void { + this.isChecked = false; + this.instructionData = reqProperty.uniqueProperty; + this.flattenTreeData = reqProperty.flattenTreeArray; + this.setRelationDataProperty(this.flattenTreeData, clickData); + this.updateCanvas(this.clientWidth); + this.drawInstructionData(this.isChecked); + } + + /** + * 鼠标移动 + */ + onMouseMove(): void { + const lastNode = this.hoverSampleStruct; + //查找鼠标所在的node + const searchResult = this.searchDataByCoord(this.instructionArray!, this.canvasX, this.canvasY); + if (searchResult) { + this.hoverSampleStruct = searchResult; + //鼠标悬浮的node未改变则不需重新渲染文字 + if (searchResult !== lastNode) { + this.updateTipContent(); + this.ctx!.clearRect(0, 0, this.instructionEle!.width, this.instructionEle!.height); + this.ctx!.beginPath(); + for (const key in this.instructionArray) { + for (let i = 0; i < this.instructionArray[key].length; i++) { + const cur = this.instructionArray[key][i]; + this.draw(this.ctx!, cur); + } + } + this.ctx!.closePath(); + } + this.showTip(); + } else { + this.hideTip(); + this.hoverSampleStruct = undefined; + } + } + + /** + * 隐藏悬浮框 + */ + hideTip(): void { + if (this.floatHint) { + this.floatHint.style.display = 'none'; + } + } + + /** + * 显示悬浮框 + */ + showTip(): void { + this.floatHint!.innerHTML = this.hintContent; + this.floatHint!.style.display = 'block'; + let x = this.canvasX; + let y = this.canvasY - this.canvasScrollTop; + //右边的函数悬浮框显示在左侧 + if (this.canvasX + this.floatHint!.clientWidth > (this.instructionEle!.clientWidth) || 0) { + x -= this.floatHint!.clientWidth - 1; + } else { + x += 30; + } + //最下边的函数悬浮框显示在上方 + y -= this.floatHint!.clientHeight - 1; + this.floatHint!.style.transform = `translate(${x}px, ${y}px)`; + } + + /** + * 更新悬浮框内容 + * @returns + */ + updateTipContent(): void { + const hoverNode = this.hoverSampleStruct; + if (!hoverNode) { + return; + } + this.hintContent = `${hoverNode.detail}(${hoverNode.name})
+ ${hoverNode.instructions} + `; + } + + /** + * 设置绘制所需的坐标及宽高 + * @param sampleNode + * @param instructions + * @param x + */ + setSampleFrame(sampleNode: SampleStruct, instructions: number, x: number): void { + if (!sampleNode.frame) { + sampleNode.frame! = new Rect(0, 0, 0, 0); + } + sampleNode.frame!.x = x; + sampleNode.frame!.y = sampleNode.depth! * SAMPLE_STRUCT_HEIGHT; + sampleNode.frame!.width = instructions; + sampleNode.frame!.height = SAMPLE_STRUCT_HEIGHT; + } + + /** + * 判断鼠标当前在那个函数上 + * @param frame + * @param x + * @param y + * @returns + */ + isContains(frame: any, x: number, y: number): boolean { + return x >= frame.x && x <= frame.x + frame.width && frame.y <= y && y <= frame.y + frame.height; + } + + /** + * 绘制 + * @param isCycles + */ + drawInstructionData(isCycles: boolean): void { + this.isChecked ? this.textEle!.innerText = "cycles数据流" : this.textEle!.innerText = "instructions数据流"; + const clientWidth = this.instructionEle!.width; + //将数据转换为层级结构 + const instructionArray = this.flattenTreeData + .filter((item: SampleStruct) => isCycles ? item.cycles : item.instructions) + .reduce((pre: any, cur: any) => { + (pre[`${cur.depth}`] = pre[`${cur.depth}`] || []).push(cur); + return pre; + }, {}); + for (const key in instructionArray) { + for (let i = 0; i < instructionArray[key].length; i++) { + const cur = instructionArray[key][i]; + //第一级节点直接将宽度设置为容器宽度 + if (key === '0') { + this.setSampleFrame(cur, clientWidth, 0) + } else { + //获取上一层级节点数据 + const preList = instructionArray[Number(key) - 1]; + //获取当前节点的父节点 + const parentNode = preList.find((node: SampleStruct) => node.name === cur.parentName); + //计算当前节点下指令数之和 用于计算每个节点所占的宽度比 + const total = isCycles ? instructionArray[key].filter((i: any) => i.parentName === parentNode.name).reduce((pre: number, cur: SampleStruct) => pre + cur.cycles!, 0) : + instructionArray[key].filter((i: any) => i.parentName === parentNode.name).reduce((pre: number, cur: SampleStruct) => pre + cur.instructions!, 0); + const curWidth = isCycles ? cur.cycles : cur.instructions; + const width = Math.floor(parentNode.frame.width * (curWidth / total)); + if (i === 0) { + this.setSampleFrame(cur, width, parentNode.frame.x); + } else { + const preNode = instructionArray[key][i - 1]; + preNode.parentName === parentNode.name ? + this.setSampleFrame(cur, width, preNode.frame.x + preNode.frame.width) : + this.setSampleFrame(cur, width, parentNode.frame.x); + } + } + } + } + this.instructionArray = instructionArray; + + this.ctx!.clearRect(0, 0, this.instructionEle!.width, this.instructionEle!.height); + this.ctx!.beginPath(); + for (const key in instructionArray) { + for (let i = 0; i < instructionArray[key].length; i++) { + const cur = instructionArray[key][i]; + this.draw(this.ctx!, cur) + } + } + this.ctx!.closePath() + } + + /** + * 更新canvas坐标 + */ + updateCanvasCoord(): void { + if (this.instructionEle instanceof HTMLCanvasElement) { + this.isUpdateCanvas = this.instructionEle.clientWidth !== 0; + if (this.instructionEle.getBoundingClientRect()) { + const box = this.instructionEle.getBoundingClientRect(); + const D = document.documentElement; + this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; + this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; + } + } + } + + /** + * 获取鼠标悬停的函数 + * @param nodes + * @param canvasX + * @param canvasY + * @returns + */ + searchDataByCoord(nodes: any, canvasX: number, canvasY: number) { + for (const key in nodes) { + for (let i = 0; i < nodes[key].length; i++) { + const cur = nodes[key][i]; + if (this.isContains(cur.frame, canvasX, canvasY)) { + return cur; + } + } + } + return null; + } + + /** + * 绘制方法 + * @param ctx + * @param data + */ + draw(ctx: CanvasRenderingContext2D, data: SampleStruct) { + let spApplication = document.getElementsByTagName('sp-application')[0]; + if (data.frame) { + ctx.globalAlpha = 1; + ctx.fillStyle = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth!, ColorUtils.FUNC_COLOR.length)]; + const textColor = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth!, ColorUtils.FUNC_COLOR.length)]; + ctx.lineWidth = 0.4; + if (this.hoverSampleStruct && data.name == this.hoverSampleStruct.name) { + if (spApplication.dark) { + ctx.strokeStyle = '#fff'; + } else { + ctx.strokeStyle = '#000'; + } + } else { + if (spApplication.dark) { + ctx.strokeStyle = '#000'; + } else { + ctx.strokeStyle = '#fff'; + } + } + ctx.strokeRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + ctx.fillRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + ctx.fillStyle = ColorUtils.funcTextColor(textColor); + drawString(ctx, `${data.detail}(${data.name})`, 5, data.frame, data); + } + } + + /** + * 关系树节点赋值 + * @param relationData + * @param clickData + */ + setRelationDataProperty(relationData: Array, clickData: SampleStruct): void { + const propertyData = this.instructionData.find((subArr: any) => subArr.some((obj: SampleStruct) => obj.begin === clickData.begin)); + //数组每一项进行对比后赋值 + propertyData.forEach((property: any) => { + const relation = relationData.find(relation => relation['name'] === property['func_name']); + relation['instructions'] = property['instructions'] === 0 ? 20 : Math.ceil(property['instructions']); + relation['cycles'] = property['cycles'] === 0 ? 2 : Math.ceil(property['cycles']); + this.maxDepth = Math.max(this.maxDepth, relation['depth']); + }) + //获取所有unknown数据 + let instructionSum = 0; + let cyclesSum = 0; + const unknownRelation = relationData.filter(relation => relation['name'].indexOf('unknown') > -1); + unknownRelation.forEach(unknownItem => { + instructionSum = 0; + cyclesSum = 0; + const children = unknownItem['children']; + for (const key in children) { + const it = relationData.find(relation => relation['name'] === key); + instructionSum += it['instructions'] ?? 0; + cyclesSum += it['cycles'] ?? 0; + } + unknownItem['instructions'] = instructionSum; + unknownItem['cycles'] = cyclesSum; + }) + } + + /** + * 监听页面size变化 + */ + listenerResize(): void { + new ResizeObserver(() => { + if (this.instructionEle!.getBoundingClientRect()) { + const box = this.instructionEle!.getBoundingClientRect(); + const element = this.parentElement!; + this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft; + this.startY = + box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop; + } + }).observe(this.parentElement!) + } + + +} diff --git a/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionDistributions.ts b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionDistributions.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd59d04df7da4a7fdd36789a3013a752cfc00c06 --- /dev/null +++ b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionDistributions.ts @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { BaseElement, element } from '../../../../../base-ui/BaseElement'; +import { SelectionParam } from '../../../../bean/BoxSelection'; +import { debounce } from '../../../Utils'; +const paddingLeft = 100; +const paddingBottom = 15; +const xStep = 50; // x轴间距 +const barWidth = 2 // 柱子宽度 + +@element('tab-sample-instructions-distrubtions-chart') +export class TabPaneSampleInstructionDistributions extends BaseElement { + private instructionChartEle: HTMLCanvasElement | undefined | null; + private ctx: CanvasRenderingContext2D | undefined | null; + private onReadableData: Array = []; + private hintContent = ""; //悬浮框内容 + private floatHint!: HTMLDivElement | undefined | null; //悬浮框 + private canvasScrollTop = 0; // tab页上下滚动位置 + private isUpdateCanvas = false; + private cacheData: Array = []; + private canvasX = -1; // 鼠标当前所在画布x坐标 + private canvasY = -1; // 鼠标当前所在画布y坐标 + private startX = 0; // 画布相对于整个界面的x坐标 + private startY = 0; // 画布相对于整个界面的y坐标 + private hoverBar: any; + private isChecked: boolean = false; + private xCount = 0; //x轴刻度个数 + private xMaxValue = 0; //x轴上数据最大值 + private xSpacing = 50; //x轴间距 + private xAvg = 0; //根据xMaxValue进行划分 用于x轴上刻度显示 + private yAvg = 0; //根据yMaxValue进行划分 用于y轴上刻度显示 + + initHtml(): string { + return ` + + +
+ ` + } + + initElements(): void { + this.instructionChartEle = this.shadowRoot?.querySelector('#instruct-chart-canvas'); + this.ctx = this.instructionChartEle?.getContext('2d'); + this.floatHint = this.shadowRoot?.querySelector('#float_hint'); + } + + set data(SampleParam: SelectionParam) { + this.onReadableData = SampleParam.sampleData[0].property; + this.calInstructionRangeCount(this.isChecked); + } + + connectedCallback(): void { + super.connectedCallback(); + this.parentElement!.onscroll = () => { + this.canvasScrollTop = this.parentElement!.scrollTop; + this.hideTip(); + } + this.instructionChartEle!.onmousemove = (e): void => { + if (!this.isUpdateCanvas) { + this.updateCanvasCoord(); + } + this.canvasX = e.clientX - this.startX; + this.canvasY = e.clientY - this.startY + this.canvasScrollTop; + this.onMouseMove(); + } + this.instructionChartEle!.onmouseleave = () => { + this.hideTip(); + } + document.addEventListener('sample-popver-change', (e: any) => { + const select = Number(e.detail.select); + this.isChecked = Boolean(select); + this.calInstructionRangeCount(this.isChecked); + }) + this.listenerResize(); + } + + /** + * 更新canvas坐标 + */ + updateCanvasCoord(): void { + if (this.instructionChartEle instanceof HTMLCanvasElement) { + this.isUpdateCanvas = this.instructionChartEle.clientWidth !== 0; + if (this.instructionChartEle.getBoundingClientRect()) { + const box = this.instructionChartEle.getBoundingClientRect(); + const D = this.parentElement!; + this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; + this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; + } + } + } + + /** + * 获取鼠标悬停的函数 + * @param nodes + * @param canvasX + * @param canvasY + * @returns + */ + searchDataByCoord(nodes: any, canvasX: number, canvasY: number) { + for (let i = 0; i < nodes.length; i++) { + const element = nodes[i]; + if (this.isContains(element, canvasX, canvasY)) { + return element; + } + } + return null; + } + + /** + * 鼠标移动 + */ + onMouseMove(): void { + const lastNode = this.hoverBar; + //查找鼠标所在的node + const searchResult = this.searchDataByCoord(this.cacheData, this.canvasX, this.canvasY); + if (searchResult) { + this.hoverBar = searchResult; + //鼠标悬浮的node未改变则不需重新渲染文字 + if (searchResult !== lastNode) { + this.updateTipContent(); + } + this.showTip(); + } else { + this.hideTip(); + this.hoverBar = undefined; + } + } + + /** + * 隐藏悬浮框 + */ + hideTip(): void { + if (this.floatHint) { + this.floatHint.style.display = 'none'; + this.instructionChartEle!.style.cursor = 'default'; + } + } + + /** + * 显示悬浮框 + */ + showTip(): void { + this.floatHint!.innerHTML = this.hintContent; + this.floatHint!.style.display = 'block'; + this.instructionChartEle!.style.cursor = 'pointer'; + let x = this.canvasX; + let y = this.canvasY - this.canvasScrollTop; + //右边的函数悬浮框显示在左侧 + if (this.canvasX + this.floatHint!.clientWidth > (this.instructionChartEle!.clientWidth || 0)) { + x -= this.floatHint!.clientWidth - 1; + } else { + x += 20; + } + //最下边的函数悬浮框显示在上方 + y -= this.floatHint!.clientHeight - 1; + this.floatHint!.style.transform = `translate(${x}px, ${y}px)`; + } + + /** + * 更新悬浮框内容 + * @returns + */ + updateTipContent(): void { + const hoverNode = this.hoverBar; + if (!hoverNode) { + return; + } + const detail = hoverNode!; + this.hintContent = ` + ${detail.instruct}
+ ${parseFloat(detail.heightPer)}% + `; + } + + /** + * 判断鼠标当前在那个函数上 + * @param frame + * @param x + * @param y + * @returns + */ + isContains(point: any, x: number, y: number): boolean { + return x >= point.x && x <= point.x + 2 && point.y <= y && y <= point.y + point.height; + } + + /** + * 统计onReadable数据各指令的个数 + * @param isCycles + */ + calInstructionRangeCount(isCycles: boolean) { + if (this.onReadableData.length === 0) return; + this.cacheData.length = 0; + const count = this.onReadableData.length; + let instructions = {}; + if (isCycles) { + instructions = this.onReadableData.reduce((pre: any, current: any) => { + (pre[`${Math.floor(current.cycles)}`] = pre[`${Math.floor(current.cycles)}`] || []).push(current); + return pre; + }, {}) + } else { + instructions = this.onReadableData.reduce((pre: any, current: any) => { + (pre[`${Math.floor(current.instructions)}`] = pre[`${Math.floor(current.instructions)}`] || []).push(current); + return pre; + }, {}) + } + this.ctx!.clearRect(0, 0, this.instructionChartEle!.width, this.instructionChartEle!.height); + this.instructionChartEle!.width = this.clientWidth; + + this.xMaxValue = Object.keys(instructions).map(i => Number(i)).reduce((pre, cur) => Math.max(pre, cur), 0) + 10; + const yMaxValue = Object.values(instructions).reduce((pre: number, cur: any) => Math.max(pre, Number((cur.length / count).toFixed(2))), 0); + this.yAvg = Number((yMaxValue / 5 * 1.5).toFixed(2)) || yMaxValue; + const height = this.instructionChartEle!.height; + const width = this.instructionChartEle!.width; + this.drawLineLabelMarkers(width, height, isCycles); + this.drawBar(instructions, height, count); + } + + /** + * 绘制柱状图 + * @param instructionData + * @param height + * @param count + */ + drawBar(instructionData: any, height: number, count: number) { + const yTotal = Number((this.yAvg * 5).toFixed(2)); + const interval = Math.floor((height - paddingBottom) / 6); + for (const x in instructionData) { + const xNum = Number(x); + const xPosition = xStep + (xNum / (this.xCount * this.xAvg)) * (this.xCount * this.xSpacing) - (barWidth / 2); + const yNum = Number((instructionData[x].length / count).toFixed(2)); + const percent = Number((yNum / yTotal).toFixed(2)); + const barHeight = (height - paddingBottom - interval) * percent; + this.drawRect(xPosition, height - paddingBottom - barHeight, barWidth, barHeight); + const existX = this.cacheData.find(i => i.instruct === x); + if (!existX) { + this.cacheData.push({ + instruct: x, + x: xPosition, + y: height - paddingBottom - barHeight, + height: barHeight, + heightPer: yNum + }) + } else { + existX.x = xPosition; + } + } + } + + /** + * 绘制x y轴 + * @param width + * @param height + * @param isCycles + */ + drawLineLabelMarkers(width: number, height: number, isCycles: boolean) { + this.ctx!.font = "12px Arial"; + this.ctx!.lineWidth = 1; + this.ctx!.fillStyle = "#333"; + this.ctx!.strokeStyle = "#ccc"; + if (isCycles) { + this.ctx!.fillText('cycles数(1e5)', width - paddingLeft + 10, height - paddingBottom); + } else { + this.ctx!.fillText('指令数(1e5)', width - paddingLeft + 10, height - paddingBottom); + } + + //绘制x轴 + this.drawLine(xStep, height - paddingBottom, width - paddingLeft, height - paddingBottom); + //绘制y轴 + this.drawLine(xStep, 5, xStep, height - paddingBottom); + //绘制标记 + this.drawMarkers(width, height) + } + + /** + * 绘制横线 + * @param x + * @param y + * @param X + * @param Y + */ + drawLine(x: number, y: number, X: number, Y: number) { + this.ctx!.beginPath; + this.ctx!.moveTo(x, y); + this.ctx!.lineTo(X, Y); + this.ctx!.stroke(); + this.ctx!.closePath(); + } + + /** + * 绘制x y轴刻度 + * @param width + * @param height + */ + drawMarkers(width: number, height: number) { + this.xCount = 0; + //绘制x轴锯齿 + let serrateX = 50; + let y = height - paddingBottom; + const clientWidth = width - paddingLeft - 50; + if (clientWidth > this.xMaxValue) { + this.xSpacing = Math.floor(clientWidth / 20) + this.xAvg = Math.ceil(this.xMaxValue / 20) + } else { + this.xSpacing = Math.floor(clientWidth / 10) + this.xAvg = Math.ceil(this.xMaxValue / 10) + } + while (serrateX <= clientWidth) { + this.xCount++; + serrateX += this.xSpacing; + this.drawLine(serrateX, y, serrateX, y + 5); + } + //绘制x轴刻度 + this.ctx!.textAlign = "center"; + for (let i = 0; i <= this.xCount; i++) { + const x = xStep + (i * this.xSpacing); + this.ctx!.fillText(`${i * this.xAvg}`, x, height); + } + //绘制y轴刻度 + this.ctx!.textAlign = "center"; + const yPadding = Math.floor((height - paddingBottom) / 6); + for (let i = 0; i < 6; i++) { + if (i === 0) { + this.ctx!.fillText(`${i}%`, 30, y); + } else { + const y = (height - paddingBottom) - (i * yPadding); + this.drawLine(xStep, y, width - paddingLeft, y); + this.ctx!.fillText(`${parseFloat((i * this.yAvg).toFixed(2))}%`, 30, y); + } + } + } + + + + /** + * 监听页面size变化 + */ + listenerResize(): void { + new ResizeObserver(debounce(() => { + if (this.instructionChartEle!.getBoundingClientRect()) { + const box = this.instructionChartEle!.getBoundingClientRect(); + const element = this.parentElement!; + this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft; + this.startY = + box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop; + this.calInstructionRangeCount(this.isChecked); + } + }, 100)).observe(this.parentElement!) + } + /** + * 绘制方块 + * @param x + * @param y + * @param X + * @param Y + */ + drawRect(x: number, y: number, X: number, Y: number) { + this.ctx!.beginPath(); + this.ctx!.rect(x, y, X, Y); + this.ctx!.fill(); + this.ctx!.closePath() + } +} diff --git a/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelection.ts b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelection.ts new file mode 100644 index 0000000000000000000000000000000000000000..889292083f86fbd240eabe3942f4a4df160ab939 --- /dev/null +++ b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelection.ts @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { BaseElement, element } from '../../../../../base-ui/BaseElement'; +import { SelectionParam } from '../../../../bean/BoxSelection'; +import { Rect, drawString } from '../../../../database/ui-worker/ProcedureWorkerCommon'; +import { ColorUtils } from '../../base/ColorUtils'; +import { SampleStruct } from '../../../../database/ui-worker/ProcedureWorkerSample'; +import { SpApplication } from '../../../../SpApplication'; + +const SAMPLE_STRUCT_HEIGHT = 30; +const Y_PADDING = 4; + +@element('tab-sample-instruction-selection') +export class TabPaneSampleInstructionSelection extends BaseElement { + private instructionEle: HTMLCanvasElement | undefined | null; + private ctx: CanvasRenderingContext2D| undefined | null; + private textEle: HTMLSpanElement | undefined | null; + private instructionArray: Array = []; + private instructionData: Array = []; + private isUpdateCanvas = false; + private canvasX = -1; // 鼠标当前所在画布x坐标 + private canvasY = -1; // 鼠标当前所在画布y坐标 + private startX = 0; // 画布相对于整个界面的x坐标 + private startY = 0; // 画布相对于整个界面的y坐标 + private hintContent = ""; //悬浮框内容 + private floatHint: HTMLDivElement | undefined | null; //悬浮框 + private canvasScrollTop = 0; // tab页上下滚动位置 + private hoverSampleStruct: SampleStruct | undefined; + private isChecked: boolean = false; + private maxDepth = 0; + + initHtml(): string { + return ` + +
+ +
+
+ 指令数数据流 +
+
+ ` + } + + initElements(): void { + this.instructionEle = this.shadowRoot?.querySelector('#instruct-select-canvas'); + this.textEle = this.shadowRoot?.querySelector('.headline'); + this.ctx = this.instructionEle?.getContext('2d'); + this.floatHint = this.shadowRoot?.querySelector('#select_float_hint'); + } + + set data(SampleParam: SelectionParam) { + this.instructionData = SampleParam.sampleData; + this.getAvgInstructionData(this.instructionData); + queueMicrotask(() => { + this.updateCanvas(this.clientWidth); + this.drawInstructionData(this.isChecked); + }) + } + + connectedCallback(): void { + super.connectedCallback(); + this.parentElement!.onscroll = () => { + this.canvasScrollTop = this.parentElement!.scrollTop; + this.hideTip(); + } + this.instructionEle!.onmousemove = (e): void => { + if (!this.isUpdateCanvas) { + this.updateCanvasCoord(); + } + this.canvasX = e.clientX - this.startX; + this.canvasY = e.clientY - this.startY + this.canvasScrollTop; + this.onMouseMove(); + } + this.instructionEle!.onmouseleave = () => { + this.hideTip(); + } + document.addEventListener('sample-popver-change', (e: any) => { + const select = Number(e.detail.select); + this.isChecked = Boolean(select); + this.drawInstructionData(this.isChecked); + }) + this.listenerResize(); + } + + /** + * 初始化窗口大小 + * @param newWidth + */ + updateCanvas(newWidth?: number): void { + if (this.instructionEle instanceof HTMLCanvasElement) { + this.instructionEle.style.width = `${100}%`; + this.instructionEle.style.height = `${(this.maxDepth + 1) * SAMPLE_STRUCT_HEIGHT}px`; + if (this.instructionEle.clientWidth === 0 && newWidth) { + this.instructionEle.width = newWidth; + } else { + this.instructionEle.width = this.instructionEle.clientWidth; + } + this.instructionEle.height = (this.maxDepth + 1) * SAMPLE_STRUCT_HEIGHT; + } + } + + /** + * 鼠标移动 + */ + onMouseMove(): void { + const lastNode = this.hoverSampleStruct; + //查找鼠标所在的node + const searchResult = this.searchDataByCoord(this.instructionArray!, this.canvasX, this.canvasY); + if (searchResult) { + this.hoverSampleStruct = searchResult; + //鼠标悬浮的node未改变则不需重新渲染文字 + if (searchResult !== lastNode) { + this.updateTipContent(); + this.ctx!.clearRect(0, 0, this.instructionEle!.width, this.instructionEle!.height); + this.ctx!.beginPath(); + for (const key in this.instructionArray) { + for (let i = 0; i < this.instructionArray[key].length; i++) { + const cur = this.instructionArray[key][i]; + this.draw(this.ctx!, cur); + } + } + this.ctx!.closePath(); + } + this.showTip(); + } else { + this.hideTip(); + this.hoverSampleStruct = undefined; + } + } + + /** + * 隐藏悬浮框 + */ + hideTip(): void { + if (this.floatHint) { + this.floatHint.style.display = 'none'; + } + } + + /** + * 显示悬浮框 + */ + showTip(): void { + this.floatHint!.innerHTML = this.hintContent; + this.floatHint!.style.display = 'block'; + let x = this.canvasX; + let y = this.canvasY - this.canvasScrollTop; + //右边的函数悬浮框显示在左侧 + if (this.canvasX + this.floatHint!.clientWidth > (this.instructionEle!.clientWidth) || 0) { + x -= this.floatHint!.clientWidth - 1; + } else { + x += 30; + } + //最下边的函数悬浮框显示在上方 + y -= this.floatHint!.clientHeight - 1; + this.floatHint!.style.transform = `translate(${x}px, ${y}px)`; + } + + /** + * 更新悬浮框内容 + * @returns + */ + updateTipContent(): void { + const hoverNode = this.hoverSampleStruct; + if (!hoverNode) { + return; + } + this.hintContent = `${hoverNode.detail}(${hoverNode.name})
+ ${hoverNode.instructions} + `; + } + + /** + * 设置绘制所需的坐标及宽高 + * @param sampleNode + * @param instructions + * @param x + */ + setSampleFrame(sampleNode: SampleStruct, instructions: number, x: number): void { + if (!sampleNode.frame) { + sampleNode.frame! = new Rect(0, 0, 0, 0); + } + sampleNode.frame!.x = x; + sampleNode.frame!.y = sampleNode.depth! * SAMPLE_STRUCT_HEIGHT; + sampleNode.frame!.width = instructions; + sampleNode.frame!.height = SAMPLE_STRUCT_HEIGHT; + } + + /** + * 判断鼠标当前在那个函数上 + * @param frame + * @param x + * @param y + * @returns + */ + isContains(frame: any, x: number, y: number): boolean { + return x >= frame.x && x <= frame.x + frame.width && frame.y <= y && y <= frame.y + frame.height; + } + + /** + * 绘制 + * @param isCycles + */ + drawInstructionData(isCycles: boolean): void { + this.isChecked ? this.textEle!.innerText = "cycles数据流" : this.textEle!.innerText = "instructions数据流"; + const clientWidth = this.instructionEle!.width; + //将数据转换为层级结构 + const instructionArray = this.instructionData + .filter((item: any) => isCycles ? item.cycles : item.instructions) + .reduce((pre: any, cur: any) => { + (pre[`${cur.depth}`] = pre[`${cur.depth}`] || []).push(cur); + return pre; + }, {}); + for (const key in instructionArray) { + for (let i = 0; i < instructionArray[key].length; i++) { + const cur = instructionArray[key][i]; + //第一级节点直接将宽度设置为容器宽度 + if (key === '0') { + this.setSampleFrame(cur, clientWidth, 0) + } else { + //获取上一层级节点数据 + const preList = instructionArray[Number(key) - 1]; + //获取当前节点的父节点 + const parentNode = preList.find((node: SampleStruct) => node.name === cur.parentName); + //计算当前节点下指令数之和 用于计算每个节点所占的宽度比 + const total = isCycles ? instructionArray[key].filter((i: any) => i.parentName === parentNode.name).reduce((pre: number, cur: SampleStruct) => pre + cur.cycles!, 0) : + instructionArray[key].filter((i: any) => i.parentName === parentNode.name).reduce((pre: number, cur: SampleStruct) => pre + cur.instructions!, 0); + const curWidth = isCycles ? cur.cycles : cur.instructions; + const width = Math.floor(parentNode.frame.width * (curWidth / total)); + if (i === 0) { + this.setSampleFrame(cur, width, parentNode.frame.x); + } else { + const preNode = instructionArray[key][i - 1]; + preNode.parentName === parentNode.name ? + this.setSampleFrame(cur, width, preNode.frame.x + preNode.frame.width) : + this.setSampleFrame(cur, width, parentNode.frame.x); + } + } + } + } + this.instructionArray = instructionArray; + + this.ctx!.clearRect(0, 0, this.instructionEle!.width, this.instructionEle!.height); + this.ctx!.beginPath(); + for (const key in instructionArray) { + for (let i = 0; i < instructionArray[key].length; i++) { + const cur = instructionArray[key][i]; + this.draw(this.ctx!, cur) + } + } + this.ctx!.closePath() + } + + /** + * 更新canvas坐标 + */ + updateCanvasCoord(): void { + if (this.instructionEle instanceof HTMLCanvasElement) { + this.isUpdateCanvas = this.instructionEle.clientWidth !== 0; + if (this.instructionEle.getBoundingClientRect()) { + const box = this.instructionEle.getBoundingClientRect(); + const D = document.documentElement; + this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; + this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; + } + } + } + + /** + * 获取鼠标悬停的函数 + * @param nodes + * @param canvasX + * @param canvasY + * @returns + */ + searchDataByCoord(nodes: any, canvasX: number, canvasY: number) { + for (const key in nodes) { + for (let i = 0; i < nodes[key].length; i++) { + const cur = nodes[key][i]; + if (this.isContains(cur.frame, canvasX, canvasY)) { + return cur; + } + } + } + return null; + } + + /** + * 绘制方法 + * @param ctx + * @param data + */ + draw(ctx: CanvasRenderingContext2D, data: SampleStruct) { + let spApplication = document.getElementsByTagName('sp-application')[0]; + if (data.frame) { + ctx.globalAlpha = 1; + ctx.fillStyle = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth!, ColorUtils.FUNC_COLOR.length)]; + const textColor = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth!, ColorUtils.FUNC_COLOR.length)]; + ctx.lineWidth = 0.4; + if (this.hoverSampleStruct && data.name == this.hoverSampleStruct.name) { + if (spApplication.dark) { + ctx.strokeStyle = '#fff'; + } else { + ctx.strokeStyle = '#000'; + } + } else { + if (spApplication.dark) { + ctx.strokeStyle = '#000'; + } else { + ctx.strokeStyle = '#fff'; + } + } + ctx.strokeRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + ctx.fillRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + ctx.fillStyle = ColorUtils.funcTextColor(textColor); + drawString(ctx, `${data.detail}(${data.name})`, 5, data.frame, data); + } + } + + /** + * 统计框选指令数的平均值 + * @param instructionData + * @returns + */ + getAvgInstructionData(instructionData: Array) { + const length = instructionData[0].property.length; + const knowData = instructionData.filter(instruction => instruction["name"].indexOf("unknown") < 0); + knowData.forEach(instruction => { + const totalInstruction = instruction["property"].reduce((pre: number, cur: SampleStruct) => pre + (cur["instructions"]! === 0 ? 3 : cur["instructions"]!), 0); + const totalCycles = instruction["property"].reduce((pre: number, cur: SampleStruct) => pre + (cur["cycles"]! === 0 ? 2 : cur["cycles"]!), 0); + instruction["instructions"] = Math.ceil(totalInstruction / length); + instruction["cycles"] = Math.ceil(totalCycles / length); + this.maxDepth = Math.max(this.maxDepth, instruction["depth"]); + }) + const unknownData = instructionData.filter(instruction => instruction["name"].indexOf("unknown") > -1); + let instructionSum = 0; + let cyclesSum = 0; + unknownData.forEach(unknown => { + instructionSum = 0; + cyclesSum = 0; + for (const key in unknown["children"]) { + const child = instructionData.find(instruction => instruction["name"] === key); + instructionSum += child['instructions']; + cyclesSum += child["cycles"]; + } + unknown["instructions"] = instructionSum; + unknown["cycles"] = cyclesSum; + }) + return instructionData; + } + + /** + * 监听页面size变化 + */ + listenerResize(): void { + new ResizeObserver(() => { + if (this.instructionEle!.getBoundingClientRect()) { + const box = this.instructionEle!.getBoundingClientRect(); + const element = document.documentElement; + this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft; + this.startY = + box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop; + } + }).observe(this.parentElement!) + } + + +} diff --git a/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelectionTotalTime.ts b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelectionTotalTime.ts new file mode 100644 index 0000000000000000000000000000000000000000..57c24589342849bec184402c3ccc0b353e5f85b8 --- /dev/null +++ b/ide/src/trace/component/trace/sheet/sample/TabPaneSampleInstructionSelectionTotalTime.ts @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { BaseElement, element } from '../../../../../base-ui/BaseElement'; +import { SelectionParam } from '../../../../bean/BoxSelection'; +import { debounce } from '../../../Utils'; + +const paddingLeft = 100; +const paddingBottom = 15; +const xStart = 50; // x轴起始位置 +const barWidth = 2 // 柱子宽度 +const millisecond = 1_000_000; + +@element('tab-sample-instructions-totaltime-selection') +export class TabPaneSampleInstructionTotalTime extends BaseElement { + private instructionChartEle: HTMLCanvasElement | undefined | null; + private ctx: CanvasRenderingContext2D| undefined | null; + private cacheData: Array = []; + private canvasX = -1; // 鼠标当前所在画布x坐标 + private canvasY = -1; // 鼠标当前所在画布y坐标 + private startX = 0; // 画布相对于整个界面的x坐标 + private startY = 0; // 画布相对于整个界面的y坐标 + private hoverBar: any; + private onReadableData: Array = []; + private hintContent = ""; //悬浮框内容 + private floatHint: HTMLDivElement | undefined | null; //悬浮框 + private canvasScrollTop = 0; // tab页上下滚动位置 + private isUpdateCanvas = false; + private xCount = 0; //x轴刻度个数 + private xMaxValue = 0; //x轴上数据最大值 + private xSpacing = 50; //x轴间距 + private xAvg = 0; //根据xMaxValue进行划分 用于x轴上刻度显示 + private yAvg = 0; //根据yMaxValue进行划分 用于y轴上刻度显示 + + initHtml(): string { + return ` + + +
+ ` + } + + initElements(): void { + this.instructionChartEle = this.shadowRoot?.querySelector('#instruct-chart-canvas'); + this.ctx = this.instructionChartEle?.getContext('2d'); + this.floatHint = this.shadowRoot?.querySelector('#float_hint'); + } + + set data(SampleParam: SelectionParam) { + this.onReadableData = SampleParam.sampleData[0].property; + this.calInstructionRangeCount(); + } + + connectedCallback(): void { + super.connectedCallback(); + this.parentElement!.onscroll = () => { + this.canvasScrollTop = this.parentElement!.scrollTop; + this.hideTip(); + } + this.instructionChartEle!.onmousemove = (e): void => { + if (!this.isUpdateCanvas) { + this.updateCanvasCoord(); + } + this.canvasX = e.clientX - this.startX; + this.canvasY = e.clientY - this.startY + this.canvasScrollTop; + this.onMouseMove(); + } + this.instructionChartEle!.onmouseleave = () => { + this.hideTip(); + } + this.listenerResize(); + } + + /** + * 更新canvas坐标 + */ + updateCanvasCoord(): void { + if (this.instructionChartEle instanceof HTMLCanvasElement) { + this.isUpdateCanvas = this.instructionChartEle.clientWidth !== 0; + if (this.instructionChartEle.getBoundingClientRect()) { + const box = this.instructionChartEle.getBoundingClientRect(); + const D = this.parentElement!; + this.startX = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; + this.startY = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop + this.canvasScrollTop; + } + } + } + + /** + * 获取鼠标悬停的函数 + * @param nodes + * @param canvasX + * @param canvasY + * @returns + */ + searchDataByCoord(nodes: any, canvasX: number, canvasY: number) { + for (let i = 0; i < nodes.length; i++) { + const element = nodes[i]; + if (this.isContains(element, canvasX, canvasY)) { + return element; + } + } + return null; + } + + /** + * 鼠标移动 + */ + onMouseMove(): void { + const lastNode = this.hoverBar; + //查找鼠标所在的node + const searchResult = this.searchDataByCoord(this.cacheData, this.canvasX, this.canvasY); + if (searchResult) { + this.hoverBar = searchResult; + //鼠标悬浮的node未改变则不需重新渲染文字 + if (searchResult !== lastNode) { + this.updateTipContent(); + } + this.showTip(); + } else { + this.hideTip(); + this.hoverBar = undefined; + } + } + + /** + * 隐藏悬浮框 + */ + hideTip(): void { + if (this.floatHint) { + this.floatHint.style.display = 'none'; + this.instructionChartEle!.style.cursor = 'default'; + } + } + + /** + * 显示悬浮框 + */ + showTip(): void { + this.floatHint!.innerHTML = this.hintContent; + this.floatHint!.style.display = 'block'; + this.instructionChartEle!.style.cursor = 'pointer'; + let x = this.canvasX; + let y = this.canvasY - this.canvasScrollTop; + //右边的函数悬浮框显示在左侧 + if (this.canvasX + this.floatHint!.clientWidth > (this.instructionChartEle!.clientWidth || 0)) { + x -= this.floatHint!.clientWidth - 1; + } else { + x += 30; + } + //最下边的函数悬浮框显示在上方 + y -= this.floatHint!.clientHeight - 1; + this.floatHint!.style.transform = `translate(${x}px, ${y}px)`; + } + + /** + * 更新悬浮框内容 + */ + updateTipContent(): void { + const hoverNode = this.hoverBar; + if (!hoverNode) { + return; + } + const detail = hoverNode!; + this.hintContent = ` + ${detail.instruct}
+ ${parseFloat(detail.heightPer)}% + `; + } + + /** + * 判断鼠标当前在那个函数上 + * @param frame + * @param x + * @param y + * @returns + */ + isContains(point: any, x: number, y: number): boolean { + return x >= point.x && x <= point.x + 2 && point.y <= y && y <= point.y + point.height; + } + + /** + * 统计onReadable数据各指令数个数 + */ + calInstructionRangeCount() { + if (this.onReadableData.length === 0) return; + this.cacheData.length = 0; + const count = this.onReadableData.length; + const onReadableData = this.onReadableData; + const instructionArray = onReadableData.reduce((pre: any, current: any) => { + const dur = ((current.end - current.begin) / millisecond).toFixed(1); + (pre[dur] = pre[dur] || []).push(current); + return pre; + }, {}); + this.ctx!.clearRect(0, 0, this.instructionChartEle!.width, this.instructionChartEle!.height); + this.instructionChartEle!.width = this.clientWidth; + + this.xMaxValue = Object.keys(instructionArray).map(i => Number(i)).reduce((pre, cur) => Math.max(pre, cur), 0) + 5; + const yMaxValue = Object.values(instructionArray).reduce((pre: number, cur: any) => Math.max(pre, Number((cur.length / count).toFixed(2))), 0); + this.yAvg = Number(((yMaxValue / 5) * 1.5).toFixed(2)); + const height = this.instructionChartEle!.height; + const width = this.instructionChartEle!.width; + this.drawLineLabelMarkers(width, height); + this.drawBar(instructionArray, height, count); + } + + /** + * 绘制柱状图 + * @param instructionData + * @param height + * @param count + */ + drawBar(instructionData: any, height: number, count: number) { + const yTotal = Number((this.yAvg * 5).toFixed(2)); + const interval = Math.floor((height - paddingBottom) / 6); + for (const x in instructionData) { + const xNum = Number(x); + const xPosition = xStart + ( xNum / (this.xCount * this.xAvg) ) * (this.xCount * this.xSpacing) - (barWidth / 2); + const yNum = Number((instructionData[x].length / count).toFixed(2)); + const percent = Number((yNum / yTotal).toFixed(2)); + const barHeight = (height - paddingBottom - interval) * percent; + this.drawRect(xPosition, height - paddingBottom - barHeight, barWidth, barHeight); + const existX = this.cacheData.find(i => i.instruct === x); + if (!existX) { + this.cacheData.push({ + instruct: x, + x: xPosition, + y: height - paddingBottom - barHeight, + height: barHeight, + heightPer: yNum + }) + } else { + existX.x = xPosition; + } + } + } + + /** + * 绘制x y轴 + * @param width + * @param height + */ + drawLineLabelMarkers(width: number, height: number) { + this.ctx!.font = "12px Arial"; + this.ctx!.lineWidth = 1; + this.ctx!.fillStyle = "#333"; + this.ctx!.strokeStyle = "#ccc"; + + this.ctx!.fillText('时长 / ms', width - paddingLeft, height - paddingBottom); + + //绘制x轴 + this.drawLine(xStart, height - paddingBottom, width - paddingLeft, height - paddingBottom); + //绘制y轴 + this.drawLine(xStart, 5, xStart, height - paddingBottom); + //绘制标记 + this.drawMarkers(width, height) + } + + /** + * 绘制横线 + * @param x + * @param y + * @param X + * @param Y + */ + drawLine(x:number, y: number, X: number, Y: number) { + this.ctx!.beginPath; + this.ctx!.moveTo(x, y); + this.ctx!.lineTo(X, Y); + this.ctx!.stroke(); + this.ctx!.closePath(); + } + + /** + * 绘制x y轴刻度 + * @param width + * @param height + */ + drawMarkers(width: number, height: number) { + this.xCount = 0; + //绘制x轴锯齿 + let serrateX = 50; + let yHeight = height - paddingBottom; + const clientWidth = width - paddingLeft - 50; + if (clientWidth > this.xMaxValue) { + this.xSpacing = Math.floor(clientWidth / 20) + this.xAvg = Math.ceil(this.xMaxValue / 20) + } else { + this.xSpacing = Math.floor(clientWidth / 10) + this.xAvg = Math.ceil(this.xMaxValue / 10) + } + while (serrateX <= clientWidth) { + this.xCount++; + serrateX += this.xSpacing; + this.drawLine(serrateX, yHeight, serrateX, yHeight + 5); + } + //绘制x轴刻度 + this.ctx!.textAlign = "center"; + for (let i = 0; i <= this.xCount; i++) { + const x = xStart + (i * this.xSpacing); + this.ctx!.fillText(`${i * this.xAvg}`, x, height); + } + //绘制y轴刻度 + this.ctx!.textAlign = "center"; + const yPadding = Math.floor((height - paddingBottom) / 6); + for (let i = 0; i < 6; i++) { + const y = (height - paddingBottom) - (i * yPadding); + if (i === 0) { + this.ctx!.fillText(`${i}%`, 30, y); + } else { + this.drawLine(xStart, y, width - paddingLeft, y); + this.ctx!.fillText(`${parseFloat((i * this.yAvg).toFixed(2))}%`, 30, y); + } + } + } + + /** + * 监听页面size变化 + */ + listenerResize(): void { + new ResizeObserver(debounce(() => { + if (this.instructionChartEle!.getBoundingClientRect()) { + const box = this.instructionChartEle!.getBoundingClientRect(); + const element = this.parentElement!; + this.startX = box.left + Math.max(element.scrollLeft, document.body.scrollLeft) - element.clientLeft; + this.startY = + box.top + Math.max(element.scrollTop, document.body.scrollTop) - element.clientTop + this.canvasScrollTop; + this.calInstructionRangeCount(); + } + }, 100)).observe(this.parentElement!) + } + /** + * 绘制方块 + * @param x + * @param y + * @param X + * @param Y + */ + drawRect(x: number, y: number, X: number, Y: number) { + this.ctx!.beginPath(); + this.ctx!.rect(x, y, X, Y); + this.ctx!.fill(); + this.ctx!.closePath() + } +} diff --git a/ide/src/trace/database/ui-worker/ProcedureWorker.ts b/ide/src/trace/database/ui-worker/ProcedureWorker.ts index 57a4f23527481db36b5b310f07dff6dd3ea38769..330d51127a910e239e8f133bcf5a1dcd52958d9b 100644 --- a/ide/src/trace/database/ui-worker/ProcedureWorker.ts +++ b/ide/src/trace/database/ui-worker/ProcedureWorker.ts @@ -61,6 +61,7 @@ import { HiSysEventRender } from './ProcedureWorkerHiSysEvent'; import { AllAppStartupRender } from './ProcedureWorkerAllAppStartup'; import { LtpoRender } from './ProcedureWorkerLTPO'; import { hitchTimeRender } from './ProcedureWorkerHitchTime'; +import { SampleRender } from './ProcedureWorkerSample'; let dataList: any = {}; let dataList2: any = {}; @@ -119,6 +120,7 @@ export let renders: any = { logs: new LogRender(), hiSysEvent: new HiSysEventRender(), 'hitch':new hitchTimeRender(), + sample: new SampleRender(), }; function match(type: string, req: RequestMessage): void { diff --git a/ide/src/trace/database/ui-worker/ProcedureWorkerSample.ts b/ide/src/trace/database/ui-worker/ProcedureWorkerSample.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5da2463cc7766d1919cb4e6ac545f65dcdcc2c5 --- /dev/null +++ b/ide/src/trace/database/ui-worker/ProcedureWorkerSample.ts @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2022 Huawei Device Co., Ltd. + * 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. + */ + +import { ColorUtils } from '../../component/trace/base/ColorUtils'; +import { + BaseStruct, + Render, + ns2x, + Rect, + drawString, + isFrameContainPoint, +} from './ProcedureWorkerCommon'; +import { TraceRow } from '../../component/trace/base/TraceRow'; + +const SAMPLE_STRUCT_HEIGHT = 20; +const Y_PADDING = 2; + +export class SampleRender extends Render { + renderMainThread( + req: { + context: CanvasRenderingContext2D, + useCache: boolean, + type: string, + start_ts: number, + uniqueProperty: Array, + flattenTreeArray: Array + }, + row: TraceRow + ) { + let startTs = req.start_ts; + let sampleList = row.dataList; + let sampleFilter = row.dataListCache; + SampleStruct.reqProperty = req; + func( + sampleList, + sampleFilter, + TraceRow.range!.startNS, + TraceRow.range!.endNS, + TraceRow.range!.totalNS, + startTs, + row.frame, + req.useCache || TraceRow.range!.refresh + ); + req.context.beginPath(); + let funcFind = false; + for ( let re of sampleFilter) { + if (row.isHover) { + if (re.frame && isFrameContainPoint(re.frame, row.hoverX, row.hoverY)) { + SampleStruct.hoverSampleStruct = re; + funcFind = true; + } + } + SampleStruct.draw(req.context, re); + } + if (!funcFind && row.isHover) SampleStruct.hoverSampleStruct = undefined; + req.context.closePath(); + } +} + +export function func ( + sampleList: Array, + sampleFilter: Array, + startNS: number, + endNS: number, + totalNS: number, + startTS: number, + frame: any, + use: boolean +) { + if (use && sampleFilter.length > 0) { + for (let i = 0, len = sampleFilter.length; i < len; i++) { + if (((sampleFilter[i].end - startTS) || 0) >= startNS && ((sampleFilter[i].begin - startTS) || 0) <= endNS) { + SampleStruct.setSampleFrame(sampleFilter[i], 0, startNS, endNS, totalNS, startTS, frame); + } else { + sampleFilter[i].frame = null; + } + } + return; + } + sampleFilter.length = 0; + if (sampleList) { + sampleList.forEach(func => { + let funcProperty: Array = func.property; + let groups = funcProperty + .filter(it => (( it.end - startTS) ?? 0) >= startNS && (( it.begin - startTS) ?? 0 ) <= endNS) + .map(it => { + SampleStruct.setSampleFrame(it, 0, startNS, endNS, totalNS, startTS, frame); + return it; + }) + .reduce((pre, current) => { + ( pre[`${current.frame.x}-${current.level}`] = pre[`${current.frame.x}-${current.level}`] || []).push(current); + return pre; + }, {}); + Reflect.ownKeys(groups).map((kv) => { + let arr = groups[kv].sort(( a: any, b: any) => (b.end - b.start) - (a.end - a.start)); + sampleFilter.push(arr[0]); + }) + }) + } +} + +export class SampleStruct extends BaseStruct { + static hoverSampleStruct: SampleStruct | undefined; + static selectSampleStruct: SampleStruct | undefined; + static reqProperty: any | undefined; + name: string | undefined; + detail: string | undefined; + property: Array | undefined; + begin: number | undefined; + end: number | undefined; + depth: number | undefined; + startTs: number | undefined; + instructions: number | undefined; + cycles: number | undefined; + static setSampleFrame( + sampleNode: SampleStruct, + padding: number, + startNS: number, + endNS: number, + totalNS: number, + startTS: number, + frame: any + ): void { + let x1: number, x2: number; + if (((sampleNode.begin! - startTS) || 0) > startNS && ((sampleNode.begin! - startTS) || 0) < endNS) { + x1 = ns2x((sampleNode.begin! - startTS) || 0, startNS, endNS, totalNS, frame); + } else { + x1 = 0; + } + if (((sampleNode.end! - startTS) || 0) > startNS && ((sampleNode.end! - startTS) || 0) < endNS) { + x2 = ns2x((sampleNode.end! - startTS) || 0, startNS, endNS, totalNS, frame) + } else { + x2 = frame.width; + } + if (!sampleNode.frame) { + sampleNode.frame! = new Rect(0, 0, 0, 0); + } + let getV: number = x2 - x1 < 1 ? 1 : x2 - x1; + sampleNode.frame!.x = Math.floor(x1); + sampleNode.frame!.y = sampleNode.depth! * SAMPLE_STRUCT_HEIGHT; + sampleNode.frame!.width = Math.ceil(getV); + sampleNode.frame!.height = SAMPLE_STRUCT_HEIGHT; + sampleNode.startTs = startTS; + } + static draw(ctx: CanvasRenderingContext2D, data: SampleStruct) { + if (data.depth === undefined || data.depth === null) { + return; + } + if (data.frame) { + ctx.globalAlpha = 0.9; + ctx.fillStyle = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth, ColorUtils.FUNC_COLOR.length)]; + const textColor = ColorUtils.FUNC_COLOR[ColorUtils.hashFunc(data.name || '', data.depth, ColorUtils.FUNC_COLOR.length)]; + if (SampleStruct.hoverSampleStruct && data.name === SampleStruct.hoverSampleStruct.name) { + ctx.globalAlpha = 0.7; + } + ctx.fillRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + if (data.frame.width > 10) { + ctx.lineWidth = 1; + ctx.strokeRect(data.frame.x, data.frame.y, data.frame.width, SAMPLE_STRUCT_HEIGHT - Y_PADDING); + ctx.fillStyle = ColorUtils.funcTextColor(textColor); + drawString(ctx, `${data.detail + '(' + data.name + ')' || ''}`, 5, data.frame, data); + } + } + } +} \ No newline at end of file