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 {BaseElement, element} from "../../../../base-ui/BaseElement.js"; 17import {CpuStruct} from "../../../bean/CpuStruct.js"; 18import {LitTable} from "../../../../base-ui/table/lit-table.js"; 19import "../../../../base-ui/table/lit-table-column.js"; 20 21import { 22 queryBinderArgsByArgset, queryBinderByArgsId, queryBinderBySliceId, 23 queryThreadWakeUp, 24 queryThreadWakeUpFrom, 25 queryWakeUpFromThread_WakeThread, 26 queryWakeUpFromThread_WakeTime, 27} from "../../../database/SqlLite.js"; 28import {WakeupBean} from "../../../bean/WakeupBean.js"; 29import {ThreadStruct} from "../../../bean/ThreadStruct.js"; 30import {ProcessMemStruct} from "../../../bean/ProcessMemStruct.js"; 31import {FuncStruct} from "../../../bean/FuncStruct.js"; 32import {SpApplication} from "../../../SpApplication.js"; 33import {TraceRow} from "../base/TraceRow.js"; 34 35const STATUS_MAP: any = { 36 D: "Uninterruptible Sleep", 37 S: "Sleeping", 38 R: "Runnable", 39 "Running": "Running", 40 "R+": "Runnable (Preempted)", 41 DK: "Uninterruptible Sleep + Wake Kill", 42 I: "Task Dead", 43 T: "Traced", 44 t: "Traced", 45 X: "Exit (Dead)", 46 Z: "Exit (Zombie)", 47 K: "Wake Kill", 48 W: "Waking", 49 P: "Parked", 50 N: "No Load" 51} 52const INPUT_WORD = "This is the interval from when the task became eligible to run \n(e.g.because of notifying a wait queue it was a suspended on) to\n when it started running." 53 54export function getTimeString(ns: number): string { 55 let currentNs = ns 56 let hour1 = 3600_000_000_000 57 let minute1 = 60_000_000_000 58 let second1 = 1_000_000_000; // 1 second 59 let millisecond1 = 1_000_000; // 1 millisecond 60 let microsecond1 = 1_000; // 1 microsecond 61 let res = ""; 62 if (currentNs >= hour1) { 63 res += Math.floor(currentNs / hour1) + "h "; 64 currentNs = currentNs - Math.floor(currentNs / hour1) * hour1 65 } 66 if (currentNs >= minute1) { 67 res += Math.floor(currentNs / minute1) + "m "; 68 currentNs = currentNs - Math.floor(ns / minute1) * minute1 69 } 70 if (currentNs >= second1) { 71 res += Math.floor(currentNs / second1) + "s "; 72 currentNs = currentNs - Math.floor(currentNs / second1) * second1 73 } 74 if (currentNs >= millisecond1) { 75 res += Math.floor(currentNs / millisecond1) + "ms "; 76 currentNs = currentNs - Math.floor(currentNs / millisecond1) * millisecond1 77 } 78 if (currentNs >= microsecond1) { 79 res += Math.floor(currentNs / microsecond1) + "μs "; 80 currentNs = currentNs - Math.floor(currentNs / microsecond1) * microsecond1 81 } 82 if (currentNs > 0) { 83 res += currentNs + "ns "; 84 } 85 return res 86} 87 88@element('tabpane-current-selection') 89export class TabPaneCurrentSelection extends BaseElement { 90 weakUpBean: WakeupBean | null | undefined; 91 private tbl: LitTable | null | undefined; 92 private tableObserver: MutationObserver | undefined 93 // @ts-ignore 94 private dpr: any = window.devicePixelRatio || window.webkitDevicePixelRatio || window.mozDevicePixelRatio || 1; 95 96 set data(value: any) { 97 this.setCpuData(value) 98 } 99 100 setCpuData(data: CpuStruct, callback: ((data: WakeupBean | null) => void) | undefined = undefined, scrollCallback?: (data: CpuStruct) => void) { 101 let leftTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#leftTitle"); 102 if (leftTitle) { 103 leftTitle.innerText = "Slice Details" 104 } 105 let list: any[] = [] 106 let process = this.transferString( data.processName || "Process"); 107 let processId = data.processId || data.tid; 108 let state = "" 109 if (data.end_state) { 110 state = STATUS_MAP[data.end_state] 111 } else if (data.end_state == "" || data.end_state == null) { 112 state = "" 113 } else { 114 state = "Unknown State" 115 } 116 117 list.push({name: 'Process', value: `${process || 'Process'} [${processId}]`}) 118 let name = this.transferString(data.name ?? ""); 119 if(data.processId){ 120 list.push({ 121 name: 'Thread', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 122<div style="white-space:pre-wrap">${name || 'Process'} [${data.tid}]</div> 123<lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="thread-id" name="select" color="#7fa1e7" size="20"></lit-icon> 124</div>` 125 }) 126 }else { 127 list.push({ 128 name: 'Thread', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 129<div style="white-space:pre-wrap">${name || 'Process'} [${data.tid}]</div> 130</div>` 131 }) 132 } 133 134 list.push({name: 'CmdLine', value: `${data.processCmdLine}`}) 135 list.push({name: 'StartTime', value: getTimeString(data.startTime || 0)}) 136 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 137 list.push({name: 'Prio', value: data.priority || 0}) 138 list.push({name: 'End State', value: state}) 139 this.queryCPUWakeUpFromData(data).then((bean) => { 140 if (callback) { 141 callback(bean) 142 } 143 this.tbl!.dataSource = list 144 let rightArea: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#right-table"); 145 let rightTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#rightTitle"); 146 let threadClick = this.tbl?.shadowRoot?.querySelector("#thread-id") 147 threadClick?.addEventListener("click", () => { 148 //cpu点击 149 if (scrollCallback) { 150 scrollCallback(data) 151 } 152 }) 153 let canvas = this.initCanvas(); 154 if (bean != null) { 155 this.weakUpBean = bean; 156 if (rightArea != null && rightArea) { 157 rightArea.style.visibility = "visible" 158 } 159 if (rightTitle != null && rightTitle) { 160 rightTitle.style.visibility = "visible" 161 } 162 this.drawRight(canvas, bean) 163 } else { 164 this.weakUpBean = null; 165 if (rightArea != null && rightArea) { 166 rightArea.style.visibility = "hidden" 167 } 168 if (rightTitle != null && rightTitle) { 169 rightTitle.style.visibility = "hidden" 170 } 171 } 172 }) 173 } 174 175 setFunctionData(data: FuncStruct,scrollCallback:Function) {//方法信息 176 this.initCanvas() 177 let leftTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#leftTitle"); 178 let rightTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#rightTitle"); 179 if (rightTitle) { 180 rightTitle.style.visibility = "hidden" 181 } 182 if (leftTitle) { 183 leftTitle.innerText = "Slice Details" 184 } 185 let list: any[] = [] 186 let name = this.transferString(data.funName ?? ""); 187 let isBinder = FuncStruct.isBinder(data); 188 let isAsyncBinder = isBinder&&FuncStruct.isBinderAsync(data); 189 if (data.argsetid != undefined&&data.argsetid != null) { 190 if(isAsyncBinder){ 191 Promise.all([queryBinderByArgsId(data.argsetid!,data.startTs!,!data.funName!.endsWith("rcv")),queryBinderArgsByArgset(data.argsetid)]).then((result)=>{ 192 let asyncBinderRes = result[0] 193 let argsBinderRes = result[1] 194 let asyncBinderStract:any; 195 if(asyncBinderRes.length > 0){ 196 asyncBinderRes[0].type = TraceRow.ROW_TYPE_FUNC 197 asyncBinderStract = asyncBinderRes[0] 198 } 199 if(argsBinderRes.length > 0){ 200 argsBinderRes.forEach((item) => { 201 list.push({name: item.keyName, value: item.strValue}) 202 }) 203 } 204 if(asyncBinderStract != undefined){ 205 list.unshift({ 206 name: 'Name', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 207<div style="white-space:pre-wrap">${name || 'binder'}</div> 208<lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="function-jump" name="select" color="#7fa1e7" size="20"></lit-icon> 209</div>` 210 }) 211 }else { 212 list.unshift({name: 'Name', value:name}) 213 } 214 list.push({name: 'StartTime', value: getTimeString(data.startTs || 0)}) 215 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 216 list.push({name: 'depth', value: data.depth}) 217 list.push({name: 'arg_set_id', value: data.argsetid}) 218 this.tbl!.dataSource = list; 219 let funcClick = this.tbl?.shadowRoot?.querySelector("#function-jump") 220 funcClick?.addEventListener("click", () => { 221 scrollCallback(asyncBinderStract) 222 }) 223 }) 224 } else if(isBinder){ 225 queryBinderArgsByArgset(data.argsetid).then((argset) => { 226 let binderSliceId = -1; 227 argset.forEach((item) => { 228 if(item.keyName == 'destination slice id'){ 229 binderSliceId = Number(item.strValue) 230 list.unshift({ 231 name: 'Name', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 232<div style="white-space:pre-wrap">${name || 'binder'}</div> 233<lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="function-jump" name="select" color="#7fa1e7" size="20"></lit-icon> 234</div>` 235 }) 236 } 237 list.push({name: item.keyName, value: item.strValue}) 238 }) 239 if(binderSliceId == -1) { 240 list.unshift({name: 'Name', value:name}) 241 } 242 list.push({name: 'StartTime', value: getTimeString(data.startTs || 0)}) 243 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 244 list.push({name: 'depth', value: data.depth}) 245 list.push({name: 'arg_set_id', value: data.argsetid}) 246 this.tbl!.dataSource = list; 247 let funcClick = this.tbl?.shadowRoot?.querySelector("#function-jump") 248 funcClick?.addEventListener("click", () => { 249 if (!Number.isNaN(binderSliceId)&&binderSliceId!=-1) { 250 queryBinderBySliceId(binderSliceId).then((result:any[])=>{ 251 if(result.length > 0){ 252 result[0].type = TraceRow.ROW_TYPE_FUNC 253 scrollCallback(result[0]) 254 } 255 }) 256 } 257 }) 258 }); 259 } else { 260 queryBinderArgsByArgset(data.argsetid).then((argset) => { 261 list.push({name: 'Name', value:name}) 262 argset.forEach((item) => { 263 list.push({name: item.keyName, value: item.strValue}) 264 }) 265 list.push({name: 'StartTime', value: getTimeString(data.startTs || 0)}) 266 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 267 list.push({name: 'depth', value: data.depth}) 268 list.push({name: 'arg_set_id', value: data.argsetid}) 269 this.tbl!.dataSource = list; 270 }) 271 } 272 }else { 273 list.push({name: 'Name', value:name}) 274 list.push({name: 'StartTime', value: getTimeString(data.startTs || 0)}) 275 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 276 list.push({name: 'depth', value: data.depth}) 277 this.tbl!.dataSource = list; 278 } 279 } 280 281 setMemData(data: ProcessMemStruct) {//时钟信息 282 this.initCanvas() 283 let leftTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#leftTitle"); 284 if (leftTitle) { 285 leftTitle.innerText = "Counter Details" 286 } 287 let list: any[] = [] 288 list.push({name: 'Start time', value: getTimeString(data.startTime || 0)}) 289 list.push({name: 'Value', value: data.value}) 290 list.push({name: 'Delta', value: data.delta}) 291 list.push({name: 'Duration', value: getTimeString(data.duration || 0)}) 292 this.tbl!.dataSource = list 293 294 } 295 296 setThreadData(data: ThreadStruct, scrollCallback: ((d: any) => void) | undefined,scrollWakeUp:(d:any) => void | undefined) {//线程信息 297 this.initCanvas() 298 let leftTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#leftTitle"); 299 let rightTitle: HTMLElement | null | undefined = this?.shadowRoot?.querySelector("#rightTitle"); 300 if (rightTitle) { 301 rightTitle.style.visibility = "hidden" 302 } 303 if (leftTitle) { 304 leftTitle.innerText = "Thread State" 305 } 306 let list: any[] = [] 307 list.push({name: 'StartTime', value: getTimeString(data.startTime || 0)}) 308 list.push({name: 'Duration', value: getTimeString(data.dur || 0)}) 309 let state = "" 310 if (data.state) { 311 state = STATUS_MAP[data.state] 312 } else if (data.state == "" || data.state == null) { 313 state = "" 314 } else { 315 state = "Unknown State" 316 } 317 if ("Running" == state) { 318 state = state + " on CPU " + data.cpu; 319 } 320 if (data.cpu == null || data.cpu == undefined) { 321 list.push({name: 'State', value: `${state}`}) 322 } else { 323 list.push({ 324 name: 'State', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 325 <div style="white-space:pre-wrap">${state}</div> 326 <lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="state-click" name="select" color="#7fa1e7" size="20"></lit-icon> 327 </div>` 328 }) 329 } 330 let processName = data.processName; 331 if (processName == null || processName == "" || processName.toLowerCase() == "null") { 332 processName = data.name; 333 } 334 list.push({name: 'Process', value: this.transferString(processName ?? "") + " [" + data.pid + "] "}) 335 let cpu = new CpuStruct(); 336 cpu.id = data.id; 337 cpu.startTime = data.startTime; 338 Promise.all([this.queryThreadWakeUpFromData(data.id!,data.startTime!,data.dur!),this.queryThreadWakeUpData(data.id!,data.startTime!,data.dur!)]).then((result)=>{ 339 let fromBean = result[0] 340 let wakeUps = result[1]; 341 if(fromBean != null && fromBean != undefined && fromBean.pid != 0 && fromBean.tid != 0){ 342 list.push({ 343 name: 'wakeup from tid', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 344 <div style="white-space:pre-wrap">${fromBean.tid}</div> 345 <lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="wakeup-from" class="wakeup-click" name="select" color="#7fa1e7" size="20"></lit-icon> 346 </div>` 347 }) 348 } 349 if(wakeUps != null){ 350 for (let key in wakeUps) { 351 list.push({ 352 name: 'wakeup tid', value: `<div style="margin-left: 5px;white-space: nowrap;display: flex;align-items: center"> 353 <div style="white-space:pre-wrap">${wakeUps[key].tid}</div> 354 <lit-icon style="cursor:pointer;transform: scaleX(-1);margin-left: 5px" id="wakeup-${key}" class="wakeup-click" name="select" color="#7fa1e7" size="20"></lit-icon> 355 </div>` 356 }) 357 } 358 } 359 this.tbl!.dataSource = list 360 this.tbl?.shadowRoot?.querySelector("#state-click")?.addEventListener("click", () => { 361 //线程点击 362 if (scrollCallback) { 363 scrollCallback(data) 364 } 365 }) 366 this.tbl?.shadowRoot?.querySelector("#wakeup-from")?.addEventListener("click", (e) => { 367 //点击跳转,唤醒和被唤醒的 线程 368 if(fromBean && scrollWakeUp){ 369 scrollWakeUp({ 370 processId: fromBean.pid, 371 tid: fromBean.tid, 372 startTime: fromBean.ts, 373 }) 374 } 375 }) 376 if(wakeUps){ 377 for (let key in wakeUps) { 378 this.tbl?.shadowRoot?.querySelector(`#wakeup-${key}`)?.addEventListener("click", (e) => { 379 //点击跳转,唤醒和被唤醒的 线程 380 let up = wakeUps[key]; 381 if(up && scrollWakeUp != undefined){ 382 scrollWakeUp({ 383 tid: up.tid, 384 startTime: up.ts, 385 processId:up.pid, 386 }) 387 } 388 }) 389 } 390 } 391 }) 392 } 393 394 /** 395 * 查询出 线程被唤醒的 线程信息 396 * @param data 397 */ 398 async queryCPUWakeUpFromData(data: CpuStruct) { 399 let wb: WakeupBean | null = null 400 if (data.id == undefined || data.startTime == undefined) { 401 return null 402 } 403 let wakeupTimes = await queryWakeUpFromThread_WakeTime(data.id, data.startTime) 404 if (wakeupTimes != undefined && wakeupTimes.length > 0) { 405 let wakeupTime = wakeupTimes[0] 406 if (wakeupTime.wakeTs != undefined && wakeupTime.preRow != undefined && wakeupTime.wakeTs < wakeupTime.preRow) { 407 return null 408 } 409 if (wakeupTime.wakeTs == undefined) { 410 return null 411 } 412 let wakeupBeans = await queryWakeUpFromThread_WakeThread(wakeupTime.wakeTs) 413 if (wakeupBeans != undefined && wakeupBeans.length > 0) { 414 wb = wakeupBeans[0] 415 if (wb != null) { 416 if (wakeupTime.wakeTs != undefined && wakeupTime.startTs != undefined) { 417 wb.wakeupTime = wakeupTime.wakeTs - wakeupTime.startTs 418 } 419 wb.schedulingLatency = (data.startTime || 0) - (wb.wakeupTime || 0) 420 if (wb.process == null) { 421 wb.process = wb.thread; 422 } 423 if (wb.pid == undefined) { 424 wb.pid = wb.tid; 425 } 426 wb.schedulingDesc = INPUT_WORD 427 } 428 } 429 } 430 return wb 431 } 432 433 /** 434 * 查询出 线程唤醒了哪些线程信息 435 * @param data 436 */ 437 async queryThreadWakeUpFromData(itid: number, startTime: number,dur:number) : Promise<WakeupBean|undefined> { 438 let wakeUps = await queryThreadWakeUpFrom(itid, startTime,dur) 439 if (wakeUps != undefined && wakeUps.length > 0) { 440 return wakeUps[0]; 441 } 442 } 443 /** 444 * 查询出 线程唤醒了哪些线程信息 445 * @param data 446 */ 447 async queryThreadWakeUpData(itid: number, startTime: number,dur:number) : Promise<Array<WakeupBean>> { 448 let list :Array<WakeupBean> = []; 449 if (itid == undefined || startTime == undefined) { 450 return list 451 } 452 let wakeUps = await queryThreadWakeUp(itid, startTime,dur)// 3,4835380000 453 if (wakeUps != undefined && wakeUps.length > 0) { 454 list.push(...wakeUps) 455 } 456 return list 457 } 458 459 initCanvas(): HTMLCanvasElement | null { 460 let canvas = this.shadowRoot!.querySelector<HTMLCanvasElement>("#rightDraw") 461 let width = getComputedStyle(this.tbl!).getPropertyValue("width") 462 let height = getComputedStyle(this.tbl!).getPropertyValue("height") 463 if (canvas != null) { 464 canvas.width = Math.round(Number(width.replace("px", "")) * this.dpr) 465 canvas.height = Math.round(Number(height.replace("px", "")) * this.dpr) 466 canvas.style.width = width 467 canvas.style.height = height 468 canvas.getContext("2d")!.scale(this.dpr, this.dpr) 469 } 470 SpApplication.skinChange = (val: boolean) => { 471 this.drawRight(canvas, this.weakUpBean!) 472 } 473 return canvas 474 } 475 476 drawRight(cavs: HTMLCanvasElement | null, wakeupBean: WakeupBean | null) { 477 if (cavs == null) { 478 return 479 } 480 let context = cavs.getContext("2d"); 481 if (context != null) { 482 //绘制竖线 483 if (document.querySelector<SpApplication>("sp-application")!.dark) { 484 context.strokeStyle = "#ffffff"; 485 context.fillStyle = "#ffffff"; 486 } else { 487 context.strokeStyle = "#000000"; 488 context.fillStyle = "#000000"; 489 } 490 context.lineWidth = 2; 491 context.moveTo(10, 15); 492 context.lineTo(10, 125); 493 context.stroke(); 494 //绘制菱形 495 context.lineWidth = 1; 496 context.beginPath() 497 context.moveTo(10, 30); 498 context.lineTo(4, 40); 499 context.lineTo(10, 50); 500 context.lineTo(16, 40); 501 context.lineTo(10, 30); 502 context.closePath() 503 context.fill() 504 context.font = 12 + "px sans-serif"; 505 //绘制wake up 文字 506 let strList = [] 507 strList.push("wakeup @ " + getTimeString(wakeupBean?.wakeupTime || 0) + " on CPU " + wakeupBean?.cpu + " by") 508 strList.push("P:" + wakeupBean?.process + " [ " + wakeupBean?.pid + " ]") 509 strList.push("T:" + wakeupBean?.thread + " [ " + wakeupBean?.tid + " ]") 510 strList.forEach((str, index) => { 511 if (context != null) { 512 context.fillText(str, 40, 40 + 16 * index) 513 } 514 }) 515 //绘制左右箭头 516 context.lineWidth = 2; 517 context.lineJoin = "bevel" 518 context.moveTo(10, 95) 519 context.lineTo(20, 90) 520 context.moveTo(10, 95) 521 context.lineTo(20, 100) 522 context.moveTo(10, 95) 523 context.lineTo(80, 95) 524 context.lineTo(70, 90) 525 context.moveTo(80, 95) 526 context.lineTo(70, 100) 527 context.stroke(); 528 //绘制latency 529 context.font = 12 + "px sans-serif"; 530 context.fillText("Scheduling latency:" + getTimeString(wakeupBean?.schedulingLatency || 0) 531 , 90, 100) 532 //绘制最下方提示语句 533 context.font = 10 + "px sans-serif"; 534 INPUT_WORD.split("\n").forEach((str, index) => { 535 context?.fillText(str, 90, 120 + 12 * index) 536 }) 537 538 } 539 } 540 541 transferString(str:string): string{ 542 let s = "" 543 if(str.length == 0){ 544 return ""; 545 } 546 s = str.replace(/&/g,"&") 547 s = s.replace(/</g,"<") 548 s = s.replace(/>/g,">") 549 s = s.replace(/ /g," ") 550 s = s.replace(/\'/g,"'") 551 s = s.replace(/\"/g,"&#quat;") 552 // s = s.replace(/(/g,"&") 553 // s = s.replace(/)/g,"&") 554 return s 555 } 556 557 initElements(): void { 558 this.tbl = this.shadowRoot?.querySelector<LitTable>('#selectionTbl'); 559 this.tbl?.addEventListener("column-click", (ev: any) => { 560 }) 561 this.addTableObserver() 562 } 563 564 addTableObserver() { 565 let MutationObserver = window.MutationObserver 566 this.tableObserver = new MutationObserver((list) => { 567 if (this.tbl) { 568 let width = getComputedStyle(this.tbl).getPropertyValue("width") 569 let height = getComputedStyle(this.tbl).getPropertyValue("height") 570 } 571 }) 572 let selector = this.shadowRoot?.querySelector(".left-table"); 573 this.tableObserver?.observe(selector!, {attributes: true, attributeFilter: ['style'], attributeOldValue: true}) 574 } 575 576 initHtml(): string { 577 return ` 578 <style> 579 .current-title{ 580 width: 100%; 581 display: flex; 582 top: 0; 583 background: var(--dark-background,#ffffff); 584 position: sticky; 585 } 586 .current-title h2{ 587 width: 50%; 588 padding: 0 10px; 589 font-size: 16px; 590 font-weight: 400; 591 visibility: visible; 592 } 593 .bottom-scroll-area{ 594 display: flex; 595 height: auto; 596 overflow-y: auto; 597 } 598 .left-table{ 599 width: 50%; 600 padding: 0 10px; 601 } 602 .right-table{ 603 width: 50%; 604 } 605 </style> 606 <div style="width: 100%;height: auto;position: relative"> 607 <div class="current-title"> 608 <h2 id="leftTitle"></h2> 609 <h2 id="rightTitle">Scheduling Latency</h2> 610 </div> 611 <div class="bottom-scroll-area"> 612 <div class="left-table"> 613 <lit-table id="selectionTbl" no-head style="height: auto"> 614 <lit-table-column title="name" data-index="name" key="name" align="flex-start" width="180px"> 615 <template><div>{{name}}</div></template> 616 </lit-table-column> 617 <lit-table-column title="value" data-index="value" key="value" align="flex-start" > 618 <template><div style="display: flex;">{{value}}</div></template> 619 </lit-table-column> 620 </lit-table> 621 </div> 622 <div class="right-table"> 623 <canvas id="rightDraw" style="width: 100%;height: 100%;"></canvas> 624 </div> 625 </div> 626 </div> 627 `; 628 } 629} 630