blob: 86984222344bd6382325c1876e47fddc9433c640 [file] [log] [blame]
Deepanjan Royabd79aa2018-08-28 07:29:15 -04001// 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 * as m from 'mithril';
Isabelle Taylora16dec22019-12-03 16:34:13 +000016import {TimestampedAreaSelection} from 'src/common/state';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040017
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040018import {assertExists, assertTrue} from '../base/logging';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040019
Isabelle Taylora16dec22019-12-03 16:34:13 +000020import {TOPBAR_HEIGHT, TRACK_SHELL_WIDTH} from './css_constants';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040021import {globals} from './globals';
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040022import {isPanelVNode, Panel, PanelSize} from './panel';
23import {
24 debugNow,
25 perfDebug,
26 perfDisplay,
27 RunningStatistics,
28 runningStatStr
29} from './perf';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040030
31/**
32 * If the panel container scrolls, the backing canvas height is
33 * SCROLLING_CANVAS_OVERDRAW_FACTOR * parent container height.
34 */
Hector Dearman0d825912019-02-15 10:33:37 +000035const SCROLLING_CANVAS_OVERDRAW_FACTOR = 1.2;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040036
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040037// We need any here so we can accept vnodes with arbitrary attrs.
38// tslint:disable-next-line:no-any
39export type AnyAttrsVnode = m.Vnode<any, {}>;
40
Isabelle Taylora16dec22019-12-03 16:34:13 +000041export interface Attrs {
Deepanjan Roy4fa0ecc2018-10-03 16:10:46 -040042 panels: AnyAttrsVnode[];
Deepanjan Royabd79aa2018-08-28 07:29:15 -040043 doesScroll: boolean;
Isabelle Taylorf41b7c72019-07-24 13:55:43 +010044 kind: 'TRACKS'|'OVERVIEW'|'DETAILS';
Deepanjan Royabd79aa2018-08-28 07:29:15 -040045}
46
Isabelle Taylora16dec22019-12-03 16:34:13 +000047interface PanelPosition {
48 height: number;
49 width: number;
50 x: number;
51 y: number;
52}
53
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040054export class PanelContainer implements m.ClassComponent<Attrs> {
55 // These values are updated with proper values in oncreate.
56 private parentWidth = 0;
57 private parentHeight = 0;
58 private scrollTop = 0;
Isabelle Taylora16dec22019-12-03 16:34:13 +000059 private panelPositions: PanelPosition[] = [];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040060 private totalPanelHeight = 0;
61 private canvasHeight = 0;
Isabelle Taylora16dec22019-12-03 16:34:13 +000062 private prevAreaSelection?: TimestampedAreaSelection;
Deepanjan Royabd79aa2018-08-28 07:29:15 -040063
Deepanjan Roy9a906ed2018-09-20 11:06:00 -040064 private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
65 private perfStats = {
66 totalPanels: 0,
67 panelsOnCanvas: 0,
68 renderStats: new RunningStatistics(10),
69 };
70
Deepanjan Roy1a225c22018-10-04 07:33:02 -040071 // Attrs received in the most recent mithril redraw. We receive a new vnode
72 // with new attrs on every redraw, and we cache it here so that resize
73 // listeners and canvas redraw callbacks can access it.
74 private attrs: Attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040075
Deepanjan Roy1f658fe2018-09-11 08:38:17 -040076 private ctx?: CanvasRenderingContext2D;
77
78 private onResize: () => void = () => {};
79 private parentOnScroll: () => void = () => {};
80 private canvasRedrawer: () => void;
81
Deepanjan Roy1a225c22018-10-04 07:33:02 -040082 get canvasOverdrawFactor() {
83 return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
84 }
85
Isabelle Taylora16dec22019-12-03 16:34:13 +000086 getPanelsInRegion(startX: number, endX: number, startY: number, endY: number):
87 AnyAttrsVnode[] {
88 const minX = Math.min(startX, endX);
89 const maxX = Math.max(startX, endX);
90 const minY = Math.min(startY, endY);
91 const maxY = Math.max(startY, endY);
92 const panels: AnyAttrsVnode[] = [];
93 for (let i = 0; i < this.panelPositions.length; i++) {
94 const pos = this.panelPositions[i];
95 const realPosX = pos.x - TRACK_SHELL_WIDTH;
96 if (realPosX + pos.width >= minX && realPosX <= maxX &&
97 pos.y + pos.height >= minY && pos.y <= maxY) {
98 panels.push(this.attrs.panels[i]);
99 }
100 }
101 return panels;
102 }
103
104 handleAreaSelection() {
105 const selection = globals.frontendLocalState.selectedArea;
106 const area = selection.area;
107 if ((this.prevAreaSelection &&
108 this.prevAreaSelection.lastUpdate >= selection.lastUpdate) ||
109 area === undefined ||
110 globals.frontendLocalState.areaY.start === undefined ||
111 globals.frontendLocalState.areaY.end === undefined) {
112 return;
113 }
114 // The Y value is given from the top of the pan and zoom region, we want it
115 // from the top of the panel container. The parent offset corrects that.
116 const panels = this.getPanelsInRegion(
117 globals.frontendLocalState.timeScale.timeToPx(area.startSec),
118 globals.frontendLocalState.timeScale.timeToPx(area.endSec),
119 globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT,
120 globals.frontendLocalState.areaY.end + TOPBAR_HEIGHT);
121 // Get the track ids from the panels.
122 const tracks = [];
123 for (const panel of panels) {
124 if (panel.attrs.id !== undefined) {
125 tracks.push(panel.attrs.id);
126 continue;
127 }
128 if (panel.attrs.trackGroupId !== undefined) {
129 const trackGroup = globals.state.trackGroups[panel.attrs.trackGroupId];
130 // Only select a track group and all child tracks if it is closed.
131 if (trackGroup.collapsed) {
132 tracks.push(panel.attrs.trackGroupId);
133 for (const track of trackGroup.tracks) {
134 tracks.push(track);
135 }
136 }
137 }
138 }
139 globals.frontendLocalState.selectArea(area.startSec, area.endSec, tracks);
140 this.prevAreaSelection = globals.frontendLocalState.selectedArea;
141 }
142
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400143 constructor(vnode: m.CVnode<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400144 this.attrs = vnode.attrs;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400145 this.canvasRedrawer = () => this.redrawCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400146 globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400147 perfDisplay.addContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400148 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400149
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400150 oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400151 // Save the canvas context in the state.
152 const canvas =
153 vnodeDom.dom.querySelector('.main-canvas') as HTMLCanvasElement;
154 const ctx = canvas.getContext('2d');
155 if (!ctx) {
156 throw Error('Cannot create canvas context');
157 }
158 this.ctx = ctx;
159
Isabelle Taylora16dec22019-12-03 16:34:13 +0000160 this.readParentSizeFromDom(vnodeDom.dom);
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400161 this.readPanelHeightsFromDom(vnodeDom.dom);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400162
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400163 this.updateCanvasDimensions();
164 this.repositionCanvas();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400165
166 // Save the resize handler in the state so we can remove it later.
167 // TODO: Encapsulate resize handling better.
168 this.onResize = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400169 this.readParentSizeFromDom(vnodeDom.dom);
170 this.updateCanvasDimensions();
171 this.repositionCanvas();
Deepanjan Roy112ff6a2018-09-10 08:31:43 -0400172 globals.rafScheduler.scheduleFullRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400173 };
174
175 // Once ResizeObservers are out, we can stop accessing the window here.
176 window.addEventListener('resize', this.onResize);
177
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400178 // TODO(dproy): Handle change in doesScroll attribute.
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400179 if (this.attrs.doesScroll) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400180 this.parentOnScroll = () => {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400181 this.scrollTop = assertExists(vnodeDom.dom.parentElement).scrollTop;
182 this.repositionCanvas();
Deepanjan Royf190cb22018-08-28 10:43:07 -0400183 globals.rafScheduler.scheduleRedraw();
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400184 };
185 vnodeDom.dom.parentElement!.addEventListener(
186 'scroll', this.parentOnScroll, {passive: true});
187 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400188 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400189
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400190 onremove({attrs, dom}: m.CVnodeDOM<Attrs>) {
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400191 window.removeEventListener('resize', this.onResize);
192 globals.rafScheduler.removeRedrawCallback(this.canvasRedrawer);
193 if (attrs.doesScroll) {
194 dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
195 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400196 perfDisplay.removeContainer(this);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400197 }
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400198
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400199 view({attrs}: m.CVnode<Attrs>) {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400200 this.attrs = attrs;
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400201 const renderPanel = (panel: m.Vnode) => perfDebug() ?
202 m('.panel', panel, m('.debug-panel-border')) :
Isabelle Taylor1acfa2b2019-05-17 17:26:40 +0100203 m('.panel', {key: panel.key}, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400204
Isabelle Taylorf4e0f682019-11-20 10:47:49 +0000205 return [
206 m(
207 '.scroll-limiter',
208 m('canvas.main-canvas'),
209 ),
210 m('.panels', attrs.panels.map(renderPanel))
211 ];
Deepanjan Royabd79aa2018-08-28 07:29:15 -0400212 }
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400213
214 onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400215 const totalPanelHeightChanged = this.readPanelHeightsFromDom(vnodeDom.dom);
216 const parentSizeChanged = this.readParentSizeFromDom(vnodeDom.dom);
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400217 const canvasSizeShouldChange =
Isabelle Taylorf41b7c72019-07-24 13:55:43 +0100218 parentSizeChanged || !this.attrs.doesScroll && totalPanelHeightChanged;
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400219 if (canvasSizeShouldChange) {
220 this.updateCanvasDimensions();
221 this.repositionCanvas();
Isabelle Taylorf41b7c72019-07-24 13:55:43 +0100222 if (this.attrs.kind === 'TRACKS') {
223 globals.frontendLocalState.timeScale.setLimitsPx(
224 0, this.parentWidth - TRACK_SHELL_WIDTH);
225 }
226 this.redrawCanvas();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400227 }
228 }
229
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400230 private updateCanvasDimensions() {
Hector Dearmanafa51b22018-12-11 16:38:30 +0000231 this.canvasHeight = Math.floor(
232 this.attrs.doesScroll ? this.parentHeight * this.canvasOverdrawFactor :
233 this.totalPanelHeight);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400234 const ctx = assertExists(this.ctx);
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400235 const canvas = assertExists(ctx.canvas);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400236 canvas.style.height = `${this.canvasHeight}px`;
Hector Dearman4f8ea6a2020-01-17 14:52:34 +0000237
238 // If're we're non-scrolling canvas and the scroll-limiter should always
239 // have the same height. Enforce this by explicitly setting the height.
240 if (!this.attrs.doesScroll) {
241 const scrollLimiter = canvas.parentElement;
242 if (scrollLimiter) {
243 scrollLimiter.style.height = `${this.canvasHeight}px`;
244 }
245 }
246
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400247 const dpr = window.devicePixelRatio;
Isabelle Tayloref902422020-01-24 10:35:55 +0000248 ctx.canvas.width = this.parentWidth * dpr;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400249 ctx.canvas.height = this.canvasHeight * dpr;
250 ctx.scale(dpr, dpr);
251 }
252
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400253 private repositionCanvas() {
254 const canvas = assertExists(assertExists(this.ctx).canvas);
Hector Dearmanafa51b22018-12-11 16:38:30 +0000255 const canvasYStart =
256 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400257 canvas.style.transform = `translateY(${canvasYStart}px)`;
258 }
259
260 /**
261 * Reads dimensions of parent node. Returns true if read dimensions are
262 * different from what was cached in the state.
263 */
264 private readParentSizeFromDom(dom: Element): boolean {
265 const oldWidth = this.parentWidth;
266 const oldHeight = this.parentHeight;
267 const clientRect = assertExists(dom.parentElement).getBoundingClientRect();
Isabelle Tayloref902422020-01-24 10:35:55 +0000268 // On non-MacOS if there is a solid scroll bar it can cover important
269 // pixels, reduce the size of the canvas so it doesn't overlap with
270 // the scroll bar.
271 this.parentWidth =
272 clientRect.width - globals.frontendLocalState.getScrollbarWidth();
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400273 this.parentHeight = clientRect.height;
274 return this.parentHeight !== oldHeight || this.parentWidth !== oldWidth;
275 }
276
277 /**
278 * Reads dimensions of panels. Returns true if total panel height is different
279 * from what was cached in state.
280 */
281 private readPanelHeightsFromDom(dom: Element): boolean {
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400282 const prevHeight = this.totalPanelHeight;
Isabelle Taylora16dec22019-12-03 16:34:13 +0000283 this.panelPositions = [];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400284 this.totalPanelHeight = 0;
285
Isabelle Taylorf4e0f682019-11-20 10:47:49 +0000286 const panels = dom.parentElement!.querySelectorAll('.panel');
Deepanjan Roy1a225c22018-10-04 07:33:02 -0400287 assertTrue(panels.length === this.attrs.panels.length);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400288 for (let i = 0; i < panels.length; i++) {
Isabelle Taylora16dec22019-12-03 16:34:13 +0000289 const rect = panels[i].getBoundingClientRect() as DOMRect;
290 this.panelPositions[i] =
291 {height: rect.height, width: rect.width, x: rect.x, y: rect.y};
292 this.totalPanelHeight += rect.height;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400293 }
294
295 return this.totalPanelHeight !== prevHeight;
296 }
297
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400298 private overlapsCanvas(yStart: number, yEnd: number) {
299 return yEnd > 0 && yStart < this.canvasHeight;
300 }
301
302 private redrawCanvas() {
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400303 const redrawStart = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400304 if (!this.ctx) return;
305 this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
Hector Dearmanf74cf222018-10-31 16:22:29 +0000306 const canvasYStart =
Hector Dearmanafa51b22018-12-11 16:38:30 +0000307 Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400308
Isabelle Taylora16dec22019-12-03 16:34:13 +0000309 if (this.attrs.kind === 'TRACKS') {
310 this.handleAreaSelection();
311 }
312
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400313 let panelYStart = 0;
314 const panels = assertExists(this.attrs).panels;
Isabelle Taylora16dec22019-12-03 16:34:13 +0000315 assertTrue(panels.length === this.panelPositions.length);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400316 let totalOnCanvas = 0;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400317 for (let i = 0; i < panels.length; i++) {
318 const panel = panels[i];
Isabelle Taylora16dec22019-12-03 16:34:13 +0000319 const panelHeight = this.panelPositions[i].height;
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400320 const yStartOnCanvas = panelYStart - canvasYStart;
321
322 if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
323 panelYStart += panelHeight;
324 continue;
325 }
326
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400327 totalOnCanvas++;
328
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400329 if (!isPanelVNode(panel)) {
330 throw Error('Vnode passed to panel container is not a panel');
331 }
332
333 this.ctx.save();
334 this.ctx.translate(0, yStartOnCanvas);
335 const clipRect = new Path2D();
336 const size = {width: this.parentWidth, height: panelHeight};
337 clipRect.rect(0, 0, size.width, size.height);
338 this.ctx.clip(clipRect);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400339 const beforeRender = debugNow();
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400340 panel.state.renderCanvas(this.ctx, size, panel);
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400341 this.updatePanelStats(
342 i, panel.state, debugNow() - beforeRender, this.ctx, size);
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400343 this.ctx.restore();
344 panelYStart += panelHeight;
345 }
Deepanjan Roy9a906ed2018-09-20 11:06:00 -0400346 const redrawDur = debugNow() - redrawStart;
347 this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
348 }
349
350 private updatePanelStats(
351 panelIndex: number, panel: Panel, renderTime: number,
352 ctx: CanvasRenderingContext2D, size: PanelSize) {
353 if (!perfDebug()) return;
354 let renderStats = this.panelPerfStats.get(panel);
355 if (renderStats === undefined) {
356 renderStats = new RunningStatistics();
357 this.panelPerfStats.set(panel, renderStats);
358 }
359 renderStats.addValue(renderTime);
360
361 const statW = 300;
362 ctx.fillStyle = 'hsl(97, 100%, 96%)';
363 ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
364 ctx.fillStyle = 'hsla(122, 77%, 22%)';
365 const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
366 ctx.fillText(statStr, size.width - statW, size.height - 10);
367 }
368
369 private updatePerfStats(
370 renderTime: number, totalPanels: number, panelsOnCanvas: number) {
371 if (!perfDebug()) return;
372 this.perfStats.renderStats.addValue(renderTime);
373 this.perfStats.totalPanels = totalPanels;
374 this.perfStats.panelsOnCanvas = panelsOnCanvas;
375 }
376
377 renderPerfStats(index: number) {
378 assertTrue(perfDebug());
379 return [m(
380 'section',
381 m('div', `Panel Container ${index + 1}`),
382 m('div',
383 `${this.perfStats.totalPanels} panels, ` +
384 `${this.perfStats.panelsOnCanvas} on canvas.`),
385 m('div', runningStatStr(this.perfStats.renderStats)), )];
Deepanjan Roy1f658fe2018-09-11 08:38:17 -0400386 }
387
388 private getCanvasOverdrawHeightPerSide() {
389 const overdrawHeight = (this.canvasOverdrawFactor - 1) * this.parentHeight;
390 return overdrawHeight / 2;
391 }
392}