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 {TimeRange} from "../timer-shaft/RangeRuler.js"; 18import '../../../../base-ui/icon/LitIcon.js' 19import {Rect} from "../timer-shaft/Rect.js"; 20import {BaseStruct} from "../../../bean/BaseStruct.js"; 21import {SpSystemTrace} from "../../SpSystemTrace.js"; 22import {ns2x} from "../TimerShaftElement.js"; 23import {TraceRowObject} from "./TraceRowObject.js"; 24import {LitCheckBox} from "../../../../base-ui/checkbox/LitCheckBox.js"; 25 26export class RangeSelectStruct { 27 startX: number | undefined 28 endX: number | undefined 29 startNS: number | undefined 30 endNS: number | undefined 31} 32 33@element('trace-row') 34export class TraceRow<T extends BaseStruct> extends BaseElement { 35 static ROW_TYPE_CPU = "cpu" 36 static ROW_TYPE_CPU_FREQ = "cpu-freq" 37 static ROW_TYPE_FPS = "fps" 38 static ROW_TYPE_PROCESS = "process" 39 static ROW_TYPE_THREAD = "thread" 40 static ROW_TYPE_MEM = "mem" 41 static ROW_TYPE_HEAP = "heap" 42 static ROW_TYPE_FUNC = "func" 43 static range: TimeRange | undefined | null; 44 static rangeSelectObject: RangeSelectStruct | undefined 45 public obj: TraceRowObject<any> | undefined | null; 46 public must: boolean = true; 47 public dataList: undefined | Array<T>; 48 public dataListCache: Array<T> = []; 49 public c: CanvasRenderingContext2D | null = null; 50 public describeEl: Element | null | undefined; 51 public canvas: HTMLCanvasElement | null | undefined; 52 public canvasContainer: HTMLDivElement | null | undefined; 53 public tipEL: HTMLDivElement | null | undefined; 54 public checkBoxEL: LitCheckBox | null | undefined; 55 public onDrawHandler: ((ctx: CanvasRenderingContext2D) => void) | undefined | null 56 public onThreadHandler: ((ctx: CanvasRenderingContext2D, useCache: boolean) => void) | undefined | null 57 public onItemDrawHandler: ((ctx: CanvasRenderingContext2D, data: T, preData?: T, nextData?: T) => void) | undefined | null 58 public supplier: (() => Promise<Array<T>>) | undefined | null 59 // @ts-ignore 60 offscreen: OffscreenCanvas | undefined; 61 canvasWidth = 0 62 canvasHeight = 0 63 isHover: boolean = false; 64 hoverX: number = 0; 65 hoverY: number = 0; 66 index: number = 0; 67 dpr = window.devicePixelRatio || 1; 68 private rootEL: HTMLDivElement | null | undefined; 69 private nameEL: HTMLLabelElement | null | undefined; 70 private isLoading: boolean = false 71 72 constructor(args: { alpha: boolean, contextId: string, isOffScreen: boolean }) { 73 super(args); 74 } 75 76 static get observedAttributes() { 77 return ["folder", "name", "expansion", "children", "height", "row-type", "row-id", "row-parent-id", "sleeping", 78 "check-type" 79 ]; 80 } 81 82 public _frame: Rect | undefined; 83 84 get frame(): Rect { 85 if (this._frame) { 86 this._frame.width = (this.parentElement?.clientWidth || 0) - 248; 87 this._frame.height = this.canvas?.clientHeight || 40; 88 return this._frame; 89 } else { 90 this._frame = new Rect(0, 0, (this.parentElement?.clientWidth || 0) - 248, this.canvas?.clientHeight || 40); 91 return this._frame; 92 } 93 } 94 95 set frame(f: Rect) { 96 this._frame = f; 97 } 98 99 private _rangeSelect: boolean = false; 100 101 get rangeSelect(): boolean { 102 return this._rangeSelect; 103 } 104 105 set rangeSelect(value: boolean) { 106 this._rangeSelect = value; 107 } 108 109 get sleeping(): boolean { 110 return this.hasAttribute("sleeping"); 111 } 112 113 set sleeping(value: boolean) { 114 if (value) { 115 this.setAttribute("sleeping", "") 116 } else { 117 this.removeAttribute("sleeping") 118 this.draw(); 119 } 120 } 121 122 get rowType(): string | undefined | null { 123 return this.getAttribute("row-type"); 124 } 125 126 set rowType(val) { 127 this.setAttribute("row-type", val || "") 128 } 129 130 get rowId(): string | undefined | null { 131 return this.getAttribute("row-id"); 132 } 133 134 set rowId(val) { 135 this.setAttribute("row-id", val || "") 136 } 137 138 get rowParentId(): string | undefined | null { 139 return this.getAttribute("row-parent-id"); 140 } 141 142 set rowParentId(val) { 143 this.setAttribute("row-parent-id", val || "") 144 } 145 146 set rowHidden(val: boolean) { 147 if (val) { 148 this.setAttribute("row-hidden", "") 149 } else { 150 this.removeAttribute("row-hidden") 151 } 152 } 153 154 get name(): string { 155 return this.getAttribute("name") || "" 156 } 157 158 set name(value: string) { 159 this.setAttribute("name", value) 160 } 161 162 get folder(): boolean { 163 return this.hasAttribute("folder"); 164 } 165 166 set folder(value: boolean) { 167 if (value) { 168 this.setAttribute("folder", '') 169 } else { 170 this.removeAttribute('folder') 171 } 172 } 173 174 get expansion(): boolean { 175 return this.hasAttribute("expansion") 176 } 177 178 set expansion(value) { 179 if (value) { 180 this.setAttribute("expansion", ''); 181 } else { 182 this.removeAttribute('expansion') 183 } 184 this.parentElement?.querySelectorAll<TraceRow<any>>(`trace-row[row-parent-id='${this.rowId}']`).forEach(it => { 185 if (this.expansion) { 186 it.rowHidden = false 187 } else { 188 it.rowHidden = true 189 } 190 }) 191 this.dispatchEvent(new CustomEvent("expansion-change", { 192 detail: { 193 expansion: this.expansion, 194 rowType: this.rowType, 195 rowId: this.rowId, 196 rowParentId: this.rowParentId 197 } 198 })) 199 } 200 201 set tip(value: string) { 202 if (this.tipEL) { 203 this.tipEL.innerHTML = value; 204 } 205 } 206 207 get checkType(): string { 208 return this.getAttribute("check-type") || ""; 209 } 210 211 set checkType(value: string) { 212 this.setAttribute("check-type", value); 213 } 214 215 initElements(): void { 216 this.rootEL = this.shadowRoot?.querySelector('.root') 217 this.checkBoxEL = this.shadowRoot?.querySelector<LitCheckBox>('.lit-check-box') 218 this.describeEl = this.shadowRoot?.querySelector('.describe') 219 this.nameEL = this.shadowRoot?.querySelector('.name') 220 this.canvas = this.shadowRoot?.querySelector('.panel') 221 this.canvasContainer = this.shadowRoot?.querySelector('.panel-container') 222 this.tipEL = this.shadowRoot?.querySelector('.tip') 223 this.describeEl?.addEventListener('click', () => { 224 if (this.folder) { 225 this.expansion = !this.expansion 226 } 227 }) 228 } 229 230 initCanvas() { 231 if (this.canvas) { 232 if (!this.args.isOffScreen) { 233 this.c = this.canvas?.getContext(this.args.contextId, {alpha: this.args.alpha}); 234 } 235 236 if (this.shadowRoot?.host.clientWidth == 0) { 237 let preCanvas = this.previousElementSibling?.shadowRoot?.querySelector<HTMLCanvasElement>("canvas"); 238 if (preCanvas) { 239 this.canvasWidth = preCanvas.width; 240 this.canvasHeight = preCanvas.height; 241 this.canvas.width = preCanvas.width; 242 this.canvas.height = preCanvas.height; 243 this.canvas.style.width = preCanvas.style.width; 244 this.canvas.style.height = preCanvas.style.height; 245 if (this.c instanceof CanvasRenderingContext2D && !this.args.isOffScreen) { 246 this.c?.scale(this.dpr, this.dpr); 247 this.c?.translate(0, 0) 248 } 249 } 250 } else { 251 let oldWidth = (this.shadowRoot!.host.clientWidth || 0) - (this.describeEl?.clientWidth || 248) - SpSystemTrace.scrollViewWidth - 1; 252 let oldHeight = parseInt(this.getAttribute("height") || '40') //this.shadowRoot!.host.clientHeight || 0; 253 this.rootEL!.style.height = `${this.getAttribute("height") || '40'}px` 254 this.canvas.style.width = '100%'; 255 this.canvas.style.height = oldHeight + 'px'; 256 this.canvas.width = Math.floor(oldWidth * this.dpr); 257 this.canvas.height = Math.floor(oldHeight * this.dpr); 258 this.canvasWidth = this.canvas.width; 259 this.canvasHeight = this.canvas.height; 260 if (this.c instanceof CanvasRenderingContext2D && !this.args.isOffScreen) { 261 this.c?.scale(this.dpr, this.dpr); 262 this.c?.translate(0, 0) 263 } 264 } 265 if (this.args.isOffScreen) { 266 // @ts-ignore 267 this.offscreen = this.canvas.transferControlToOffscreen(); 268 } 269 } 270 } 271 272 updateWidth(width: number) { 273 let dpr = window.devicePixelRatio || 1; 274 this.canvasWidth = Math.round((width - (this.describeEl?.clientWidth || 248) - SpSystemTrace.scrollViewWidth) * dpr); 275 this.canvasHeight = Math.round((this.shadowRoot!.host.clientHeight || 40) * dpr); 276 if (this.args.isOffScreen) { 277 this.draw(true); 278 return; 279 } 280 this.canvas!.width = width - (this.describeEl?.clientWidth || 248) - SpSystemTrace.scrollViewWidth; 281 this.canvas!.height = this.shadowRoot!.host.clientHeight || 40; 282 let oldWidth = this.canvas!.width; 283 let oldHeight = this.canvas!.height; 284 this.canvas!.width = Math.round((oldWidth) * dpr); 285 this.canvas!.height = Math.round(oldHeight * dpr); 286 this.canvas!.style.width = oldWidth + 'px'; 287 this.canvas!.style.height = oldHeight + 'px'; 288 if (this.c instanceof CanvasRenderingContext2D) { 289 this.c?.scale(dpr, dpr); 290 } 291 } 292 293 connectedCallback() { 294 this.checkBoxEL!.onchange = (ev: any) => { 295 if (!ev.target.checked) { 296 this.rangeSelect = false; 297 this.draw(); 298 } else { 299 this.rangeSelect = true; 300 this.draw(); 301 } 302 } 303 this.initCanvas(); 304 } 305 306 onMouseHover(x: number, y: number, tip: boolean = true): T | undefined | null { 307 if (this.dataListCache && this.dataListCache.length > 0) { 308 return this.dataListCache.find(it => (it.frame && Rect.contains(it.frame, x, y))); 309 } else if (this.dataList && this.dataList.length > 0) { 310 return this.dataList.find(it => (it.frame && Rect.contains(it.frame, x, y))); 311 } 312 if (this.tipEL) { 313 this.tipEL.style.display = 'none'; 314 } 315 return null; 316 } 317 318 setTipLeft(x: number, struct: any) { 319 if (struct == null && this.tipEL) { 320 this.tipEL.style.display = 'none'; 321 return 322 } 323 if (this.tipEL) { 324 this.tipEL.style.display = 'flex'; 325 if (x + this.tipEL.clientWidth > (this.canvas?.style.width.replace("px", "") || 0)) { 326 this.tipEL.style.transform = `translateX(${x - this.tipEL.clientWidth - 1}px)`; 327 } else { 328 this.tipEL.style.transform = `translateX(${x}px)`; 329 } 330 } 331 } 332 333 onMouseLeave(x: number, y: number) { 334 if (this.tipEL) { 335 this.tipEL.style.display = 'none'; 336 } 337 } 338 339 draw(useCache: boolean = false) { 340 if (this.sleeping) { 341 return; 342 } 343 if (!this.dataList) { 344 if (this.supplier && !this.isLoading) { 345 this.isLoading = true; 346 if (this.supplier) { 347 let promise = this.supplier(); 348 if (promise) { 349 promise.then(res => { 350 this.dataList = res 351 this.isLoading = false; 352 this.draw(false); 353 }) 354 } else { 355 this.dataList = []; 356 this.isLoading = false; 357 this.draw(false); 358 } 359 360 } 361 } 362 if (this.c && this.c instanceof CanvasRenderingContext2D) { 363 this.c.clearRect(0, 0, this.canvas?.clientWidth || 0, this.canvas?.clientHeight || 0) 364 this.c.beginPath(); 365 this.drawLines(this.c!); 366 this.drawSelection(this.c!); 367 this.c.fillStyle = window.getComputedStyle(this.nameEL!, null).getPropertyValue("color"); 368 this.c.fillText("Loading...", this.frame.x + 10, Math.ceil(this.frame.y + this.frame.height / 2)) 369 this.c.stroke(); 370 this.c.closePath(); 371 } 372 } else { 373 if (this.onDrawHandler && this.dataList && this.c instanceof CanvasRenderingContext2D) { 374 if (this.c && TraceRow.range) { 375 this.clearCanvas(this.c!) 376 this.c.beginPath(); 377 this.drawLines(this.c!); 378 this.onDrawHandler!(this.c!) 379 this.drawSelection(this.c!); 380 this.c.closePath(); 381 } 382 } else if (this.onItemDrawHandler) { 383 if (this.c && this.c instanceof CanvasRenderingContext2D && TraceRow.range) { 384 this.clearCanvas(this.c!) 385 this.c.beginPath(); 386 this.drawLines(this.c!); 387 if (this.onItemDrawHandler && this.dataList) { 388 for (let i = 0; i < this.dataList.length; i++) { 389 let preData = this.dataList[i - 1] || undefined; 390 let data = this.dataList[i]; 391 let nextData = this.dataList[i + 1] || undefined; 392 this.onItemDrawHandler(this.c!, data, preData, nextData) 393 } 394 } 395 this.drawSelection(this.c!); 396 this.c.closePath(); 397 } 398 } else if (this.onThreadHandler) { 399 this.onThreadHandler!(this.c!, useCache) 400 } 401 } 402 } 403 404 drawObject() { 405 if (!this.obj) return; 406 if (!this.obj.dataList) { 407 if (this.obj.supplier && !this.obj.isLoading) { 408 this.obj.isLoading = true; 409 if (this.obj.supplier) { 410 let promise = this.obj.supplier(); 411 if (promise) { 412 promise.then(res => { 413 console.log(res); 414 this.obj!.dataList = res 415 this.obj!.isLoading = false; 416 this.drawObject(); 417 }) 418 } else { 419 this.obj.dataList = []; 420 this.obj.isLoading = false; 421 this.drawObject(); 422 } 423 424 } 425 } 426 window.requestAnimationFrame(() => { 427 if (this.c && this.c instanceof CanvasRenderingContext2D) { 428 this.c.clearRect(0, 0, this.canvas?.clientWidth || 0, this.canvas?.clientHeight || 0) 429 this.c.beginPath(); 430 this.c.lineWidth = 1; 431 this.c.strokeStyle = "#dadada" 432 if (TraceRow.range) { 433 TraceRow.range.xs.forEach(it => { 434 // @ts-ignore 435 this.c!.moveTo(Math.floor(it), 0) 436 // @ts-ignore 437 this.c!.lineTo(Math.floor(it), this.shadowRoot?.host.clientHeight || 0) 438 }) 439 } 440 this.drawSelection(this.c!); 441 this.c.fillStyle = window.getComputedStyle(this.nameEL!, null).getPropertyValue("color"); 442 this.c.fillText("Loading...", this.frame.x + 10, Math.ceil(this.frame.y + this.frame.height / 2)) 443 this.c.stroke(); 444 this.c.closePath(); 445 } 446 }) 447 } else { 448 if (this.obj.onDrawHandler && this.obj.dataList) { 449 window.requestAnimationFrame(() => { 450 if (this.c && TraceRow.range) { 451 this.clearCanvas(this.c!) 452 // @ts-ignore 453 this.c.beginPath(); 454 this.drawLines(<CanvasRenderingContext2D>this.c!); 455 this.obj!.onDrawHandler!(this.c!) 456 this.drawSelection(<CanvasRenderingContext2D>this.c!); 457 // @ts-ignore 458 this.c.closePath(); 459 } 460 }) 461 } else if (this.obj.onThreadHandler) { 462 this.obj.onThreadHandler(this, this.c!) 463 } 464 } 465 } 466 467 clearCanvas(ctx: CanvasRenderingContext2D) { 468 if (ctx) { 469 ctx.clearRect(0, 0, this.canvas?.clientWidth || 0, this.canvas?.clientHeight || 0) 470 } 471 } 472 473 drawLines(ctx: CanvasRenderingContext2D) { 474 if (ctx) { 475 ctx.lineWidth = 1; 476 ctx.strokeStyle = this.getLineColor(); 477 TraceRow.range?.xs.forEach(it => { 478 ctx.moveTo(Math.floor(it), 0) 479 ctx.lineTo(Math.floor(it), this.shadowRoot?.host.clientHeight || 0) 480 }) 481 ctx.stroke(); 482 } 483 } 484 485 getLineColor() { 486 return window.getComputedStyle(this.rootEL!, null).getPropertyValue("border-bottom-color") 487 } 488 489 drawSelection(ctx: CanvasRenderingContext2D) { 490 if (this.rangeSelect) { 491 TraceRow.rangeSelectObject!.startX = Math.floor(ns2x(TraceRow.rangeSelectObject!.startNS!, TraceRow.range!.startNS, TraceRow.range!.endNS, TraceRow.range!.totalNS!, this.frame)); 492 TraceRow.rangeSelectObject!.endX = Math.floor(ns2x(TraceRow.rangeSelectObject!.endNS!, TraceRow.range!.startNS, TraceRow.range!.endNS, TraceRow.range!.totalNS!, this.frame)); 493 if (ctx) { 494 ctx.globalAlpha = 0.5 495 ctx.fillStyle = "#666666" 496 ctx.fillRect(TraceRow.rangeSelectObject!.startX!, this.frame.y, TraceRow.rangeSelectObject!.endX! - TraceRow.rangeSelectObject!.startX!, this.frame.height) 497 ctx.globalAlpha = 1 498 } 499 } 500 } 501 502 isInTimeRange(startTime: number, duration: number): boolean { 503 return ((startTime || 0) + (duration || 0) > (TraceRow.range?.startNS || 0) && (startTime || 0) < (TraceRow.range?.endNS || 0)); 504 } 505 506 attributeChangedCallback(name: string, oldValue: string, newValue: string) { 507 switch (name) { 508 case "name": 509 if (this.nameEL) { 510 this.nameEL.textContent = newValue; 511 this.nameEL.title = newValue; 512 } 513 break; 514 case "height": 515 if (newValue != oldValue) { 516 if (!this.args.isOffScreen) { 517 this.initCanvas(); 518 } 519 } 520 break; 521 case "check-type": 522 if (newValue === "check") { 523 this.checkBoxEL?.setAttribute("checked", ""); 524 } else { 525 this.checkBoxEL?.removeAttribute("checked"); 526 } 527 break; 528 } 529 } 530 531 initHtml(): string { 532 return ` 533<style> 534*{ 535 box-sizing: border-box; 536} 537:host(:not([row-hidden])){ 538 box-sizing: border-box; 539 display: flex; 540 flex-direction: column; 541 width: 100%; 542} 543:host([row-hidden]){ 544 width: 100%; 545 display: none; 546} 547.root{ 548 height: 40px; 549 width: 100%; 550 display: grid; 551 grid-template-rows: 100%; 552 grid-template-columns: 248px 1fr; 553 border-bottom: 1px solid var(--dark-border1,#dadada); 554 box-sizing: border-box; 555} 556.describe{ 557 box-sizing: border-box; 558 border-right: 1px solid var(--dark-border1,#c9d0da); 559 background-color: transparent; 560 align-items: center; 561 position: relative; 562} 563.panel{ 564 width: 100%; 565 height: 100%; 566 overflow: visible; 567 background-color: transparent; 568} 569.panel-container{ 570 width: 100%; 571 height: 100%; 572 position: relative; 573 pointer-events: none; 574} 575.tip{ 576 position:absolute; 577 top: 0; 578 left: 0; 579 height: 100%; 580 background-color: white; 581 border: 1px solid #f9f9f9; 582 width: auto; 583 font-size: 8px; 584 color: #50809e; 585 flex-direction: column; 586 justify-content: center; 587 align-items: flex-start; 588 padding: 2px 10px; 589 display: none; 590 user-select: none; 591} 592.name{ 593 color: var(--dark-color1,#4b5766); 594 margin-left: 10px; 595 font-size: .9rem; 596 font-weight: normal; 597 width: 100%; 598 max-height: 100%; 599 text-align: left; 600 overflow: hidden; 601 user-select: none; 602} 603.icon{ 604 color: var(--dark-color1,#151515); 605 margin-left: 10px; 606} 607.describe:hover { 608 cursor: pointer; 609} 610:host([folder]) .describe:hover > .icon{ 611 color:#ecb93f; 612 margin-left: 10px; 613} 614:host([folder]){ 615 background-color: var(--dark-background1,#f5fafb); 616} 617:host([folder]) .icon{ 618 display: flex; 619} 620:host(:not([folder])){ 621 background-color: var(--dark-background,#FFFFFF); 622} 623:host(:not([folder]):not([children])) { 624} 625:host(:not([folder]):not([children])) .icon{ 626 display: none; 627} 628:host(:not([folder])[children]) .icon{ 629 visibility: hidden; 630 color:#fff 631} 632 633:host(:not([folder])[children]) .name{ 634} 635:host([expansion]) { 636 background-color: var(--bark-expansion,#0C65D1); 637} 638:host([expansion]) .name,:host([expansion]) .icon{ 639 color: #fff; 640} 641:host([expansion]) .describe{ 642 border-right: 0px; 643} 644:host([expansion]:not(sleeping)) .panel-container{ 645 display: none; 646} 647:host([expansion]) .children{ 648 flex-direction: column; 649 width: 100%; 650} 651:host([expansion]) .icon{ 652 transform: rotateZ(0deg); 653} 654:host(:not([expansion])) .children{ 655 display: none; 656 flex-direction: column; 657 width: 100%; 658} 659:host(:not([expansion])) .icon{ 660 transform: rotateZ(-90deg); 661} 662:host([sleeping]) .describe{ 663 display: none; 664} 665:host([sleeping]) .panel-container{ 666 display: none; 667} 668:host([sleeping]) .children{ 669 display: none; 670} 671:host(:not([sleeping])) .describe{ 672 display: flex;; 673} 674:host(:not([sleeping])) .panel-container{ 675 display: flex; 676} 677:host(:not([sleeping])) .children{ 678 display: flex; 679} 680 681 :host([check-type]) .lit-check-box{ 682 display: none; 683} 684:host(:not([check-type])) .lit-check-box{ 685 display: none; 686} 687</style> 688<div class="root"> 689 <div class="describe"> 690 <lit-icon class="icon" name="caret-down" size="13"></lit-icon> 691 <label class="name"></label> 692 <lit-check-box class="lit-check-box"></lit-check-box> 693 </div> 694 <div class="panel-container"> 695 <canvas class="panel"></canvas> 696 <div class="tip"> 697 P:process [1573]<br> 698 T:Thread [675] 699 </div> 700 </div> 701</div> 702 `; 703 } 704 705} 706