blob: 5d0cc4539ee54ed3f41c86ffaca75f1c266295e6 [file]
// Copyright (C) 2026 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 type {Engine} from '../../../trace_processor/engine';
import type {SqlValue} from '../../../trace_processor/query_result';
import {DataGrid} from '../../../components/widgets/datagrid/datagrid';
import {SQLDataSource} from '../../../components/widgets/datagrid/sql_data_source';
import {createSimpleSchema} from '../../../components/widgets/datagrid/sql_schema';
import type {SchemaRegistry} from '../../../components/widgets/datagrid/datagrid_schema';
import type {Filter} from '../../../components/widgets/datagrid/model';
import {
type NavFn,
sizeRenderer,
countRenderer,
RowCounter,
COL_INFO,
colHeader,
} from '../components';
import * as queries from '../queries';
import {dumpFilterSql, type HeapDump} from '../queries';
interface ClassesViewAttrs {
readonly engine: Engine;
readonly activeDump: HeapDump;
readonly navigate: NavFn;
readonly clearNavParam: (key: string) => void;
readonly initialRootClass?: string;
}
const PREAMBLE =
'INCLUDE PERFETTO MODULE android.memory.heap_graph.heap_graph_class_aggregation';
function buildQuery(activeDump: HeapDump): string {
return `
SELECT
type_name AS cls,
reachable_obj_count AS cnt,
reachable_size_bytes AS shallow,
reachable_native_size_bytes AS native_shallow,
dominated_size_bytes AS retained,
dominated_native_size_bytes AS retained_native,
dominated_obj_count AS retained_count
FROM android_heap_graph_class_aggregation a
WHERE a.reachable_obj_count > 0 AND ${dumpFilterSql(activeDump, 'a')}
`;
}
function makeUiSchema(navigate: NavFn): SchemaRegistry {
return {
query: {
cls: {
title: 'Class',
columnType: 'text',
cellRenderer: (value: SqlValue) =>
m(
'button',
{
class: 'pf-hde-link',
onclick: () => navigate('objects', {cls: String(value)}),
},
String(value),
),
},
cnt: {
title: 'Count',
columnType: 'quantitative',
cellRenderer: countRenderer,
},
shallow: {
title: colHeader('Shallow', COL_INFO.shallow),
titleString: 'Shallow',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
native_shallow: {
title: colHeader('Shallow Native', COL_INFO.shallowNative),
titleString: 'Shallow Native',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
retained: {
title: colHeader('Retained', COL_INFO.retained),
titleString: 'Retained',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
retained_native: {
title: colHeader('Retained Native', COL_INFO.retainedNative),
titleString: 'Retained Native',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
retained_count: {
title: colHeader('Retained #', COL_INFO.retainedCount),
titleString: 'Retained #',
columnType: 'quantitative',
cellRenderer: countRenderer,
},
},
};
}
function ClassesView(): m.Component<ClassesViewAttrs> {
let dataSource: SQLDataSource | null = null;
let alive = true;
const counter = new RowCounter();
let filters: Filter[] = [];
async function applyNavFilter(
engine: Engine,
activeDump: HeapDump,
root: string | undefined,
clearNavParam: (key: string) => void,
) {
if (!root) return;
clearNavParam('rootClass');
const names = await queries.getSubclassNames(engine, activeDump, root);
if (!alive || names.length === 0) return;
filters = [{field: 'cls', op: 'in' as const, value: names}];
counter.onFiltersChanged(filters);
m.redraw();
}
return {
oninit(vnode) {
const {engine, activeDump} = vnode.attrs;
const query = buildQuery(activeDump);
dataSource = new SQLDataSource({
engine,
sqlSchema: createSimpleSchema(query),
rootSchemaName: 'query',
preamble: PREAMBLE,
});
counter.init(engine, query, PREAMBLE);
applyNavFilter(
engine,
activeDump,
vnode.attrs.initialRootClass,
vnode.attrs.clearNavParam,
).catch(console.error);
},
onupdate(vnode) {
applyNavFilter(
vnode.attrs.engine,
vnode.attrs.activeDump,
vnode.attrs.initialRootClass,
vnode.attrs.clearNavParam,
).catch(console.error);
},
onremove() {
alive = false;
},
view(vnode) {
const {navigate} = vnode.attrs;
if (!dataSource) return null;
return m('div', {class: 'pf-hde-view-content'}, [
m('h2', {class: 'pf-hde-view-heading'}, counter.heading('Classes')),
m(DataGrid, {
schema: makeUiSchema(navigate),
rootSchema: 'query',
data: dataSource,
fillHeight: true,
initialColumns: [
{id: 'cls', field: 'cls'},
{id: 'cnt', field: 'cnt'},
{id: 'shallow', field: 'shallow'},
{id: 'native_shallow', field: 'native_shallow'},
{id: 'retained', field: 'retained', sort: 'DESC' as const},
{id: 'retained_native', field: 'retained_native'},
{id: 'retained_count', field: 'retained_count'},
],
filters,
showExportButton: true,
onFiltersChanged: (f) => {
filters = [...f];
counter.onFiltersChanged(f);
},
}),
]);
},
};
}
export default ClassesView;