xref: /aosp_15_r20/external/perfetto/ui/src/widgets/flamegraph.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2024 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} from '../base/dom_utils';
17*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists, assertTrue} from '../base/logging';
18*6dbdd20aSAndroid Build Coastguard Workerimport {Monitor} from '../base/monitor';
19*6dbdd20aSAndroid Build Coastguard Workerimport {Button, ButtonBar} from './button';
20*6dbdd20aSAndroid Build Coastguard Workerimport {EmptyState} from './empty_state';
21*6dbdd20aSAndroid Build Coastguard Workerimport {Popup, PopupPosition} from './popup';
22*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf';
23*6dbdd20aSAndroid Build Coastguard Workerimport {Select} from './select';
24*6dbdd20aSAndroid Build Coastguard Workerimport {Spinner} from './spinner';
25*6dbdd20aSAndroid Build Coastguard Workerimport {TagInput} from './tag_input';
26*6dbdd20aSAndroid Build Coastguard Workerimport {SegmentedButtons} from './segmented_buttons';
27*6dbdd20aSAndroid Build Coastguard Workerimport {z} from 'zod';
28*6dbdd20aSAndroid Build Coastguard Worker
29*6dbdd20aSAndroid Build Coastguard Workerconst LABEL_FONT_STYLE = '12px Roboto';
30*6dbdd20aSAndroid Build Coastguard Workerconst NODE_HEIGHT = 20;
31*6dbdd20aSAndroid Build Coastguard Workerconst MIN_PIXEL_DISPLAYED = 3;
32*6dbdd20aSAndroid Build Coastguard Workerconst FILTER_COMMON_TEXT = `
33*6dbdd20aSAndroid Build Coastguard Worker- "Show Stack: foo" or "SS: foo" or "foo" to show only stacks containing "foo"
34*6dbdd20aSAndroid Build Coastguard Worker- "Hide Stack: foo" or "HS: foo" to hide all stacks containing "foo"
35*6dbdd20aSAndroid Build Coastguard Worker- "Show From Frame: foo" or "SFF: foo" to show frames containing "foo" and all descendants
36*6dbdd20aSAndroid Build Coastguard Worker- "Hide Frame: foo" or "HF: foo" to hide all frames containing "foo"
37*6dbdd20aSAndroid Build Coastguard Worker- "Pivot: foo" or "P: foo" to pivot on frames containing "foo".
38*6dbdd20aSAndroid Build Coastguard WorkerNote: Pivot applies after all other filters and only one pivot can be active at a time.
39*6dbdd20aSAndroid Build Coastguard Worker`;
40*6dbdd20aSAndroid Build Coastguard Workerconst FILTER_EMPTY_TEXT = `
41*6dbdd20aSAndroid Build Coastguard WorkerAvailable filters:${FILTER_COMMON_TEXT}
42*6dbdd20aSAndroid Build Coastguard Worker`;
43*6dbdd20aSAndroid Build Coastguard Workerconst LABEL_PADDING_PX = 5;
44*6dbdd20aSAndroid Build Coastguard Workerconst LABEL_MIN_WIDTH_FOR_TEXT_PX = 5;
45*6dbdd20aSAndroid Build Coastguard Workerconst PADDING_NODE_COUNT = 8;
46*6dbdd20aSAndroid Build Coastguard Worker
47*6dbdd20aSAndroid Build Coastguard Workerinterface BaseSource {
48*6dbdd20aSAndroid Build Coastguard Worker  readonly queryXStart: number;
49*6dbdd20aSAndroid Build Coastguard Worker  readonly queryXEnd: number;
50*6dbdd20aSAndroid Build Coastguard Worker  readonly type: 'ABOVE_ROOT' | 'BELOW_ROOT' | 'ROOT';
51*6dbdd20aSAndroid Build Coastguard Worker}
52*6dbdd20aSAndroid Build Coastguard Worker
53*6dbdd20aSAndroid Build Coastguard Workerinterface MergedSource extends BaseSource {
54*6dbdd20aSAndroid Build Coastguard Worker  readonly kind: 'MERGED';
55*6dbdd20aSAndroid Build Coastguard Worker}
56*6dbdd20aSAndroid Build Coastguard Worker
57*6dbdd20aSAndroid Build Coastguard Workerinterface RootSource extends BaseSource {
58*6dbdd20aSAndroid Build Coastguard Worker  readonly kind: 'ROOT';
59*6dbdd20aSAndroid Build Coastguard Worker}
60*6dbdd20aSAndroid Build Coastguard Worker
61*6dbdd20aSAndroid Build Coastguard Workerinterface NodeSource extends BaseSource {
62*6dbdd20aSAndroid Build Coastguard Worker  readonly kind: 'NODE';
63*6dbdd20aSAndroid Build Coastguard Worker  readonly queryIdx: number;
64*6dbdd20aSAndroid Build Coastguard Worker}
65*6dbdd20aSAndroid Build Coastguard Worker
66*6dbdd20aSAndroid Build Coastguard Workertype Source = MergedSource | NodeSource | RootSource;
67*6dbdd20aSAndroid Build Coastguard Worker
68*6dbdd20aSAndroid Build Coastguard Workerinterface RenderNode {
69*6dbdd20aSAndroid Build Coastguard Worker  readonly x: number;
70*6dbdd20aSAndroid Build Coastguard Worker  readonly y: number;
71*6dbdd20aSAndroid Build Coastguard Worker  readonly width: number;
72*6dbdd20aSAndroid Build Coastguard Worker  readonly source: Source;
73*6dbdd20aSAndroid Build Coastguard Worker  readonly state: 'NORMAL' | 'PARTIAL' | 'SELECTED';
74*6dbdd20aSAndroid Build Coastguard Worker}
75*6dbdd20aSAndroid Build Coastguard Worker
76*6dbdd20aSAndroid Build Coastguard Workerinterface ZoomRegion {
77*6dbdd20aSAndroid Build Coastguard Worker  readonly queryXStart: number;
78*6dbdd20aSAndroid Build Coastguard Worker  readonly queryXEnd: number;
79*6dbdd20aSAndroid Build Coastguard Worker  readonly type: 'ABOVE_ROOT' | 'BELOW_ROOT' | 'ROOT';
80*6dbdd20aSAndroid Build Coastguard Worker}
81*6dbdd20aSAndroid Build Coastguard Worker
82*6dbdd20aSAndroid Build Coastguard Workerexport interface FlamegraphQueryData {
83*6dbdd20aSAndroid Build Coastguard Worker  readonly nodes: ReadonlyArray<{
84*6dbdd20aSAndroid Build Coastguard Worker    readonly id: number;
85*6dbdd20aSAndroid Build Coastguard Worker    readonly parentId: number;
86*6dbdd20aSAndroid Build Coastguard Worker    readonly depth: number;
87*6dbdd20aSAndroid Build Coastguard Worker    readonly name: string;
88*6dbdd20aSAndroid Build Coastguard Worker    readonly selfValue: number;
89*6dbdd20aSAndroid Build Coastguard Worker    readonly cumulativeValue: number;
90*6dbdd20aSAndroid Build Coastguard Worker    readonly parentCumulativeValue?: number;
91*6dbdd20aSAndroid Build Coastguard Worker    readonly properties: ReadonlyMap<string, string>;
92*6dbdd20aSAndroid Build Coastguard Worker    readonly xStart: number;
93*6dbdd20aSAndroid Build Coastguard Worker    readonly xEnd: number;
94*6dbdd20aSAndroid Build Coastguard Worker  }>;
95*6dbdd20aSAndroid Build Coastguard Worker  readonly unfilteredCumulativeValue: number;
96*6dbdd20aSAndroid Build Coastguard Worker  readonly allRootsCumulativeValue: number;
97*6dbdd20aSAndroid Build Coastguard Worker  readonly minDepth: number;
98*6dbdd20aSAndroid Build Coastguard Worker  readonly maxDepth: number;
99*6dbdd20aSAndroid Build Coastguard Worker}
100*6dbdd20aSAndroid Build Coastguard Worker
101*6dbdd20aSAndroid Build Coastguard Workerconst FLAMEGRAPH_FILTER_SCHEMA = z
102*6dbdd20aSAndroid Build Coastguard Worker  .object({
103*6dbdd20aSAndroid Build Coastguard Worker    kind: z
104*6dbdd20aSAndroid Build Coastguard Worker      .union([
105*6dbdd20aSAndroid Build Coastguard Worker        z.literal('SHOW_STACK').readonly(),
106*6dbdd20aSAndroid Build Coastguard Worker        z.literal('HIDE_STACK').readonly(),
107*6dbdd20aSAndroid Build Coastguard Worker        z.literal('SHOW_FROM_FRAME').readonly(),
108*6dbdd20aSAndroid Build Coastguard Worker        z.literal('HIDE_FRAME').readonly(),
109*6dbdd20aSAndroid Build Coastguard Worker      ])
110*6dbdd20aSAndroid Build Coastguard Worker      .readonly(),
111*6dbdd20aSAndroid Build Coastguard Worker    filter: z.string().readonly(),
112*6dbdd20aSAndroid Build Coastguard Worker  })
113*6dbdd20aSAndroid Build Coastguard Worker  .readonly();
114*6dbdd20aSAndroid Build Coastguard Worker
115*6dbdd20aSAndroid Build Coastguard Workertype FlamegraphFilter = z.infer<typeof FLAMEGRAPH_FILTER_SCHEMA>;
116*6dbdd20aSAndroid Build Coastguard Worker
117*6dbdd20aSAndroid Build Coastguard Workerconst FLAMEGRAPH_VIEW_SCHEMA = z
118*6dbdd20aSAndroid Build Coastguard Worker  .discriminatedUnion('kind', [
119*6dbdd20aSAndroid Build Coastguard Worker    z.object({kind: z.literal('TOP_DOWN').readonly()}),
120*6dbdd20aSAndroid Build Coastguard Worker    z.object({kind: z.literal('BOTTOM_UP').readonly()}),
121*6dbdd20aSAndroid Build Coastguard Worker    z.object({
122*6dbdd20aSAndroid Build Coastguard Worker      kind: z.literal('PIVOT').readonly(),
123*6dbdd20aSAndroid Build Coastguard Worker      pivot: z.string().readonly(),
124*6dbdd20aSAndroid Build Coastguard Worker    }),
125*6dbdd20aSAndroid Build Coastguard Worker  ])
126*6dbdd20aSAndroid Build Coastguard Worker  .readonly();
127*6dbdd20aSAndroid Build Coastguard Worker
128*6dbdd20aSAndroid Build Coastguard Workerexport type FlamegraphView = z.infer<typeof FLAMEGRAPH_VIEW_SCHEMA>;
129*6dbdd20aSAndroid Build Coastguard Worker
130*6dbdd20aSAndroid Build Coastguard Workerexport const FLAMEGRAPH_STATE_SCHEMA = z
131*6dbdd20aSAndroid Build Coastguard Worker  .object({
132*6dbdd20aSAndroid Build Coastguard Worker    selectedMetricName: z.string().readonly(),
133*6dbdd20aSAndroid Build Coastguard Worker    filters: z.array(FLAMEGRAPH_FILTER_SCHEMA).readonly(),
134*6dbdd20aSAndroid Build Coastguard Worker    view: FLAMEGRAPH_VIEW_SCHEMA,
135*6dbdd20aSAndroid Build Coastguard Worker  })
136*6dbdd20aSAndroid Build Coastguard Worker  .readonly();
137*6dbdd20aSAndroid Build Coastguard Worker
138*6dbdd20aSAndroid Build Coastguard Workerexport type FlamegraphState = z.infer<typeof FLAMEGRAPH_STATE_SCHEMA>;
139*6dbdd20aSAndroid Build Coastguard Worker
140*6dbdd20aSAndroid Build Coastguard Workerinterface FlamegraphMetric {
141*6dbdd20aSAndroid Build Coastguard Worker  readonly name: string;
142*6dbdd20aSAndroid Build Coastguard Worker  readonly unit: string;
143*6dbdd20aSAndroid Build Coastguard Worker}
144*6dbdd20aSAndroid Build Coastguard Worker
145*6dbdd20aSAndroid Build Coastguard Workerexport interface FlamegraphAttrs {
146*6dbdd20aSAndroid Build Coastguard Worker  readonly metrics: ReadonlyArray<FlamegraphMetric>;
147*6dbdd20aSAndroid Build Coastguard Worker  readonly state: FlamegraphState;
148*6dbdd20aSAndroid Build Coastguard Worker  readonly data: FlamegraphQueryData | undefined;
149*6dbdd20aSAndroid Build Coastguard Worker
150*6dbdd20aSAndroid Build Coastguard Worker  readonly onStateChange: (filters: FlamegraphState) => void;
151*6dbdd20aSAndroid Build Coastguard Worker}
152*6dbdd20aSAndroid Build Coastguard Worker
153*6dbdd20aSAndroid Build Coastguard Worker/*
154*6dbdd20aSAndroid Build Coastguard Worker * Widget for visualizing "tree-like" data structures using an interactive
155*6dbdd20aSAndroid Build Coastguard Worker * flamegraph visualization.
156*6dbdd20aSAndroid Build Coastguard Worker *
157*6dbdd20aSAndroid Build Coastguard Worker * To use this widget, provide an array of "metrics", which correspond to
158*6dbdd20aSAndroid Build Coastguard Worker * different properties of the tree to switch between (e.g. object size
159*6dbdd20aSAndroid Build Coastguard Worker * and object count) and the data which should be displayed.
160*6dbdd20aSAndroid Build Coastguard Worker *
161*6dbdd20aSAndroid Build Coastguard Worker * Note that it's valid to pass "undefined" as the data: this will cause a
162*6dbdd20aSAndroid Build Coastguard Worker * loading container to be shown.
163*6dbdd20aSAndroid Build Coastguard Worker *
164*6dbdd20aSAndroid Build Coastguard Worker * Example:
165*6dbdd20aSAndroid Build Coastguard Worker *
166*6dbdd20aSAndroid Build Coastguard Worker * ```
167*6dbdd20aSAndroid Build Coastguard Worker * const metrics = [...];
168*6dbdd20aSAndroid Build Coastguard Worker * let state = ...;
169*6dbdd20aSAndroid Build Coastguard Worker * let data = ...;
170*6dbdd20aSAndroid Build Coastguard Worker *
171*6dbdd20aSAndroid Build Coastguard Worker * m(Flamegraph, {
172*6dbdd20aSAndroid Build Coastguard Worker *   metrics,
173*6dbdd20aSAndroid Build Coastguard Worker *   state,
174*6dbdd20aSAndroid Build Coastguard Worker *   data,
175*6dbdd20aSAndroid Build Coastguard Worker *   onStateChange: (newState) => {
176*6dbdd20aSAndroid Build Coastguard Worker *     state = newState,
177*6dbdd20aSAndroid Build Coastguard Worker *     data = undefined;
178*6dbdd20aSAndroid Build Coastguard Worker *     fetchData();
179*6dbdd20aSAndroid Build Coastguard Worker *   },
180*6dbdd20aSAndroid Build Coastguard Worker * });
181*6dbdd20aSAndroid Build Coastguard Worker * ```
182*6dbdd20aSAndroid Build Coastguard Worker */
183*6dbdd20aSAndroid Build Coastguard Workerexport class Flamegraph implements m.ClassComponent<FlamegraphAttrs> {
184*6dbdd20aSAndroid Build Coastguard Worker  private attrs: FlamegraphAttrs;
185*6dbdd20aSAndroid Build Coastguard Worker
186*6dbdd20aSAndroid Build Coastguard Worker  private rawFilterText: string = '';
187*6dbdd20aSAndroid Build Coastguard Worker  private filterFocus: boolean = false;
188*6dbdd20aSAndroid Build Coastguard Worker
189*6dbdd20aSAndroid Build Coastguard Worker  private dataChangeMonitor = new Monitor([() => this.attrs.data]);
190*6dbdd20aSAndroid Build Coastguard Worker  private zoomRegion?: ZoomRegion;
191*6dbdd20aSAndroid Build Coastguard Worker
192*6dbdd20aSAndroid Build Coastguard Worker  private renderNodesMonitor = new Monitor([
193*6dbdd20aSAndroid Build Coastguard Worker    () => this.attrs.data,
194*6dbdd20aSAndroid Build Coastguard Worker    () => this.canvasWidth,
195*6dbdd20aSAndroid Build Coastguard Worker    () => this.zoomRegion,
196*6dbdd20aSAndroid Build Coastguard Worker  ]);
197*6dbdd20aSAndroid Build Coastguard Worker  private renderNodes?: ReadonlyArray<RenderNode>;
198*6dbdd20aSAndroid Build Coastguard Worker
199*6dbdd20aSAndroid Build Coastguard Worker  private tooltipPos?: {
200*6dbdd20aSAndroid Build Coastguard Worker    node: RenderNode;
201*6dbdd20aSAndroid Build Coastguard Worker    x: number;
202*6dbdd20aSAndroid Build Coastguard Worker    state: 'HOVER' | 'CLICK' | 'DECLICK';
203*6dbdd20aSAndroid Build Coastguard Worker  };
204*6dbdd20aSAndroid Build Coastguard Worker  private lastClickedNode?: RenderNode;
205*6dbdd20aSAndroid Build Coastguard Worker
206*6dbdd20aSAndroid Build Coastguard Worker  private hoveredX?: number;
207*6dbdd20aSAndroid Build Coastguard Worker  private hoveredY?: number;
208*6dbdd20aSAndroid Build Coastguard Worker
209*6dbdd20aSAndroid Build Coastguard Worker  private canvasWidth = 0;
210*6dbdd20aSAndroid Build Coastguard Worker  private labelCharWidth = 0;
211*6dbdd20aSAndroid Build Coastguard Worker
212*6dbdd20aSAndroid Build Coastguard Worker  constructor({attrs}: m.Vnode<FlamegraphAttrs, {}>) {
213*6dbdd20aSAndroid Build Coastguard Worker    this.attrs = attrs;
214*6dbdd20aSAndroid Build Coastguard Worker  }
215*6dbdd20aSAndroid Build Coastguard Worker
216*6dbdd20aSAndroid Build Coastguard Worker  view({attrs}: m.Vnode<FlamegraphAttrs, this>): void | m.Children {
217*6dbdd20aSAndroid Build Coastguard Worker    this.attrs = attrs;
218*6dbdd20aSAndroid Build Coastguard Worker    if (this.dataChangeMonitor.ifStateChanged()) {
219*6dbdd20aSAndroid Build Coastguard Worker      this.zoomRegion = undefined;
220*6dbdd20aSAndroid Build Coastguard Worker      this.lastClickedNode = undefined;
221*6dbdd20aSAndroid Build Coastguard Worker      this.tooltipPos = undefined;
222*6dbdd20aSAndroid Build Coastguard Worker    }
223*6dbdd20aSAndroid Build Coastguard Worker    if (attrs.data === undefined) {
224*6dbdd20aSAndroid Build Coastguard Worker      return m(
225*6dbdd20aSAndroid Build Coastguard Worker        '.pf-flamegraph',
226*6dbdd20aSAndroid Build Coastguard Worker        this.renderFilterBar(attrs),
227*6dbdd20aSAndroid Build Coastguard Worker        m(
228*6dbdd20aSAndroid Build Coastguard Worker          '.loading-container',
229*6dbdd20aSAndroid Build Coastguard Worker          m(
230*6dbdd20aSAndroid Build Coastguard Worker            EmptyState,
231*6dbdd20aSAndroid Build Coastguard Worker            {
232*6dbdd20aSAndroid Build Coastguard Worker              icon: 'bar_chart',
233*6dbdd20aSAndroid Build Coastguard Worker              title: 'Computing graph ...',
234*6dbdd20aSAndroid Build Coastguard Worker              className: 'flamegraph-loading',
235*6dbdd20aSAndroid Build Coastguard Worker            },
236*6dbdd20aSAndroid Build Coastguard Worker            m(Spinner, {easing: true}),
237*6dbdd20aSAndroid Build Coastguard Worker          ),
238*6dbdd20aSAndroid Build Coastguard Worker        ),
239*6dbdd20aSAndroid Build Coastguard Worker      );
240*6dbdd20aSAndroid Build Coastguard Worker    }
241*6dbdd20aSAndroid Build Coastguard Worker    const {minDepth, maxDepth} = attrs.data;
242*6dbdd20aSAndroid Build Coastguard Worker    const canvasHeight =
243*6dbdd20aSAndroid Build Coastguard Worker      Math.max(maxDepth - minDepth + PADDING_NODE_COUNT, PADDING_NODE_COUNT) *
244*6dbdd20aSAndroid Build Coastguard Worker      NODE_HEIGHT;
245*6dbdd20aSAndroid Build Coastguard Worker    return m(
246*6dbdd20aSAndroid Build Coastguard Worker      '.pf-flamegraph',
247*6dbdd20aSAndroid Build Coastguard Worker      this.renderFilterBar(attrs),
248*6dbdd20aSAndroid Build Coastguard Worker      m(
249*6dbdd20aSAndroid Build Coastguard Worker        '.canvas-container[ref=canvas-container]',
250*6dbdd20aSAndroid Build Coastguard Worker        {
251*6dbdd20aSAndroid Build Coastguard Worker          onscroll: () => scheduleFullRedraw(),
252*6dbdd20aSAndroid Build Coastguard Worker        },
253*6dbdd20aSAndroid Build Coastguard Worker        m(
254*6dbdd20aSAndroid Build Coastguard Worker          Popup,
255*6dbdd20aSAndroid Build Coastguard Worker          {
256*6dbdd20aSAndroid Build Coastguard Worker            trigger: m('.popup-anchor', {
257*6dbdd20aSAndroid Build Coastguard Worker              style: {
258*6dbdd20aSAndroid Build Coastguard Worker                left: this.tooltipPos?.x + 'px',
259*6dbdd20aSAndroid Build Coastguard Worker                top: this.tooltipPos?.node.y + 'px',
260*6dbdd20aSAndroid Build Coastguard Worker              },
261*6dbdd20aSAndroid Build Coastguard Worker            }),
262*6dbdd20aSAndroid Build Coastguard Worker            position: PopupPosition.Bottom,
263*6dbdd20aSAndroid Build Coastguard Worker            isOpen:
264*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos?.state === 'HOVER' ||
265*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos?.state === 'CLICK',
266*6dbdd20aSAndroid Build Coastguard Worker            className: 'pf-flamegraph-tooltip-popup',
267*6dbdd20aSAndroid Build Coastguard Worker            offset: NODE_HEIGHT,
268*6dbdd20aSAndroid Build Coastguard Worker          },
269*6dbdd20aSAndroid Build Coastguard Worker          this.renderTooltip(),
270*6dbdd20aSAndroid Build Coastguard Worker        ),
271*6dbdd20aSAndroid Build Coastguard Worker        m(`canvas[ref=canvas]`, {
272*6dbdd20aSAndroid Build Coastguard Worker          style: `height:${canvasHeight}px; width:100%`,
273*6dbdd20aSAndroid Build Coastguard Worker          onmousemove: ({offsetX, offsetY}: MouseEvent) => {
274*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
275*6dbdd20aSAndroid Build Coastguard Worker            this.hoveredX = offsetX;
276*6dbdd20aSAndroid Build Coastguard Worker            this.hoveredY = offsetY;
277*6dbdd20aSAndroid Build Coastguard Worker            if (this.tooltipPos?.state === 'CLICK') {
278*6dbdd20aSAndroid Build Coastguard Worker              return;
279*6dbdd20aSAndroid Build Coastguard Worker            }
280*6dbdd20aSAndroid Build Coastguard Worker            const renderNode = this.renderNodes?.find((n) =>
281*6dbdd20aSAndroid Build Coastguard Worker              isIntersecting(offsetX, offsetY, n),
282*6dbdd20aSAndroid Build Coastguard Worker            );
283*6dbdd20aSAndroid Build Coastguard Worker            if (renderNode === undefined) {
284*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos = undefined;
285*6dbdd20aSAndroid Build Coastguard Worker              return;
286*6dbdd20aSAndroid Build Coastguard Worker            }
287*6dbdd20aSAndroid Build Coastguard Worker            if (
288*6dbdd20aSAndroid Build Coastguard Worker              isIntersecting(
289*6dbdd20aSAndroid Build Coastguard Worker                this.tooltipPos?.x,
290*6dbdd20aSAndroid Build Coastguard Worker                this.tooltipPos?.node.y,
291*6dbdd20aSAndroid Build Coastguard Worker                renderNode,
292*6dbdd20aSAndroid Build Coastguard Worker              )
293*6dbdd20aSAndroid Build Coastguard Worker            ) {
294*6dbdd20aSAndroid Build Coastguard Worker              return;
295*6dbdd20aSAndroid Build Coastguard Worker            }
296*6dbdd20aSAndroid Build Coastguard Worker            this.tooltipPos = {
297*6dbdd20aSAndroid Build Coastguard Worker              x: offsetX,
298*6dbdd20aSAndroid Build Coastguard Worker              node: renderNode,
299*6dbdd20aSAndroid Build Coastguard Worker              state: 'HOVER',
300*6dbdd20aSAndroid Build Coastguard Worker            };
301*6dbdd20aSAndroid Build Coastguard Worker          },
302*6dbdd20aSAndroid Build Coastguard Worker          onmouseout: () => {
303*6dbdd20aSAndroid Build Coastguard Worker            this.hoveredX = undefined;
304*6dbdd20aSAndroid Build Coastguard Worker            this.hoveredY = undefined;
305*6dbdd20aSAndroid Build Coastguard Worker            document.body.style.cursor = 'default';
306*6dbdd20aSAndroid Build Coastguard Worker            if (
307*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos?.state === 'HOVER' ||
308*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos?.state === 'DECLICK'
309*6dbdd20aSAndroid Build Coastguard Worker            ) {
310*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos = undefined;
311*6dbdd20aSAndroid Build Coastguard Worker            }
312*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
313*6dbdd20aSAndroid Build Coastguard Worker          },
314*6dbdd20aSAndroid Build Coastguard Worker          onclick: ({offsetX, offsetY}: MouseEvent) => {
315*6dbdd20aSAndroid Build Coastguard Worker            const renderNode = this.renderNodes?.find((n) =>
316*6dbdd20aSAndroid Build Coastguard Worker              isIntersecting(offsetX, offsetY, n),
317*6dbdd20aSAndroid Build Coastguard Worker            );
318*6dbdd20aSAndroid Build Coastguard Worker            this.lastClickedNode = renderNode;
319*6dbdd20aSAndroid Build Coastguard Worker            if (renderNode === undefined) {
320*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos = undefined;
321*6dbdd20aSAndroid Build Coastguard Worker            } else if (
322*6dbdd20aSAndroid Build Coastguard Worker              isIntersecting(
323*6dbdd20aSAndroid Build Coastguard Worker                this.tooltipPos?.x,
324*6dbdd20aSAndroid Build Coastguard Worker                this.tooltipPos?.node.y,
325*6dbdd20aSAndroid Build Coastguard Worker                renderNode,
326*6dbdd20aSAndroid Build Coastguard Worker              )
327*6dbdd20aSAndroid Build Coastguard Worker            ) {
328*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos!.state =
329*6dbdd20aSAndroid Build Coastguard Worker                this.tooltipPos?.state === 'CLICK' ? 'DECLICK' : 'CLICK';
330*6dbdd20aSAndroid Build Coastguard Worker            } else {
331*6dbdd20aSAndroid Build Coastguard Worker              this.tooltipPos = {
332*6dbdd20aSAndroid Build Coastguard Worker                x: offsetX,
333*6dbdd20aSAndroid Build Coastguard Worker                node: renderNode,
334*6dbdd20aSAndroid Build Coastguard Worker                state: 'CLICK',
335*6dbdd20aSAndroid Build Coastguard Worker              };
336*6dbdd20aSAndroid Build Coastguard Worker            }
337*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
338*6dbdd20aSAndroid Build Coastguard Worker          },
339*6dbdd20aSAndroid Build Coastguard Worker          ondblclick: ({offsetX, offsetY}: MouseEvent) => {
340*6dbdd20aSAndroid Build Coastguard Worker            const renderNode = this.renderNodes?.find((n) =>
341*6dbdd20aSAndroid Build Coastguard Worker              isIntersecting(offsetX, offsetY, n),
342*6dbdd20aSAndroid Build Coastguard Worker            );
343*6dbdd20aSAndroid Build Coastguard Worker            // TODO(lalitm): ignore merged nodes for now as we haven't quite
344*6dbdd20aSAndroid Build Coastguard Worker            // figured out the UX for this.
345*6dbdd20aSAndroid Build Coastguard Worker            if (renderNode?.source.kind === 'MERGED') {
346*6dbdd20aSAndroid Build Coastguard Worker              return;
347*6dbdd20aSAndroid Build Coastguard Worker            }
348*6dbdd20aSAndroid Build Coastguard Worker            this.zoomRegion = renderNode?.source;
349*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
350*6dbdd20aSAndroid Build Coastguard Worker          },
351*6dbdd20aSAndroid Build Coastguard Worker        }),
352*6dbdd20aSAndroid Build Coastguard Worker      ),
353*6dbdd20aSAndroid Build Coastguard Worker    );
354*6dbdd20aSAndroid Build Coastguard Worker  }
355*6dbdd20aSAndroid Build Coastguard Worker
356*6dbdd20aSAndroid Build Coastguard Worker  oncreate({dom}: m.VnodeDOM<FlamegraphAttrs, this>) {
357*6dbdd20aSAndroid Build Coastguard Worker    this.drawCanvas(dom);
358*6dbdd20aSAndroid Build Coastguard Worker  }
359*6dbdd20aSAndroid Build Coastguard Worker
360*6dbdd20aSAndroid Build Coastguard Worker  onupdate({dom}: m.VnodeDOM<FlamegraphAttrs, this>) {
361*6dbdd20aSAndroid Build Coastguard Worker    this.drawCanvas(dom);
362*6dbdd20aSAndroid Build Coastguard Worker  }
363*6dbdd20aSAndroid Build Coastguard Worker
364*6dbdd20aSAndroid Build Coastguard Worker  static createDefaultState(
365*6dbdd20aSAndroid Build Coastguard Worker    metrics: ReadonlyArray<FlamegraphMetric>,
366*6dbdd20aSAndroid Build Coastguard Worker  ): FlamegraphState {
367*6dbdd20aSAndroid Build Coastguard Worker    return {
368*6dbdd20aSAndroid Build Coastguard Worker      selectedMetricName: metrics[0].name,
369*6dbdd20aSAndroid Build Coastguard Worker      filters: [],
370*6dbdd20aSAndroid Build Coastguard Worker      view: {kind: 'TOP_DOWN'},
371*6dbdd20aSAndroid Build Coastguard Worker    };
372*6dbdd20aSAndroid Build Coastguard Worker  }
373*6dbdd20aSAndroid Build Coastguard Worker
374*6dbdd20aSAndroid Build Coastguard Worker  private drawCanvas(dom: Element) {
375*6dbdd20aSAndroid Build Coastguard Worker    // TODO(lalitm): consider migrating to VirtualCanvas to improve performance here.
376*6dbdd20aSAndroid Build Coastguard Worker    const canvasContainer = findRef(dom, 'canvas-container');
377*6dbdd20aSAndroid Build Coastguard Worker    if (canvasContainer === null) {
378*6dbdd20aSAndroid Build Coastguard Worker      return;
379*6dbdd20aSAndroid Build Coastguard Worker    }
380*6dbdd20aSAndroid Build Coastguard Worker    const canvas = findRef(dom, 'canvas');
381*6dbdd20aSAndroid Build Coastguard Worker    if (canvas === null || !(canvas instanceof HTMLCanvasElement)) {
382*6dbdd20aSAndroid Build Coastguard Worker      return;
383*6dbdd20aSAndroid Build Coastguard Worker    }
384*6dbdd20aSAndroid Build Coastguard Worker    const ctx = canvas.getContext('2d');
385*6dbdd20aSAndroid Build Coastguard Worker    if (ctx === null) {
386*6dbdd20aSAndroid Build Coastguard Worker      return;
387*6dbdd20aSAndroid Build Coastguard Worker    }
388*6dbdd20aSAndroid Build Coastguard Worker    canvas.width = canvas.offsetWidth * devicePixelRatio;
389*6dbdd20aSAndroid Build Coastguard Worker    canvas.height = canvas.offsetHeight * devicePixelRatio;
390*6dbdd20aSAndroid Build Coastguard Worker    this.canvasWidth = canvas.offsetWidth;
391*6dbdd20aSAndroid Build Coastguard Worker
392*6dbdd20aSAndroid Build Coastguard Worker    if (this.renderNodesMonitor.ifStateChanged()) {
393*6dbdd20aSAndroid Build Coastguard Worker      if (this.attrs.data === undefined) {
394*6dbdd20aSAndroid Build Coastguard Worker        this.renderNodes = undefined;
395*6dbdd20aSAndroid Build Coastguard Worker        this.lastClickedNode = undefined;
396*6dbdd20aSAndroid Build Coastguard Worker      } else {
397*6dbdd20aSAndroid Build Coastguard Worker        this.renderNodes = computeRenderNodes(
398*6dbdd20aSAndroid Build Coastguard Worker          this.attrs.data,
399*6dbdd20aSAndroid Build Coastguard Worker          this.zoomRegion ?? {
400*6dbdd20aSAndroid Build Coastguard Worker            queryXStart: 0,
401*6dbdd20aSAndroid Build Coastguard Worker            queryXEnd: this.attrs.data.allRootsCumulativeValue,
402*6dbdd20aSAndroid Build Coastguard Worker            type: 'ROOT',
403*6dbdd20aSAndroid Build Coastguard Worker          },
404*6dbdd20aSAndroid Build Coastguard Worker          canvas.offsetWidth,
405*6dbdd20aSAndroid Build Coastguard Worker        );
406*6dbdd20aSAndroid Build Coastguard Worker        this.lastClickedNode = this.renderNodes?.find((n) =>
407*6dbdd20aSAndroid Build Coastguard Worker          isIntersecting(this.lastClickedNode?.x, this.lastClickedNode?.y, n),
408*6dbdd20aSAndroid Build Coastguard Worker        );
409*6dbdd20aSAndroid Build Coastguard Worker      }
410*6dbdd20aSAndroid Build Coastguard Worker      this.tooltipPos = undefined;
411*6dbdd20aSAndroid Build Coastguard Worker    }
412*6dbdd20aSAndroid Build Coastguard Worker    if (this.attrs.data === undefined || this.renderNodes === undefined) {
413*6dbdd20aSAndroid Build Coastguard Worker      return;
414*6dbdd20aSAndroid Build Coastguard Worker    }
415*6dbdd20aSAndroid Build Coastguard Worker
416*6dbdd20aSAndroid Build Coastguard Worker    const containerRect = canvasContainer.getBoundingClientRect();
417*6dbdd20aSAndroid Build Coastguard Worker    const canvasRect = canvas.getBoundingClientRect();
418*6dbdd20aSAndroid Build Coastguard Worker
419*6dbdd20aSAndroid Build Coastguard Worker    const yStart = containerRect.top - canvasRect.top;
420*6dbdd20aSAndroid Build Coastguard Worker    const yEnd = containerRect.bottom - canvasRect.top;
421*6dbdd20aSAndroid Build Coastguard Worker
422*6dbdd20aSAndroid Build Coastguard Worker    const {allRootsCumulativeValue, unfilteredCumulativeValue, nodes} =
423*6dbdd20aSAndroid Build Coastguard Worker      this.attrs.data;
424*6dbdd20aSAndroid Build Coastguard Worker    const unit = assertExists(this.selectedMetric).unit;
425*6dbdd20aSAndroid Build Coastguard Worker
426*6dbdd20aSAndroid Build Coastguard Worker    ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
427*6dbdd20aSAndroid Build Coastguard Worker    ctx.save();
428*6dbdd20aSAndroid Build Coastguard Worker    ctx.scale(devicePixelRatio, devicePixelRatio);
429*6dbdd20aSAndroid Build Coastguard Worker
430*6dbdd20aSAndroid Build Coastguard Worker    ctx.font = LABEL_FONT_STYLE;
431*6dbdd20aSAndroid Build Coastguard Worker    ctx.textBaseline = 'middle';
432*6dbdd20aSAndroid Build Coastguard Worker
433*6dbdd20aSAndroid Build Coastguard Worker    ctx.strokeStyle = 'white';
434*6dbdd20aSAndroid Build Coastguard Worker    ctx.lineWidth = 0.5;
435*6dbdd20aSAndroid Build Coastguard Worker
436*6dbdd20aSAndroid Build Coastguard Worker    if (this.labelCharWidth === 0) {
437*6dbdd20aSAndroid Build Coastguard Worker      this.labelCharWidth = ctx.measureText('_').width;
438*6dbdd20aSAndroid Build Coastguard Worker    }
439*6dbdd20aSAndroid Build Coastguard Worker
440*6dbdd20aSAndroid Build Coastguard Worker    let hoveredNode: RenderNode | undefined = undefined;
441*6dbdd20aSAndroid Build Coastguard Worker    for (let i = 0; i < this.renderNodes.length; i++) {
442*6dbdd20aSAndroid Build Coastguard Worker      const node = this.renderNodes[i];
443*6dbdd20aSAndroid Build Coastguard Worker      const {x, y, width: width, source, state} = node;
444*6dbdd20aSAndroid Build Coastguard Worker      if (y + NODE_HEIGHT <= yStart || y >= yEnd) {
445*6dbdd20aSAndroid Build Coastguard Worker        continue;
446*6dbdd20aSAndroid Build Coastguard Worker      }
447*6dbdd20aSAndroid Build Coastguard Worker
448*6dbdd20aSAndroid Build Coastguard Worker      const hover = isIntersecting(this.hoveredX, this.hoveredY, node);
449*6dbdd20aSAndroid Build Coastguard Worker      if (hover) {
450*6dbdd20aSAndroid Build Coastguard Worker        hoveredNode = node;
451*6dbdd20aSAndroid Build Coastguard Worker      }
452*6dbdd20aSAndroid Build Coastguard Worker      let name: string;
453*6dbdd20aSAndroid Build Coastguard Worker      if (source.kind === 'ROOT') {
454*6dbdd20aSAndroid Build Coastguard Worker        const val = displaySize(allRootsCumulativeValue, unit);
455*6dbdd20aSAndroid Build Coastguard Worker        const percent = displayPercentage(
456*6dbdd20aSAndroid Build Coastguard Worker          allRootsCumulativeValue,
457*6dbdd20aSAndroid Build Coastguard Worker          unfilteredCumulativeValue,
458*6dbdd20aSAndroid Build Coastguard Worker        );
459*6dbdd20aSAndroid Build Coastguard Worker        name = `root: ${val} (${percent})`;
460*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = generateColor('root', state === 'PARTIAL', hover);
461*6dbdd20aSAndroid Build Coastguard Worker      } else if (source.kind === 'MERGED') {
462*6dbdd20aSAndroid Build Coastguard Worker        name = '(merged)';
463*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = generateColor(name, state === 'PARTIAL', false);
464*6dbdd20aSAndroid Build Coastguard Worker      } else {
465*6dbdd20aSAndroid Build Coastguard Worker        name = nodes[source.queryIdx].name;
466*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = generateColor(name, state === 'PARTIAL', hover);
467*6dbdd20aSAndroid Build Coastguard Worker      }
468*6dbdd20aSAndroid Build Coastguard Worker      ctx.fillRect(x, y, width - 1, NODE_HEIGHT - 1);
469*6dbdd20aSAndroid Build Coastguard Worker
470*6dbdd20aSAndroid Build Coastguard Worker      const widthNoPadding = width - LABEL_PADDING_PX * 2;
471*6dbdd20aSAndroid Build Coastguard Worker      if (widthNoPadding >= LABEL_MIN_WIDTH_FOR_TEXT_PX) {
472*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillStyle = 'black';
473*6dbdd20aSAndroid Build Coastguard Worker        ctx.fillText(
474*6dbdd20aSAndroid Build Coastguard Worker          name.substring(0, widthNoPadding / this.labelCharWidth),
475*6dbdd20aSAndroid Build Coastguard Worker          x + LABEL_PADDING_PX,
476*6dbdd20aSAndroid Build Coastguard Worker          y + (NODE_HEIGHT - 1) / 2,
477*6dbdd20aSAndroid Build Coastguard Worker          widthNoPadding,
478*6dbdd20aSAndroid Build Coastguard Worker        );
479*6dbdd20aSAndroid Build Coastguard Worker      }
480*6dbdd20aSAndroid Build Coastguard Worker      if (this.lastClickedNode?.x === x && this.lastClickedNode?.y === y) {
481*6dbdd20aSAndroid Build Coastguard Worker        ctx.strokeStyle = 'blue';
482*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineWidth = 2;
483*6dbdd20aSAndroid Build Coastguard Worker        ctx.beginPath();
484*6dbdd20aSAndroid Build Coastguard Worker        ctx.moveTo(x, y);
485*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineTo(x + width, y);
486*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineTo(x + width, y + NODE_HEIGHT - 1);
487*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineTo(x, y + NODE_HEIGHT - 1);
488*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineTo(x, y);
489*6dbdd20aSAndroid Build Coastguard Worker        ctx.stroke();
490*6dbdd20aSAndroid Build Coastguard Worker        ctx.strokeStyle = 'white';
491*6dbdd20aSAndroid Build Coastguard Worker        ctx.lineWidth = 0.5;
492*6dbdd20aSAndroid Build Coastguard Worker      }
493*6dbdd20aSAndroid Build Coastguard Worker    }
494*6dbdd20aSAndroid Build Coastguard Worker    if (hoveredNode === undefined) {
495*6dbdd20aSAndroid Build Coastguard Worker      canvas.style.cursor = 'default';
496*6dbdd20aSAndroid Build Coastguard Worker    } else {
497*6dbdd20aSAndroid Build Coastguard Worker      canvas.style.cursor = 'pointer';
498*6dbdd20aSAndroid Build Coastguard Worker    }
499*6dbdd20aSAndroid Build Coastguard Worker    ctx.restore();
500*6dbdd20aSAndroid Build Coastguard Worker  }
501*6dbdd20aSAndroid Build Coastguard Worker
502*6dbdd20aSAndroid Build Coastguard Worker  private renderFilterBar(attrs: FlamegraphAttrs) {
503*6dbdd20aSAndroid Build Coastguard Worker    const self = this;
504*6dbdd20aSAndroid Build Coastguard Worker    return m(
505*6dbdd20aSAndroid Build Coastguard Worker      '.filter-bar',
506*6dbdd20aSAndroid Build Coastguard Worker      m(
507*6dbdd20aSAndroid Build Coastguard Worker        Select,
508*6dbdd20aSAndroid Build Coastguard Worker        {
509*6dbdd20aSAndroid Build Coastguard Worker          value: attrs.state.selectedMetricName,
510*6dbdd20aSAndroid Build Coastguard Worker          onchange: (e: Event) => {
511*6dbdd20aSAndroid Build Coastguard Worker            const el = e.target as HTMLSelectElement;
512*6dbdd20aSAndroid Build Coastguard Worker            attrs.onStateChange({
513*6dbdd20aSAndroid Build Coastguard Worker              ...self.attrs.state,
514*6dbdd20aSAndroid Build Coastguard Worker              selectedMetricName: el.value,
515*6dbdd20aSAndroid Build Coastguard Worker            });
516*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
517*6dbdd20aSAndroid Build Coastguard Worker          },
518*6dbdd20aSAndroid Build Coastguard Worker        },
519*6dbdd20aSAndroid Build Coastguard Worker        attrs.metrics.map((x) => {
520*6dbdd20aSAndroid Build Coastguard Worker          return m('option', {value: x.name}, x.name);
521*6dbdd20aSAndroid Build Coastguard Worker        }),
522*6dbdd20aSAndroid Build Coastguard Worker      ),
523*6dbdd20aSAndroid Build Coastguard Worker      m(
524*6dbdd20aSAndroid Build Coastguard Worker        Popup,
525*6dbdd20aSAndroid Build Coastguard Worker        {
526*6dbdd20aSAndroid Build Coastguard Worker          trigger: m(TagInput, {
527*6dbdd20aSAndroid Build Coastguard Worker            tags: toTags(self.attrs.state),
528*6dbdd20aSAndroid Build Coastguard Worker            value: this.rawFilterText,
529*6dbdd20aSAndroid Build Coastguard Worker            onChange: (value: string) => {
530*6dbdd20aSAndroid Build Coastguard Worker              self.rawFilterText = value;
531*6dbdd20aSAndroid Build Coastguard Worker              scheduleFullRedraw();
532*6dbdd20aSAndroid Build Coastguard Worker            },
533*6dbdd20aSAndroid Build Coastguard Worker            onTagAdd: (tag: string) => {
534*6dbdd20aSAndroid Build Coastguard Worker              self.rawFilterText = '';
535*6dbdd20aSAndroid Build Coastguard Worker              self.attrs.onStateChange(updateState(self.attrs.state, tag));
536*6dbdd20aSAndroid Build Coastguard Worker              scheduleFullRedraw();
537*6dbdd20aSAndroid Build Coastguard Worker            },
538*6dbdd20aSAndroid Build Coastguard Worker            onTagRemove(index: number) {
539*6dbdd20aSAndroid Build Coastguard Worker              if (index === self.attrs.state.filters.length) {
540*6dbdd20aSAndroid Build Coastguard Worker                self.attrs.onStateChange({
541*6dbdd20aSAndroid Build Coastguard Worker                  ...self.attrs.state,
542*6dbdd20aSAndroid Build Coastguard Worker                  view: {kind: 'TOP_DOWN'},
543*6dbdd20aSAndroid Build Coastguard Worker                });
544*6dbdd20aSAndroid Build Coastguard Worker              } else {
545*6dbdd20aSAndroid Build Coastguard Worker                const filters = Array.from(self.attrs.state.filters);
546*6dbdd20aSAndroid Build Coastguard Worker                filters.splice(index, 1);
547*6dbdd20aSAndroid Build Coastguard Worker                self.attrs.onStateChange({
548*6dbdd20aSAndroid Build Coastguard Worker                  ...self.attrs.state,
549*6dbdd20aSAndroid Build Coastguard Worker                  filters,
550*6dbdd20aSAndroid Build Coastguard Worker                });
551*6dbdd20aSAndroid Build Coastguard Worker              }
552*6dbdd20aSAndroid Build Coastguard Worker              scheduleFullRedraw();
553*6dbdd20aSAndroid Build Coastguard Worker            },
554*6dbdd20aSAndroid Build Coastguard Worker            onfocus() {
555*6dbdd20aSAndroid Build Coastguard Worker              self.filterFocus = true;
556*6dbdd20aSAndroid Build Coastguard Worker            },
557*6dbdd20aSAndroid Build Coastguard Worker            onblur() {
558*6dbdd20aSAndroid Build Coastguard Worker              self.filterFocus = false;
559*6dbdd20aSAndroid Build Coastguard Worker            },
560*6dbdd20aSAndroid Build Coastguard Worker            placeholder: 'Add filter...',
561*6dbdd20aSAndroid Build Coastguard Worker          }),
562*6dbdd20aSAndroid Build Coastguard Worker          isOpen: self.filterFocus && this.rawFilterText.length === 0,
563*6dbdd20aSAndroid Build Coastguard Worker          position: PopupPosition.Bottom,
564*6dbdd20aSAndroid Build Coastguard Worker        },
565*6dbdd20aSAndroid Build Coastguard Worker        m('.pf-flamegraph-filter-bar-popup-content', FILTER_EMPTY_TEXT.trim()),
566*6dbdd20aSAndroid Build Coastguard Worker      ),
567*6dbdd20aSAndroid Build Coastguard Worker      m(SegmentedButtons, {
568*6dbdd20aSAndroid Build Coastguard Worker        options: [{label: 'Top Down'}, {label: 'Bottom Up'}],
569*6dbdd20aSAndroid Build Coastguard Worker        selectedOption: this.attrs.state.view.kind === 'TOP_DOWN' ? 0 : 1,
570*6dbdd20aSAndroid Build Coastguard Worker        onOptionSelected: (num) => {
571*6dbdd20aSAndroid Build Coastguard Worker          self.attrs.onStateChange({
572*6dbdd20aSAndroid Build Coastguard Worker            ...this.attrs.state,
573*6dbdd20aSAndroid Build Coastguard Worker            view: {kind: num === 0 ? 'TOP_DOWN' : 'BOTTOM_UP'},
574*6dbdd20aSAndroid Build Coastguard Worker          });
575*6dbdd20aSAndroid Build Coastguard Worker          scheduleFullRedraw();
576*6dbdd20aSAndroid Build Coastguard Worker        },
577*6dbdd20aSAndroid Build Coastguard Worker        disabled: this.attrs.state.view.kind === 'PIVOT',
578*6dbdd20aSAndroid Build Coastguard Worker      }),
579*6dbdd20aSAndroid Build Coastguard Worker    );
580*6dbdd20aSAndroid Build Coastguard Worker  }
581*6dbdd20aSAndroid Build Coastguard Worker
582*6dbdd20aSAndroid Build Coastguard Worker  private renderTooltip() {
583*6dbdd20aSAndroid Build Coastguard Worker    if (this.tooltipPos === undefined) {
584*6dbdd20aSAndroid Build Coastguard Worker      return undefined;
585*6dbdd20aSAndroid Build Coastguard Worker    }
586*6dbdd20aSAndroid Build Coastguard Worker    const {node} = this.tooltipPos;
587*6dbdd20aSAndroid Build Coastguard Worker    if (node.source.kind === 'MERGED') {
588*6dbdd20aSAndroid Build Coastguard Worker      return m(
589*6dbdd20aSAndroid Build Coastguard Worker        'div',
590*6dbdd20aSAndroid Build Coastguard Worker        m('.tooltip-bold-text', '(merged)'),
591*6dbdd20aSAndroid Build Coastguard Worker        m('.tooltip-text', 'Nodes too small to show, please use filters'),
592*6dbdd20aSAndroid Build Coastguard Worker      );
593*6dbdd20aSAndroid Build Coastguard Worker    }
594*6dbdd20aSAndroid Build Coastguard Worker    const {nodes, allRootsCumulativeValue, unfilteredCumulativeValue} =
595*6dbdd20aSAndroid Build Coastguard Worker      assertExists(this.attrs.data);
596*6dbdd20aSAndroid Build Coastguard Worker    const {unit} = assertExists(this.selectedMetric);
597*6dbdd20aSAndroid Build Coastguard Worker    if (node.source.kind === 'ROOT') {
598*6dbdd20aSAndroid Build Coastguard Worker      const val = displaySize(allRootsCumulativeValue, unit);
599*6dbdd20aSAndroid Build Coastguard Worker      const percent = displayPercentage(
600*6dbdd20aSAndroid Build Coastguard Worker        allRootsCumulativeValue,
601*6dbdd20aSAndroid Build Coastguard Worker        unfilteredCumulativeValue,
602*6dbdd20aSAndroid Build Coastguard Worker      );
603*6dbdd20aSAndroid Build Coastguard Worker      return m(
604*6dbdd20aSAndroid Build Coastguard Worker        'div',
605*6dbdd20aSAndroid Build Coastguard Worker        m('.tooltip-bold-text', 'root'),
606*6dbdd20aSAndroid Build Coastguard Worker        m(
607*6dbdd20aSAndroid Build Coastguard Worker          '.tooltip-text-line',
608*6dbdd20aSAndroid Build Coastguard Worker          m('.tooltip-bold-text', 'Cumulative:'),
609*6dbdd20aSAndroid Build Coastguard Worker          m('.tooltip-text', `${val}, ${percent}`),
610*6dbdd20aSAndroid Build Coastguard Worker        ),
611*6dbdd20aSAndroid Build Coastguard Worker      );
612*6dbdd20aSAndroid Build Coastguard Worker    }
613*6dbdd20aSAndroid Build Coastguard Worker    const {queryIdx} = node.source;
614*6dbdd20aSAndroid Build Coastguard Worker    const {
615*6dbdd20aSAndroid Build Coastguard Worker      name,
616*6dbdd20aSAndroid Build Coastguard Worker      cumulativeValue,
617*6dbdd20aSAndroid Build Coastguard Worker      selfValue,
618*6dbdd20aSAndroid Build Coastguard Worker      parentCumulativeValue,
619*6dbdd20aSAndroid Build Coastguard Worker      properties,
620*6dbdd20aSAndroid Build Coastguard Worker    } = nodes[queryIdx];
621*6dbdd20aSAndroid Build Coastguard Worker    const filterButtonClick = (state: FlamegraphState) => {
622*6dbdd20aSAndroid Build Coastguard Worker      this.attrs.onStateChange(state);
623*6dbdd20aSAndroid Build Coastguard Worker      this.tooltipPos = undefined;
624*6dbdd20aSAndroid Build Coastguard Worker      scheduleFullRedraw();
625*6dbdd20aSAndroid Build Coastguard Worker    };
626*6dbdd20aSAndroid Build Coastguard Worker
627*6dbdd20aSAndroid Build Coastguard Worker    const percent = displayPercentage(
628*6dbdd20aSAndroid Build Coastguard Worker      cumulativeValue,
629*6dbdd20aSAndroid Build Coastguard Worker      unfilteredCumulativeValue,
630*6dbdd20aSAndroid Build Coastguard Worker    );
631*6dbdd20aSAndroid Build Coastguard Worker    const selfPercent = displayPercentage(selfValue, unfilteredCumulativeValue);
632*6dbdd20aSAndroid Build Coastguard Worker
633*6dbdd20aSAndroid Build Coastguard Worker    let percentText = `all: ${percent}`;
634*6dbdd20aSAndroid Build Coastguard Worker    let selfPercentText = `all: ${selfPercent}`;
635*6dbdd20aSAndroid Build Coastguard Worker    if (parentCumulativeValue !== undefined) {
636*6dbdd20aSAndroid Build Coastguard Worker      const parentPercent = displayPercentage(
637*6dbdd20aSAndroid Build Coastguard Worker        cumulativeValue,
638*6dbdd20aSAndroid Build Coastguard Worker        parentCumulativeValue,
639*6dbdd20aSAndroid Build Coastguard Worker      );
640*6dbdd20aSAndroid Build Coastguard Worker      percentText += `, parent: ${parentPercent}`;
641*6dbdd20aSAndroid Build Coastguard Worker      const parentSelfPercent = displayPercentage(
642*6dbdd20aSAndroid Build Coastguard Worker        selfValue,
643*6dbdd20aSAndroid Build Coastguard Worker        parentCumulativeValue,
644*6dbdd20aSAndroid Build Coastguard Worker      );
645*6dbdd20aSAndroid Build Coastguard Worker      selfPercentText += `, parent: ${parentSelfPercent}`;
646*6dbdd20aSAndroid Build Coastguard Worker    }
647*6dbdd20aSAndroid Build Coastguard Worker    return m(
648*6dbdd20aSAndroid Build Coastguard Worker      'div',
649*6dbdd20aSAndroid Build Coastguard Worker      m('.tooltip-bold-text', name),
650*6dbdd20aSAndroid Build Coastguard Worker      m(
651*6dbdd20aSAndroid Build Coastguard Worker        '.tooltip-text-line',
652*6dbdd20aSAndroid Build Coastguard Worker        m('.tooltip-bold-text', 'Cumulative:'),
653*6dbdd20aSAndroid Build Coastguard Worker        m(
654*6dbdd20aSAndroid Build Coastguard Worker          '.tooltip-text',
655*6dbdd20aSAndroid Build Coastguard Worker          `${displaySize(cumulativeValue, unit)} (${percentText})`,
656*6dbdd20aSAndroid Build Coastguard Worker        ),
657*6dbdd20aSAndroid Build Coastguard Worker      ),
658*6dbdd20aSAndroid Build Coastguard Worker      m(
659*6dbdd20aSAndroid Build Coastguard Worker        '.tooltip-text-line',
660*6dbdd20aSAndroid Build Coastguard Worker        m('.tooltip-bold-text', 'Self:'),
661*6dbdd20aSAndroid Build Coastguard Worker        m(
662*6dbdd20aSAndroid Build Coastguard Worker          '.tooltip-text',
663*6dbdd20aSAndroid Build Coastguard Worker          `${displaySize(selfValue, unit)} (${selfPercentText})`,
664*6dbdd20aSAndroid Build Coastguard Worker        ),
665*6dbdd20aSAndroid Build Coastguard Worker      ),
666*6dbdd20aSAndroid Build Coastguard Worker      Array.from(properties, ([key, value]) => {
667*6dbdd20aSAndroid Build Coastguard Worker        return m(
668*6dbdd20aSAndroid Build Coastguard Worker          '.tooltip-text-line',
669*6dbdd20aSAndroid Build Coastguard Worker          m('.tooltip-bold-text', key + ':'),
670*6dbdd20aSAndroid Build Coastguard Worker          m('.tooltip-text', value),
671*6dbdd20aSAndroid Build Coastguard Worker        );
672*6dbdd20aSAndroid Build Coastguard Worker      }),
673*6dbdd20aSAndroid Build Coastguard Worker      m(
674*6dbdd20aSAndroid Build Coastguard Worker        ButtonBar,
675*6dbdd20aSAndroid Build Coastguard Worker        {},
676*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
677*6dbdd20aSAndroid Build Coastguard Worker          label: 'Zoom',
678*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
679*6dbdd20aSAndroid Build Coastguard Worker            this.zoomRegion = node.source;
680*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
681*6dbdd20aSAndroid Build Coastguard Worker          },
682*6dbdd20aSAndroid Build Coastguard Worker        }),
683*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
684*6dbdd20aSAndroid Build Coastguard Worker          label: 'Show Stack',
685*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
686*6dbdd20aSAndroid Build Coastguard Worker            filterButtonClick(
687*6dbdd20aSAndroid Build Coastguard Worker              addFilter(this.attrs.state, {
688*6dbdd20aSAndroid Build Coastguard Worker                kind: 'SHOW_STACK',
689*6dbdd20aSAndroid Build Coastguard Worker                filter: `^${name}$`,
690*6dbdd20aSAndroid Build Coastguard Worker              }),
691*6dbdd20aSAndroid Build Coastguard Worker            );
692*6dbdd20aSAndroid Build Coastguard Worker          },
693*6dbdd20aSAndroid Build Coastguard Worker        }),
694*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
695*6dbdd20aSAndroid Build Coastguard Worker          label: 'Hide Stack',
696*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
697*6dbdd20aSAndroid Build Coastguard Worker            filterButtonClick(
698*6dbdd20aSAndroid Build Coastguard Worker              addFilter(this.attrs.state, {
699*6dbdd20aSAndroid Build Coastguard Worker                kind: 'HIDE_STACK',
700*6dbdd20aSAndroid Build Coastguard Worker                filter: `^${name}$`,
701*6dbdd20aSAndroid Build Coastguard Worker              }),
702*6dbdd20aSAndroid Build Coastguard Worker            );
703*6dbdd20aSAndroid Build Coastguard Worker          },
704*6dbdd20aSAndroid Build Coastguard Worker        }),
705*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
706*6dbdd20aSAndroid Build Coastguard Worker          label: 'Hide Frame',
707*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
708*6dbdd20aSAndroid Build Coastguard Worker            filterButtonClick(
709*6dbdd20aSAndroid Build Coastguard Worker              addFilter(this.attrs.state, {
710*6dbdd20aSAndroid Build Coastguard Worker                kind: 'HIDE_FRAME',
711*6dbdd20aSAndroid Build Coastguard Worker                filter: `^${name}$`,
712*6dbdd20aSAndroid Build Coastguard Worker              }),
713*6dbdd20aSAndroid Build Coastguard Worker            );
714*6dbdd20aSAndroid Build Coastguard Worker          },
715*6dbdd20aSAndroid Build Coastguard Worker        }),
716*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
717*6dbdd20aSAndroid Build Coastguard Worker          label: 'Show From Frame',
718*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
719*6dbdd20aSAndroid Build Coastguard Worker            filterButtonClick(
720*6dbdd20aSAndroid Build Coastguard Worker              addFilter(this.attrs.state, {
721*6dbdd20aSAndroid Build Coastguard Worker                kind: 'SHOW_FROM_FRAME',
722*6dbdd20aSAndroid Build Coastguard Worker                filter: `^${name}$`,
723*6dbdd20aSAndroid Build Coastguard Worker              }),
724*6dbdd20aSAndroid Build Coastguard Worker            );
725*6dbdd20aSAndroid Build Coastguard Worker          },
726*6dbdd20aSAndroid Build Coastguard Worker        }),
727*6dbdd20aSAndroid Build Coastguard Worker        m(Button, {
728*6dbdd20aSAndroid Build Coastguard Worker          label: 'Pivot',
729*6dbdd20aSAndroid Build Coastguard Worker          onclick: () => {
730*6dbdd20aSAndroid Build Coastguard Worker            filterButtonClick({
731*6dbdd20aSAndroid Build Coastguard Worker              ...this.attrs.state,
732*6dbdd20aSAndroid Build Coastguard Worker              view: {kind: 'PIVOT', pivot: name},
733*6dbdd20aSAndroid Build Coastguard Worker            });
734*6dbdd20aSAndroid Build Coastguard Worker          },
735*6dbdd20aSAndroid Build Coastguard Worker        }),
736*6dbdd20aSAndroid Build Coastguard Worker      ),
737*6dbdd20aSAndroid Build Coastguard Worker    );
738*6dbdd20aSAndroid Build Coastguard Worker  }
739*6dbdd20aSAndroid Build Coastguard Worker
740*6dbdd20aSAndroid Build Coastguard Worker  private get selectedMetric() {
741*6dbdd20aSAndroid Build Coastguard Worker    return this.attrs.metrics.find(
742*6dbdd20aSAndroid Build Coastguard Worker      (x) => x.name === this.attrs.state.selectedMetricName,
743*6dbdd20aSAndroid Build Coastguard Worker    );
744*6dbdd20aSAndroid Build Coastguard Worker  }
745*6dbdd20aSAndroid Build Coastguard Worker}
746*6dbdd20aSAndroid Build Coastguard Worker
747*6dbdd20aSAndroid Build Coastguard Workerfunction computeRenderNodes(
748*6dbdd20aSAndroid Build Coastguard Worker  {nodes, allRootsCumulativeValue, minDepth}: FlamegraphQueryData,
749*6dbdd20aSAndroid Build Coastguard Worker  zoomRegion: ZoomRegion,
750*6dbdd20aSAndroid Build Coastguard Worker  canvasWidth: number,
751*6dbdd20aSAndroid Build Coastguard Worker): ReadonlyArray<RenderNode> {
752*6dbdd20aSAndroid Build Coastguard Worker  const renderNodes: RenderNode[] = [];
753*6dbdd20aSAndroid Build Coastguard Worker
754*6dbdd20aSAndroid Build Coastguard Worker  const mergedKeyToX = new Map<string, number>();
755*6dbdd20aSAndroid Build Coastguard Worker  const keyToChildMergedIdx = new Map<string, number>();
756*6dbdd20aSAndroid Build Coastguard Worker  renderNodes.push({
757*6dbdd20aSAndroid Build Coastguard Worker    x: 0,
758*6dbdd20aSAndroid Build Coastguard Worker    y: -minDepth * NODE_HEIGHT,
759*6dbdd20aSAndroid Build Coastguard Worker    width: canvasWidth,
760*6dbdd20aSAndroid Build Coastguard Worker    source: {
761*6dbdd20aSAndroid Build Coastguard Worker      kind: 'ROOT',
762*6dbdd20aSAndroid Build Coastguard Worker      queryXStart: 0,
763*6dbdd20aSAndroid Build Coastguard Worker      queryXEnd: allRootsCumulativeValue,
764*6dbdd20aSAndroid Build Coastguard Worker      type: 'ROOT',
765*6dbdd20aSAndroid Build Coastguard Worker    },
766*6dbdd20aSAndroid Build Coastguard Worker    state:
767*6dbdd20aSAndroid Build Coastguard Worker      zoomRegion.queryXStart === 0 &&
768*6dbdd20aSAndroid Build Coastguard Worker      zoomRegion.queryXEnd === allRootsCumulativeValue
769*6dbdd20aSAndroid Build Coastguard Worker        ? 'NORMAL'
770*6dbdd20aSAndroid Build Coastguard Worker        : 'PARTIAL',
771*6dbdd20aSAndroid Build Coastguard Worker  });
772*6dbdd20aSAndroid Build Coastguard Worker
773*6dbdd20aSAndroid Build Coastguard Worker  const zoomQueryWidth = zoomRegion.queryXEnd - zoomRegion.queryXStart;
774*6dbdd20aSAndroid Build Coastguard Worker  for (let i = 0; i < nodes.length; i++) {
775*6dbdd20aSAndroid Build Coastguard Worker    const {id, parentId, depth, xStart: qXStart, xEnd: qXEnd} = nodes[i];
776*6dbdd20aSAndroid Build Coastguard Worker    assertTrue(depth !== 0);
777*6dbdd20aSAndroid Build Coastguard Worker
778*6dbdd20aSAndroid Build Coastguard Worker    const depthMatchingZoom = isDepthMatchingZoom(depth, zoomRegion);
779*6dbdd20aSAndroid Build Coastguard Worker    if (
780*6dbdd20aSAndroid Build Coastguard Worker      depthMatchingZoom &&
781*6dbdd20aSAndroid Build Coastguard Worker      (qXEnd <= zoomRegion.queryXStart || qXStart >= zoomRegion.queryXEnd)
782*6dbdd20aSAndroid Build Coastguard Worker    ) {
783*6dbdd20aSAndroid Build Coastguard Worker      continue;
784*6dbdd20aSAndroid Build Coastguard Worker    }
785*6dbdd20aSAndroid Build Coastguard Worker    const queryXPerPx = depthMatchingZoom
786*6dbdd20aSAndroid Build Coastguard Worker      ? zoomQueryWidth / canvasWidth
787*6dbdd20aSAndroid Build Coastguard Worker      : allRootsCumulativeValue / canvasWidth;
788*6dbdd20aSAndroid Build Coastguard Worker    const relativeXStart = depthMatchingZoom
789*6dbdd20aSAndroid Build Coastguard Worker      ? qXStart - zoomRegion.queryXStart
790*6dbdd20aSAndroid Build Coastguard Worker      : qXStart;
791*6dbdd20aSAndroid Build Coastguard Worker    const relativeXEnd = depthMatchingZoom
792*6dbdd20aSAndroid Build Coastguard Worker      ? qXEnd - zoomRegion.queryXStart
793*6dbdd20aSAndroid Build Coastguard Worker      : qXEnd;
794*6dbdd20aSAndroid Build Coastguard Worker    const relativeWidth = relativeXEnd - relativeXStart;
795*6dbdd20aSAndroid Build Coastguard Worker
796*6dbdd20aSAndroid Build Coastguard Worker    const x = Math.max(0, relativeXStart) / queryXPerPx;
797*6dbdd20aSAndroid Build Coastguard Worker    const y = NODE_HEIGHT * (depth - minDepth);
798*6dbdd20aSAndroid Build Coastguard Worker    const width = depthMatchingZoom
799*6dbdd20aSAndroid Build Coastguard Worker      ? Math.min(relativeWidth, zoomQueryWidth) / queryXPerPx
800*6dbdd20aSAndroid Build Coastguard Worker      : relativeWidth / queryXPerPx;
801*6dbdd20aSAndroid Build Coastguard Worker    const state = computeState(qXStart, qXEnd, zoomRegion, depthMatchingZoom);
802*6dbdd20aSAndroid Build Coastguard Worker
803*6dbdd20aSAndroid Build Coastguard Worker    if (width < MIN_PIXEL_DISPLAYED) {
804*6dbdd20aSAndroid Build Coastguard Worker      const parentChildMergeKey = `${parentId}_${depth}`;
805*6dbdd20aSAndroid Build Coastguard Worker      const mergedXKey = `${id}_${depth > 0 ? depth + 1 : depth - 1}`;
806*6dbdd20aSAndroid Build Coastguard Worker      const childMergedIdx = keyToChildMergedIdx.get(parentChildMergeKey);
807*6dbdd20aSAndroid Build Coastguard Worker      if (childMergedIdx !== undefined) {
808*6dbdd20aSAndroid Build Coastguard Worker        const r = renderNodes[childMergedIdx];
809*6dbdd20aSAndroid Build Coastguard Worker        const mergedWidth = isDepthMatchingZoom(depth, zoomRegion)
810*6dbdd20aSAndroid Build Coastguard Worker          ? Math.min(qXEnd - r.source.queryXStart, zoomQueryWidth) / queryXPerPx
811*6dbdd20aSAndroid Build Coastguard Worker          : (qXEnd - r.source.queryXStart) / queryXPerPx;
812*6dbdd20aSAndroid Build Coastguard Worker        renderNodes[childMergedIdx] = {
813*6dbdd20aSAndroid Build Coastguard Worker          ...r,
814*6dbdd20aSAndroid Build Coastguard Worker          width: Math.max(mergedWidth, MIN_PIXEL_DISPLAYED),
815*6dbdd20aSAndroid Build Coastguard Worker          source: {
816*6dbdd20aSAndroid Build Coastguard Worker            ...(r.source as MergedSource),
817*6dbdd20aSAndroid Build Coastguard Worker            queryXEnd: qXEnd,
818*6dbdd20aSAndroid Build Coastguard Worker          },
819*6dbdd20aSAndroid Build Coastguard Worker        };
820*6dbdd20aSAndroid Build Coastguard Worker        mergedKeyToX.set(mergedXKey, r.x);
821*6dbdd20aSAndroid Build Coastguard Worker        continue;
822*6dbdd20aSAndroid Build Coastguard Worker      }
823*6dbdd20aSAndroid Build Coastguard Worker      const mergedX = mergedKeyToX.get(`${parentId}_${depth}`) ?? x;
824*6dbdd20aSAndroid Build Coastguard Worker      renderNodes.push({
825*6dbdd20aSAndroid Build Coastguard Worker        x: mergedX,
826*6dbdd20aSAndroid Build Coastguard Worker        y,
827*6dbdd20aSAndroid Build Coastguard Worker        width: Math.max(width, MIN_PIXEL_DISPLAYED),
828*6dbdd20aSAndroid Build Coastguard Worker        source: {
829*6dbdd20aSAndroid Build Coastguard Worker          kind: 'MERGED',
830*6dbdd20aSAndroid Build Coastguard Worker          queryXStart: qXStart,
831*6dbdd20aSAndroid Build Coastguard Worker          queryXEnd: qXEnd,
832*6dbdd20aSAndroid Build Coastguard Worker          type: depth > 0 ? 'BELOW_ROOT' : 'ABOVE_ROOT',
833*6dbdd20aSAndroid Build Coastguard Worker        },
834*6dbdd20aSAndroid Build Coastguard Worker        state,
835*6dbdd20aSAndroid Build Coastguard Worker      });
836*6dbdd20aSAndroid Build Coastguard Worker      keyToChildMergedIdx.set(parentChildMergeKey, renderNodes.length - 1);
837*6dbdd20aSAndroid Build Coastguard Worker      mergedKeyToX.set(mergedXKey, mergedX);
838*6dbdd20aSAndroid Build Coastguard Worker      continue;
839*6dbdd20aSAndroid Build Coastguard Worker    }
840*6dbdd20aSAndroid Build Coastguard Worker    renderNodes.push({
841*6dbdd20aSAndroid Build Coastguard Worker      x,
842*6dbdd20aSAndroid Build Coastguard Worker      y,
843*6dbdd20aSAndroid Build Coastguard Worker      width,
844*6dbdd20aSAndroid Build Coastguard Worker      source: {
845*6dbdd20aSAndroid Build Coastguard Worker        kind: 'NODE',
846*6dbdd20aSAndroid Build Coastguard Worker        queryXStart: qXStart,
847*6dbdd20aSAndroid Build Coastguard Worker        queryXEnd: qXEnd,
848*6dbdd20aSAndroid Build Coastguard Worker        queryIdx: i,
849*6dbdd20aSAndroid Build Coastguard Worker        type: depth > 0 ? 'BELOW_ROOT' : 'ABOVE_ROOT',
850*6dbdd20aSAndroid Build Coastguard Worker      },
851*6dbdd20aSAndroid Build Coastguard Worker      state,
852*6dbdd20aSAndroid Build Coastguard Worker    });
853*6dbdd20aSAndroid Build Coastguard Worker  }
854*6dbdd20aSAndroid Build Coastguard Worker  return renderNodes;
855*6dbdd20aSAndroid Build Coastguard Worker}
856*6dbdd20aSAndroid Build Coastguard Worker
857*6dbdd20aSAndroid Build Coastguard Workerfunction isDepthMatchingZoom(depth: number, zoomRegion: ZoomRegion): boolean {
858*6dbdd20aSAndroid Build Coastguard Worker  assertTrue(
859*6dbdd20aSAndroid Build Coastguard Worker    depth !== 0,
860*6dbdd20aSAndroid Build Coastguard Worker    'Handling zooming root not possible in this function',
861*6dbdd20aSAndroid Build Coastguard Worker  );
862*6dbdd20aSAndroid Build Coastguard Worker  return (
863*6dbdd20aSAndroid Build Coastguard Worker    (depth > 0 && zoomRegion.type === 'BELOW_ROOT') ||
864*6dbdd20aSAndroid Build Coastguard Worker    (depth < 0 && zoomRegion.type === 'ABOVE_ROOT')
865*6dbdd20aSAndroid Build Coastguard Worker  );
866*6dbdd20aSAndroid Build Coastguard Worker}
867*6dbdd20aSAndroid Build Coastguard Worker
868*6dbdd20aSAndroid Build Coastguard Workerfunction computeState(
869*6dbdd20aSAndroid Build Coastguard Worker  qXStart: number,
870*6dbdd20aSAndroid Build Coastguard Worker  qXEnd: number,
871*6dbdd20aSAndroid Build Coastguard Worker  zoomRegion: ZoomRegion,
872*6dbdd20aSAndroid Build Coastguard Worker  isDepthMatchingZoom: boolean,
873*6dbdd20aSAndroid Build Coastguard Worker) {
874*6dbdd20aSAndroid Build Coastguard Worker  if (!isDepthMatchingZoom) {
875*6dbdd20aSAndroid Build Coastguard Worker    return 'NORMAL';
876*6dbdd20aSAndroid Build Coastguard Worker  }
877*6dbdd20aSAndroid Build Coastguard Worker  if (qXStart === zoomRegion.queryXStart && qXEnd === zoomRegion.queryXEnd) {
878*6dbdd20aSAndroid Build Coastguard Worker    return 'SELECTED';
879*6dbdd20aSAndroid Build Coastguard Worker  }
880*6dbdd20aSAndroid Build Coastguard Worker  if (qXStart < zoomRegion.queryXStart || qXEnd > zoomRegion.queryXEnd) {
881*6dbdd20aSAndroid Build Coastguard Worker    return 'PARTIAL';
882*6dbdd20aSAndroid Build Coastguard Worker  }
883*6dbdd20aSAndroid Build Coastguard Worker  return 'NORMAL';
884*6dbdd20aSAndroid Build Coastguard Worker}
885*6dbdd20aSAndroid Build Coastguard Worker
886*6dbdd20aSAndroid Build Coastguard Workerfunction isIntersecting(
887*6dbdd20aSAndroid Build Coastguard Worker  needleX: number | undefined,
888*6dbdd20aSAndroid Build Coastguard Worker  needleY: number | undefined,
889*6dbdd20aSAndroid Build Coastguard Worker  {x, y, width}: RenderNode,
890*6dbdd20aSAndroid Build Coastguard Worker) {
891*6dbdd20aSAndroid Build Coastguard Worker  if (needleX === undefined || needleY === undefined) {
892*6dbdd20aSAndroid Build Coastguard Worker    return false;
893*6dbdd20aSAndroid Build Coastguard Worker  }
894*6dbdd20aSAndroid Build Coastguard Worker  return (
895*6dbdd20aSAndroid Build Coastguard Worker    needleX >= x &&
896*6dbdd20aSAndroid Build Coastguard Worker    needleX < x + width &&
897*6dbdd20aSAndroid Build Coastguard Worker    needleY >= y &&
898*6dbdd20aSAndroid Build Coastguard Worker    needleY < y + NODE_HEIGHT
899*6dbdd20aSAndroid Build Coastguard Worker  );
900*6dbdd20aSAndroid Build Coastguard Worker}
901*6dbdd20aSAndroid Build Coastguard Worker
902*6dbdd20aSAndroid Build Coastguard Workerfunction displaySize(totalSize: number, unit: string): string {
903*6dbdd20aSAndroid Build Coastguard Worker  if (unit === '') return totalSize.toLocaleString();
904*6dbdd20aSAndroid Build Coastguard Worker  if (totalSize === 0) return `0 ${unit}`;
905*6dbdd20aSAndroid Build Coastguard Worker  let step: number;
906*6dbdd20aSAndroid Build Coastguard Worker  let units: string[];
907*6dbdd20aSAndroid Build Coastguard Worker  switch (unit) {
908*6dbdd20aSAndroid Build Coastguard Worker    case 'B':
909*6dbdd20aSAndroid Build Coastguard Worker      step = 1024;
910*6dbdd20aSAndroid Build Coastguard Worker      units = ['B', 'KiB', 'MiB', 'GiB'];
911*6dbdd20aSAndroid Build Coastguard Worker      break;
912*6dbdd20aSAndroid Build Coastguard Worker    case 'ns':
913*6dbdd20aSAndroid Build Coastguard Worker      step = 1000;
914*6dbdd20aSAndroid Build Coastguard Worker      units = ['ns', 'us', 'ms', 's'];
915*6dbdd20aSAndroid Build Coastguard Worker      break;
916*6dbdd20aSAndroid Build Coastguard Worker    default:
917*6dbdd20aSAndroid Build Coastguard Worker      step = 1000;
918*6dbdd20aSAndroid Build Coastguard Worker      units = [unit, `K${unit}`, `M${unit}`, `G${unit}`];
919*6dbdd20aSAndroid Build Coastguard Worker      break;
920*6dbdd20aSAndroid Build Coastguard Worker  }
921*6dbdd20aSAndroid Build Coastguard Worker  const unitsIndex = Math.min(
922*6dbdd20aSAndroid Build Coastguard Worker    Math.trunc(Math.log(totalSize) / Math.log(step)),
923*6dbdd20aSAndroid Build Coastguard Worker    units.length - 1,
924*6dbdd20aSAndroid Build Coastguard Worker  );
925*6dbdd20aSAndroid Build Coastguard Worker  const pow = Math.pow(step, unitsIndex);
926*6dbdd20aSAndroid Build Coastguard Worker  const result = totalSize / pow;
927*6dbdd20aSAndroid Build Coastguard Worker  const resultString =
928*6dbdd20aSAndroid Build Coastguard Worker    totalSize % pow === 0 ? result.toString() : result.toFixed(2);
929*6dbdd20aSAndroid Build Coastguard Worker  return `${resultString} ${units[unitsIndex]}`;
930*6dbdd20aSAndroid Build Coastguard Worker}
931*6dbdd20aSAndroid Build Coastguard Worker
932*6dbdd20aSAndroid Build Coastguard Workerfunction displayPercentage(size: number, totalSize: number): string {
933*6dbdd20aSAndroid Build Coastguard Worker  if (totalSize === 0) {
934*6dbdd20aSAndroid Build Coastguard Worker    return `[NULL]%`;
935*6dbdd20aSAndroid Build Coastguard Worker  }
936*6dbdd20aSAndroid Build Coastguard Worker  return `${((size / totalSize) * 100.0).toFixed(2)}%`;
937*6dbdd20aSAndroid Build Coastguard Worker}
938*6dbdd20aSAndroid Build Coastguard Worker
939*6dbdd20aSAndroid Build Coastguard Workerfunction updateState(state: FlamegraphState, filter: string): FlamegraphState {
940*6dbdd20aSAndroid Build Coastguard Worker  const lwr = filter.toLowerCase();
941*6dbdd20aSAndroid Build Coastguard Worker  if (lwr.startsWith('ss: ') || lwr.startsWith('show stack: ')) {
942*6dbdd20aSAndroid Build Coastguard Worker    return addFilter(state, {
943*6dbdd20aSAndroid Build Coastguard Worker      kind: 'SHOW_STACK',
944*6dbdd20aSAndroid Build Coastguard Worker      filter: filter.split(': ', 2)[1],
945*6dbdd20aSAndroid Build Coastguard Worker    });
946*6dbdd20aSAndroid Build Coastguard Worker  } else if (lwr.startsWith('hs: ') || lwr.startsWith('hide stack: ')) {
947*6dbdd20aSAndroid Build Coastguard Worker    return addFilter(state, {
948*6dbdd20aSAndroid Build Coastguard Worker      kind: 'HIDE_STACK',
949*6dbdd20aSAndroid Build Coastguard Worker      filter: filter.split(': ', 2)[1],
950*6dbdd20aSAndroid Build Coastguard Worker    });
951*6dbdd20aSAndroid Build Coastguard Worker  } else if (lwr.startsWith('sff: ') || lwr.startsWith('show from frame: ')) {
952*6dbdd20aSAndroid Build Coastguard Worker    return addFilter(state, {
953*6dbdd20aSAndroid Build Coastguard Worker      kind: 'SHOW_FROM_FRAME',
954*6dbdd20aSAndroid Build Coastguard Worker      filter: filter.split(': ', 2)[1],
955*6dbdd20aSAndroid Build Coastguard Worker    });
956*6dbdd20aSAndroid Build Coastguard Worker  } else if (lwr.startsWith('hf: ') || lwr.startsWith('hide frame: ')) {
957*6dbdd20aSAndroid Build Coastguard Worker    return addFilter(state, {
958*6dbdd20aSAndroid Build Coastguard Worker      kind: 'HIDE_FRAME',
959*6dbdd20aSAndroid Build Coastguard Worker      filter: filter.split(': ', 2)[1],
960*6dbdd20aSAndroid Build Coastguard Worker    });
961*6dbdd20aSAndroid Build Coastguard Worker  } else if (lwr.startsWith('p:') || lwr.startsWith('pivot: ')) {
962*6dbdd20aSAndroid Build Coastguard Worker    return {
963*6dbdd20aSAndroid Build Coastguard Worker      ...state,
964*6dbdd20aSAndroid Build Coastguard Worker      view: {kind: 'PIVOT', pivot: filter.split(': ', 2)[1]},
965*6dbdd20aSAndroid Build Coastguard Worker    };
966*6dbdd20aSAndroid Build Coastguard Worker  }
967*6dbdd20aSAndroid Build Coastguard Worker  return addFilter(state, {
968*6dbdd20aSAndroid Build Coastguard Worker    kind: 'SHOW_STACK',
969*6dbdd20aSAndroid Build Coastguard Worker    filter: filter,
970*6dbdd20aSAndroid Build Coastguard Worker  });
971*6dbdd20aSAndroid Build Coastguard Worker}
972*6dbdd20aSAndroid Build Coastguard Worker
973*6dbdd20aSAndroid Build Coastguard Workerfunction toTags(state: FlamegraphState): ReadonlyArray<string> {
974*6dbdd20aSAndroid Build Coastguard Worker  const toString = (x: FlamegraphFilter) => {
975*6dbdd20aSAndroid Build Coastguard Worker    switch (x.kind) {
976*6dbdd20aSAndroid Build Coastguard Worker      case 'HIDE_FRAME':
977*6dbdd20aSAndroid Build Coastguard Worker        return 'Hide Frame: ' + x.filter;
978*6dbdd20aSAndroid Build Coastguard Worker      case 'HIDE_STACK':
979*6dbdd20aSAndroid Build Coastguard Worker        return 'Hide Stack: ' + x.filter;
980*6dbdd20aSAndroid Build Coastguard Worker      case 'SHOW_FROM_FRAME':
981*6dbdd20aSAndroid Build Coastguard Worker        return 'Show From Frame: ' + x.filter;
982*6dbdd20aSAndroid Build Coastguard Worker      case 'SHOW_STACK':
983*6dbdd20aSAndroid Build Coastguard Worker        return 'Show Stack: ' + x.filter;
984*6dbdd20aSAndroid Build Coastguard Worker    }
985*6dbdd20aSAndroid Build Coastguard Worker  };
986*6dbdd20aSAndroid Build Coastguard Worker  const filters = state.filters.map((x) => toString(x));
987*6dbdd20aSAndroid Build Coastguard Worker  return filters.concat(
988*6dbdd20aSAndroid Build Coastguard Worker    state.view.kind === 'PIVOT' ? ['Pivot: ' + state.view.pivot] : [],
989*6dbdd20aSAndroid Build Coastguard Worker  );
990*6dbdd20aSAndroid Build Coastguard Worker}
991*6dbdd20aSAndroid Build Coastguard Worker
992*6dbdd20aSAndroid Build Coastguard Workerfunction addFilter(
993*6dbdd20aSAndroid Build Coastguard Worker  state: FlamegraphState,
994*6dbdd20aSAndroid Build Coastguard Worker  filter: FlamegraphFilter,
995*6dbdd20aSAndroid Build Coastguard Worker): FlamegraphState {
996*6dbdd20aSAndroid Build Coastguard Worker  return {
997*6dbdd20aSAndroid Build Coastguard Worker    ...state,
998*6dbdd20aSAndroid Build Coastguard Worker    filters: state.filters.concat([filter]),
999*6dbdd20aSAndroid Build Coastguard Worker  };
1000*6dbdd20aSAndroid Build Coastguard Worker}
1001*6dbdd20aSAndroid Build Coastguard Worker
1002*6dbdd20aSAndroid Build Coastguard Workerfunction generateColor(name: string, greyed: boolean, hovered: boolean) {
1003*6dbdd20aSAndroid Build Coastguard Worker  if (greyed) {
1004*6dbdd20aSAndroid Build Coastguard Worker    return `hsl(0deg, 0%, ${hovered ? 85 : 80}%)`;
1005*6dbdd20aSAndroid Build Coastguard Worker  }
1006*6dbdd20aSAndroid Build Coastguard Worker  if (name === 'unknown' || name === 'root') {
1007*6dbdd20aSAndroid Build Coastguard Worker    return `hsl(0deg, 0%, ${hovered ? 78 : 73}%)`;
1008*6dbdd20aSAndroid Build Coastguard Worker  }
1009*6dbdd20aSAndroid Build Coastguard Worker  let x = 0;
1010*6dbdd20aSAndroid Build Coastguard Worker  for (let i = 0; i < name.length; ++i) {
1011*6dbdd20aSAndroid Build Coastguard Worker    x += name.charCodeAt(i) % 64;
1012*6dbdd20aSAndroid Build Coastguard Worker  }
1013*6dbdd20aSAndroid Build Coastguard Worker  return `hsl(${x % 360}deg, 45%, ${hovered ? 78 : 73}%)`;
1014*6dbdd20aSAndroid Build Coastguard Worker}
1015