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