• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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