积分47 / 贡献0

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

作者动态

[开发者活动] 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!

返回顶部