blob: 5b4525654d1871131f29dfe00431d8038f12d253 [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this 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 from 'mithril';
import {assertExists, assertFalse} from '../../base/logging';
import {time} from '../../base/time';
import {
QueryFlamegraph,
QueryFlamegraphMetric,
metricsFromTableOrSubquery,
} from '../../public/lib/query_flamegraph';
import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter';
import {Timestamp} from '../../public/lib/widgets/timestamp';
import {
TrackEventDetailsPanel,
TrackEventDetailsPanelSerializeArgs,
} from '../../public/details_panel';
import {ProfileType, TrackEventSelection} from '../../public/selection';
import {Trace} from '../../public/trace';
import {NUM} from '../../trace_processor/query_result';
import {Button} from '../../widgets/button';
import {Intent} from '../../widgets/common';
import {DetailsShell} from '../../widgets/details_shell';
import {Icon} from '../../widgets/icon';
import {Modal, showModal} from '../../widgets/modal';
import {Popup} from '../../widgets/popup';
import {
Flamegraph,
FLAMEGRAPH_STATE_SCHEMA,
FlamegraphState,
} from '../../widgets/flamegraph';
interface Props {
ts: time;
type: ProfileType;
}
export class HeapProfileFlamegraphDetailsPanel
implements TrackEventDetailsPanel
{
private readonly flamegraph: QueryFlamegraph;
private readonly props: Props;
private flamegraphModalDismissed = false;
readonly serialization: TrackEventDetailsPanelSerializeArgs<FlamegraphState>;
constructor(
private trace: Trace,
private heapGraphIncomplete: boolean,
private upid: number,
sel: TrackEventSelection,
) {
const {profileType, ts} = sel;
const metrics = flamegraphMetrics(assertExists(profileType), ts, upid);
this.serialization = {
schema: FLAMEGRAPH_STATE_SCHEMA,
state: Flamegraph.createDefaultState(metrics),
};
this.flamegraph = new QueryFlamegraph(trace, metrics, this.serialization);
this.props = {ts, type: assertExists(profileType)};
}
render() {
const {type, ts} = this.props;
return m(
'.flamegraph-profile',
this.maybeShowModal(this.trace, type, this.heapGraphIncomplete),
m(
DetailsShell,
{
fillParent: true,
title: m(
'.title',
getFlamegraphTitle(type),
type === 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: [],
buttons: [
m('.time', `Snapshot time: `, m(Timestamp, {ts})),
(type === ProfileType.NATIVE_HEAP_PROFILE ||
type === ProfileType.JAVA_HEAP_SAMPLES) &&
m(Button, {
icon: 'file_download',
intent: Intent.Primary,
onclick: () => {
downloadPprof(this.trace, this.upid, ts);
this.trace.scheduleFullRedraw();
},
}),
],
},
assertExists(this.flamegraph).render(),
),
);
}
private maybeShowModal(
trace: Trace,
type: ProfileType,
heapGraphIncomplete: boolean,
) {
if (type !== ProfileType.JAVA_HEAP_GRAPH || !heapGraphIncomplete) {
return undefined;
}
if (this.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: () => trace.navigate('#!/info'),
},
{
text: 'Skip',
action: () => {
this.flamegraphModalDismissed = true;
trace.scheduleFullRedraw();
},
},
],
});
}
}
function flamegraphMetrics(
type: ProfileType,
ts: time,
upid: number,
): ReadonlyArray<QueryFlamegraphMetric> {
switch (type) {
case ProfileType.NATIVE_HEAP_PROFILE:
return flamegraphMetricsForHeapProfile(ts, upid, [
{
name: 'Unreleased Malloc Size',
unit: 'B',
columnName: 'self_size',
},
{
name: 'Unreleased Malloc Count',
unit: '',
columnName: 'self_count',
},
{
name: 'Total Malloc Size',
unit: 'B',
columnName: 'self_alloc_size',
},
{
name: 'Total Malloc Count',
unit: '',
columnName: 'self_alloc_count',
},
]);
case ProfileType.HEAP_PROFILE:
return flamegraphMetricsForHeapProfile(ts, upid, [
{
name: 'Unreleased Size',
unit: 'B',
columnName: 'self_size',
},
{
name: 'Unreleased Count',
unit: '',
columnName: 'self_count',
},
{
name: 'Total Size',
unit: 'B',
columnName: 'self_alloc_size',
},
{
name: 'Total Count',
unit: '',
columnName: 'self_alloc_count',
},
]);
case ProfileType.JAVA_HEAP_SAMPLES:
return flamegraphMetricsForHeapProfile(ts, upid, [
{
name: 'Unreleased Allocation Size',
unit: 'B',
columnName: 'self_size',
},
{
name: 'Unreleased Allocation Count',
unit: '',
columnName: 'self_count',
},
]);
case ProfileType.MIXED_HEAP_PROFILE:
return flamegraphMetricsForHeapProfile(ts, upid, [
{
name: 'Unreleased Allocation Size (malloc + java)',
unit: 'B',
columnName: 'self_size',
},
{
name: 'Unreleased Allocation Count (malloc + java)',
unit: '',
columnName: 'self_count',
},
]);
case ProfileType.JAVA_HEAP_GRAPH:
return [
{
name: 'Object Size',
unit: 'B',
dependencySql:
'include perfetto module android.memory.heap_graph.class_tree;',
statement: `
select
id,
parent_id as parentId,
ifnull(name, '[Unknown]') as name,
root_type,
self_size as value,
self_count
from _heap_graph_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
],
aggregatableProperties: [
{
name: 'self_count',
displayName: 'Self Count',
mergeAggregation: 'SUM',
},
],
},
{
name: 'Object Count',
unit: '',
dependencySql:
'include perfetto module android.memory.heap_graph.class_tree;',
statement: `
select
id,
parent_id as parentId,
ifnull(name, '[Unknown]') as name,
root_type,
self_size,
self_count as value
from _heap_graph_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
],
},
{
name: 'Dominated Object Size',
unit: 'B',
dependencySql:
'include perfetto module android.memory.heap_graph.dominator_class_tree;',
statement: `
select
id,
parent_id as parentId,
ifnull(name, '[Unknown]') as name,
root_type,
self_size as value,
self_count
from _heap_graph_dominator_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
],
aggregatableProperties: [
{
name: 'self_count',
displayName: 'Self Count',
mergeAggregation: 'SUM',
},
],
},
{
name: 'Dominated Object Count',
unit: '',
dependencySql:
'include perfetto module android.memory.heap_graph.dominator_class_tree;',
statement: `
select
id,
parent_id as parentId,
ifnull(name, '[Unknown]') as name,
root_type,
self_size,
self_count as value
from _heap_graph_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
],
},
];
case ProfileType.PERF_SAMPLE:
throw new Error('Perf sample not supported');
}
}
function flamegraphMetricsForHeapProfile(
ts: time,
upid: number,
metrics: {name: string; unit: string; columnName: string}[],
) {
return metricsFromTableOrSubquery(
`
(
select
id,
parent_id as parentId,
name,
mapping_name,
source_file,
cast(line_number AS text) as line_number,
self_size,
self_count,
self_alloc_size,
self_alloc_count
from _android_heap_profile_callstacks_for_allocations!((
select
callsite_id,
size,
count,
max(size, 0) as alloc_size,
max(count, 0) as alloc_count
from heap_profile_allocation a
where a.ts <= ${ts} and a.upid = ${upid}
))
)
`,
metrics,
'include perfetto module android.memory.heap_profile.callstacks',
[{name: 'mapping_name', displayName: 'Mapping'}],
[
{
name: 'source_file',
displayName: 'Source File',
mergeAggregation: 'ONE_OR_NULL',
},
{
name: 'line_number',
displayName: 'Line Number',
mergeAggregation: 'ONE_OR_NULL',
},
],
);
}
function getFlamegraphTitle(type: ProfileType) {
switch (type) {
case ProfileType.HEAP_PROFILE:
return 'Heap profile';
case ProfileType.JAVA_HEAP_GRAPH:
return 'Java heap graph';
case ProfileType.JAVA_HEAP_SAMPLES:
return 'Java heap samples';
case ProfileType.MIXED_HEAP_PROFILE:
return 'Mixed heap profile';
case ProfileType.NATIVE_HEAP_PROFILE:
return 'Native heap profile';
case ProfileType.PERF_SAMPLE:
assertFalse(false, 'Perf sample not supported');
return 'Impossible';
}
}
async function downloadPprof(trace: Trace, upid: number, ts: time) {
const pid = await trace.engine.query(
`select pid from process where upid = ${upid}`,
);
if (!trace.traceInfo.downloadable) {
showModal({
title: 'Download not supported',
content: m('div', 'This trace file does not support downloads'),
});
}
const blob = await trace.getTraceFile();
convertTraceToPprofAndDownload(blob, pid.firstRow({pid: NUM}).pid, ts);
}