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
- 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 的核心功能:泳道图绘制这一部分 的主要数据流向和函数调用关系。