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