blob: 676a6b4480df49de6ff1f952831baf76169b14f6 [file]
// 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 {Trace} from '../../public/trace';
import {PerfettoPlugin} from '../../public/plugin';
import {NUM} from '../../trace_processor/query_result';
import {createHeapProfileTrack} from './heap_profile_track';
import {TrackNode} from '../../public/workspace';
import {createPerfettoTable} from '../../trace_processor/sql_utils';
import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
import {Track} from '../../public/track';
import {FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
import {Store} from '../../base/store';
import {z} from 'zod';
import {assertExists} from '../../base/logging';
const EVENT_TABLE_NAME = 'heap_profile_events';
const HEAP_PROFILE_PLUGIN_STATE_SCHEMA = z.object({
detailsPanelFlamegraphState: FLAMEGRAPH_STATE_SCHEMA.optional(),
});
type HeapProfilePluginState = z.infer<typeof HEAP_PROFILE_PLUGIN_STATE_SCHEMA>;
export default class HeapProfilePlugin implements PerfettoPlugin {
static readonly id = 'dev.perfetto.HeapProfile';
static readonly dependencies = [ProcessThreadGroupsPlugin];
private readonly trackMap = new Map<number, Track>();
private store?: Store<HeapProfilePluginState>;
private migrateHeapProfilePluginState(init: unknown): HeapProfilePluginState {
const result = HEAP_PROFILE_PLUGIN_STATE_SCHEMA.safeParse(init);
return result.data ?? {};
}
async onTraceLoad(trace: Trace): Promise<void> {
this.store = trace.mountStore(HeapProfilePlugin.id, (init) =>
this.migrateHeapProfilePluginState(init),
);
await this.createHeapProfileTable(trace);
await this.addProcessTracks(trace);
trace.onTraceReady.addListener(async () => {
await this.selectFirstHeapProfile(trace);
});
}
private async createHeapProfileTable(trace: Trace) {
await createPerfettoTable({
engine: trace.engine,
name: EVENT_TABLE_NAME,
as: `
SELECT
MIN(id) as id,
graph_sample_ts AS ts,
upid,
0 AS dur,
0 AS depth,
'graph' AS type
FROM heap_graph_object
GROUP BY graph_sample_ts, upid
UNION ALL
SELECT
MIN(id) as id,
ts,
upid,
0 AS dur,
0 AS depth,
'heap_profile:' || GROUP_CONCAT(DISTINCT heap_name) AS type
FROM heap_profile_allocation
GROUP BY ts, upid
`,
});
}
private async addProcessTracks(trace: Trace) {
const trackGroupsPlugin = trace.plugins.getPlugin(
ProcessThreadGroupsPlugin,
);
const incomplete = await this.getIncomplete(trace);
const result = await trace.engine.query(`
SELECT DISTINCT
upid
FROM ${EVENT_TABLE_NAME}
`);
for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const uri = `/process_${upid}/heap_profile`;
const store = assertExists(this.store);
const track: Track = {
uri,
tags: {
upid,
},
renderer: createHeapProfileTrack(
trace,
uri,
EVENT_TABLE_NAME,
upid,
incomplete,
store.state.detailsPanelFlamegraphState,
(state) => {
store.edit((draft) => {
draft.detailsPanelFlamegraphState = state;
});
},
),
};
trace.tracks.registerTrack(track);
this.trackMap.set(upid, track);
const group = trackGroupsPlugin.getGroupForProcess(upid);
const trackNode = new TrackNode({
uri,
name: 'Heap Profile',
sortOrder: -30,
});
group?.addChildInOrder(trackNode);
}
}
private async getIncomplete(trace: Trace): Promise<boolean> {
const it = await trace.engine.query(`
SELECT value
FROM stats
WHERE name = 'heap_graph_non_finalized_graph'
`);
const incomplete = it.firstRow({value: NUM}).value > 0;
return incomplete;
}
private async selectFirstHeapProfile(ctx: Trace) {
// Select the first sample from each track
const result = await ctx.engine.query(`
SELECT
id,
upid
FROM ${EVENT_TABLE_NAME}
ORDER BY ts
LIMIT 1
`);
const iter = result.maybeFirstRow({id: NUM, upid: NUM});
if (!iter) return;
const track = this.trackMap.get(iter.upid);
if (!track) return;
ctx.selection.selectTrackEvent(track.uri, iter.id);
}
}