• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15import { JsCpuProfilerChartFrame, JsCpuProfilerTabStruct, JsCpuProfilerUIStruct } from '../../bean/JsStruct.js';
16import { DataCache, JsProfilerSymbol, LogicHandler, convertJSON } from './ProcedureLogicWorkerCommon.js';
17
18const ROOT_ID = 1;
19const LAMBDA_FUNCTION_NAME = '(anonymous)';
20export class ProcedureLogicWorkerJsCpuProfiler extends LogicHandler {
21  private currentEventId!: string;
22  private dataCache = DataCache.getInstance();
23  private samples = Array<JsCpuProfilerSample>(); // Array index equals id;
24  private chartId = 0;
25  private tabDataId = 0;
26
27  public handle(msg: any): void {
28    this.currentEventId = msg.id;
29
30    if (msg && msg.type) {
31      switch (msg.type) {
32        case 'jsCpuProfiler-init':
33          this.chartId = 0;
34          if (!this.dataCache.dataDict || this.dataCache.dataDict.size === 0) {
35            this.dataCache.dataDict = msg.params as Map<number, string>;
36          }
37          this.initCallChain();
38          break;
39        case 'jsCpuProfiler-call-chain':
40          if (!this.dataCache.jsCallChain || this.dataCache.jsCallChain.length === 0) {
41            this.dataCache.jsCallChain = convertJSON(msg.params.list) || [];
42            this.createCallChain();
43          }
44          this.queryChartData();
45          break;
46        case 'jsCpuProfiler-samples':
47          this.samples = convertJSON(msg.params.list) || [];
48          self.postMessage({
49            id: msg.id,
50            action: msg.action,
51            results: this.combineChartData(),
52          });
53          // 合并完泳道图数据之后,Tab页不再需要缓存数据
54          this.dataCache.clearJsCache();
55          break;
56        case 'jsCpuProfiler-call-tree':
57          this.tabDataId = 0;
58          self.postMessage({
59            id: msg.id,
60            action: msg.action,
61            results: this.combineTopDownData(msg.params, null),
62          });
63          break;
64        case 'jsCpuProfiler-bottom-up':
65          this.tabDataId = 0;
66          self.postMessage({
67            id: msg.id,
68            action: msg.action,
69            results: this.combineBottomUpData(msg.params),
70          });
71          break;
72        case 'jsCpuProfiler-statistics':
73          self.postMessage({
74            id: msg.id,
75            action: msg.action,
76            results: this.calStatistic(msg.params.data, msg.params.leftNs, msg.params.rightNs),
77          });
78          break;
79      }
80    }
81  }
82
83  public clearAll(): void {
84    this.dataCache.clearAll();
85    this.samples.length = 0;
86  }
87
88  private calStatistic(
89    chartData: Array<JsCpuProfilerChartFrame>,
90    leftNs: number | undefined,
91    rightNs: number | undefined
92  ): Map<SampleType, number> {
93    const typeMap = new Map<SampleType, number>();
94    const samplesIdsArr: Array<any> = [];
95    const samplesIds = this.findSamplesIds(chartData, [], []);
96    for (const id of samplesIds) {
97      const sample = this.samples[id];
98      if (!sample) {
99        continue;
100      }
101      let sampleTotalTime = sample.dur;
102      if (leftNs && rightNs) {
103        // 不在框选范围内的不做处理
104        if (sample.startTime > rightNs || sample.endTime < leftNs){
105          continue;
106        }
107        // 在框选范围内的被只框选到一部分的根据框选范围调整时间
108        const startTime = sample.startTime < leftNs ? leftNs : sample.startTime;
109        const endTime = sample.endTime > rightNs ? rightNs : sample.endTime;
110        sampleTotalTime = endTime - startTime;
111      }
112
113      if (!samplesIdsArr.includes(sample)) {
114        samplesIdsArr.push(sample);
115        let typeDur = typeMap.get(sample.type);
116        if (typeDur) {
117          typeMap.set(sample.type, typeDur + sampleTotalTime);
118        } else {
119          typeMap.set(sample.type, sampleTotalTime);
120        }
121      }
122    }
123    return typeMap;
124  }
125
126  private findSamplesIds(
127    chartData: Array<JsCpuProfilerChartFrame>,
128    lastLayerData: Array<JsCpuProfilerChartFrame>,
129    samplesIds: Array<number>
130  ) {
131    for (const data of chartData) {
132      if (data.isSelect && data.selfTime > 0 && !lastLayerData.includes(data)) {
133        lastLayerData.push(data);
134        samplesIds.push(...data.samplesIds);
135      } else if (data.children.length > 0) {
136        this.findSamplesIds(data.children, lastLayerData, samplesIds);
137      }
138    }
139    return samplesIds;
140  }
141
142  /**
143   * 建立callChain每个函数的联系,设置depth跟children
144   */
145  private createCallChain(): void {
146    const jsSymbolMap = this.dataCache.jsSymbolMap;
147    for (const item of this.dataCache.jsCallChain!) {
148      jsSymbolMap.set(item.id, item);
149      //root不需要显示,depth为-1
150      if (item.id === ROOT_ID) {
151        item.depth = -1;
152      }
153      item.name = this.dataCache.dataDict?.get(item.nameId) || LAMBDA_FUNCTION_NAME;
154      item.url = this.dataCache.dataDict?.get(item.urlId) || 'unknown';
155      if (item.parentId > 0) {
156        let parentSymbol = jsSymbolMap.get(item.parentId);
157        if (parentSymbol) {
158          if (!parentSymbol.children) {
159            parentSymbol.children = new Array<JsProfilerSymbol>();
160          }
161          parentSymbol.children.push(item);
162          item.depth = parentSymbol.depth + 1;
163        }
164      }
165    }
166  }
167
168  private combineChartData(): Array<JsCpuProfilerChartFrame> {
169    const combineSample = new Array<JsCpuProfilerChartFrame>();
170    for (let sample of this.samples) {
171      const stackTopSymbol = this.dataCache.jsSymbolMap.get(sample.functionId);
172      // root 节点不需要显示
173      if (stackTopSymbol?.id === ROOT_ID) {
174        sample.type = SampleType.OTHER;
175        continue;
176      }
177      if (stackTopSymbol) {
178        let type: string;
179        if (stackTopSymbol.name) {
180          type = stackTopSymbol.name.substring(
181            stackTopSymbol.name!.lastIndexOf('(') + 1,
182            stackTopSymbol.name!.lastIndexOf(')')
183          );
184          switch (type) {
185            case 'NAPI':
186              sample.type = SampleType.NAPI;
187              break;
188            case 'ARKUI_ENGINE':
189              sample.type = SampleType.ARKUI_ENGINE;
190              break;
191            case 'BUILTIN':
192              sample.type = SampleType.BUILTIN;
193              break;
194            case 'GC':
195              sample.type = SampleType.GC;
196              break;
197            case 'AINT':
198              sample.type = SampleType.AINT;
199              break;
200            case 'CINT':
201              sample.type = SampleType.CINT;
202              break;
203            case 'AOT':
204              sample.type = SampleType.AOT;
205              break;
206            case 'RUNTIME':
207              sample.type = SampleType.RUNTIME;
208              break;
209            default:
210              sample.type = SampleType.OTHER;
211              break;
212          }
213        }
214
215        // 获取栈顶函数的整条调用栈为一个数组 下标0为触发的栈底函数
216        sample.stack = this.getFullCallChainOfNode(stackTopSymbol);
217        if (combineSample.length === 0) {
218          // 首次combineSample没有数据时,用第一条数据创建一个调用树
219          this.createNewChartFrame(sample, combineSample);
220        } else {
221          const lastCallChart = combineSample[combineSample.length - 1];
222          if (this.isSymbolEqual(sample.stack[0], lastCallChart) && lastCallChart.endTime === sample.startTime) {
223            this.combineCallChain(lastCallChart, sample);
224          } else {
225            // 一个调用链栈底函数与前一个不同时,需要新加入到combineSample
226            this.createNewChartFrame(sample, combineSample);
227          }
228        }
229      }
230    }
231    return combineSample;
232  }
233
234  /**
235   * 同级使用广度优先算法,非同级使用深度优先算法,遍历泳道图树结构所有数据,
236   * 将name,url,depth,parent相同的函数合并,构建成一个top down的树结构
237   * @param combineSample 泳道图第一层数据,非第一层为null
238   * @param parent 泳道图合并过的函数,第一层为null
239   * @returns 返回第一层树结构(第一层数据通过children囊括了所有的函数)
240   */
241  private combineTopDownData(
242    combineSample: Array<JsCpuProfilerChartFrame> | null,
243    parent: JsCpuProfilerTabStruct | null
244  ): Array<JsCpuProfilerTabStruct> {
245    const sameSymbolMap = new Map<string, JsCpuProfilerTabStruct>();
246    const currentLevelData = new Array<JsCpuProfilerTabStruct>();
247
248    const chartArray = combineSample || parent?.chartFrameChildren;
249    if (!chartArray) {
250      return [];
251    }
252    // 同级广度优先 便于数据合并
253    for (const chartFrame of chartArray) {
254      if (!chartFrame.isSelect) {
255        continue;
256      }
257      // 该递归函数已经保证depth跟parent相同,固只需要判断name跟url相同即可
258      let symbolKey = chartFrame.name + ' ' + chartFrame.url;
259      // lambda 表达式需要根据行列号区分是不是同一个函数
260      if (chartFrame.name === LAMBDA_FUNCTION_NAME) {
261        symbolKey += ' ' + chartFrame.line + ' ' + chartFrame.column;
262      }
263      let tabCallFrame: JsCpuProfilerTabStruct;
264      if (sameSymbolMap.has(symbolKey)) {
265        tabCallFrame = sameSymbolMap.get(symbolKey)!;
266        tabCallFrame.totalTime += chartFrame.totalTime;
267        tabCallFrame.selfTime += chartFrame.selfTime;
268      } else {
269        tabCallFrame = this.chartFrameToTabStruct(chartFrame);
270        sameSymbolMap.set(symbolKey, tabCallFrame);
271        currentLevelData.push(tabCallFrame);
272        if (parent) {
273          parent.children.push(tabCallFrame);
274        }
275      }
276      tabCallFrame.chartFrameChildren?.push(...chartFrame.children);
277    }
278
279    // 非同级深度优先,便于设置children,同时保证下一级函数depth跟parent都相同
280    for (const data of currentLevelData) {
281      this.combineTopDownData(null, data);
282      data.chartFrameChildren = [];
283    }
284
285    if (combineSample) {
286      // 第一层为返回给Tab页的数据
287      return currentLevelData;
288    } else {
289      return [];
290    }
291  }
292
293  /**
294   * copy整体调用链,从栈顶函数一直copy到栈底函数,
295   * 给Parent设置selfTime,totalTime设置为children的selfTime,totalTime
296   *  */
297  private copyParent(frame: JsCpuProfilerChartFrame, chartFrame: JsCpuProfilerChartFrame): void {
298    frame.children = [];
299    if (chartFrame.parent) {
300      const copyParent = this.cloneChartFrame(chartFrame.parent);
301      copyParent.selfTime = frame.selfTime;
302      copyParent.totalTime = frame.totalTime;
303      frame.children.push(copyParent);
304      this.copyParent(copyParent, chartFrame.parent);
305    }
306  }
307
308  /**
309   * 步骤1:框选/点选的chart树逆序
310   * 步骤2:将name,url,parent,层级相同的函数合并
311   * @param chartTreeArray ui传递的树结构
312   * @returns 合并的Array<JsCpuProfilerChartFrame>树结构
313   */
314  private combineBottomUpData(chartTreeArray: Array<JsCpuProfilerChartFrame>): Array<JsCpuProfilerTabStruct> {
315    const reverseTreeArray = new Array<JsCpuProfilerChartFrame>();
316    // 将树结构逆序,parent变成children
317    this.reverseChartFrameTree(chartTreeArray, reverseTreeArray);
318    // 将逆序的树结构合并返回
319    return this.combineTopDownData(reverseTreeArray, null);
320  }
321
322  /**
323   * 树结构逆序
324   * @param chartTreeArray 正序的树结构
325   * @param reverseTreeArray 逆序的树结构
326   */
327  private reverseChartFrameTree(
328    chartTreeArray: Array<JsCpuProfilerChartFrame>,
329    reverseTreeArray: Array<JsCpuProfilerChartFrame>
330  ) {
331    const that = this;
332    function recursionTree(chartFrame: JsCpuProfilerChartFrame) {
333      // isSelect为框选/点选范围内的函数,其他都不需要处理
334      if (!chartFrame.isSelect) {
335        return;
336      }
337      //界面第一层只显示栈顶函数,只有栈顶函数的selfTime > 0
338      if (chartFrame.selfTime > 0) {
339        const copyFrame = that.cloneChartFrame(chartFrame);
340        // 每个栈顶函数的parent的时间为栈顶函数的时间
341        copyFrame.selfTime = chartFrame.selfTime;
342        copyFrame.totalTime = chartFrame.totalTime;
343        reverseTreeArray.push(copyFrame);
344        // 递归处理parent的的totalTime selfTime
345        that.copyParent(copyFrame, chartFrame);
346      }
347
348      if (chartFrame.children.length > 0) {
349        for (const children of chartFrame.children) {
350          children.parent = chartFrame;
351          recursionTree(children);
352        }
353      }
354    }
355
356    //递归树结构
357    for (const chartFrame of chartTreeArray) {
358      recursionTree(chartFrame);
359    }
360  }
361
362  private createNewChartFrame(sample: JsCpuProfilerSample, combineSample: Array<JsCpuProfilerChartFrame>): void {
363    let lastSymbol: JsCpuProfilerChartFrame;
364    for (const [idx, symbol] of sample.stack!.entries()) {
365      if (idx === 0) {
366        lastSymbol = this.symbolToChartFrame(sample, symbol);
367        combineSample.push(lastSymbol);
368      } else {
369        const callFrame = this.symbolToChartFrame(sample, symbol);
370        lastSymbol!.children.push(callFrame);
371        callFrame.parentId = lastSymbol!.id;
372        lastSymbol = callFrame;
373      }
374      if (idx + 1 === sample.stack?.length) {
375        lastSymbol.selfTime = sample.dur;
376      }
377    }
378  }
379
380  /**
381   * 相邻的两个sample的name,url,depth相同,且上一个的endTime等于下一个的startTime,
382   * 则两个sample的调用栈合并
383   * @param lastCallTree 上一个已经合并的树结构调用栈
384   * @param sample 当前样本数据
385   */
386  private combineCallChain(lastCallTree: JsCpuProfilerChartFrame, sample: JsCpuProfilerSample): void {
387    let lastCallTreeSymbol = lastCallTree;
388    let parentCallFrame: JsCpuProfilerChartFrame;
389    let isEqual = true;
390    for (const [idx, symbol] of sample.stack!.entries()) {
391      // 是否为每次采样的栈顶函数
392      const isLastSymbol = idx + 1 === sample.stack?.length;
393      if (
394        isEqual &&
395        this.isSymbolEqual(symbol, lastCallTreeSymbol) &&
396        lastCallTreeSymbol.depth === idx &&
397        lastCallTreeSymbol.endTime === sample.startTime
398      ) {
399        // 如果函数名跟depth匹配,则更新函数的持续时间
400        lastCallTreeSymbol.endTime = sample.endTime;
401        lastCallTreeSymbol.totalTime = sample.endTime - lastCallTreeSymbol.startTime;
402        lastCallTreeSymbol.samplesIds.push(sample.id);
403        let lastChildren = lastCallTreeSymbol.children;
404        parentCallFrame = lastCallTreeSymbol;
405        if (lastChildren && lastChildren.length > 0) {
406          lastCallTreeSymbol = lastChildren[lastChildren.length - 1];
407        }
408        isEqual = true;
409      } else {
410        // 如果不匹配,则作为新的分支添加到lastCallTree
411        const deltaFrame = this.symbolToChartFrame(sample, symbol);
412        parentCallFrame!.children.push(deltaFrame);
413        deltaFrame.parentId = parentCallFrame!.id;
414        parentCallFrame = deltaFrame;
415        isEqual = false;
416      }
417      // 每次采样的栈顶函数的selfTime为该次采样数据的时间
418      if (isLastSymbol) {
419        parentCallFrame.selfTime += sample.dur;
420      }
421    }
422  }
423
424  /**
425   * 根据每个sample的栈顶函数,获取完整的调用栈
426   * @param node 栈顶函数
427   * @returns 完整的调用栈
428   */
429  private getFullCallChainOfNode(node: JsProfilerSymbol): Array<JsProfilerSymbol> {
430    const callChain = new Array<JsProfilerSymbol>();
431    callChain.push(node);
432    while (node.parentId !== 0) {
433      const parent = this.dataCache.jsSymbolMap.get(node.parentId);
434      // id 1 is root Node
435      if (!parent || parent.id <= ROOT_ID) {
436        break;
437      }
438      callChain.push(parent);
439      node = parent;
440    }
441    callChain.reverse();
442    return callChain;
443  }
444
445  /**
446   * 创建一个JsCpuProfilerChartFrame 作为绘制泳道图的结构
447   * @param sample 数据库样本数据
448   * @param symbol 样本的每一个函数
449   * @returns JsCpuProfilerChartFrame
450   */
451  private symbolToChartFrame(sample: JsCpuProfilerSample, symbol: JsProfilerSymbol): JsCpuProfilerChartFrame {
452    const chartFrame = new JsCpuProfilerChartFrame(
453      this.chartId++,
454      symbol.name || LAMBDA_FUNCTION_NAME,
455      sample.startTime,
456      sample.endTime,
457      sample.dur,
458      symbol.depth,
459      symbol.url,
460      symbol.line,
461      symbol.column
462    );
463    chartFrame.samplesIds.push(sample.id);
464    return chartFrame;
465  }
466
467  /**
468   * 将泳道图数据JsCpuProfilerChartFrame转化为JsCpuProfilerTabStruct 作为绘制Ta页的结构
469   * @param chartCallChain 泳道图函数信息
470   * @returns JsCpuProfilerTabStruct
471   */
472  private chartFrameToTabStruct(chartCallChain: JsCpuProfilerChartFrame): JsCpuProfilerTabStruct {
473    const tabData = new JsCpuProfilerTabStruct(
474      chartCallChain.name,
475      chartCallChain.selfTime,
476      chartCallChain.totalTime,
477      chartCallChain.depth,
478      chartCallChain.url,
479      chartCallChain.line,
480      chartCallChain.column,
481      this.tabDataId++
482    );
483    return tabData;
484  }
485
486  private cloneChartFrame(frame: JsCpuProfilerChartFrame): JsCpuProfilerChartFrame {
487    const copyFrame = new JsCpuProfilerChartFrame(
488      frame.id,
489      frame.name,
490      frame.startTime,
491      frame.endTime,
492      frame.totalTime,
493      frame.depth,
494      frame.url,
495      frame.line,
496      frame.column
497    );
498    copyFrame.parentId = frame.parentId;
499    copyFrame.isSelect = true;
500    return copyFrame;
501  }
502
503  private isSymbolEqual(symbol: JsProfilerSymbol, uiData: JsCpuProfilerUIStruct): boolean {
504    return symbol.name === uiData.name && symbol.url === uiData.url;
505  }
506
507  private initCallChain(): void {
508    const sql = `SELECT function_id AS id,
509                        function_index AS nameId,
510                        script_id AS scriptId,
511                        url_index AS urlId,
512                        line_number as line,
513                        column_number as column,
514                        hit_count AS hitCount,
515                        children AS childrenString,
516                        parent_id AS parentId
517                    FROM
518                        js_cpu_profiler_node`;
519    this.queryData(this.currentEventId!, 'jsCpuProfiler-call-chain', sql, {});
520  }
521
522  private queryChartData(): void {
523    const sql = `SELECT id,
524                    function_id AS functionId,
525                    start_time - start_ts AS startTime,
526                    end_time - start_ts AS endTime,
527                    dur
528                  FROM
529                    js_cpu_profiler_sample,trace_range`;
530    this.queryData(this.currentEventId!, 'jsCpuProfiler-samples', sql, {});
531  }
532}
533
534class JsCpuProfilerSample {
535  id: number = 0;
536  functionId: number = 0;
537  startTime: number = 0;
538  endTime: number = 0;
539  dur: number = 0;
540  type: SampleType = SampleType.OTHER;
541  stack?: Array<JsProfilerSymbol>;
542}
543
544export enum SampleType {
545  OTHER = 'OTHER',
546  NAPI = 'NAPI',
547  ARKUI_ENGINE = 'ARKUI_ENGINE',
548  BUILTIN = 'BUILTIN',
549  GC = 'GC',
550  AINT = 'AINT',
551  CINT = 'CINT',
552  AOT = 'AOT',
553  RUNTIME = 'RUNTIME',
554}
555