blob: b6e43f02e50e99f3eb47487284bf319f58193f59 [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 {createPerfettoTable} from '../../trace_processor/sql_utils';
import {extensions} from '../../components/extensions';
import {time} from '../../base/time';
import {uuidv4Sql} from '../../base/uuid';
import {
QueryFlamegraph,
QueryFlamegraphMetric,
metricsFromTableOrSubquery,
} from '../../components/query_flamegraph';
import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter';
import {Timestamp} from '../../components/widgets/timestamp';
import {
TrackEventDetailsPanel,
TrackEventDetailsPanelSerializeArgs,
} from '../../public/details_panel';
import {Trace} from '../../public/trace';
import {NUM} from '../../trace_processor/query_result';
import {Button, ButtonVariant} 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,
FlamegraphOptionalAction,
} from '../../widgets/flamegraph';
import {SqlTableDescription} from '../../components/widgets/sql/table/table_description';
import {StandardColumn} from '../../components/widgets/sql/table/columns';
export enum ProfileType {
HEAP_PROFILE = 'heap_profile',
MIXED_HEAP_PROFILE = 'heap_profile:com.android.art,libc.malloc',
NATIVE_HEAP_PROFILE = 'heap_profile:libc.malloc',
JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
JAVA_HEAP_GRAPH = 'graph',
PERF_SAMPLE = 'perf',
INSTRUMENTS_SAMPLE = 'instruments',
}
export function profileType(s: string): ProfileType {
if (s === 'heap_profile:libc.malloc,com.android.art') {
s = 'heap_profile:com.android.art,libc.malloc';
}
if (Object.values(ProfileType).includes(s as ProfileType)) {
return s as ProfileType;
}
if (s.startsWith('heap_profile')) {
return ProfileType.HEAP_PROFILE;
}
throw new Error('Unknown type ${s}');
}
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,
profileType: ProfileType,
ts: time,
) {
const metrics = flamegraphMetrics(trace, 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: 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,
variant: ButtonVariant.Filled,
onclick: () => {
downloadPprof(this.trace, this.upid, ts);
},
}),
],
},
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;
},
},
],
});
}
}
function flamegraphMetrics(
trace: Trace,
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: 'Total Allocation Size',
unit: 'B',
columnName: 'self_size',
},
{
name: 'Total Allocation Count',
unit: '',
columnName: 'self_count',
},
]);
case ProfileType.MIXED_HEAP_PROFILE:
return flamegraphMetricsForHeapProfile(ts, upid, [
{
name: 'Allocation Size (malloc + java)',
unit: 'B',
columnName: 'self_size',
},
{
name: '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,
heap_type,
self_size as value,
self_count,
path_hash_stable
from _heap_graph_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
{name: 'heap_type', displayName: 'Heap Type'},
],
aggregatableProperties: [
{
name: 'self_count',
displayName: 'Self Count',
mergeAggregation: 'SUM',
},
{
name: 'path_hash_stable',
displayName: 'Path Hash',
mergeAggregation: 'CONCAT_WITH_COMMA',
isVisible: false,
},
],
optionalNodeActions: getHeapGraphNodeOptionalActions(trace, false),
optionalRootActions: getHeapGraphRootOptionalActions(trace, false),
},
{
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,
heap_type,
self_size,
self_count as value,
path_hash_stable
from _heap_graph_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
{name: 'heap_type', displayName: 'Heap Type'},
],
aggregatableProperties: [
{
name: 'path_hash_stable',
displayName: 'Path Hash',
mergeAggregation: 'CONCAT_WITH_COMMA',
isVisible: false,
},
],
optionalNodeActions: getHeapGraphNodeOptionalActions(trace, false),
optionalRootActions: getHeapGraphRootOptionalActions(trace, false),
},
{
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,
heap_type,
self_size as value,
self_count,
path_hash_stable
from _heap_graph_dominator_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
{name: 'heap_type', displayName: 'Heap Type'},
],
aggregatableProperties: [
{
name: 'self_count',
displayName: 'Self Count',
mergeAggregation: 'SUM',
},
{
name: 'path_hash_stable',
displayName: 'Path Hash',
mergeAggregation: 'CONCAT_WITH_COMMA',
isVisible: false,
},
],
optionalNodeActions: getHeapGraphNodeOptionalActions(trace, true),
optionalRootActions: getHeapGraphRootOptionalActions(trace, true),
},
{
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,
heap_type,
self_size,
self_count as value,
path_hash_stable
from _heap_graph_dominator_class_tree
where graph_sample_ts = ${ts} and upid = ${upid}
`,
unaggregatableProperties: [
{name: 'root_type', displayName: 'Root Type'},
{name: 'heap_type', displayName: 'Heap Type'},
],
aggregatableProperties: [
{
name: 'path_hash_stable',
displayName: 'Path Hash',
mergeAggregation: 'CONCAT_WITH_COMMA',
isVisible: false,
},
],
optionalNodeActions: getHeapGraphNodeOptionalActions(trace, true),
optionalRootActions: getHeapGraphRootOptionalActions(trace, true),
},
];
case ProfileType.PERF_SAMPLE:
throw new Error('Perf sample not supported');
case ProfileType.INSTRUMENTS_SAMPLE:
throw new Error('Instruments 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';
case ProfileType.INSTRUMENTS_SAMPLE:
assertFalse(false, 'Instruments 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'),
});
return;
}
const blob = await trace.getTraceFile();
convertTraceToPprofAndDownload(blob, pid.firstRow({pid: NUM}).pid, ts);
}
function getHeapGraphObjectReferencesView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}object_references`,
columns: [
new StandardColumn('path_hash'),
new StandardColumn('outgoing_reference_count'),
new StandardColumn('class_name'),
new StandardColumn('self_size'),
new StandardColumn('native_size'),
new StandardColumn('heap_type'),
new StandardColumn('root_type'),
new StandardColumn('reachable'),
],
};
}
function getHeapGraphIncomingReferencesView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}incoming_references`,
columns: [
new StandardColumn('path_hash'),
new StandardColumn('class_name'),
new StandardColumn('field_name'),
new StandardColumn('field_type_name'),
new StandardColumn('self_size'),
new StandardColumn('native_size'),
new StandardColumn('heap_type'),
new StandardColumn('root_type'),
new StandardColumn('reachable'),
],
};
}
function getHeapGraphOutgoingReferencesView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}outgoing_references`,
columns: [
new StandardColumn('path_hash'),
new StandardColumn('class_name'),
new StandardColumn('field_name'),
new StandardColumn('field_type_name'),
new StandardColumn('self_size'),
new StandardColumn('native_size'),
new StandardColumn('heap_type'),
new StandardColumn('root_type'),
new StandardColumn('reachable'),
],
};
}
function getHeapGraphRetainingObjectCountsView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}retaining_object_counts`,
columns: [
new StandardColumn('class_name'),
new StandardColumn('count'),
new StandardColumn('total_size'),
new StandardColumn('total_native_size'),
new StandardColumn('heap_type'),
new StandardColumn('root_type'),
new StandardColumn('reachable'),
],
};
}
function getHeapGraphRetainedObjectCountsView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}retained_object_counts`,
columns: [
new StandardColumn('class_name'),
new StandardColumn('count'),
new StandardColumn('total_size'),
new StandardColumn('total_native_size'),
new StandardColumn('heap_type'),
new StandardColumn('root_type'),
new StandardColumn('reachable'),
],
};
}
function getHeapGraphDuplicateObjectsView(
isDominator: boolean,
): SqlTableDescription {
return {
name: `_heap_graph${tableModifier(isDominator)}duplicate_objects`,
columns: [
new StandardColumn('class_name'),
new StandardColumn('path_count'),
new StandardColumn('object_count'),
new StandardColumn('total_size'),
new StandardColumn('total_native_size'),
],
};
}
function getHeapGraphNodeOptionalActions(
trace: Trace,
isDominator: boolean,
): ReadonlyArray<FlamegraphOptionalAction> {
return [
{
name: 'Objects',
execute: async (kv: ReadonlyMap<string, string>) => {
const value = kv.get('path_hash_stable');
if (value !== undefined) {
const uuid = uuidv4Sql();
const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`;
await createPerfettoTable(
trace.engine,
pathHashTableName,
pathHashesToTableStatement(value),
);
const tableName = `_heap_graph${tableModifier(isDominator)}object_references`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`;
const macroExpr = `_heap_graph_object_references_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphObjectReferencesView(isDominator),
});
}
},
},
// Group for Direct References
{
name: 'Direct References',
// No execute function for parent menu items
subActions: [
{
name: 'Incoming references',
execute: async (kv: ReadonlyMap<string, string>) => {
const value = kv.get('path_hash_stable');
if (value !== undefined) {
const uuid = uuidv4Sql();
const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`;
await createPerfettoTable(
trace.engine,
pathHashTableName,
pathHashesToTableStatement(value),
);
const tableName = `_heap_graph${tableModifier(isDominator)}incoming_references`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`;
const macroExpr = `_heap_graph_incoming_references_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphIncomingReferencesView(isDominator),
});
}
},
},
{
name: 'Outgoing references',
execute: async (kv: ReadonlyMap<string, string>) => {
const value = kv.get('path_hash_stable');
if (value !== undefined) {
const uuid = uuidv4Sql();
const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`;
await createPerfettoTable(
trace.engine,
pathHashTableName,
pathHashesToTableStatement(value),
);
const tableName = `_heap_graph${tableModifier(isDominator)}outgoing_references`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`;
const macroExpr = `_heap_graph_outgoing_references_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphOutgoingReferencesView(isDominator),
});
}
},
},
],
},
// Group for Indirect References
{
name: 'Indirect References',
// No execute function for parent menu items
subActions: [
{
name: 'Retained objects',
execute: async (kv: ReadonlyMap<string, string>) => {
const value = kv.get('path_hash_stable');
if (value !== undefined) {
const uuid = uuidv4Sql();
const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`;
await createPerfettoTable(
trace.engine,
pathHashTableName,
pathHashesToTableStatement(value),
);
const tableName = `_heap_graph${tableModifier(isDominator)}retained_object_counts`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`;
const macroExpr = `_heap_graph_retained_object_count_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphRetainedObjectCountsView(isDominator),
});
}
},
},
{
name: 'Retaining objects',
execute: async (kv: ReadonlyMap<string, string>) => {
const value = kv.get('path_hash_stable');
if (value !== undefined) {
const uuid = uuidv4Sql();
const pathHashTableName = `_heap_graph_filtered_path_hashes_${uuid}`;
await createPerfettoTable(
trace.engine,
pathHashTableName,
pathHashesToTableStatement(value),
);
const tableName = `_heap_graph${tableModifier(isDominator)}retaining_object_counts`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes, ${pathHashTableName}`;
const macroExpr = `_heap_graph_retaining_object_count_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO TABLE ${tableName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphRetainingObjectCountsView(isDominator),
});
}
},
},
],
},
];
}
function getHeapGraphRootOptionalActions(
trace: Trace,
isDominator: boolean,
): ReadonlyArray<FlamegraphOptionalAction> {
return [
{
name: 'Reference paths by class',
execute: async (_kv: ReadonlyMap<string, string>) => {
const viewName = `_heap_graph${tableModifier(isDominator)}duplicate_objects`;
const macroArgs = `_heap_graph${tableModifier(isDominator)}path_hashes`;
const macroExpr = `_heap_graph_duplicate_objects_agg!(${macroArgs})`;
const statement = `CREATE OR REPLACE PERFETTO VIEW ${viewName} AS SELECT * FROM ${macroExpr};`;
// Create view to be returned
await trace.engine.query(statement);
extensions.addLegacySqlTableTab(trace, {
table: getHeapGraphDuplicateObjectsView(isDominator),
});
},
},
];
}
function tableModifier(isDominator: boolean): string {
return isDominator ? '_dominator_' : '_';
}
function pathHashesToTableStatement(commaSeparatedValues: string): string {
// Split the string by commas and trim whitespace
const individualValues = commaSeparatedValues.split(',').map((v) => v.trim());
// Wrap each value with parentheses
const wrappedValues = individualValues.map((value) => `(${value})`);
// Join with commas and create the complete WITH clause
const valuesClause = `values${wrappedValues.join(', ')}`;
return `WITH temp_table(path_hash) AS (${valuesClause}) SELECT * FROM temp_table`;
}