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 */ 15 16import {SpSystemTrace} from "../SpSystemTrace.js"; 17import { 18 getAsyncEvents, 19 getFunDataByTid, 20 queryEventCountMap, 21 queryProcess, 22 queryProcessAsyncFunc, 23 queryProcessByTable, queryProcessContentCount, 24 queryProcessData, 25 queryProcessMem, 26 queryProcessMemData, 27 queryProcessThreads, 28 queryProcessThreadsByTable, 29 queryThreadData 30} from "../../database/SqlLite.js"; 31import {Utils} from "../trace/base/Utils.js"; 32import {info} from "../../../log/Log.js"; 33import {TraceRow} from "../trace/base/TraceRow.js"; 34import {ProcessStruct} from "../../bean/ProcessStruct.js"; 35import {procedurePool} from "../../database/Procedure.js"; 36import {CpuStruct} from "../../bean/CpuStruct.js"; 37import {FuncStruct} from "../../bean/FuncStruct.js"; 38import {ProcessMemStruct} from "../../bean/ProcessMemStruct.js"; 39import {ThreadStruct} from "../../bean/ThreadStruct.js"; 40 41export class SpProcessChart { 42 private trace: SpSystemTrace; 43 private processAsyncFuncMap: any = {} 44 private eventCountMap: any; 45 private processThreads: Array<ThreadStruct> = [] 46 private processAsyncEvent: Array<ProcessMemStruct> = [] 47 private processMem: Array<any> = [] 48 private processThreadDataCountMap: Map<number, number> = new Map(); 49 private processFuncDataCountMap: Map<number, number> = new Map(); 50 private processMemDataCountMap: Map<number, number> = new Map(); 51 52 constructor(trace: SpSystemTrace) { 53 this.trace = trace; 54 } 55 56 initAsyncFuncData = async () => { 57 let asyncFuncList: any[] = await queryProcessAsyncFunc(); 58 info("AsyncFuncData Count is: ", asyncFuncList!.length) 59 this.processAsyncFuncMap = Utils.groupBy(asyncFuncList, "pid"); 60 } 61 62 async init() { 63 let pidCountArray = await queryProcessContentCount(); 64 pidCountArray.forEach(it => { 65 this.processThreadDataCountMap.set(it.pid, it.switch_count); 66 this.processFuncDataCountMap.set(it.pid, it.slice_count); 67 this.processMemDataCountMap.set(it.pid, it.mem_count); 68 }) 69 let queryProcessThreadResult = await queryProcessThreads(); 70 let queryProcessThreadsByTableResult = await queryProcessThreadsByTable() 71 this.processAsyncEvent = await getAsyncEvents(); 72 info("The amount of initialized process Event data is : ", this.processAsyncEvent!.length) 73 this.processMem = await queryProcessMem(); 74 info("The amount of initialized process memory data is : ", this.processMem!.length) 75 let eventCountList: Array<any> = await queryEventCountMap(); 76 this.eventCountMap = eventCountList.reduce((pre, current) => { 77 pre[`${current.eventName}`] = current.count; 78 return pre; 79 }, {}); 80 this.processThreads = Utils.removeDuplicates(queryProcessThreadResult, queryProcessThreadsByTableResult, "tid") 81 info("The amount of initialized process threads data is : ", this.processThreads!.length) 82 if (this.eventCountMap["print"] == 0 && 83 this.eventCountMap["tracing_mark_write"] == 0 && 84 this.eventCountMap["sched_switch"] == 0) { 85 return; 86 } 87 let time = new Date().getTime(); 88 let processes = await queryProcess(); 89 let processFromTable = await queryProcessByTable(); 90 let processList = Utils.removeDuplicates(processes, processFromTable, "pid") 91 info("ProcessList Data size is: ", processList!.length) 92 for (let i = 0; i < processList.length; i++) { 93 const it = processList[i]; 94 if ((this.processThreadDataCountMap.get(it.pid) || 0) == 0 && 95 (this.processFuncDataCountMap.get(it.pid) || 0) == 0 && 96 (this.processMemDataCountMap.get(it.pid) || 0) == 0) { 97 continue; 98 } 99 let processRow = new TraceRow<ProcessStruct>({ 100 canvasNumber: 1, alpha: false, contextId: "2d", isOffScreen: true, skeleton: false 101 }); 102 processRow.rowId = `${it.pid}` 103 processRow.index = i; 104 processRow.rowType = TraceRow.ROW_TYPE_PROCESS 105 processRow.rowParentId = ''; 106 processRow.folder = true; 107 processRow.name = `${it.processName || "Process"} ${it.pid}`; 108 processRow.supplier = () => queryProcessData(it.pid || -1, 0, TraceRow.range?.totalNS || 0); 109 processRow.favoriteChangeHandler = this.trace.favoriteChangeHandler; 110 processRow.selectChangeHandler = this.trace.selectChangeHandler; 111 processRow.onThreadHandler = (useCache) => { 112 procedurePool.submitWithName(`process${(processRow.index) % procedurePool.processLen.length}`, `process ${processRow.index} ${it.processName}`, { 113 list: processRow.must ? processRow.dataList : undefined, 114 offscreen: !processRow.isTransferCanvas ? processRow.offscreen[0] : undefined, 115 pid: it.pid, 116 xs: TraceRow.range?.xs, 117 dpr: processRow.dpr, 118 isHover: processRow.isHover, 119 flagMoveInfo: this.trace.hoverFlag, 120 flagSelectedInfo: this.trace.selectFlag, 121 hoverX: processRow.hoverX, 122 hoverY: processRow.hoverY, 123 canvasWidth: processRow.canvasWidth, 124 canvasHeight: processRow.canvasHeight, 125 isRangeSelect: processRow.rangeSelect, 126 rangeSelectObject: TraceRow.rangeSelectObject, 127 wakeupBean: CpuStruct.wakeupBean, 128 cpuCount: CpuStruct.cpuCount, 129 useCache: useCache, 130 lineColor: processRow.getLineColor(), 131 startNS: TraceRow.range?.startNS || 0, 132 endNS: TraceRow.range?.endNS || 0, 133 totalNS: TraceRow.range?.totalNS || 0, 134 slicesTime: TraceRow.range?.slicesTime, 135 range: TraceRow.range, 136 frame: processRow.frame 137 }, !processRow.isTransferCanvas ? processRow.offscreen[0] : undefined, () => { 138 processRow.must = false; 139 }) 140 processRow.isTransferCanvas = true; 141 } 142 this.trace.rowsEL?.appendChild(processRow) 143 /** 144 * Async Function 145 */ 146 let asyncFuncList = this.processAsyncFuncMap[it.pid] || []; 147 let asyncFuncGroup = Utils.groupBy(asyncFuncList, "funName"); 148 Reflect.ownKeys(asyncFuncGroup).map((key: any) => { 149 let asyncFunctions: Array<any> = asyncFuncGroup[key]; 150 if (asyncFunctions.length > 0) { 151 let isIntersect = (a: any, b: any) => (Math.max(a.startTs + a.dur, b.startTs + b.dur) - Math.min(a.startTs, b.startTs) < a.dur + b.dur); 152 let depthArray: any = [] 153 let createDepth = (currentDepth: number, index: number) => { 154 if (depthArray[currentDepth] == undefined || !isIntersect(depthArray[currentDepth], asyncFunctions[index])) { 155 asyncFunctions[index].depth = currentDepth; 156 depthArray[currentDepth] = asyncFunctions[index] 157 } else { 158 createDepth(++currentDepth, index) 159 } 160 } 161 asyncFunctions.forEach((it, i) => { 162 if (it.dur == -1) { 163 it.dur = TraceRow.range?.endNS || 0 - it.startTs; 164 } 165 createDepth(0, i); 166 }); 167 const groupedBy: Array<any> = []; 168 for (let i = 0; i < asyncFunctions.length; i++) { 169 if (groupedBy[asyncFunctions[i].depth || 0]) { 170 groupedBy[asyncFunctions[i].depth || 0].push(asyncFunctions[i]); 171 } else { 172 groupedBy[asyncFunctions[i].depth || 0] = [asyncFunctions[i]]; 173 } 174 } 175 let max = Math.max(...asyncFunctions.map(it => it.depth || 0)) + 1 176 let maxHeight = max * 20; 177 let funcRow = new TraceRow<FuncStruct>({ 178 canvasNumber: max, 179 alpha: false, 180 contextId: '2d', 181 isOffScreen: SpSystemTrace.isCanvasOffScreen, 182 skeleton: false 183 }); 184 funcRow.rowId = `${asyncFunctions[0].funName}-${it.pid}` 185 funcRow.asyncFuncName = asyncFunctions[0].funName; 186 funcRow.asyncFuncNamePID = it.pid; 187 funcRow.rowType = TraceRow.ROW_TYPE_FUNC 188 funcRow.rowParentId = `${it.pid}` 189 funcRow.rowHidden = !processRow.expansion 190 funcRow.style.width = `100%`; 191 funcRow.setAttribute("height", `${maxHeight}`); 192 funcRow.name = `${asyncFunctions[0].funName}`; 193 funcRow.setAttribute('children', '') 194 funcRow.supplier = () => new Promise((resolve) => resolve(asyncFunctions)) 195 funcRow.favoriteChangeHandler = this.trace.favoriteChangeHandler; 196 funcRow.selectChangeHandler = this.trace.selectChangeHandler; 197 funcRow.onThreadHandler = (useCache) => { 198 let asy = async (useCache: boolean) => { 199 let scrollTop = this.trace.rowsEL?.scrollTop || 0; 200 let scrollHeight = this.trace.rowsEL?.clientHeight || 0; 201 let promises: Array<any> = []; 202 for (let k = 0; k < groupedBy.length; k++) { 203 let top = funcRow.offsetTop - (this.trace.rowsEL?.offsetTop || 0) - scrollTop + funcRow.canvas[k].offsetTop; 204 let isLive = ((top + funcRow.canvas[k].clientHeight >= 0) && (top < scrollHeight)) || funcRow.collect 205 let promise = await procedurePool.submitWithNamePromise(`cpu${k % procedurePool.cpusLen.length}`, `func-${asyncFunctions[0].funName}-${it.pid}-${k}`, { 206 isLive: isLive, 207 list: funcRow.must ? groupedBy[k] : undefined, 208 offscreen: !funcRow.isTransferCanvas ? funcRow.offscreen[k] : undefined,//是否离屏 209 dpr: funcRow.dpr,//屏幕dpr值 210 xs: TraceRow.range?.xs,//线条坐标信息 211 isHover: funcRow.isHover, 212 flagMoveInfo: this.trace.hoverFlag, 213 flagSelectedInfo: this.trace.selectFlag, 214 hoverX: funcRow.hoverX, 215 hoverY: funcRow.hoverY, 216 depth: k, 217 canvasWidth: funcRow.canvasWidth, 218 canvasHeight: funcRow.canvasHeight, 219 maxHeight: maxHeight, 220 hoverFuncStruct: FuncStruct.hoverFuncStruct, 221 selectFuncStruct: FuncStruct.selectFuncStruct, 222 wakeupBean: CpuStruct.wakeupBean, 223 isRangeSelect: funcRow.rangeSelect, 224 rangeSelectObject: TraceRow.rangeSelectObject, 225 useCache: useCache, 226 lineColor: funcRow.getLineColor(), 227 startNS: TraceRow.range?.startNS || 0, 228 endNS: TraceRow.range?.endNS || 0, 229 totalNS: TraceRow.range?.totalNS || 0, 230 slicesTime: TraceRow.range?.slicesTime, 231 range: TraceRow.range, 232 frame: funcRow.frame 233 }, !funcRow.isTransferCanvas ? funcRow.offscreen[k] : undefined) 234 if (funcRow.isHover && promise.hover) { 235 FuncStruct.hoverFuncStruct = promise.hover; 236 } 237 promises.push(promise); 238 } 239 if (funcRow.isHover && promises.every(it => !it.hover)) { 240 FuncStruct.hoverFuncStruct = undefined; 241 } 242 funcRow.must = false; 243 funcRow.isTransferCanvas = true; 244 } 245 asy(useCache).then() 246 } 247 this.trace.rowsEL?.appendChild(funcRow); 248 } 249 }); 250 251 /** 252 * 添加进程内存信息 253 */ 254 let processMem = this.processMem.filter(mem => mem.pid === it.pid); 255 processMem.forEach(mem => { 256 let row = new TraceRow<ProcessMemStruct>( 257 {canvasNumber: 1, alpha: false, contextId: "2d", isOffScreen: true, skeleton: false} 258 ); 259 row.rowId = `${mem.trackId}` 260 row.rowType = TraceRow.ROW_TYPE_MEM 261 row.rowParentId = `${it.pid}` 262 row.rowHidden = !processRow.expansion 263 row.style.height = '40px' 264 row.style.width = `100%`; 265 row.name = `${mem.trackName}`; 266 row.setAttribute('children', ''); 267 row.favoriteChangeHandler = this.trace.favoriteChangeHandler; 268 row.selectChangeHandler = this.trace.selectChangeHandler; 269 row.supplier = () => queryProcessMemData(mem.trackId).then(res => { 270 let maxValue = Math.max(...res.map(it => it.value || 0)) 271 for (let j = 0; j < res.length; j++) { 272 res[j].maxValue = maxValue; 273 if (j == res.length - 1) { 274 res[j].duration = (TraceRow.range?.totalNS || 0) - (res[j].startTime || 0); 275 } else { 276 res[j].duration = (res[j + 1].startTime || 0) - (res[j].startTime || 0); 277 } 278 if (j > 0) { 279 res[j].delta = (res[j].value || 0) - (res[j - 1].value || 0); 280 } else { 281 res[j].delta = 0; 282 } 283 } 284 return res 285 }); 286 row.onThreadHandler = (useCache) => { 287 procedurePool.submitWithName(`cpu${mem.trackId % procedurePool.cpusLen.length}`, `mem ${mem.trackId} ${mem.trackName}`, { 288 list: row.must ? row.dataList : undefined, 289 offscreen: !row.isTransferCanvas ? row.offscreen[0] : undefined,//是否离屏 290 dpr: row.dpr,//屏幕dpr值 291 xs: TraceRow.range?.xs,//线条坐标信息 292 isHover: row.isHover, 293 flagMoveInfo: this.trace.hoverFlag, 294 flagSelectedInfo: this.trace.selectFlag, 295 hoverX: row.hoverX, 296 hoverY: row.hoverY, 297 canvasWidth: row.canvasWidth, 298 canvasHeight: row.canvasHeight, 299 wakeupBean: CpuStruct.wakeupBean, 300 isRangeSelect: row.rangeSelect, 301 rangeSelectObject: TraceRow.rangeSelectObject, 302 useCache: useCache, 303 lineColor: row.getLineColor(), 304 startNS: TraceRow.range?.startNS || 0, 305 endNS: TraceRow.range?.endNS || 0, 306 totalNS: TraceRow.range?.totalNS || 0, 307 slicesTime: TraceRow.range?.slicesTime, 308 hoverProcessMemStruct: ProcessMemStruct.hoverProcessMemStruct, 309 range: TraceRow.range, 310 frame: row.frame 311 }, row.getTransferArray(), (res: any, hover: any) => { 312 row.must = false; 313 if (row.isHover) { 314 ProcessMemStruct.hoverProcessMemStruct = hover; 315 } 316 return; 317 }); 318 row.isTransferCanvas = true; 319 } 320 this.trace.rowsEL?.appendChild(row) 321 }); 322 /** 323 * add thread list 324 */ 325 let threads = this.processThreads.filter(thread => thread.pid === it.pid && thread.tid != 0); 326 for (let j = 0; j < threads.length; j++) { 327 let thread = threads[j]; 328 let threadRow = new TraceRow<ThreadStruct>({ 329 canvasNumber: 1, 330 alpha: false, 331 contextId: "2d", 332 isOffScreen: true, 333 skeleton: false 334 }); 335 threadRow.rowId = `${thread.tid}` 336 threadRow.rowType = TraceRow.ROW_TYPE_THREAD 337 threadRow.rowParentId = `${it.pid}` 338 threadRow.rowHidden = !processRow.expansion 339 threadRow.index = j 340 threadRow.style.height = '30px' 341 threadRow.setAttribute("height", `30`); 342 threadRow.style.width = `100%`; 343 threadRow.name = `${thread.threadName || 'Thread'} ${thread.tid}`; 344 threadRow.setAttribute('children', '') 345 threadRow.favoriteChangeHandler = this.trace.favoriteChangeHandler; 346 threadRow.selectChangeHandler = this.trace.selectChangeHandler; 347 threadRow.supplier = () => queryThreadData(thread.tid || 0).then(res => { 348 getFunDataByTid(thread.tid || 0).then((funs: Array<FuncStruct>) => { 349 if (funs.length > 0) { 350 let isBinder = (data: FuncStruct): boolean => { 351 return data.funName != null && ( 352 data.funName.toLowerCase().startsWith("binder transaction async") //binder transaction 353 || data.funName.toLowerCase().startsWith("binder async") 354 || data.funName.toLowerCase().startsWith("binder reply") 355 ); 356 } 357 funs.forEach(fun => { 358 if (isBinder(fun)) { 359 } else { 360 if (fun.dur == -1) { 361 fun.dur = (TraceRow.range?.totalNS || 0) - (fun.startTs || 0); 362 } 363 } 364 }) 365 const groupedBy: Array<any> = []; 366 for (let i = 0; i < funs.length; i++) { 367 if (groupedBy[funs[i].depth || 0]) { 368 groupedBy[funs[i].depth || 0].push(funs[i]); 369 } else { 370 groupedBy[funs[i].depth || 0] = [funs[i]]; 371 } 372 } 373 let max = Math.max(...funs.map(it => it.depth || 0)) + 1 374 let maxHeight = max * 20; 375 let funcRow = new TraceRow<FuncStruct>({ 376 canvasNumber: max, 377 alpha: false, 378 contextId: '2d', 379 isOffScreen: SpSystemTrace.isCanvasOffScreen, 380 skeleton: false 381 }); 382 funcRow.rowId = `${thread.tid}` 383 funcRow.rowType = TraceRow.ROW_TYPE_FUNC 384 funcRow.rowParentId = `${it.pid}` 385 funcRow.rowHidden = !processRow.expansion 386 funcRow.checkType = threadRow.checkType; 387 funcRow.style.width = `100%`; 388 funcRow.setAttribute("height", `${maxHeight}`); 389 funcRow.name = `${thread.threadName || 'Thread'} ${thread.tid}`; 390 funcRow.setAttribute('children', '') 391 funcRow.supplier = () => new Promise((resolve) => resolve(funs)) 392 funcRow.favoriteChangeHandler = this.trace.favoriteChangeHandler; 393 funcRow.selectChangeHandler = this.trace.selectChangeHandler; 394 funcRow.onThreadHandler = (useCache) => { 395 let asy = async (useCache: boolean) => { 396 let scrollTop = this.trace.rowsEL?.scrollTop || 0; 397 let scrollHeight = this.trace.rowsEL?.clientHeight || 0; 398 let promises: Array<any> = []; 399 for (let k = 0; k < groupedBy.length; k++) { 400 let top = funcRow.offsetTop - (this.trace.rowsEL?.offsetTop || 0) - scrollTop + funcRow.canvas[k].offsetTop; 401 let isLive = ((top + funcRow.canvas[k].clientHeight >= 0) && (top < scrollHeight)) || funcRow.collect 402 let promise = await procedurePool.submitWithNamePromise(`cpu${k % procedurePool.cpusLen.length}`, `func${thread.tid}${k}${thread.threadName}`, { 403 isLive: isLive, 404 list: funcRow.must ? groupedBy[k] : undefined, 405 offscreen: !funcRow.isTransferCanvas ? funcRow.offscreen[k] : undefined,//是否离屏 406 dpr: funcRow.dpr,//屏幕dpr值 407 xs: TraceRow.range?.xs,//线条坐标信息 408 isHover: funcRow.isHover, 409 flagMoveInfo: this.trace.hoverFlag, 410 flagSelectedInfo: this.trace.selectFlag, 411 hoverX: funcRow.hoverX, 412 hoverY: funcRow.hoverY, 413 depth: k, 414 canvasWidth: funcRow.canvasWidth, 415 canvasHeight: funcRow.canvasHeight, 416 maxHeight: maxHeight, 417 hoverFuncStruct: FuncStruct.hoverFuncStruct, 418 selectFuncStruct: FuncStruct.selectFuncStruct, 419 wakeupBean: CpuStruct.wakeupBean, 420 isRangeSelect: funcRow.rangeSelect, 421 rangeSelectObject: TraceRow.rangeSelectObject, 422 useCache: useCache, 423 lineColor: funcRow.getLineColor(), 424 startNS: TraceRow.range?.startNS || 0, 425 endNS: TraceRow.range?.endNS || 0, 426 totalNS: TraceRow.range?.totalNS || 0, 427 slicesTime: TraceRow.range?.slicesTime, 428 range: TraceRow.range, 429 frame: funcRow.frame 430 }, !funcRow.isTransferCanvas ? funcRow.offscreen[k] : undefined); 431 if (funcRow.isHover && promise.hover) { 432 FuncStruct.hoverFuncStruct = promise.hover; 433 } 434 promises.push(promise); 435 } 436 if (funcRow.isHover && promises.every(it => !it.hover)) { 437 FuncStruct.hoverFuncStruct = undefined; 438 } 439 funcRow.must = false; 440 funcRow.isTransferCanvas = true; 441 } 442 asy(useCache).then(); 443 } 444 this.insertAfter(funcRow, threadRow) 445 this.trace.observer.observe(funcRow) 446 funcRow.draw(); 447 if (threadRow.onComplete) { 448 threadRow.onComplete() 449 } 450 this.trace.getVisibleRows();//function 由于后插入dom,所以需要重新获取可见行 451 } 452 }) 453 if ((res instanceof ArrayBuffer && res.byteLength <= 0) || (res.length <= 0)) { 454 threadRow.rowDiscard = true; 455 } 456 return res; 457 }) 458 threadRow.onThreadHandler = (useCache) => { 459 procedurePool.submitWithName(`process${(threadRow.index) % procedurePool.processLen.length}`, `thread ${thread.tid} ${thread.threadName}`, { 460 list: threadRow.must ? threadRow.dataList : undefined, 461 offscreen: !threadRow.isTransferCanvas ? threadRow.offscreen[0] : undefined,//是否离屏 462 dpr: threadRow.dpr,//屏幕dpr值 463 xs: TraceRow.range?.xs,//线条坐标信息 464 isHover: threadRow.isHover, 465 flagMoveInfo: this.trace.hoverFlag, 466 flagSelectedInfo: this.trace.selectFlag, 467 hoverX: threadRow.hoverX, 468 hoverY: threadRow.hoverY, 469 canvasWidth: threadRow.canvasWidth, 470 canvasHeight: threadRow.canvasHeight, 471 hoverThreadStruct: ThreadStruct.hoverThreadStruct, 472 selectThreadStruct: ThreadStruct.selectThreadStruct, 473 wakeupBean: CpuStruct.wakeupBean, 474 isRangeSelect: threadRow.rangeSelect, 475 rangeSelectObject: TraceRow.rangeSelectObject, 476 useCache: useCache, 477 lineColor: threadRow.getLineColor(), 478 startNS: TraceRow.range?.startNS || 0, 479 endNS: TraceRow.range?.endNS || 0, 480 totalNS: TraceRow.range?.totalNS || 0, 481 slicesTime: TraceRow.range?.slicesTime, 482 range: TraceRow.range, 483 frame: threadRow.frame 484 }, !threadRow.isTransferCanvas ? threadRow.offscreen[0] : undefined, (res: any, hover: any) => { 485 threadRow.must = false; 486 if (threadRow.args.isOffScreen == true) { 487 if (threadRow.isHover) { 488 ThreadStruct.hoverThreadStruct = hover; 489 } 490 return; 491 } 492 }) 493 threadRow.isTransferCanvas = true; 494 } 495 if (threadRow.rowId == threadRow.rowParentId) { 496 this.insertAfter(threadRow, processRow) 497 } else { 498 this.trace.rowsEL?.appendChild(threadRow) 499 } 500 } 501 502 } 503 let durTime = new Date().getTime() - time; 504 info('The time to load the Process data is: ', durTime) 505 } 506 507 insertAfter(newEl: HTMLElement, targetEl: HTMLElement) { 508 let parentEl = targetEl.parentNode; 509 if (parentEl!.lastChild == targetEl) { 510 parentEl!.appendChild(newEl); 511 } else { 512 parentEl!.insertBefore(newEl, targetEl.nextSibling); 513 } 514 } 515}