1/* 2 * Copyright (C) 2023 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 { BaseElement, element } from '../../../../../base-ui/BaseElement'; 17import { LitTable, RedrawTreeForm } from '../../../../../base-ui/table/lit-table'; 18import { SelectionParam } from '../../../../bean/BoxSelection'; 19import '../../../StackBar'; 20import { getTabRunningPercent } from '../../../../database/sql/ProcessThread.sql'; 21import { queryCpuFreqUsageData, queryCpuFreqFilterId } from '../../../../database/sql/Cpu.sql'; 22import { Utils } from '../../base/Utils'; 23import { resizeObserver } from '../SheetUtils'; 24import { SpSegmentationChart } from '../../../chart/SpSegmentationChart'; 25import { type CpuFreqData, type RunningFreqData, type RunningData, type CpuFreqTd } from './TabPaneFreqUsageConfig'; 26 27@element('tabpane-frequsage') 28export class TabPaneFreqUsage extends BaseElement { 29 private threadStatesTbl: LitTable | null | undefined; 30 private currentSelectionParam: SelectionParam | undefined; 31 private result: Array<RunningFreqData> = []; 32 33 set data(threadStatesParam: SelectionParam) { 34 if (this.currentSelectionParam === threadStatesParam) { 35 return; 36 } 37 this.threadStatesTbl!.loading = true; 38 this.currentSelectionParam = threadStatesParam; 39 this.threadStatesTbl!.recycleDataSource = []; 40 // @ts-ignore 41 this.threadStatesTbl.value = []; 42 this.result = []; 43 this.queryAllData(threadStatesParam); 44 } 45 async queryAllData(threadStatesParam: SelectionParam): Promise<void> { 46 let runningResult: Array<RunningData> = await getTabRunningPercent( 47 threadStatesParam.threadIds, 48 threadStatesParam.processIds, 49 threadStatesParam.leftNs, 50 threadStatesParam.rightNs 51 ); 52 // 查询cpu及id信息 53 let cpuIdResult: Array<{ id: number; cpu: number }> = await queryCpuFreqFilterId(); 54 // 以键值对形式将cpu及id进行对应,后续会将频点数据与其对应cpu进行整合 55 let IdMap: Map<number, number> = new Map(); 56 let queryId: Array<number> = []; 57 let cpuArray: Array<number> = []; 58 for (let i = 0; i < cpuIdResult.length; i++) { 59 queryId.push(cpuIdResult[i].id); 60 IdMap.set(cpuIdResult[i].id, cpuIdResult[i].cpu); 61 cpuArray.push(cpuIdResult[i].cpu); 62 } 63 // 通过id去查询频点数据 64 let cpuFreqResult: Array<CpuFreqTd> = await queryCpuFreqUsageData(queryId); 65 let cpuFreqData: Array<CpuFreqData> = []; 66 for (let i of cpuFreqResult) { 67 cpuFreqData.push({ 68 ts: i.startNS + threadStatesParam.recordStartNs, 69 cpu: IdMap.get(i.filter_id)!, 70 value: i.value, 71 dur: i.dur, 72 }); 73 } 74 const LEFT_TIME: number = threadStatesParam.leftNs + threadStatesParam.recordStartNs; 75 const RIGHT_TIME: number = threadStatesParam.rightNs + threadStatesParam.recordStartNs; 76 const args = { leftNs: LEFT_TIME, rightNs: RIGHT_TIME, cpuArray: cpuArray }; 77 let resultArr: Array<RunningFreqData> = orgnazitionMap(runningResult, cpuFreqData, args); 78 // 递归拿出来最底层的数据,并以进程层级的数据作为分割 79 this.recursion(resultArr); 80 this.result = JSON.parse(JSON.stringify(this.result)); 81 mergeTotal(resultArr, fixTotal(this.result)); 82 this.fixedDeal(resultArr, threadStatesParam.traceId); 83 this.threadClick(resultArr); 84 this.threadStatesTbl!.recycleDataSource = resultArr; 85 this.threadStatesTbl!.loading = false; 86 } 87 88 /** 89 * 递归整理数据小数位 90 */ 91 fixedDeal(arr: Array<RunningFreqData>, traceId?: string | null): void { 92 if (arr == undefined) { 93 return; 94 } 95 const TIME_MUTIPLE: number = 1000000; 96 // KHz->MHz * ns->ms 97 const CONS_MUTIPLE: number = 1000000000; 98 const FREQ_MUTIPLE: number = 1000; 99 const MIN_PERCENT: number = 2; 100 const MIN_FREQ: number = 3; 101 for (let i = 0; i < arr.length; i++) { 102 let trackId: number; 103 // 若存在空位元素则进行删除处理 104 if (arr[i] === undefined) { 105 arr.splice(i, 1); 106 i--; 107 continue; 108 } 109 if (arr[i].thread?.indexOf('P') !== -1) { 110 trackId = Number(arr[i].thread?.slice(1)!); 111 arr[i].thread = `${Utils.getInstance().getProcessMap(traceId).get(trackId) || 'Process'} ${trackId}`; 112 } else if (arr[i].thread === 'summary data') { 113 } else { 114 trackId = Number(arr[i].thread!.split('_')[1]); 115 arr[i].thread = `${Utils.getInstance().getThreadMap(traceId).get(trackId) || 'Thread'} ${trackId}`; 116 } 117 if (arr[i].cpu < 0) { 118 // @ts-ignore 119 arr[i].cpu = ''; 120 } 121 // @ts-ignore 122 if (arr[i].frequency < 0) { 123 arr[i].frequency = ''; 124 } 125 // @ts-ignore 126 arr[i].percent = arr[i].percent.toFixed(MIN_PERCENT); 127 // @ts-ignore 128 arr[i].dur = (arr[i].dur / TIME_MUTIPLE).toFixed(MIN_FREQ); 129 // @ts-ignore 130 arr[i].consumption = (arr[i].consumption / CONS_MUTIPLE).toFixed(MIN_FREQ); 131 if (arr[i].frequency !== '') { 132 if (arr[i].frequency === 'unknown') { 133 arr[i].frequency = 'unknown'; 134 } else { 135 arr[i].frequency = Number(arr[i].frequency) / FREQ_MUTIPLE; 136 } 137 } 138 this.fixedDeal(arr[i].children!, traceId); 139 } 140 } 141 142 /** 143 * 表头点击事件 144 */ 145 private threadClick(data: Array<RunningFreqData>): void { 146 let labels = this.threadStatesTbl?.shadowRoot?.querySelector('.th > .td')!.querySelectorAll('label'); 147 if (labels) { 148 for (let i = 0; i < labels.length; i++) { 149 let label = labels[i].innerHTML; 150 labels[i].addEventListener('click', (e) => { 151 if (label.includes('Process') && i === 0) { 152 this.threadStatesTbl!.setStatus(data, false); 153 this.threadStatesTbl!.recycleDs = this.threadStatesTbl!.meauseTreeRowElement(data, RedrawTreeForm.Retract); 154 } else if (label.includes('Thread') && i === 1) { 155 for (let item of data) { 156 // @ts-ignore 157 item.status = true; 158 if (item.children !== undefined && item.children.length > 0) { 159 this.threadStatesTbl!.setStatus(item.children, false); 160 } 161 } 162 this.threadStatesTbl!.recycleDs = this.threadStatesTbl!.meauseTreeRowElement(data, RedrawTreeForm.Retract); 163 } else if (label.includes('CPU') && i === 2) { 164 this.threadStatesTbl!.setStatus(data, true); 165 this.threadStatesTbl!.recycleDs = this.threadStatesTbl!.meauseTreeRowElement(data, RedrawTreeForm.Expand); 166 } 167 }); 168 } 169 } 170 } 171 172 /** 173 * 174 * @param arr 待整理的数组,会经过递归取到最底层的数据 175 */ 176 recursion(arr: Array<RunningFreqData>): void { 177 for (let idx = 0; idx < arr.length; idx++) { 178 if (arr[idx].cpu === -1) { 179 this.result.push(arr[idx]); 180 } 181 if (arr[idx].children) { 182 this.recursion(arr[idx].children!); 183 } else { 184 this.result.push(arr[idx]); 185 } 186 } 187 } 188 189 initElements(): void { 190 this.threadStatesTbl = this.shadowRoot?.querySelector<LitTable>('#tb-running-percent'); 191 } 192 connectedCallback(): void { 193 super.connectedCallback(); 194 resizeObserver(this.parentElement!, this.threadStatesTbl!); 195 } 196 initHtml(): string { 197 return ` 198 <style> 199 :host{ 200 padding: 10px 10px; 201 display: flex; 202 flex-direction: column; 203 } 204 </style> 205 <lit-table id="tb-running-percent" style="height: auto; overflow-x:auto;width:calc(100vw - 270px)" tree> 206 <lit-table-column class="running-percent-column" width="320px" title="Process/Thread/CPU" data-index="thread" key="thread" align="flex-start" retract> 207 </lit-table-column> 208 <lit-table-column class="running-percent-column" width="120px" title="CPU" data-index="cpu" key="cpu" align="flex-start"> 209 </lit-table-column> 210 <lit-table-column class="running-percent-column" width="240px" title="Consumption" data-index="consumption" key="consumption" align="flex-start"> 211 </lit-table-column> 212 <lit-table-column class="running-percent-column" width="200px" title="Freq(MHz)" data-index="frequency" key="frequency" align="flex-start"> 213 </lit-table-column> 214 <lit-table-column class="running-percent-column" width="200px" title="duration(ms)" data-index="dur" key="dur" align="flex-start"> 215 </lit-table-column> 216 <lit-table-column class="running-percent-column" width="240px" title="Percent(%)" data-index="percent" key="percent" align="flex-start"> 217 </lit-table-column> 218 </lit-table> 219 `; 220 } 221} 222 223/** 224 * 225 * @param runData 数据库查询上来的running数据,此函数会将数据整理成map结构,分组规则:'pid_tid'为键,running数据数字为值 226 * @returns 返回map对象及所有running数据的dur和,后续会依此计算百分比 227 */ 228function orgnazitionMap( 229 runData: Array<RunningData>, 230 cpuFreqData: Array<CpuFreqData>, 231 args: { 232 leftNs: number, 233 rightNs: number, 234 cpuArray: number[] 235 } 236): Array<RunningFreqData> { 237 let result: Map<string, Array<RunningData>> = new Map(); 238 let sum: number = 0; 239 // 循环分组 240 for (let i = 0; i < runData.length; i++) { 241 let mapKey: string = runData[i].pid + '_' + runData[i].tid; 242 // 该running数据若在map对象中不包含其'pid_tid'构成的键,则新加key-value值 243 if (!result.has(mapKey)) { 244 result.set(mapKey, new Array()); 245 } 246 // 整理左右边界数据问题, 因为涉及多线程,所以必须放在循环里 247 if (runData[i].ts < args.leftNs && runData[i].ts + runData[i].dur > args.leftNs) { 248 runData[i].dur = runData[i].ts + runData[i].dur - args.leftNs; 249 runData[i].ts = args.leftNs; 250 } 251 if (runData[i].ts + runData[i].dur > args.rightNs) { 252 runData[i].dur = args.rightNs - runData[i].ts; 253 } 254 // 特殊处理数据表中dur为负值的情况 255 if (runData[i].dur < 0) { 256 runData[i].dur = 0; 257 } 258 // 分组整理数据 259 result.get(mapKey)?.push({ 260 pid: runData[i].pid, 261 tid: runData[i].tid, 262 cpu: runData[i].cpu, 263 dur: runData[i].dur, 264 ts: runData[i].ts, 265 }); 266 sum += runData[i].dur; 267 } 268 return dealCpuFreqData(cpuFreqData, result, sum, args.cpuArray); 269} 270 271/** 272 * 273 * @param cpuFreqData cpu频点数据的数组 274 * @param result running数据的map对象 275 * @param sum running数据的时间和 276 * @returns 返回cpu频点数据map,'pid_tid'为键,频点算力值数据的数组为值 277 */ 278function dealCpuFreqData( 279 cpuFreqData: Array<CpuFreqData>, 280 result: Map<string, Array<RunningData>>, 281 sum: number, 282 cpuList: number[] 283): Array<RunningFreqData> { 284 let runningFreqData: Map<string, Array<RunningFreqData>> = new Map(); 285 result.forEach((item, key) => { 286 let resultList: Array<RunningFreqData> = new Array(); 287 for (let i = 0; i < item.length; i++) { 288 for (let j = 0; j < cpuFreqData.length; j++) { 289 let flag: number; 290 if (item[i].cpu === cpuFreqData[j].cpu) { 291 // 当running状态数据的开始时间大于频点数据开始时间,小于频点结束时间。且running数据的持续时间小于频点结束时间减去running数据开始时间的差值的情况 292 if ( 293 item[i].ts > cpuFreqData[j].ts && 294 item[i].ts < cpuFreqData[j].ts + cpuFreqData[j].dur && 295 item[i].dur < cpuFreqData[j].ts + cpuFreqData[j].dur - item[i].ts 296 ) { 297 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 1))!); 298 item.splice(i, 1); 299 i--; 300 break; 301 } 302 if ( 303 item[i].ts > cpuFreqData[j].ts && 304 item[i].ts < cpuFreqData[j].ts + cpuFreqData[j].dur && 305 item[i].dur >= cpuFreqData[j].ts + cpuFreqData[j].dur - item[i].ts 306 ) { 307 // 当running状态数据的开始时间大于频点数据开始时间,小于频点结束时间。且running数据的持续时间大于等于频点结束时间减去running数据开始时间的差值的情况 308 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 2))!); 309 } 310 // 当running状态数据的开始时间小于等于频点数据开始时间,结束时间大于频点开始时间。且running数据的持续时间减去频点数据开始时间的差值小于频点数据持续时间的情况 311 if ( 312 item[i].ts <= cpuFreqData[j].ts && 313 item[i].ts + item[i].dur > cpuFreqData[j].ts && 314 item[i].dur + item[i].ts - cpuFreqData[j].ts < cpuFreqData[j].dur 315 ) { 316 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 3))!); 317 item.splice(i, 1); 318 i--; 319 break; 320 } 321 if ( 322 item[i].ts <= cpuFreqData[j].ts && 323 item[i].ts + item[i].dur > cpuFreqData[j].ts && 324 item[i].dur + item[i].ts - cpuFreqData[j].ts >= cpuFreqData[j].dur 325 ) { 326 // 当running状态数据的开始时间小于等于频点数据开始时间,结束时间大于频点开始时间。且running数据的持续时间减去频点数据开始时间的差值大于等于频点数据持续时间的情况 327 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 4))!); 328 } 329 if (item[i].ts <= cpuFreqData[j].ts && item[i].ts + item[i].dur <= cpuFreqData[j].ts) { 330 // 当running状态数据的开始时间小于等于频点数据开始时间,结束时间小于等于频点开始时间的情况 331 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 5))!); 332 item.splice(i, 1); 333 i--; 334 break; 335 } 336 } else { 337 if (!cpuList.includes(item[i].cpu)) { 338 resultList.push(returnObj(item[i], cpuFreqData[j], sum, (flag = 5))!); 339 item.splice(i, 1); 340 i--; 341 break; 342 } 343 } 344 } 345 } 346 runningFreqData.set(key, mergeSameData(resultList)); 347 }); 348 return dealTree(runningFreqData); 349} 350 351/** 352 * 353 * @param item running数据 354 * @param cpuFreqData 频点数据 355 * @param sum running总和 356 * @param flag 标志位,根据不同值返回不同结果 357 * @returns 返回新的对象 358 */ 359function returnObj( 360 item: RunningData, 361 cpuFreqData: CpuFreqData, 362 sum: number, 363 flag: number 364): RunningFreqData | undefined { 365 const PERCENT: number = 100; 366 const consumption: number = ( 367 SpSegmentationChart.freqInfoMapData.size > 0 368 ? SpSegmentationChart.freqInfoMapData.get(item.cpu)?.get(cpuFreqData.value) 369 : cpuFreqData.value 370 )!; 371 let result: RunningFreqData | undefined; 372 switch (flag) { 373 case 1: 374 result = { 375 thread: item.pid + '_' + item.tid, 376 consumption: consumption * item.dur, 377 cpu: item.cpu, 378 frequency: cpuFreqData.value, 379 dur: item.dur, 380 percent: (item.dur / sum) * PERCENT, 381 }; 382 case 2: 383 result = { 384 thread: item.pid + '_' + item.tid, 385 consumption: consumption * (cpuFreqData.ts + cpuFreqData.dur - item.ts), 386 cpu: item.cpu, 387 frequency: cpuFreqData.value, 388 dur: cpuFreqData.ts + cpuFreqData.dur - item.ts, 389 percent: ((cpuFreqData.ts + cpuFreqData.dur - item.ts) / sum) * PERCENT, 390 }; 391 case 3: 392 result = { 393 thread: item.pid + '_' + item.tid, 394 consumption: consumption * (item.dur + item.ts - cpuFreqData.ts), 395 cpu: item.cpu, 396 frequency: cpuFreqData.value, 397 dur: item.dur + item.ts - cpuFreqData.ts, 398 percent: ((item.dur + item.ts - cpuFreqData.ts) / sum) * PERCENT, 399 }; 400 case 4: 401 result = { 402 thread: item.pid + '_' + item.tid, 403 consumption: consumption * cpuFreqData.dur, 404 cpu: item.cpu, 405 frequency: cpuFreqData.value, 406 dur: cpuFreqData.dur, 407 percent: (cpuFreqData.dur / sum) * PERCENT, 408 }; 409 case 5: 410 result = { 411 thread: item.pid + '_' + item.tid, 412 consumption: 0, 413 cpu: item.cpu, 414 frequency: 'unknown', 415 dur: item.dur, 416 percent: (item.dur / sum) * PERCENT, 417 }; 418 } 419 return result; 420} 421 422/** 423 * 424 * @param resultList 单线程内running数据与cpu频点数据整合成的数组 425 */ 426function mergeSameData(resultList: Array<RunningFreqData>): Array<RunningFreqData> { 427 let cpuFreqArr: Array<RunningFreqData> = []; 428 let cpuArr: Array<number> = []; 429 //合并同一线程内,当运行所在cpu和频点相同时,dur及percent进行累加求和 430 for (let i = 0; i < resultList.length; i++) { 431 if (!cpuArr.includes(resultList[i].cpu)) { 432 cpuArr.push(resultList[i].cpu); 433 cpuFreqArr.push(creatNewObj(resultList[i].cpu)); 434 } 435 for (let j = i + 1; j < resultList.length; j++) { 436 if (resultList[i].cpu === resultList[j].cpu && resultList[i].frequency === resultList[j].frequency) { 437 resultList[i].dur += resultList[j].dur; 438 resultList[i].percent += resultList[j].percent; 439 resultList[i].consumption += resultList[j].consumption; 440 resultList.splice(j, 1); 441 j--; 442 } 443 } 444 cpuFreqArr.find(function (item) { 445 if (item.cpu === resultList[i].cpu) { 446 item.children?.push(resultList[i]); 447 item.children?.sort((a, b) => b.consumption - a.consumption); 448 item.dur += resultList[i].dur; 449 item.percent += resultList[i].percent; 450 item.consumption += resultList[i].consumption; 451 item.thread = resultList[i].thread; 452 } 453 }); 454 } 455 cpuFreqArr.sort((a, b) => a.cpu - b.cpu); 456 return cpuFreqArr; 457} 458 459/** 460 * 461 * @param params cpu层级的数据 462 * @returns 整理好的进程级数据 463 */ 464function dealTree(params: Map<string, Array<RunningFreqData>>): Array<RunningFreqData> { 465 let result: Array<RunningFreqData> = []; 466 params.forEach((item, key) => { 467 let process: RunningFreqData = creatNewObj(-1, false); 468 let thread: RunningFreqData = creatNewObj(-2); 469 for (let i = 0; i < item.length; i++) { 470 thread.children?.push(item[i]); 471 thread.dur += item[i].dur; 472 thread.percent += item[i].percent; 473 thread.consumption += item[i].consumption; 474 thread.thread = item[i].thread; 475 } 476 process.children?.push(thread); 477 process.dur += thread.dur; 478 process.percent += thread.percent; 479 process.consumption += thread.consumption; 480 process.thread = process.thread! + key.split('_')[0]; 481 result.push(process); 482 }); 483 for (let i = 0; i < result.length; i++) { 484 for (let j = i + 1; j < result.length; j++) { 485 if (result[i].thread === result[j].thread) { 486 result[i].children?.push(result[j].children![0]); 487 result[i].dur += result[j].dur; 488 result[i].percent += result[j].percent; 489 result[i].consumption += result[j].consumption; 490 result.splice(j, 1); 491 j--; 492 } 493 } 494 } 495 return result; 496} 497 498/** 499 * 500 * @param cpu 根据cpu值创建层级结构,cpu < 0为线程、进程层级,其余为cpu层级 501 * @returns 502 */ 503function creatNewObj(cpu: number, flag: boolean = true): RunningFreqData { 504 return { 505 thread: flag ? '' : 'P', 506 consumption: 0, 507 cpu: cpu, 508 frequency: -1, 509 dur: 0, 510 percent: 0, 511 children: [], 512 }; 513} 514 515/** 516 * 517 * @param arr 需要整理汇总的频点级数据 518 * @returns 返回一个total->cpu->频点的三级树结构数组 519 */ 520function fixTotal(arr: Array<RunningFreqData>): Array<RunningFreqData> { 521 let result: Array<RunningFreqData> = []; 522 let flag: number = -1; 523 // 数据入参的情况是,第一条为进程数据,其后是该进程下所有线程的数据。以进程数据做分割 524 for (let i = 0; i < arr.length; i++) { 525 // 判断如果是进程数据,则将其children的数组清空,并以其作为最顶层数据 526 if (arr[i].thread?.indexOf('P') !== -1) { 527 arr[i].children = []; 528 arr[i].thread = arr[i].thread + '-summary data'; 529 result.push(arr[i]); 530 // 标志判定当前数组的长度,也可用.length判断 531 flag++; 532 } else { 533 // 非进程数据会进入到else中,去判断当前线程数据的cpu分组是否存在,不存在则进行创建 534 if (result[flag].children![arr[i].cpu] === undefined) { 535 result[flag].children![arr[i].cpu] = { 536 thread: 'summary data', 537 consumption: 0, 538 cpu: arr[i].cpu, 539 frequency: -1, 540 dur: 0, 541 percent: 0, 542 children: [], 543 }; 544 } 545 // 每有一条数据要放到cpu分组下时,则将该cpu分组的各项数据累和 546 result[flag].children![arr[i].cpu].consumption += arr[i].consumption; 547 result[flag].children![arr[i].cpu].dur += arr[i].dur; 548 result[flag].children![arr[i].cpu].percent += arr[i].percent; 549 // 查找当前cpu分组下是否存在与当前数据的频点相同的数据,返回相同数据的索引值 550 let index: number = result[flag].children![arr[i].cpu].children?.findIndex( 551 (item) => item.frequency === arr[i].frequency 552 )!; 553 // 若存在相同频点的数据,则进行合并,不同直接push 554 if (index === -1) { 555 arr[i].thread = 'summary data'; 556 result[flag].children![arr[i].cpu].children?.push(arr[i]); 557 } else { 558 result[flag].children![arr[i].cpu].children![index].consumption += arr[i].consumption; 559 result[flag].children![arr[i].cpu].children![index].dur += arr[i].dur; 560 result[flag].children![arr[i].cpu].children![index].percent += arr[i].percent; 561 } 562 } 563 } 564 return result; 565} 566 567/** 568 * 569 * @param arr1 前次整理好的区分线程的数据 570 * @param arr2 不区分线程的Total数据 571 */ 572function mergeTotal(arr1: Array<RunningFreqData>, arr2: Array<RunningFreqData>): void { 573 for (let i = 0; i < arr1.length; i++) { 574 const num: number = arr2.findIndex((item) => item.thread?.includes(arr1[i].thread!)); 575 arr2[num].thread = 'summary data'; 576 arr1[i].children?.unshift(arr2[num]); 577 arr2.splice(num, 1); 578 } 579} 580