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