OpenHarmony开发者论坛

标题: SmartPerf 性能分析工具 代码结构分析(一) [打印本页]

作者: 深开鸿 李雨溪    时间: 2024-6-24 11:22
标题: SmartPerf 性能分析工具 代码结构分析(一)
[md]# SmartPerf 性能分析工具 代码结构分析(一)

## 前言

**Smartperf\_Host是一款深入挖掘数据、细粒度地展示数据的性能功耗调优工具,旨在为开发者提供一套性能调优平台,支持对CPU调度、频点、进程线程时间片、堆内存、帧率等数据进行采集和展示,展示方式为泳道图,支持GUI(图形用户界面)操作进行详细数据分析。**

**(摘自 **[developtools\_smartperf\_host 开源仓库](https://gitee.com/openharmony/developtools_smartperf_host) readme.md)

**本文将从IDE部分,即前端代码的角度入手,简要分析该项目的代码结构,帮助阅读该开源项目的代码,方便各位开发者阅读源码并积极参与开源社区的维护。**

**本文主要分析泳道图绘制相关部分的代码层级逻辑。**

## 目录结构

**smartperf 的项目结构比较庞大,尤其是泳道图的绘制部分代码比较复杂,且代码自主性较强,没有对其他框架的依赖。**

**我们先从外部目录结构入手(忽略部分次要目录):**

* **bin**
  * 二进制文件依赖。SmartPerf IDE 对于 Trace 文件的解析依赖于 tracestreamer,该目录用于存放 WASM 格式的 tracestreamer 库,在执行 **`npm run build` 前需要手动编译 tracestreamer 并放入该路径下。
* **dist**
  * **程序打包目录**
* **server**
  * **起静态服务器,使用 go 语言编写。**
* **src**
  * **核心代码**
* **test**
  * **结构化测试用例**
* **third-party**
  * **第三方依赖。这里主要是 SQLite 的 WASM 库,编译前需要手动放入三方依赖。**

## 泳道图绘制

有关泳道图绘制功能的部分代码主要在 `src/trace/database` 路径下。我们以 **clock** 泳道为例,一条 clock 泳道绘制的过程主要包括:

* **SpClockChart.ts**
  * **作为整个 Clock 泳道组的抽象**
  * *负责初始化*父泳道*  `Trace<unknown>` (当父泳道无需绘制内容时,对其数据类型不敏感)
  * 负责初始化*各个线程的 Clock 信息子泳道* `Trace<ClockStruct>`(子泳道需要绘制内容,泛型参数需要和原数据类型一一对应)
    * **当需要渲染数据时**
      * 由基类 `TraceRow<T>` 所维护的 `supplierFrame()` 和 `getCacheData()` 方法来实时更新渲染数据
      * 对于每一条子泳道,通过调用 `clockThreadHandler` 来执行泳道内的绘制事件
    * 鼠标悬停、选中等等事件也由该基类 `TracecRow<T>`下的 `focusHandler()` 和 `findHoverStruct()` 等方法来保证

```
export class SpClockChart {
 private trace: SpSystemTrace;

 constructor(trace: SpSystemTrace) {
   this.trace = trace;
  }

 async init(): Promise<void> {
   let folder = await this.initFolder();
   await this.initData(folder);
  }

 private clockSupplierFrame(
   traceRow: TraceRow<ClockStruct>,
   it: {
     name: string;
     num: number;
     srcname: string;
     maxValue?: number;
   },
   isState: boolean,
   isScreenState: boolean
  ): void {
   traceRow.supplierFrame = (): Promise<ClockStruct[]> => {
     let promiseData = null;
     if (it.name.endsWith(' Frequency')) {
       promiseData = clockDataSender(it.srcname, 'clockFrequency', traceRow);
     } else if (isState) {
       promiseData = clockDataSender(it.srcname, 'clockState', traceRow);
     } else if (isScreenState) {
       promiseData = clockDataSender('', 'screenState', traceRow);
     }
     if (promiseData === null) {
       // @ts-ignore
       return new Promise<Array<unknown>>((resolve) => resolve([]));
     } else {
       // @ts-ignore
       return promiseData.then((resultClock: Array<unknown>) => {
         for (let j = 0; j < resultClock.length; j++) {
           // @ts-ignore
           resultClock[j].type = 'measure'; // @ts-ignore
           if ((resultClock[j].value || 0) > it.maxValue!) {
             // @ts-ignore
             it.maxValue = resultClock[j].value || 0;
           }
           if (j > 0) {
             // @ts-ignore
             resultClock[j].delta = (resultClock[j].value || 0) - (resultClock[j - 1].value || 0);
           } else {
             // @ts-ignore
             resultClock[j].delta = 0;
           }
         }
         return resultClock;
       });
     }
   };
  }

 private clockThreadHandler(
   traceRow: TraceRow<ClockStruct>,
   it: {
     name: string;
     num: number;
     srcname: string;
     maxValue?: number;
   },
   isState: boolean,
   isScreenState: boolean,
   clockId: number
  ): void {
   traceRow.onThreadHandler = (useCache): void => {
     let context: CanvasRenderingContext2D;
     if (traceRow.currentContext) {
       context = traceRow.currentContext;
     } else {
       context = traceRow.collect ? this.trace.canvasFavoritePanelCtx! : this.trace.canvasPanelCtx!;
     }
     traceRow.canvasSave(context);
     (renders.clock as ClockRender).renderMainThread(
       {
         context: context,
         useCache: useCache,
         type: it.name,
         maxValue: it.maxValue === 0 ? 1 : it.maxValue!,
         index: clockId,
         maxName:
           isState || isScreenState
             ? it.maxValue!.toString()
             : Utils.getFrequencyWithUnit(it.maxValue! / 1000).maxFreqName,
       },
       traceRow
     );
     traceRow.canvasRestore(context, this.trace);
   };
  }
 // @ts-ignore
 async initData(folder: TraceRow<unknown>): Promise<void> {
   let clockStartTime = new Date().getTime();
   let clockList = await queryClockData();
   if (clockList.length === 0) {
     return;
   }
   info('clockList data size is: ', clockList!.length);
   this.trace.rowsEL?.appendChild(folder);
   ClockStruct.maxValue = clockList.map((item) => item.num).reduce((a, b) => Math.max(a, b));
   for (let i = 0; i < clockList.length; i++) {
     const it = clockList;
     it.maxValue = 0;
     let traceRow = TraceRow.skeleton<ClockStruct>();
     let isState = it.name.endsWith(' State');
     let isScreenState = it.name.endsWith('ScreenState');
     traceRow.rowId = it.name;
     traceRow.rowType = TraceRow.ROW_TYPE_CLOCK;
     traceRow.rowParentId = folder.rowId;
     traceRow.style.height = '40px';
     traceRow.name = it.name;
     traceRow.rowHidden = !folder.expansion;
     traceRow.setAttribute('children', '');
     traceRow.favoriteChangeHandler = this.trace.favoriteChangeHandler;
     traceRow.selectChangeHandler = this.trace.selectChangeHandler;
     this.clockSupplierFrame(traceRow, it, isState, isScreenState);
     traceRow.getCacheData = (args: unknown): Promise<Array<unknown>> => {
       if (it.name.endsWith(' Frequency')) {
         return clockDataSender(it.srcname, 'clockFrequency', traceRow, args);
       } else if (isState) {
         return clockDataSender(it.srcname, 'clockState', traceRow, args);
       } else if (isScreenState) {
         return clockDataSender('', 'screenState', traceRow, args);
       } else {
         return new Promise((): void => {});
       }
     };
     traceRow.focusHandler = (ev): void => {
       this.trace?.displayTip(
         traceRow,
         ClockStruct.hoverClockStruct,
         `<span>${ColorUtils.formatNumberComma(ClockStruct.hoverClockStruct?.value!)}</span>`
       );
     };
     traceRow.findHoverStruct = (): void => {
       ClockStruct.hoverClockStruct = traceRow.getHoverStruct();
     };
     this.clockThreadHandler(traceRow, it, isState, isScreenState, i);
     folder.addChildTraceRow(traceRow);
   }
   let durTime = new Date().getTime() - clockStartTime;
   info('The time to load the ClockData is: ', durTime);
  }
 // @ts-ignore
 async initFolder(): Promise<TraceRow<unknown>> {
   let clockFolder = TraceRow.skeleton();
   clockFolder.rowId = 'Clocks';
   clockFolder.index = 0;
   clockFolder.rowType = TraceRow.ROW_TYPE_CLOCK_GROUP;
   clockFolder.rowParentId = '';
   clockFolder.style.height = '40px';
   clockFolder.folder = true;
   clockFolder.name = 'Clocks';
   clockFolder.favoriteChangeHandler = this.trace.favoriteChangeHandler;
   clockFolder.selectChangeHandler = this.trace.selectChangeHandler; // @ts-ignore
   clockFolder.supplier = (): Promise<unknown[]> => new Promise<Array<unknown>>((resolve) => resolve([]));
   clockFolder.onThreadHandler = (useCache): void => {
     clockFolder.canvasSave(this.trace.canvasPanelCtx!);
     if (clockFolder.expansion) {
       // @ts-ignore
       this.trace.canvasPanelCtx?.clearRect(0, 0, clockFolder.frame.width, clockFolder.frame.height);
     } else {
       (renders.empty as EmptyRender).renderMainThread(
         {
           context: this.trace.canvasPanelCtx,
           useCache: useCache,
           type: '',
         },
         clockFolder
       );
     }
     clockFolder.canvasRestore(this.trace.canvasPanelCtx!, this.trace);
   };
   return clockFolder;
  }
}

```

* **ClockRender.ts**
  * 实现 `ClockStruct` 用作绘制泳道图图块的数据模型
  * 实现 `ClockRender` 用于执行渲染图块前的数据预处理,以及调用图块的渲染方法
    * 具体绘制行为在 `ClockStruct.draw` 方法中实现,诸如图块的绘制,被选中后图块的样式变化等都在这个部分实现

```
export class ClockRender extends Render {
 renderMainThread(
   clockReq: {
     context: CanvasRenderingContext2D;
     useCache: boolean;
     type: string;
     maxValue: number;
     index: number;
     maxName: string;
   },
   row: TraceRow<ClockStruct>
  ): void {
   ClockStruct.index = clockReq.index;
   let clockList = row.dataList;
   let clockFilter = row.dataListCache;
   dataFilterHandler(clockList, clockFilter, {
     startKey: 'startNS',
     durKey: 'dur',
     startNS: TraceRow.range?.startNS ?? 0,
     endNS: TraceRow.range?.endNS ?? 0,
     totalNS: TraceRow.range?.totalNS ?? 0,
     frame: row.frame,
     paddingTop: 5,
     useCache: clockReq.useCache || !(TraceRow.range?.refresh ?? false),
   });
   drawLoadingFrame(clockReq.context, clockFilter, row);
   clockReq.context.beginPath();
   let find = false;
   for (let re of clockFilter) {
     ClockStruct.draw(clockReq.context, re, clockReq.maxValue);
     if (row.isHover && re.frame && isFrameContainPoint(re.frame, row.hoverX, row.hoverY)) {
       ClockStruct.hoverClockStruct = re;
       find = true;
     }
   }
   if (!find && row.isHover) {
     ClockStruct.hoverClockStruct = undefined;
   }
   clockReq.context.closePath();
   let s = clockReq.maxName;
   let textMetrics = clockReq.context.measureText(s);
   clockReq.context.globalAlpha = 0.8;
   clockReq.context.fillStyle = '#f0f0f0';
   clockReq.context.fillRect(0, 5, textMetrics.width + 8, 18);
   clockReq.context.globalAlpha = 1;
   clockReq.context.fillStyle = '#333';
   clockReq.context.textBaseline = 'middle';
   clockReq.context.fillText(s, 4, 5 + 9);
  }
}
export function ClockStructOnClick(clickRowType: string, sp: SpSystemTrace): Promise<unknown> {
 return new Promise((resolve, reject) => {
   if (clickRowType === TraceRow.ROW_TYPE_CLOCK && ClockStruct.hoverClockStruct) {
     ClockStruct.selectClockStruct = ClockStruct.hoverClockStruct;
     sp.traceSheetEL?.displayClockData(ClockStruct.selectClockStruct);
     sp.timerShaftEL?.modifyFlagList(undefined);
     reject(new Error());
   } else {
     resolve(null);
   }
  });
}
export class ClockStruct extends BaseStruct {
 static maxValue: number = 0;
 static maxName: string = '';
 static hoverClockStruct: ClockStruct | undefined;
 static selectClockStruct: ClockStruct | undefined;
 static index = 0;
 filterId: number | undefined;
 value: number | undefined;
 startNS: number | undefined;
 dur: number | undefined; //自补充,数据库没有返回
 delta: number | undefined; //自补充,数据库没有返回

 static draw(clockContext: CanvasRenderingContext2D, data: ClockStruct, maxValue: number): void {
   if (data.frame) {
     let width = data.frame.width || 0;
     clockContext.fillStyle = ColorUtils.colorForTid(ClockStruct.index);
     clockContext.strokeStyle = ColorUtils.colorForTid(ClockStruct.index);
     let drawHeight: number = Math.floor(((data.value || 0) * (data.frame.height || 0) * 1.0) / maxValue);
     if (drawHeight === 0) {
       drawHeight = 1;
     }
     if (ClockStruct.isHover(data)) {
         ...   // Canvas
     } else {
         ...   // Canvas
     }
   }
   ...
  }

 static isHover(clock: ClockStruct): boolean {
   return clock === ClockStruct.hoverClockStruct || clock === ClockStruct.selectClockStruct;
  }
}
```

* **ClockDataSender.ts**
  * 负责生成需要向数据库获取数据时的回调函数
  * 通过 `threadPool.submitProto`发送一条消息,由全局Worker接收,进行SQL查询获取所需数据。
    * 注意,此处数据只能通过 `Transferable`的形式传递

```
export function clockDataSender(
 clockName: string = '',
 sqlType: string,
 row: TraceRow<ClockStruct>,
 args?: unknown
): Promise<ClockStruct[]> {
 let trafic: number = TraficEnum.Memory;
 let width = row.clientWidth - CHART_OFFSET_LEFT;
 if (trafic === TraficEnum.SharedArrayBuffer && !row.sharedArrayBuffers) {
   row.sharedArrayBuffers = {
     filterId: new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * MAX_COUNT),
     value: new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * MAX_COUNT),
     startNS: new SharedArrayBuffer(Float64Array.BYTES_PER_ELEMENT * MAX_COUNT),
     dur: new SharedArrayBuffer(Float64Array.BYTES_PER_ELEMENT * MAX_COUNT),
   };
  }
 return new Promise((resolve, reject): void => {
   threadPool.submitProto(
     QueryEnum.ClockData,
     {
       clockName: clockName,
       sqlType: sqlType,
       startNS: TraceRow.range?.startNS || 0,
       endNS: TraceRow.range?.endNS || 0,
       totalNS: TraceRow.range?.totalNS || 0,
       recordStartNS: window.recordStartNS,
       recordEndNS: window.recordEndNS,
       // @ts-ignore
       queryAll: args && args.queryAll,
       // @ts-ignore
       selectStartNS: args ? args.startNS : 0,
       // @ts-ignore
       selectEndNS: args ? args.endNS : 0,
       // @ts-ignore
       selectTotalNS: args ? args.endNS - args.startNS : 0,
       t: Date.now(),
       width: width,
       trafic: trafic,
       sharedArrayBuffers: row.sharedArrayBuffers,
     },
     (res: unknown, len: number, transfer: boolean): void => {
       resolve(arrayBufferHandler(transfer ? res : row.sharedArrayBuffers, len));
     }
   );
  });
}

function arrayBufferHandler(buffers: unknown, len: number): ClockStruct[] {
 let outArr: ClockStruct[] = [];
 // @ts-ignore
 let filterId = new Int32Array(buffers.filterId);
 // @ts-ignore
 let value = new Int32Array(buffers.value);
 // @ts-ignore
 let startNS = new Float64Array(buffers.startNS);
 // @ts-ignore
 let dur = new Float64Array(buffers.dur);
 for (let i = 0; i < len; i++) {
   outArr.push({
     filterId: filterId,
     value: value,
     startNS: startNS,
     dur: dur,
   } as unknown as ClockStruct);
  }
 return outArr;
}
```

* **ClockDataReceiver.ts**
  * 负责实现 DataSender 所发送消息的响应
  * 实现方法 `clockDataReceiver`,通过 SQL 查询 trace 信息
  * 实现 `arrayBufferHandler` 将 SQL 查询到的JS对象信息转化为 `Transferable`

```
import { TraficEnum } from './utils/QueryEnum';
import { filterDataByGroup } from './utils/DataFilter';
import { clockList } from './utils/AllMemoryCache';
import { Args } from './CommonArgs';

export const chartClockDataSql = (args: Args): string => {
   ... // SQL
};

export const chartClockDataSqlMem = (args: Args): string => {
   ... // SQL
};

export function clockDataReceiver(data: unknown, proc: Function): void {
 // @ts-ignore
 if (data.params.trafic === TraficEnum.Memory) {
   let res: unknown[];
   let list: unknown[];
   // @ts-ignore
   if (!clockList.has(data.params.sqlType + data.params.clockName)) {
     // @ts-ignore
     let sql = chartClockDataSqlMem(data.params);
     // @ts-ignore
     list = proc(sql);
     for (let j = 0; j < list.length; j++) {
       if (j === list.length - 1) {
         // @ts-ignore
         list[j].dur = (data.params.totalNS || 0) - (list[j].startNs || 0);
       } else {
         // @ts-ignore
         list[j].dur = (list[j + 1].startNs || 0) - (list[j].startNs || 0);
       }
     }
     // @ts-ignore
     clockList.set(data.params.sqlType + data.params.clockName, list);
   } else {
     // @ts-ignore
     list = clockList.get(data.params.sqlType + data.params.clockName) || [];
   }
   // @ts-ignore
   if (data.params.queryAll) {
     //框选时候取数据,只需要根据时间过滤数据
     res = (list || []).filter(
       // @ts-ignore
       (it) => it.startNs + it.dur >= data.params.selectStartNS && it.startNs <= data.params.selectEndNS
     );
   } else {
     res = filterDataByGroup(
       list || [],
       'startNs',
       'dur',
       // @ts-ignore
       data.params.startNS,
       // @ts-ignore
       data.params.endNS,
       // @ts-ignore
       data.params.width,
       'value'
     );
   }
   arrayBufferHandler(data, res, true);
  } else {
   // @ts-ignore
   let sql = chartClockDataSql(data.params);
   let res = proc(sql);
   // @ts-ignore
   arrayBufferHandler(data, res, data.params.trafic !== TraficEnum.SharedArrayBuffer);
  }
}

function arrayBufferHandler(data: unknown, res: unknown[], transfer: boolean): void {
 // @ts-ignore
 let dur = new Float64Array(transfer ? res.length : data.params.sharedArrayBuffers.dur);
 // @ts-ignore
 let startNS = new Float64Array(transfer ? res.length : data.params.sharedArrayBuffers.startNS);
 // @ts-ignore
 let value = new Int32Array(transfer ? res.length : data.params.sharedArrayBuffers.value);
 // @ts-ignore
 let filterId = new Int32Array(transfer ? res.length : data.params.sharedArrayBuffers.filterId);
 res.forEach((it, i) => {
   // @ts-ignore
   data.params.trafic === TraficEnum.ProtoBuffer && (it = it.clockData);
   // @ts-ignore
   dur = it.dur;
   // @ts-ignore
   startNS = it.startNs;
   // @ts-ignore
   filterId = it.filterId;
   // @ts-ignore
   value = it.value;
  });

 let arg1 = {
   // @ts-ignore
   id: data.id,
   // @ts-ignore
   action: data.action,
   results: transfer
     ? {
         dur: dur.buffer,
         startNS: startNS.buffer,
         value: value.buffer,
         filterId: filterId.buffer,
       }
     : {},
   len: res.length,
   transfer: transfer,
  };
 let arg2 = transfer ? [dur.buffer, startNS.buffer, value.buffer, filterId.buffer] : [];

  (self as Worker).postMessage(
   arg1,
   arg2
  );
}

```

* **ExecProtoForWorker.ts**
  * 统一管理所有 traficHandlers (疑似拼写错误),响应 ClockDataSender 发送的消息,调用 ClockDataReceiver

```
const traficHandlers: Map<number, unknown> = new Map<number, unknown>([]); // @ts-ignore
export const execProtoForWorker = (data: unknown, proc: Function): void => traficHandlers.get(data.name)?.(data, proc);

...
traficHandlers.set(QueryEnum.ClockData, clockDataReceiver);
...
```

## 总结

以上内容主要梳理了 smartperf IDE 的核心功能:泳道图绘制这一部分 的主要数据流向和函数调用关系。
[/md]




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5