1// Copyright (C) 2018 The Android Open Source Project 2// 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 m from 'mithril'; 16import {findRef, toHTMLElement} from '../base/dom_utils'; 17import {assertExists, assertFalse} from '../base/logging'; 18import { 19 PerfStats, 20 PerfStatsContainer, 21 runningStatStr, 22} from '../core/perf_stats'; 23import {raf} from '../core/raf_scheduler'; 24import {SimpleResizeObserver} from '../base/resize_observer'; 25import {canvasClip} from '../base/canvas_utils'; 26import {SELECTION_STROKE_COLOR, TRACK_SHELL_WIDTH} from './css_constants'; 27import {Bounds2D, Size2D, VerticalBounds} from '../base/geom'; 28import {VirtualCanvas} from './virtual_canvas'; 29import {DisposableStack} from '../base/disposable_stack'; 30import {TimeScale} from '../base/time_scale'; 31import {TrackNode} from '../public/workspace'; 32import {HTMLAttrs} from '../widgets/common'; 33import {TraceImpl, TraceImplAttrs} from '../core/trace_impl'; 34 35const CANVAS_OVERDRAW_PX = 100; 36 37export interface Panel { 38 readonly kind: 'panel'; 39 render(): m.Children; 40 readonly selectable: boolean; 41 // TODO(stevegolton): Remove this - panel container should know nothing of 42 // tracks! 43 readonly trackNode?: TrackNode; 44 renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void; 45 getSliceVerticalBounds?(depth: number): VerticalBounds | undefined; 46} 47 48export interface PanelGroup { 49 readonly kind: 'group'; 50 readonly collapsed: boolean; 51 readonly header?: Panel; 52 readonly topOffsetPx: number; 53 readonly sticky: boolean; 54 readonly childPanels: PanelOrGroup[]; 55} 56 57export type PanelOrGroup = Panel | PanelGroup; 58 59export interface PanelContainerAttrs extends TraceImplAttrs { 60 panels: PanelOrGroup[]; 61 className?: string; 62 selectedYRange: VerticalBounds | undefined; 63 64 onPanelStackResize?: (width: number, height: number) => void; 65 66 // Called after all panels have been rendered to the canvas, to give the 67 // caller the opportunity to render an overlay on top of the panels. 68 renderOverlay?( 69 ctx: CanvasRenderingContext2D, 70 size: Size2D, 71 panels: ReadonlyArray<RenderedPanelInfo>, 72 ): void; 73 74 // Called before the panels are rendered 75 renderUnderlay?(ctx: CanvasRenderingContext2D, size: Size2D): void; 76} 77 78interface PanelInfo { 79 trackNode?: TrackNode; // Can be undefined for singleton panels. 80 panel: Panel; 81 height: number; 82 width: number; 83 clientX: number; 84 clientY: number; 85 absY: number; 86} 87 88export interface RenderedPanelInfo { 89 panel: Panel; 90 rect: Bounds2D; 91} 92 93export class PanelContainer 94 implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer 95{ 96 private readonly trace: TraceImpl; 97 private attrs: PanelContainerAttrs; 98 99 // Updated every render cycle in the view() hook 100 private panelById = new Map<string, Panel>(); 101 102 // Updated every render cycle in the oncreate/onupdate hook 103 private panelInfos: PanelInfo[] = []; 104 105 private perfStatsEnabled = false; 106 private panelPerfStats = new WeakMap<Panel, PerfStats>(); 107 private perfStats = { 108 totalPanels: 0, 109 panelsOnCanvas: 0, 110 renderStats: new PerfStats(10), 111 }; 112 113 private ctx?: CanvasRenderingContext2D; 114 115 private readonly trash = new DisposableStack(); 116 117 private readonly OVERLAY_REF = 'overlay'; 118 private readonly PANEL_STACK_REF = 'panel-stack'; 119 120 constructor({attrs}: m.CVnode<PanelContainerAttrs>) { 121 this.attrs = attrs; 122 this.trace = attrs.trace; 123 this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas())); 124 this.trash.use(attrs.trace.perfDebugging.addContainer(this)); 125 } 126 127 getPanelsInRegion( 128 startX: number, 129 endX: number, 130 startY: number, 131 endY: number, 132 ): Panel[] { 133 const minX = Math.min(startX, endX); 134 const maxX = Math.max(startX, endX); 135 const minY = Math.min(startY, endY); 136 const maxY = Math.max(startY, endY); 137 const panels: Panel[] = []; 138 for (let i = 0; i < this.panelInfos.length; i++) { 139 const pos = this.panelInfos[i]; 140 const realPosX = pos.clientX - TRACK_SHELL_WIDTH; 141 if ( 142 realPosX + pos.width >= minX && 143 realPosX <= maxX && 144 pos.absY + pos.height >= minY && 145 pos.absY <= maxY && 146 pos.panel.selectable 147 ) { 148 panels.push(pos.panel); 149 } 150 } 151 return panels; 152 } 153 154 // This finds the tracks covered by the in-progress area selection. When 155 // editing areaY is not set, so this will not be used. 156 handleAreaSelection() { 157 const {selectedYRange} = this.attrs; 158 const area = this.trace.timeline.selectedArea; 159 if ( 160 area === undefined || 161 selectedYRange === undefined || 162 this.panelInfos.length === 0 163 ) { 164 return; 165 } 166 167 // TODO(stevegolton): We shouldn't know anything about visible time scale 168 // right now, that's a job for our parent, but we can put one together so we 169 // don't have to refactor this entire bit right now... 170 171 const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, { 172 left: 0, 173 right: this.virtualCanvas!.size.width - TRACK_SHELL_WIDTH, 174 }); 175 176 // The Y value is given from the top of the pan and zoom region, we want it 177 // from the top of the panel container. The parent offset corrects that. 178 const panels = this.getPanelsInRegion( 179 visibleTimeScale.timeToPx(area.start), 180 visibleTimeScale.timeToPx(area.end), 181 selectedYRange.top, 182 selectedYRange.bottom, 183 ); 184 185 // Get the track ids from the panels. 186 const trackUris: string[] = []; 187 for (const panel of panels) { 188 if (panel.trackNode) { 189 if (panel.trackNode.isSummary) { 190 const groupNode = panel.trackNode; 191 // Select a track group and all child tracks if it is collapsed 192 if (groupNode.collapsed) { 193 for (const track of groupNode.flatTracks) { 194 track.uri && trackUris.push(track.uri); 195 } 196 } 197 } else { 198 panel.trackNode.uri && trackUris.push(panel.trackNode.uri); 199 } 200 } 201 } 202 this.trace.timeline.selectArea(area.start, area.end, trackUris); 203 } 204 205 private virtualCanvas?: VirtualCanvas; 206 207 oncreate(vnode: m.CVnodeDOM<PanelContainerAttrs>) { 208 const {dom, attrs} = vnode; 209 210 const overlayElement = toHTMLElement( 211 assertExists(findRef(dom, this.OVERLAY_REF)), 212 ); 213 214 const virtualCanvas = new VirtualCanvas(overlayElement, dom, { 215 overdrawPx: CANVAS_OVERDRAW_PX, 216 }); 217 this.trash.use(virtualCanvas); 218 this.virtualCanvas = virtualCanvas; 219 220 const ctx = virtualCanvas.canvasElement.getContext('2d'); 221 if (!ctx) { 222 throw Error('Cannot create canvas context'); 223 } 224 this.ctx = ctx; 225 226 virtualCanvas.setCanvasResizeListener((canvas, width, height) => { 227 const dpr = window.devicePixelRatio; 228 canvas.width = width * dpr; 229 canvas.height = height * dpr; 230 }); 231 232 virtualCanvas.setLayoutShiftListener(() => { 233 this.renderCanvas(); 234 }); 235 236 this.onupdate(vnode); 237 238 const panelStackElement = toHTMLElement( 239 assertExists(findRef(dom, this.PANEL_STACK_REF)), 240 ); 241 242 // Listen for when the panel stack changes size 243 this.trash.use( 244 new SimpleResizeObserver(panelStackElement, () => { 245 attrs.onPanelStackResize?.( 246 panelStackElement.clientWidth, 247 panelStackElement.clientHeight, 248 ); 249 }), 250 ); 251 } 252 253 onremove() { 254 this.trash.dispose(); 255 } 256 257 renderPanel(node: Panel, panelId: string, htmlAttrs?: HTMLAttrs): m.Vnode { 258 assertFalse(this.panelById.has(panelId)); 259 this.panelById.set(panelId, node); 260 return m( 261 `.pf-panel`, 262 {...htmlAttrs, 'data-panel-id': panelId}, 263 node.render(), 264 ); 265 } 266 267 // Render a tree of panels into one vnode. Argument `path` is used to build 268 // `key` attribute for intermediate tree vnodes: otherwise Mithril internals 269 // will complain about keyed and non-keyed vnodes mixed together. 270 renderTree(node: PanelOrGroup, panelId: string): m.Vnode { 271 if (node.kind === 'group') { 272 const style = { 273 position: 'sticky', 274 top: `${node.topOffsetPx}px`, 275 zIndex: `${2000 - node.topOffsetPx}`, 276 }; 277 return m( 278 'div.pf-panel-group', 279 node.header && 280 this.renderPanel(node.header, `${panelId}-header`, { 281 style: !node.collapsed && node.sticky ? style : {}, 282 }), 283 ...node.childPanels.map((child, index) => 284 this.renderTree(child, `${panelId}-${index}`), 285 ), 286 ); 287 } 288 return this.renderPanel(node, panelId); 289 } 290 291 view({attrs}: m.CVnode<PanelContainerAttrs>) { 292 this.attrs = attrs; 293 this.panelById.clear(); 294 const children = attrs.panels.map((panel, index) => 295 this.renderTree(panel, `${index}`), 296 ); 297 298 return m( 299 '.pf-panel-container', 300 {className: attrs.className}, 301 m( 302 '.pf-panel-stack', 303 {ref: this.PANEL_STACK_REF}, 304 m('.pf-overlay', {ref: this.OVERLAY_REF}), 305 children, 306 ), 307 ); 308 } 309 310 onupdate({dom}: m.CVnodeDOM<PanelContainerAttrs>) { 311 this.readPanelRectsFromDom(dom); 312 } 313 314 private readPanelRectsFromDom(dom: Element): void { 315 this.panelInfos = []; 316 317 const panel = dom.querySelectorAll('.pf-panel'); 318 const panels = assertExists(findRef(dom, this.PANEL_STACK_REF)); 319 const {top} = panels.getBoundingClientRect(); 320 panel.forEach((panelElement) => { 321 const panelHTMLElement = toHTMLElement(panelElement); 322 const panelId = assertExists(panelHTMLElement.dataset.panelId); 323 const panel = assertExists(this.panelById.get(panelId)); 324 325 // NOTE: the id can be undefined for singletons like overview timeline. 326 const rect = panelElement.getBoundingClientRect(); 327 this.panelInfos.push({ 328 trackNode: panel.trackNode, 329 height: rect.height, 330 width: rect.width, 331 clientX: rect.x, 332 clientY: rect.y, 333 absY: rect.y - top, 334 panel, 335 }); 336 }); 337 } 338 339 private renderCanvas() { 340 if (!this.ctx) return; 341 if (!this.virtualCanvas) return; 342 343 const ctx = this.ctx; 344 const vc = this.virtualCanvas; 345 const redrawStart = performance.now(); 346 347 ctx.resetTransform(); 348 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 349 350 const dpr = window.devicePixelRatio; 351 ctx.scale(dpr, dpr); 352 ctx.translate(-vc.canvasRect.left, -vc.canvasRect.top); 353 354 this.handleAreaSelection(); 355 356 const totalRenderedPanels = this.renderPanels(ctx, vc); 357 this.drawTopLayerOnCanvas(ctx, vc); 358 359 // Collect performance as the last thing we do. 360 const redrawDur = performance.now() - redrawStart; 361 this.updatePerfStats( 362 redrawDur, 363 this.panelInfos.length, 364 totalRenderedPanels, 365 ); 366 } 367 368 private renderPanels( 369 ctx: CanvasRenderingContext2D, 370 vc: VirtualCanvas, 371 ): number { 372 this.attrs.renderUnderlay?.(ctx, vc.size); 373 374 let panelTop = 0; 375 let totalOnCanvas = 0; 376 377 const renderedPanels = Array<RenderedPanelInfo>(); 378 379 for (let i = 0; i < this.panelInfos.length; i++) { 380 const { 381 panel, 382 width: panelWidth, 383 height: panelHeight, 384 } = this.panelInfos[i]; 385 386 const panelRect = { 387 left: 0, 388 top: panelTop, 389 bottom: panelTop + panelHeight, 390 right: panelWidth, 391 }; 392 const panelSize = {width: panelWidth, height: panelHeight}; 393 394 if (vc.overlapsCanvas(panelRect)) { 395 totalOnCanvas++; 396 397 ctx.save(); 398 ctx.translate(0, panelTop); 399 canvasClip(ctx, 0, 0, panelWidth, panelHeight); 400 const beforeRender = performance.now(); 401 panel.renderCanvas(ctx, panelSize); 402 this.updatePanelStats( 403 i, 404 panel, 405 performance.now() - beforeRender, 406 ctx, 407 panelSize, 408 ); 409 ctx.restore(); 410 } 411 412 renderedPanels.push({ 413 panel, 414 rect: { 415 top: panelTop, 416 bottom: panelTop + panelHeight, 417 left: 0, 418 right: panelWidth, 419 }, 420 }); 421 422 panelTop += panelHeight; 423 } 424 425 this.attrs.renderOverlay?.(ctx, vc.size, renderedPanels); 426 427 return totalOnCanvas; 428 } 429 430 // The panels each draw on the canvas but some details need to be drawn across 431 // the whole canvas rather than per panel. 432 private drawTopLayerOnCanvas( 433 ctx: CanvasRenderingContext2D, 434 vc: VirtualCanvas, 435 ): void { 436 const {selectedYRange} = this.attrs; 437 const area = this.trace.timeline.selectedArea; 438 if (area === undefined || selectedYRange === undefined) { 439 return; 440 } 441 if (this.panelInfos.length === 0 || area.trackUris.length === 0) { 442 return; 443 } 444 445 // Find the minY and maxY of the selected tracks in this panel container. 446 let selectedTracksMinY = selectedYRange.top; 447 let selectedTracksMaxY = selectedYRange.bottom; 448 for (let i = 0; i < this.panelInfos.length; i++) { 449 const trackUri = this.panelInfos[i].trackNode?.uri; 450 if (trackUri && area.trackUris.includes(trackUri)) { 451 selectedTracksMinY = Math.min( 452 selectedTracksMinY, 453 this.panelInfos[i].absY, 454 ); 455 selectedTracksMaxY = Math.max( 456 selectedTracksMaxY, 457 this.panelInfos[i].absY + this.panelInfos[i].height, 458 ); 459 } 460 } 461 462 // TODO(stevegolton): We shouldn't know anything about visible time scale 463 // right now, that's a job for our parent, but we can put one together so we 464 // don't have to refactor this entire bit right now... 465 466 const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, { 467 left: 0, 468 right: vc.size.width - TRACK_SHELL_WIDTH, 469 }); 470 471 const startX = visibleTimeScale.timeToPx(area.start); 472 const endX = visibleTimeScale.timeToPx(area.end); 473 ctx.save(); 474 ctx.strokeStyle = SELECTION_STROKE_COLOR; 475 ctx.lineWidth = 1; 476 477 ctx.translate(TRACK_SHELL_WIDTH, 0); 478 479 // Clip off any drawing happening outside the bounds of the timeline area 480 canvasClip(ctx, 0, 0, vc.size.width - TRACK_SHELL_WIDTH, vc.size.height); 481 482 ctx.strokeRect( 483 startX, 484 selectedTracksMaxY, 485 endX - startX, 486 selectedTracksMinY - selectedTracksMaxY, 487 ); 488 ctx.restore(); 489 } 490 491 private updatePanelStats( 492 panelIndex: number, 493 panel: Panel, 494 renderTime: number, 495 ctx: CanvasRenderingContext2D, 496 size: Size2D, 497 ) { 498 if (!this.perfStatsEnabled) return; 499 let renderStats = this.panelPerfStats.get(panel); 500 if (renderStats === undefined) { 501 renderStats = new PerfStats(); 502 this.panelPerfStats.set(panel, renderStats); 503 } 504 renderStats.addValue(renderTime); 505 506 // Draw a green box around the whole panel 507 ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)'; 508 const lineWidth = 1; 509 ctx.lineWidth = lineWidth; 510 ctx.strokeRect( 511 lineWidth / 2, 512 lineWidth / 2, 513 size.width - lineWidth, 514 size.height - lineWidth, 515 ); 516 517 const statW = 300; 518 ctx.fillStyle = 'hsl(97, 100%, 96%)'; 519 ctx.fillRect(size.width - statW, size.height - 20, statW, 20); 520 ctx.fillStyle = 'hsla(122, 77%, 22%)'; 521 const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats); 522 ctx.fillText(statStr, size.width - statW, size.height - 10); 523 } 524 525 private updatePerfStats( 526 renderTime: number, 527 totalPanels: number, 528 panelsOnCanvas: number, 529 ) { 530 if (!this.perfStatsEnabled) return; 531 this.perfStats.renderStats.addValue(renderTime); 532 this.perfStats.totalPanels = totalPanels; 533 this.perfStats.panelsOnCanvas = panelsOnCanvas; 534 } 535 536 setPerfStatsEnabled(enable: boolean): void { 537 this.perfStatsEnabled = enable; 538 } 539 540 renderPerfStats() { 541 return [ 542 m( 543 'div', 544 `${this.perfStats.totalPanels} panels, ` + 545 `${this.perfStats.panelsOnCanvas} on canvas.`, 546 ), 547 m('div', runningStatStr(this.perfStats.renderStats)), 548 ]; 549 } 550} 551