blob: f06764fa946e27f579c77ff8fca71f3e6d391352 [file] [edit]
// Copyright (C) 2021 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 './styles.scss';
import type {Trace} from '../../public/trace';
import type {PerfettoPlugin} from '../../public/plugin';
import type {time} from '../../base/time';
import {NUM, STR} from '../../trace_processor/query_result';
import {createHeapProfileTrack} from './heap_profile_track';
import {TrackNode} from '../../public/workspace';
import {
createPerfettoTable,
createPerfettoView,
} from '../../trace_processor/sql_utils';
import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
import type {Track} from '../../public/track';
import {FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
import type {Store} from '../../base/store';
import {z} from 'zod';
import {ensureExists} from '../../base/assert';
import {
isProfileDescriptor,
type ProfileDescriptor,
profileDescriptor,
ProfileType,
} from './common';
import {
type AreaSelection,
areaSelectionsEqual,
type AreaSelectionTab,
} from '../../public/selection';
import {HeapProfileFlamegraphDetailsPanel} from './heap_profile_details_panel';
import {isHeapGraphIncomplete} from './incomplete_flamegraph';
import {EvtSource} from '../../base/events';
import type {App} from '../../public/app';
import type {Flag} from '../../public/feature_flag';
const EVENT_TABLE_NAME = 'heap_profile_events';
const HEAP_PROFILE_PLUGIN_STATE_SCHEMA = z.record(
z.enum(ProfileType),
z.object({
trackFlamegraphState: FLAMEGRAPH_STATE_SCHEMA.optional(),
areaSelectionFlamegraphState: FLAMEGRAPH_STATE_SCHEMA.optional(),
}),
);
type HeapProfilePluginState = z.infer<typeof HEAP_PROFILE_PLUGIN_STATE_SCHEMA>;
function trackUri(upid: number, type: string): string {
return `/process_${upid}/${type}_heap_profile`;
}
export default class HeapProfilePlugin implements PerfettoPlugin {
static readonly id = 'dev.perfetto.HeapProfile';
static readonly dependencies = [ProcessThreadGroupsPlugin];
// Defined here (the Heap Dump Explorer's dependency) so both the Heap
// Dump Explorer and HeapProfile can read the flag without a circular
// import.
static openHeapDumpExplorerByDefaultFlag: Flag;
static onActivate(app: App) {
HeapProfilePlugin.openHeapDumpExplorerByDefaultFlag =
app.featureFlags.register({
id: 'openHeapDumpExplorerByDefault',
name: 'Open Heap Dump Explorer by default',
description:
'When enabled, traces that contain Java heap-graph data and no ' +
'common timeline data (slice / sched) open directly in the Heap ' +
'Dump Explorer instead of the timeline.',
defaultValue: true,
});
}
private readonly trackMap = new Map<string, Track>();
private store?: Store<HeapProfilePluginState>;
private readonly nodeSelectedEvt = new EvtSource<{
pathHashes: string;
isDominator: boolean;
upid: number;
ts: time;
}>();
registerOnNodeSelectedListener(
cb: (args: {
pathHashes: string;
isDominator: boolean;
upid: number;
ts: time;
}) => void,
): Disposable {
return this.nodeSelectedEvt.addListener(cb);
}
private migrateHeapProfilePluginState(init: unknown): HeapProfilePluginState {
const result = HEAP_PROFILE_PLUGIN_STATE_SCHEMA.safeParse(init);
return (
result.data ?? {
[ProfileType.NATIVE_HEAP_PROFILE]: {},
[ProfileType.GENERIC_HEAP_PROFILE]: {},
[ProfileType.JAVA_HEAP_SAMPLES]: {},
[ProfileType.JAVA_HEAP_GRAPH]: {},
[ProfileType.OOME_CALLSTACK]: {},
}
);
}
async onTraceLoad(trace: Trace): Promise<void> {
this.store = trace.mountStore(HeapProfilePlugin.id, (init) =>
this.migrateHeapProfilePluginState(init),
);
await this.createHeapProfileTable(trace);
// Ordered by priority, so the tracks and the area-selection flamegraph tabs
// registered below come out in the right order.
const heapTypes = await this.getHeapTypes(trace);
await this.addProcessTracks(trace, heapTypes);
// For applicable heap types, register an area selection
for (const heapType of heapTypes) {
const descriptor = profileDescriptor(heapType.type);
if (
descriptor.type === ProfileType.JAVA_HEAP_GRAPH ||
descriptor.type === ProfileType.OOME_CALLSTACK
) {
// There's no area selection for java heap dumps or OOME callstacks.
continue;
}
trace.selection.registerAreaSelectionTab(
this.heapProfileSelectionHandler(trace, descriptor, heapType.priority),
);
}
trace.onTraceReady.addListener(async () => {
await this.selectHeapProfile(trace);
});
}
private async createHeapProfileTable(trace: Trace) {
await trace.engine.query(
'INCLUDE PERFETTO MODULE android.memory.heap_graph.oome;',
);
await trace.engine.query(
'INCLUDE PERFETTO MODULE android.memory.heap_profile.intervals;',
);
await createPerfettoTable({
engine: trace.engine,
name: EVENT_TABLE_NAME,
as: `
WITH events AS (
-- heap_graph already has exactly one row per dump (with its own id),
-- so read it directly rather than de-duplicating the much larger
-- heap_graph_object table down to one row per dump.
SELECT
id,
ts,
upid,
0 AS dur,
0 AS depth,
'java_heap_graph' AS type,
NULL AS retained,
NULL AS allocated,
NULL AS delta
FROM heap_graph
UNION ALL
-- Draw each dump over its profiling interval (see the module). The
-- byte totals are surfaced in the slice name / tooltip.
SELECT
id,
ts,
upid,
dur,
0 AS depth,
'heap_profile:' || heap_name AS type,
retained,
allocated,
delta
FROM _android_heap_profile_intervals
UNION ALL
SELECT
id,
ts,
upid,
0 AS dur,
0 AS depth,
'oome_callstack' AS type,
NULL AS retained,
NULL AS allocated,
NULL AS delta
FROM heap_graph
WHERE dump_reason = 'OOME'
)
-- Display/selection priority: lower comes first. This is the single
-- source of truth; TypeScript reads it back for track ordering.
SELECT
*,
CASE type
WHEN 'java_heap_graph' THEN 0
WHEN 'heap_profile:libc.malloc' THEN 1
WHEN 'heap_profile:com.android.art' THEN 2
WHEN 'oome_callstack' THEN 4
ELSE 3
END AS priority
FROM events
`,
});
}
private async getHeapTypes(
trace: Trace,
): Promise<ReadonlyArray<{type: string; priority: number}>> {
const heapTypesResult = await trace.engine.query(`
SELECT DISTINCT type, priority
FROM ${EVENT_TABLE_NAME}
ORDER BY priority
`);
const heapTypes = [];
for (
const it = heapTypesResult.iter({type: STR, priority: NUM});
it.valid();
it.next()
) {
heapTypes.push({type: it.type, priority: it.priority});
}
return heapTypes;
}
private async addProcessTracks(
trace: Trace,
heapTypes: ReadonlyArray<{type: string; priority: number}>,
) {
const trackGroupsPlugin = trace.plugins.getPlugin(
ProcessThreadGroupsPlugin,
);
const incomplete = await this.getIncomplete(trace);
let typeIdx = 0;
for (const heapType of heapTypes) {
// Create a view for this particular type
const viewName = `${EVENT_TABLE_NAME}_view_${typeIdx}`;
await createPerfettoView({
engine: trace.engine,
name: viewName,
as: `
SELECT *
FROM ${EVENT_TABLE_NAME}
WHERE type = '${heapType.type}'
`,
});
typeIdx++;
const upidResult = await trace.engine.query(`
SELECT DISTINCT upid
FROM ${viewName}
`);
const upids = [];
for (const it = upidResult.iter({upid: NUM}); it.valid(); it.next()) {
upids.push(it.upid);
}
for (const upid of upids) {
const group = trackGroupsPlugin.getGroupForProcess(upid);
if (!group) continue;
const store = ensureExists(this.store);
const uri = trackUri(upid, heapType.type);
const descriptor = profileDescriptor(heapType.type);
const track: Track = {
uri,
tags: {
upid: upid,
kinds: [heapType.type],
},
renderer: createHeapProfileTrack(
trace,
uri,
viewName,
upid,
incomplete,
store.state[descriptor.type].trackFlamegraphState,
(state) => {
store.edit((draft) => {
draft[descriptor.type].trackFlamegraphState = state;
});
},
heapType.type === 'java_heap_graph'
? (args) => this.nodeSelectedEvt.notify(args)
: undefined,
),
};
trace.tracks.registerTrack(track);
this.trackMap.set(uri, track);
const trackNode = new TrackNode({
uri,
name: descriptor.label,
sortOrder: -30 + heapType.priority,
});
group.addChildInOrder(trackNode);
}
}
}
private async getIncomplete(trace: Trace): Promise<boolean> {
return isHeapGraphIncomplete(trace);
}
private async selectHeapProfile(ctx: Trace) {
const hdeWillTakeOver =
HeapProfilePlugin.openHeapDumpExplorerByDefaultFlag.get() &&
!(await traceHasTimelineData(ctx));
const javaHeapGraphFilter = hdeWillTakeOver
? `WHERE type != 'java_heap_graph' AND type != 'oome_callstack'`
: '';
const result = await ctx.engine.query(`
SELECT id, upid, type
FROM ${EVENT_TABLE_NAME}
${javaHeapGraphFilter}
ORDER BY priority, ts
LIMIT 1
`);
const iter = result.maybeFirstRow({id: NUM, upid: NUM, type: STR});
if (!iter) return;
const uri = trackUri(iter.upid, iter.type);
const track = this.trackMap.get(uri);
if (!track) return;
const profileType = profileDescriptor(iter.type).type;
if (
profileType === ProfileType.JAVA_HEAP_GRAPH ||
profileType === ProfileType.OOME_CALLSTACK
) {
ctx.selection.selectTrackEvent(track.uri, iter.id);
} else {
// Select every area-selectable heap track for this process so each heap
// flamegraph tab has its track in the selection and actually renders.
const tracksResult = await ctx.engine.query(`
SELECT DISTINCT type
FROM ${EVENT_TABLE_NAME}
WHERE upid = ${iter.upid}
AND type != 'java_heap_graph'
AND type != 'oome_callstack'
`);
const trackUris = [];
for (const it = tracksResult.iter({type: STR}); it.valid(); it.next()) {
const trackUriForType = trackUri(iter.upid, it.type);
if (this.trackMap.has(trackUriForType)) {
trackUris.push(trackUriForType);
}
}
ctx.selection.selectArea({
start: ctx.traceInfo.start,
end: ctx.traceInfo.end,
trackUris: trackUris.length > 0 ? trackUris : [uri],
});
}
}
private heapProfileSelectionHandler(
trace: Trace,
descriptor: ProfileDescriptor,
priority: number,
): AreaSelectionTab {
let previousSelection: AreaSelection | undefined;
let flamegraphPanel: HeapProfileFlamegraphDetailsPanel | undefined;
return {
id: `heap_profiler_flamegraph_selection_${descriptor.heapName}`,
name: `${descriptor.label} flamegraph`,
// AreaSelectionTab priority is "higher first", so negate our
// "lower first" priority to keep the same order (native before ART).
priority: -priority,
render: (selection: AreaSelection) => {
const store = ensureExists(this.store);
const selectionChanged =
previousSelection === undefined ||
!areaSelectionsEqual(previousSelection, selection);
previousSelection = selection;
if (selectionChanged) {
const upids = matchingTracks(selection, descriptor.type).map(
(track) => track.tags!.upid,
);
// For the time being support selecting exactly one process.
flamegraphPanel =
upids.length !== 1
? undefined
: new HeapProfileFlamegraphDetailsPanel(
trace,
false,
upids[0]!,
descriptor,
selection.start,
selection.end,
store.state[descriptor.type].areaSelectionFlamegraphState,
(state) => {
store.edit((draft) => {
draft[descriptor.type].areaSelectionFlamegraphState =
state;
});
},
);
}
// Hide the tab entirely when this selection has no flamegraph for this
// heap type, rather than showing a tab handle with empty content.
if (flamegraphPanel === undefined) {
return undefined;
}
return {isLoading: false, content: flamegraphPanel.render()};
},
};
}
}
function matchingTracks(
selection: AreaSelection,
profileType: ProfileType,
): Track[] {
return selection.tracks.filter((track) => {
for (const kind of track.tags?.kinds || []) {
if (
isProfileDescriptor(kind) &&
profileDescriptor(kind).type === profileType
) {
return true;
}
}
return false;
});
}
export async function traceHasTimelineData(ctx: Trace): Promise<boolean> {
// We treat a small number of slices as not having timeline data cos
// there are some inevitable slices like trace triggers on oom etc.
const res = await ctx.engine.query(`
SELECT
(SELECT count(id) FROM slice) > 50 OR
EXISTS(SELECT 1 FROM sched) OR
EXISTS(SELECT 1 FROM heap_profile_allocation) OR
EXISTS(SELECT 1 FROM perf_sample)
AS res
`);
return res.firstRow({res: NUM}).res > 0;
}