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