积分21 / 贡献0

提问0答案被采纳0文章1

作者动态

    [开发者活动] SmartPerf 性能分析工具 代码结构分析(一) 精华

    深开鸿 李雨溪 显示全部楼层 发表于 2024-6-24 11:22:21

    SmartPerf 性能分析工具 代码结构分析(一)

    前言

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

    (摘自 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[i];
          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[i],
          value: value[i],
          startNS: startNS[i],
          dur: dur[i],
        } 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[i] = it.dur;
        // @ts-ignore
        startNS[i] = it.startNs;
        // @ts-ignore
        filterId[i] = it.filterId;
        // @ts-ignore
        value[i] = 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 的核心功能:泳道图绘制这一部分 的主要数据流向和函数调用关系。

    无用

    ©著作权归作者所有,转载或内容合作请联系作者

    您尚未登录,无法参与评论,登录后可以:
    参与开源共建问题交流
    认同或收藏高质量问答
    获取积分成为开源共建先驱

    Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

    返回顶部