blob: c80713c0b78883f6a507f71328df5f4a31cbf9b3 [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m, {Vnode} from 'mithril';
import {findRef} from '../base/dom_utils';
import {assertExists, assertTrue} from '../base/logging';
import {Duration, time} from '../base/time';
import {Actions} from '../common/actions';
import {
CallsiteInfo,
FlamegraphViewingOption,
defaultViewingOption,
expandCallsites,
findRootSize,
mergeCallsites,
viewingOptions,
} from '../common/flamegraph_util';
import {ProfileType} from '../common/state';
import {raf} from '../core/raf_scheduler';
import {Button} from '../widgets/button';
import {Icon} from '../widgets/icon';
import {Modal, ModalAttrs} from '../widgets/modal';
import {Popup} from '../widgets/popup';
import {EmptyState} from '../widgets/empty_state';
import {Spinner} from '../widgets/spinner';
import {Flamegraph, NodeRendering} from './flamegraph';
import {globals} from './globals';
import {debounce} from './rate_limiters';
import {Router} from './router';
import {ButtonBar} from '../widgets/button';
import {DurationWidget} from './widgets/duration';
import {DetailsShell} from '../widgets/details_shell';
import {Intent} from '../widgets/common';
import {Engine, NUM, STR} from '../public';
import {Monitor} from '../base/monitor';
import {arrayEquals} from '../base/array_utils';
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
import {AsyncLimiter} from '../base/async_limiter';
import {FlamegraphCache} from '../core/flamegraph_cache';
const HEADER_HEIGHT = 30;
export function profileType(s: string): ProfileType {
if (isProfileType(s)) {
return s;
}
if (s.startsWith('heap_profile')) {
return ProfileType.HEAP_PROFILE;
}
throw new Error('Unknown type ${s}');
}
function isProfileType(s: string): s is ProfileType {
return Object.values(ProfileType).includes(s as ProfileType);
}
function getFlamegraphType(type: ProfileType) {
switch (type) {
case ProfileType.HEAP_PROFILE:
case ProfileType.MIXED_HEAP_PROFILE:
case ProfileType.NATIVE_HEAP_PROFILE:
case ProfileType.JAVA_HEAP_SAMPLES:
return 'native';
case ProfileType.JAVA_HEAP_GRAPH:
return 'graph';
case ProfileType.PERF_SAMPLE:
return 'perf';
default:
const exhaustiveCheck: never = type;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
}
}
const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [
FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
] as const;
export type HeapGraphDominatorTreeViewingOption =
(typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number];
export function isHeapGraphDominatorTreeViewingOption(
option: FlamegraphViewingOption,
): option is HeapGraphDominatorTreeViewingOption {
return (
HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphViewingOption[]
).includes(option);
}
const MIN_PIXEL_DISPLAYED = 1;
function toSelectedCallsite(c: CallsiteInfo | undefined): string {
if (c !== undefined && c.name !== undefined) {
return c.name;
}
return '(none)';
}
const RENDER_SELF_AND_TOTAL: NodeRendering = {
selfSize: 'Self',
totalSize: 'Total',
};
const RENDER_OBJ_COUNT: NodeRendering = {
selfSize: 'Self objects',
totalSize: 'Subtree objects',
};
export interface FlamegraphSelectionParams {
readonly profileType: ProfileType;
readonly upids: number[];
readonly start: time;
readonly end: time;
}
interface FlamegraphDetailsPanelAttrs {
cache: FlamegraphCache;
selection: FlamegraphSelectionParams;
}
interface FlamegraphResult {
queryResults: ReadonlyArray<CallsiteInfo>;
incomplete: boolean;
renderResults?: ReadonlyArray<CallsiteInfo>;
}
interface FlamegraphState {
selection: FlamegraphSelectionParams;
viewingOption: FlamegraphViewingOption;
focusRegex: string;
result?: FlamegraphResult;
selectedCallsites: Readonly<{
[key: string]: CallsiteInfo | undefined;
}>;
}
export class FlamegraphDetailsPanel
implements m.ClassComponent<FlamegraphDetailsPanelAttrs>
{
private undebouncedFocusRegex = '';
private updateFocusRegexDebounced = debounce(() => {
if (this.state === undefined) {
return;
}
this.state.focusRegex = this.undebouncedFocusRegex;
raf.scheduleFullRedraw();
}, 20);
private flamegraph: Flamegraph = new Flamegraph([]);
private queryLimiter = new AsyncLimiter();
private state?: FlamegraphState;
private queryMonitor = new Monitor([
() => this.state?.selection,
() => this.state?.focusRegex,
() => this.state?.viewingOption,
]);
private selectedCallsitesMonitor = new Monitor([
() => this.state?.selection,
() => this.state?.focusRegex,
]);
private renderResultMonitor = new Monitor([
() => this.state?.result?.queryResults,
() => this.state?.selectedCallsites,
]);
view({attrs}: Vnode<FlamegraphDetailsPanelAttrs>) {
if (attrs.selection === undefined) {
this.state = undefined;
} else if (
attrs.selection.profileType !== this.state?.selection.profileType ||
attrs.selection.start !== this.state.selection.start ||
attrs.selection.end !== this.state.selection.end ||
!arrayEquals(attrs.selection.upids, this.state.selection.upids)
) {
this.state = {
selection: attrs.selection,
focusRegex: '',
viewingOption: defaultViewingOption(attrs.selection.profileType),
selectedCallsites: {},
};
}
if (this.state === undefined) {
return m(
'.details-panel',
m('.details-panel-heading', m('h2', `Flamegraph Profile`)),
);
}
if (this.queryMonitor.ifStateChanged()) {
this.state.result = undefined;
const state = this.state;
this.queryLimiter.schedule(() => {
return FlamegraphDetailsPanel.fetchQueryResults(
assertExists(this.getCurrentEngine()),
attrs.cache,
state,
);
});
}
if (this.selectedCallsitesMonitor.ifStateChanged()) {
this.state.selectedCallsites = {};
}
if (
this.renderResultMonitor.ifStateChanged() &&
this.state.result !== undefined
) {
const selected = this.state.selectedCallsites[this.state.viewingOption];
const expanded = expandCallsites(
this.state.result.queryResults,
selected?.id ?? -1,
);
this.state.result.renderResults = mergeCallsites(
expanded,
FlamegraphDetailsPanel.getMinSizeDisplayed(
expanded,
selected?.totalSize,
),
);
}
let height: number | undefined;
if (this.state.result?.renderResults !== undefined) {
this.flamegraph.updateDataIfChanged(
this.nodeRendering(),
this.state.result.renderResults,
this.state.selectedCallsites[this.state.viewingOption],
);
height = this.flamegraph.getHeight() + HEADER_HEIGHT;
} else {
height = undefined;
}
return m(
'.flamegraph-profile',
this.maybeShowModal(),
m(
DetailsShell,
{
fillParent: true,
title: m(
'div.title',
this.getTitle(),
this.state.selection.profileType ===
ProfileType.MIXED_HEAP_PROFILE &&
m(
Popup,
{
trigger: m(Icon, {icon: 'warning'}),
},
m(
'',
{style: {width: '300px'}},
'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
),
),
':',
),
description: this.getViewingOptionButtons(),
buttons: [
m(
'div.selected',
`Selected function: ${toSelectedCallsite(
this.state.selectedCallsites[this.state.viewingOption],
)}`,
),
m(
'div.time',
`Snapshot time: `,
m(DurationWidget, {
dur: this.state.selection.end - this.state.selection.start,
}),
),
m('input[type=text][placeholder=Focus]', {
oninput: (e: Event) => {
const target = e.target as HTMLInputElement;
this.undebouncedFocusRegex = target.value;
this.updateFocusRegexDebounced();
},
// Required to stop hot-key handling:
onkeydown: (e: Event) => e.stopPropagation(),
}),
(this.state.selection.profileType ===
ProfileType.NATIVE_HEAP_PROFILE ||
this.state.selection.profileType ===
ProfileType.JAVA_HEAP_SAMPLES) &&
m(Button, {
icon: 'file_download',
intent: Intent.Primary,
onclick: () => {
this.downloadPprof();
raf.scheduleFullRedraw();
},
}),
],
},
m(
'.flamegraph-content',
this.state.result === undefined
? m(
'.loading-container',
m(
EmptyState,
{
icon: 'bar_chart',
title: 'Computing graph ...',
className: 'flamegraph-loading',
},
m(Spinner, {easing: true}),
),
)
: m(`canvas[ref=canvas]`, {
style: `height:${height}px; width:100%`,
onmousemove: (e: MouseEvent) => {
const {offsetX, offsetY} = e;
this.flamegraph.onMouseMove({x: offsetX, y: offsetY});
raf.scheduleFullRedraw();
},
onmouseout: () => {
this.flamegraph.onMouseOut();
raf.scheduleFullRedraw();
},
onclick: (e: MouseEvent) => {
if (
this.state === undefined ||
this.state.result === undefined
) {
return;
}
const {offsetX, offsetY} = e;
const cs = {...this.state.selectedCallsites};
cs[this.state.viewingOption] = this.flamegraph.onMouseClick({
x: offsetX,
y: offsetY,
});
this.state.selectedCallsites = cs;
raf.scheduleFullRedraw();
},
}),
),
),
);
}
private getTitle(): string {
const state = assertExists(this.state);
switch (state.selection.profileType) {
case ProfileType.MIXED_HEAP_PROFILE:
return 'Mixed heap profile';
case ProfileType.HEAP_PROFILE:
return 'Heap profile';
case ProfileType.NATIVE_HEAP_PROFILE:
return 'Native heap profile';
case ProfileType.JAVA_HEAP_SAMPLES:
return 'Java heap samples';
case ProfileType.JAVA_HEAP_GRAPH:
return 'Java heap graph';
case ProfileType.PERF_SAMPLE:
return 'Profile';
default:
throw new Error('unknown type');
}
}
private nodeRendering(): NodeRendering {
const state = assertExists(this.state);
const profileType = state.selection.profileType;
switch (profileType) {
case ProfileType.JAVA_HEAP_GRAPH:
if (
state.viewingOption ===
FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
state.viewingOption ===
FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
) {
return RENDER_OBJ_COUNT;
} else {
return RENDER_SELF_AND_TOTAL;
}
case ProfileType.MIXED_HEAP_PROFILE:
case ProfileType.HEAP_PROFILE:
case ProfileType.NATIVE_HEAP_PROFILE:
case ProfileType.JAVA_HEAP_SAMPLES:
case ProfileType.PERF_SAMPLE:
return RENDER_SELF_AND_TOTAL;
default:
const exhaustiveCheck: never = profileType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
}
}
private getViewingOptionButtons(): m.Children {
const ret = [];
const state = assertExists(this.state);
for (const {option, name} of viewingOptions(state.selection.profileType)) {
ret.push(
m(Button, {
label: name,
active: option === state.viewingOption,
onclick: () => {
const state = assertExists(this.state);
state.viewingOption = option;
raf.scheduleFullRedraw();
},
}),
);
}
return m(ButtonBar, ret);
}
onupdate({dom}: m.VnodeDOM<FlamegraphDetailsPanelAttrs>) {
const canvas = findRef(dom, 'canvas');
if (canvas === null || !(canvas instanceof HTMLCanvasElement)) {
return;
}
if (!this.state?.result?.renderResults) {
return;
}
canvas.width = canvas.offsetWidth * devicePixelRatio;
canvas.height = canvas.offsetHeight * devicePixelRatio;
const ctx = canvas.getContext('2d');
if (ctx === null) {
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(devicePixelRatio, devicePixelRatio);
const {offsetWidth: width, offsetHeight: height} = canvas;
const unit =
this.state.viewingOption ===
FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
this.state.viewingOption ===
FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
this.state.viewingOption ===
FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
? 'B'
: '';
this.flamegraph.draw(ctx, width, height, 0, 0, unit);
ctx.restore();
}
private static async fetchQueryResults(
engine: Engine,
cache: FlamegraphCache,
state: FlamegraphState,
) {
const table = await FlamegraphDetailsPanel.prepareViewsAndTables(
engine,
cache,
state,
);
const queryResults =
await FlamegraphDetailsPanel.getFlamegraphDataFromTables(
engine,
table,
state.viewingOption,
state.focusRegex,
);
let incomplete = false;
if (state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH) {
const it = await engine.query(`
select value from stats
where severity = 'error' and name = 'heap_graph_non_finalized_graph'
`);
incomplete = it.firstRow({value: NUM}).value > 0;
}
state.result = {
queryResults,
incomplete,
};
raf.scheduleFullRedraw();
}
private static async prepareViewsAndTables(
engine: Engine,
cache: FlamegraphCache,
state: FlamegraphState,
): Promise<string> {
const flamegraphType = getFlamegraphType(state.selection.profileType);
if (state.selection.profileType === ProfileType.PERF_SAMPLE) {
let upid: string;
let upidGroup: string;
if (state.selection.upids.length > 1) {
upid = `NULL`;
upidGroup = `'${this.serializeUpidGroup(state.selection.upids)}'`;
} else {
upid = `${state.selection.upids[0]}`;
upidGroup = `NULL`;
}
return cache.getTableName(
engine,
`
select
id,
name,
map_name,
parent_id,
depth,
cumulative_size,
cumulative_alloc_size,
cumulative_count,
cumulative_alloc_count,
size,
alloc_size,
count,
alloc_count,
source_file,
line_number
from experimental_flamegraph(
'${flamegraphType}',
NULL,
'>=${state.selection.start},<=${state.selection.end}',
${upid},
${upidGroup},
'${state.focusRegex}'
)
`,
);
}
if (
state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH &&
isHeapGraphDominatorTreeViewingOption(state.viewingOption)
) {
assertTrue(state.selection.start == state.selection.end);
return cache.getTableName(
engine,
await this.loadHeapGraphDominatorTreeQuery(
engine,
cache,
state.selection.upids[0],
state.selection.start,
),
);
}
assertTrue(state.selection.start == state.selection.end);
return cache.getTableName(
engine,
`
select
id,
name,
map_name,
parent_id,
depth,
cumulative_size,
cumulative_alloc_size,
cumulative_count,
cumulative_alloc_count,
size,
alloc_size,
count,
alloc_count,
source_file,
line_number
from experimental_flamegraph(
'${flamegraphType}',
${state.selection.start},
NULL,
${state.selection.upids[0]},
NULL,
'${state.focusRegex}'
)
`,
);
}
private static async loadHeapGraphDominatorTreeQuery(
engine: Engine,
cache: FlamegraphCache,
upid: number,
timestamp: time,
) {
const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`;
const outputQuery = `SELECT * FROM ${outputTableName}`;
if (cache.hasQuery(outputQuery)) {
return outputQuery;
}
await engine.query(`
INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
-- heap graph dominator tree with objects as nodes and all relavant
-- object self stats and dominated stats
CREATE PERFETTO TABLE _heap_graph_object_dominated AS
SELECT
node.id,
node.idom_id,
node.dominated_obj_count,
node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
node.depth,
obj.type_id,
obj.root_type,
obj.self_size + obj.native_size AS self_size
FROM heap_graph_dominator_tree node
JOIN heap_graph_object obj USING(id)
WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
-- required to accelerate the recursive cte below
ORDER BY idom_id;
-- calculate for each object node in the dominator tree the
-- HASH(path of type_id's from the super root to the object)
CREATE PERFETTO TABLE _dominator_tree_path_hash AS
WITH RECURSIVE _tree_visitor(id, path_hash) AS (
SELECT
id,
HASH(
CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '')
) AS path_hash
FROM _heap_graph_object_dominated
WHERE depth = 1
UNION ALL
SELECT
child.id,
HASH(CAST(parent.path_hash AS TEXT) || '/' || CAST(type_id AS TEXT)) AS path_hash
FROM _heap_graph_object_dominated child
JOIN _tree_visitor parent ON child.idom_id = parent.id
)
SELECT * from _tree_visitor
ORDER BY id;
-- merge object nodes with the same path into one "class type node", so the
-- end result is a tree where nodes are identified by their types and the
-- dominator relationships are preserved.
CREATE PERFETTO TABLE ${outputTableName} AS
SELECT
map.path_hash as id,
COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
node.root_type IS NOT NULL,
' [' || node.root_type || ']', ''
) AS name,
IFNULL(parent_map.path_hash, -1) AS parent_id,
node.depth - 1 AS depth,
sum(dominated_size) AS cumulative_size,
-1 AS cumulative_alloc_size,
sum(dominated_obj_count) AS cumulative_count,
-1 AS cumulative_alloc_count,
'' as map_name,
'' as source_file,
-1 as line_number,
sum(self_size) AS size,
count(*) AS count
FROM _heap_graph_object_dominated node
JOIN _dominator_tree_path_hash map USING(id)
LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
JOIN heap_graph_class cls ON node.type_id = cls.id
GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;
-- These are intermediates and not needed
DROP TABLE _heap_graph_object_dominated;
DROP TABLE _dominator_tree_path_hash;
`);
return outputQuery;
}
private static async getFlamegraphDataFromTables(
engine: Engine,
tableName: string,
viewingOption: FlamegraphViewingOption,
focusRegex: string,
) {
let orderBy = '';
let totalColumnName:
| 'cumulativeSize'
| 'cumulativeAllocSize'
| 'cumulativeCount'
| 'cumulativeAllocCount' = 'cumulativeSize';
let selfColumnName: 'size' | 'count' = 'size';
// TODO(fmayer): Improve performance so this is no longer necessary.
// Alternatively consider collapsing frames of the same label.
const maxDepth = 100;
switch (viewingOption) {
case FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
orderBy = `where cumulative_alloc_size > 0 and depth < ${maxDepth} order by depth, parent_id,
cumulative_alloc_size desc, name`;
totalColumnName = 'cumulativeAllocSize';
selfColumnName = 'size';
break;
case FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY:
orderBy = `where cumulative_count > 0 and depth < ${maxDepth} order by depth, parent_id,
cumulative_count desc, name`;
totalColumnName = 'cumulativeCount';
selfColumnName = 'count';
break;
case FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY:
orderBy = `where cumulative_alloc_count > 0 and depth < ${maxDepth} order by depth, parent_id,
cumulative_alloc_count desc, name`;
totalColumnName = 'cumulativeAllocCount';
selfColumnName = 'count';
break;
case FlamegraphViewingOption.PERF_SAMPLES_KEY:
case FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
orderBy = `where cumulative_size > 0 and depth < ${maxDepth} order by depth, parent_id,
cumulative_size desc, name`;
totalColumnName = 'cumulativeSize';
selfColumnName = 'size';
break;
case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
orderBy = `where depth < ${maxDepth} order by depth,
cumulativeCount desc, name`;
totalColumnName = 'cumulativeCount';
selfColumnName = 'count';
break;
case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
orderBy = `where depth < ${maxDepth} order by depth,
cumulativeSize desc, name`;
totalColumnName = 'cumulativeSize';
selfColumnName = 'size';
break;
default:
const exhaustiveCheck: never = viewingOption;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
break;
}
const callsites = await engine.query(`
SELECT
id as hash,
IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
IFNULL(parent_id, -1) as parentHash,
depth,
cumulative_size as cumulativeSize,
cumulative_alloc_size as cumulativeAllocSize,
cumulative_count as cumulativeCount,
cumulative_alloc_count as cumulativeAllocCount,
map_name as mapping,
size,
count,
IFNULL(source_file, '') as sourceFile,
IFNULL(line_number, -1) as lineNumber
from ${tableName}
${orderBy}
`);
const flamegraphData: CallsiteInfo[] = [];
const hashToindex: Map<number, number> = new Map();
const it = callsites.iter({
hash: NUM,
name: STR,
parentHash: NUM,
depth: NUM,
cumulativeSize: NUM,
cumulativeAllocSize: NUM,
cumulativeCount: NUM,
cumulativeAllocCount: NUM,
mapping: STR,
sourceFile: STR,
lineNumber: NUM,
size: NUM,
count: NUM,
});
for (let i = 0; it.valid(); ++i, it.next()) {
const hash = it.hash;
let name = it.name;
const parentHash = it.parentHash;
const depth = it.depth;
const totalSize = it[totalColumnName];
const selfSize = it[selfColumnName];
const mapping = it.mapping;
const highlighted =
focusRegex !== '' &&
name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
const parentId = hashToindex.has(+parentHash)
? hashToindex.get(+parentHash)!
: -1;
let location: string | undefined;
if (/[a-zA-Z]/i.test(it.sourceFile)) {
location = it.sourceFile;
if (it.lineNumber !== -1) {
location += `:${it.lineNumber}`;
}
}
if (depth === maxDepth - 1) {
name += ' [tree truncated]';
}
// Instead of hash, we will store index of callsite in this original array
// as an id of callsite. That way, we have quicker access to parent and it
// will stay unique:
hashToindex.set(hash, i);
flamegraphData.push({
id: i,
totalSize,
depth,
parentId,
name,
selfSize,
mapping,
merged: false,
highlighted,
location,
});
}
return flamegraphData;
}
private async downloadPprof() {
if (this.state === undefined) {
return;
}
const engine = this.getCurrentEngine();
if (engine === undefined) {
return;
}
try {
assertTrue(
this.state.selection.upids.length === 1,
'Native profiles can only contain one pid.',
);
const pid = await engine.query(
`select pid from process where upid = ${this.state.selection.upids[0]}`,
);
const trace = await getCurrentTrace();
convertTraceToPprofAndDownload(
trace,
pid.firstRow({pid: NUM}).pid,
this.state.selection.start,
);
} catch (error) {
throw new Error(`Failed to get current trace ${error}`);
}
}
private maybeShowModal() {
const state = assertExists(this.state);
if (state.result?.incomplete === undefined || !state.result.incomplete) {
return undefined;
}
if (globals.state.flamegraphModalDismissed) {
return undefined;
}
return m(Modal, {
title: 'The flamegraph is incomplete',
vAlign: 'TOP',
content: m(
'div',
'The current trace does not have a fully formed flamegraph',
),
buttons: [
{
text: 'Show the errors',
primary: true,
action: () => Router.navigate('#!/info'),
},
{
text: 'Skip',
action: () => {
globals.dispatch(Actions.dismissFlamegraphModal({}));
raf.scheduleFullRedraw();
},
},
],
} as ModalAttrs);
}
private static getMinSizeDisplayed(
flamegraphData: ReadonlyArray<CallsiteInfo>,
rootSize?: number,
): number {
const timeState = globals.state.frontendLocalState.visibleState;
const dur = globals.stateVisibleTime().duration;
// TODO(stevegolton): Does this actually do what we want???
let width = Duration.toSeconds(dur / timeState.resolution);
// TODO(168048193): Remove screen size hack:
width = Math.max(width, 800);
if (rootSize === undefined) {
rootSize = findRootSize(flamegraphData);
}
return (MIN_PIXEL_DISPLAYED * rootSize) / width;
}
private static serializeUpidGroup(upids: number[]) {
return new Array(upids).join();
}
private getCurrentEngine() {
const engineId = globals.getCurrentEngine()?.id;
if (engineId === undefined) return undefined;
return globals.engines.get(engineId);
}
}