blob: 53622c8ff3ac715253804b8347adbccae9f37956 [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 {
STR,
type Row,
type SqlValue,
} from '../../../trace_processor/query_result';
import {Spinner} from '../../../widgets/spinner';
import {DataGrid} from '../../../components/widgets/datagrid/datagrid';
import type {
SchemaRegistry,
CellRenderResult,
} from '../../../components/widgets/datagrid/datagrid_schema';
import type {InstanceRow, InstanceDetail, HeapInfo, PrimOrRef} from '../types';
import {fmtSize, fmtHex} from '../format';
import {downloadBlob} from '../download';
import {
type NavFn,
sizeRenderer,
countRenderer,
shortClassName,
InstanceLink,
Section,
PrimOrRefCell,
BitmapImage,
COL_INFO,
colHeader,
} from '../components';
import * as queries from '../queries';
import type {HeapDump} from '../queries';
export interface ObjectParams {
readonly id: number;
}
// Open the flamegraph pivoted at the given path. Routed through the
// session so flamegraph state has a single owner.
export type OpenFlamegraphPivotedAt = (
pathHash: string,
label: string,
isDominator: boolean,
) => void;
interface ObjectViewAttrs {
readonly engine: Engine;
readonly activeDump: HeapDump;
readonly heaps: ReadonlyArray<HeapInfo>;
readonly navigate: NavFn;
readonly openFlamegraphPivotedAt: OpenFlamegraphPivotedAt;
readonly params: ObjectParams;
}
const JAVA_PRIM_SIZE: Record<string, number> = {
boolean: 1,
byte: 1,
char: 2,
short: 2,
int: 4,
float: 4,
long: 8,
double: 8,
};
function instanceRowToRow(r: InstanceRow): Row {
let retained = 0;
let retainedNative = 0;
for (const h of r.retainedByHeap) {
retained += h.java;
retainedNative += h.native_;
}
return {
id: r.id,
cls: r.className,
self_size: r.shallowJava,
native_size: r.shallowNative,
retained,
retained_native: retainedNative,
retained_count: r.retainedCount,
reachable_size: r.reachableSize,
reachable_native: r.reachableNative,
reachable_count: r.reachableCount,
heap: r.heap,
str: r.str ?? null,
};
}
type FieldRow = {name: string; typeName: string; value: PrimOrRef};
function fieldRowToRow(f: FieldRow): Row {
const v = f.value;
if (v.kind === 'ref') {
return {
name: f.name,
type_name: f.typeName,
value_display: v.display,
value_kind: 'ref',
ref_id: v.id,
ref_str: v.str,
shallow: v.shallowJava ?? 0,
shallow_native: v.shallowNative ?? 0,
retained: v.retainedJava ?? 0,
retained_native: v.retainedNative ?? 0,
reachable: v.reachableJava ?? null,
reachable_native: v.reachableNative ?? null,
reachable_count: v.reachableCount ?? null,
};
}
return {
name: f.name,
type_name: f.typeName,
value_display: v.v,
value_kind: 'prim',
ref_id: null,
ref_str: null,
shallow: JAVA_PRIM_SIZE[f.typeName] ?? 0,
shallow_native: 0,
retained: 0,
retained_native: 0,
reachable: null,
reachable_native: null,
reachable_count: null,
};
}
type ArrayElemRow = {idx: number; value: PrimOrRef};
function arrayElemToRow(e: ArrayElemRow, elemTypeName: string): Row {
const v = e.value;
if (v.kind === 'ref') {
return {
idx: e.idx,
value_display: v.display,
value_kind: 'ref',
ref_id: v.id,
ref_str: v.str,
shallow: v.shallowJava ?? 0,
shallow_native: v.shallowNative ?? 0,
retained: v.retainedJava ?? 0,
retained_native: v.retainedNative ?? 0,
reachable: v.reachableJava ?? null,
reachable_native: v.reachableNative ?? null,
reachable_count: v.reachableCount ?? null,
};
}
return {
idx: e.idx,
value_display: v.v,
value_kind: 'prim',
ref_id: null,
ref_str: null,
shallow: JAVA_PRIM_SIZE[elemTypeName] ?? 0,
shallow_native: 0,
retained: 0,
retained_native: 0,
reachable: null,
reachable_native: null,
reachable_count: null,
};
}
function nullableSizeRenderer(value: SqlValue): CellRenderResult {
if (value === null) {
return {
content: m('span', {class: 'pf-hde-mono pf-hde-opacity-60'}, '\u2026'),
align: 'right',
};
}
return {
content: m('span', {class: 'pf-hde-mono'}, fmtSize(Number(value ?? 0))),
align: 'right',
};
}
// Per-row info for the Object Size grid; the row label carries the metric.
const METRIC_INFO: Record<string, string> = {
Shallow: 'Memory used by this object alone, excluding referenced objects.',
Retained:
'Memory exclusively held by this object (dominator subtree, self ' +
'inclusive). The Native column is often zero in multi-rooted graphs ' +
'where Bitmaps are reachable via multiple paths — see Reachable below.',
Reachable:
"Memory reachable from this object along the heap graph's BFS " +
'shortest-path tree. Includes objects also reachable via other paths.',
};
const SIZE_SCHEMA: SchemaRegistry = {
query: {
metric: {
title: 'Metric',
columnType: 'text',
cellRenderer: (value: SqlValue): CellRenderResult => {
const label = String(value ?? '');
const info = METRIC_INFO[label];
return {content: info ? colHeader(label, info) : label};
},
},
java: {
title: 'Java',
columnType: 'quantitative',
cellRenderer: nullableSizeRenderer,
},
native: {
title: 'Native',
columnType: 'quantitative',
cellRenderer: nullableSizeRenderer,
},
count: {
title: 'Count',
columnType: 'quantitative',
cellRenderer: (value: SqlValue): CellRenderResult => {
if (value === null) {
return {
content: m(
'span',
{class: 'pf-hde-mono pf-hde-opacity-60'},
'\u2026',
),
align: 'right',
};
}
return {
content: m(
'span',
{class: 'pf-hde-mono'},
Number(value).toLocaleString(),
),
align: 'right',
};
},
},
},
};
function makeInstanceSchema(navigate: NavFn): SchemaRegistry {
return {
query: {
id: {
title: 'Object',
columnType: 'identifier',
cellRenderer: (value: SqlValue, row) => {
const id = Number(value);
const cls = String(row.cls ?? '');
const display = `${shortClassName(cls)} ${fmtHex(id)}`;
const str = row.str != null ? String(row.str) : null;
return m('span', [
m(
'button',
{
class: 'pf-hde-link',
onclick: () =>
navigate('object', {id, label: str ? `"${str}"` : display}),
},
display,
),
str
? m(
'span',
{class: 'pf-hde-str-badge'},
` "${str.length > 40 ? str.slice(0, 40) + '\u2026' : str}"`,
)
: null,
]);
},
},
self_size: {
title: colHeader('Shallow', COL_INFO.shallow),
titleString: 'Shallow',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
native_size: {
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,
},
reachable_size: {
title: colHeader('Reachable', COL_INFO.reachable),
titleString: 'Reachable',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_native: {
title: colHeader('Reachable Native', COL_INFO.reachableNative),
titleString: 'Reachable Native',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_count: {
title: colHeader('Reachable #', COL_INFO.reachableCount),
titleString: 'Reachable #',
columnType: 'quantitative',
cellRenderer: countRenderer,
},
heap: {
title: 'Heap',
columnType: 'text',
},
cls: {
title: 'Class',
columnType: 'text',
},
str: {
title: 'String Value',
columnType: 'text',
},
},
};
}
function makeFieldSchema(navigate: NavFn): SchemaRegistry {
return {
query: {
name: {
title: 'Name',
columnType: 'text',
cellRenderer: (value: SqlValue, row) => {
if (row.value_kind === 'ref' && row.ref_id !== null) {
return m(
'button',
{
class: 'pf-hde-link',
onclick: () =>
navigate('object', {
id: Number(row.ref_id),
label: String(row.value_display ?? ''),
}),
},
String(value),
);
}
return m('span', String(value ?? ''));
},
},
type_name: {
title: 'Type',
columnType: 'text',
},
value_display: {
title: 'Value',
columnType: 'text',
cellRenderer: (value: SqlValue, row) => {
if (row.value_kind === 'ref' && row.ref_id !== null) {
return m(PrimOrRefCell, {
v: {
kind: 'ref',
id: Number(row.ref_id),
display: String(value),
str: row.ref_str != null ? String(row.ref_str) : null,
},
navigate,
});
}
return m('span', {class: 'pf-hde-mono'}, String(value ?? ''));
},
},
shallow: {
title: colHeader('Shallow', COL_INFO.shallow),
titleString: 'Shallow',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
shallow_native: {
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,
},
reachable: {
title: colHeader('Reachable', COL_INFO.reachable),
titleString: 'Reachable',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_native: {
title: colHeader('Reachable Native', COL_INFO.reachableNative),
titleString: 'Reachable Native',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_count: {
title: colHeader('Reachable #', COL_INFO.reachableCount),
titleString: 'Reachable #',
columnType: 'quantitative',
cellRenderer: countRenderer,
},
value_kind: {
title: 'Kind',
columnType: 'text',
},
ref_id: {
title: 'Ref ID',
columnType: 'identifier',
},
ref_str: {
title: 'Ref String',
columnType: 'text',
},
},
};
}
function makeArraySchema(
navigate: NavFn,
elemTypeName: string,
): SchemaRegistry {
return {
query: {
idx: {
title: 'Index',
columnType: 'quantitative',
cellRenderer: (value: SqlValue): CellRenderResult => ({
content: m('span', {class: 'pf-hde-mono'}, String(value ?? 0)),
align: 'right',
}),
},
value_display: {
title: `Value (${elemTypeName})`,
columnType: 'text',
cellRenderer: (value: SqlValue, row) => {
if (row.value_kind === 'ref' && row.ref_id !== null) {
return m(PrimOrRefCell, {
v: {
kind: 'ref',
id: Number(row.ref_id),
display: String(value),
str: row.ref_str != null ? String(row.ref_str) : null,
},
navigate,
});
}
return m('span', {class: 'pf-hde-mono'}, String(value ?? ''));
},
},
shallow: {
title: colHeader('Shallow', COL_INFO.shallow),
titleString: 'Shallow',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
shallow_native: {
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,
},
reachable: {
title: colHeader('Reachable', COL_INFO.reachable),
titleString: 'Reachable',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_native: {
title: colHeader('Reachable Native', COL_INFO.reachableNative),
titleString: 'Reachable Native',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
reachable_count: {
title: colHeader('Reachable #', COL_INFO.reachableCount),
titleString: 'Reachable #',
columnType: 'quantitative',
cellRenderer: countRenderer,
},
value_kind: {
title: 'Kind',
columnType: 'text',
},
ref_id: {
title: 'Ref ID',
columnType: 'identifier',
},
ref_str: {
title: 'Ref String',
columnType: 'text',
},
},
};
}
function ObjectView(): m.Component<ObjectViewAttrs> {
let detail: InstanceDetail | null | 'loading' = 'loading';
let prevId: number | undefined;
let alive = true;
let fetchSeq = 0;
function fetchData(attrs: ObjectViewAttrs) {
detail = 'loading';
prevId = attrs.params.id;
const seq = ++fetchSeq;
queries
.getInstance(attrs.engine, attrs.activeDump, attrs.params.id)
.then((d) => {
if (!alive || seq !== fetchSeq) return;
detail = d;
m.redraw();
if (d) {
// Enrich all sections with reachable sizes asynchronously.
const enrichTasks: Promise<void>[] = [
queries.enrichWithReachable(attrs.engine, [d.row]),
queries.enrichWithReachable(attrs.engine, d.reverseRefs),
queries.enrichWithReachable(attrs.engine, d.dominated),
];
if (d.isClassObj) {
enrichTasks.push(
queries.enrichFieldsWithReachable(attrs.engine, d.staticFields),
);
}
if (d.isClassInstance && d.instanceFields.length > 0) {
enrichTasks.push(
queries.enrichFieldsWithReachable(attrs.engine, d.instanceFields),
);
}
if (d.isArrayInstance) {
enrichTasks.push(
queries.enrichArrayElemsWithReachable(attrs.engine, d.arrayElems),
);
}
Promise.all(enrichTasks).then(() => {
if (alive && seq === fetchSeq) m.redraw();
});
}
})
.catch((err) => {
console.error(err);
if (!alive || seq !== fetchSeq) return;
detail = null;
m.redraw();
});
}
return {
oninit(vnode) {
fetchData(vnode.attrs);
},
onupdate(vnode) {
if (vnode.attrs.params.id !== prevId) {
fetchData(vnode.attrs);
}
},
onremove() {
alive = false;
},
view(vnode) {
const {navigate, params} = vnode.attrs;
if (detail === 'loading') {
return m('div', {class: 'pf-hde-loading'}, m(Spinner, {easing: true}));
}
if (!detail) {
return m(
'div',
{class: 'pf-hde-error-text'},
'No object with id ' + fmtHex(params.id),
);
}
const {row} = detail;
const flamegraphAction = (isDominator: boolean) =>
row.className
? m(
'button',
{
class: 'pf-hde-link',
title: isDominator
? 'Open in Flamegraph pivoted on this dominator path'
: 'Open in Flamegraph pivoted on this shortest path',
onclick: () =>
openInFlamegraph(
vnode.attrs.engine,
row.id,
row.className,
isDominator,
vnode.attrs.openFlamegraphPivotedAt,
),
},
'View in Flamegraph',
)
: null;
return m('div', {class: 'pf-hde-view-scroll pf-hde-view-stack'}, [
m('div', [
m(
'h2',
{class: 'pf-hde-view-heading pf-hde-view-heading--tight'},
'Object ' + fmtHex(row.id),
),
m('div', {class: 'pf-hde-action-row'}, [
m(InstanceLink, {row, navigate}),
]),
]),
detail.bitmap
? m(Section, {title: 'Bitmap Image'}, [
m(BitmapImage, {
width: detail.bitmap.width,
height: detail.bitmap.height,
format: detail.bitmap.format,
data: detail.bitmap.data,
}),
m('div', {class: 'pf-hde-bitmap-meta pf-hde-mt-1'}, [
m(
'span',
detail.bitmap.width +
' x ' +
detail.bitmap.height +
' px (' +
detail.bitmap.format.toUpperCase() +
')',
),
m(
'button',
{
class: 'pf-hde-download-link',
onclick: () => {
if (
detail === null ||
detail === 'loading' ||
detail.bitmap === null
) {
return;
}
const ext = detail.bitmap.format;
downloadBlob(
`bitmap-${fmtHex(row.id)}.${ext}`,
detail.bitmap.data,
);
},
},
'Download image',
),
]),
])
: null,
m(
Section,
{
title: 'Shortest Path from GC Root',
actions: detail.shortestPath ? flamegraphAction(false) : null,
},
detail.shortestPath
? m(
'div',
{class: 'pf-hde-view-stack--tight'},
detail.shortestPath.map((pe, i) =>
m(
'div',
{
key: i,
class: 'pf-hde-path-entry',
style: {'--pf-hde-depth': String(i)},
},
[
m(
'span',
{class: 'pf-hde-path-arrow'},
i === 0 ? '' : '\u2192',
),
m(InstanceLink, {row: pe.row, navigate}),
pe.field
? m('span', {class: 'pf-hde-path-field'}, pe.field)
: null,
],
),
),
)
: m('p', {class: 'pf-hde-muted'}, 'No path to GC root.'),
),
m(
Section,
{
title: 'Dominator Tree Path',
actions: detail.dominatorPath ? flamegraphAction(true) : null,
},
detail.dominatorPath
? m(
'div',
{class: 'pf-hde-view-stack--tight'},
detail.dominatorPath.map((pe, i) =>
m(
'div',
{
key: i,
class: `pf-hde-path-entry${pe.isDominator ? ' pf-hde-semibold' : ''}`,
style: {'--pf-hde-depth': String(i)},
},
[
m(
'span',
{class: 'pf-hde-path-arrow'},
i === 0 ? '' : '\u2192',
),
m(InstanceLink, {row: pe.row, navigate}),
pe.field
? m('span', {class: 'pf-hde-path-field'}, pe.field)
: null,
],
),
),
)
: m('p', {class: 'pf-hde-muted'}, 'No path to GC root.'),
),
m(Section, {title: 'Object Info'}, [
m('div', {class: 'pf-hde-info-grid'}, [
m('span', {class: 'pf-hde-info-grid__label'}, 'Class:'),
m(
'span',
detail.classObjRow
? m(InstanceLink, {
row: detail.classObjRow,
navigate,
})
: '???',
),
m('span', {class: 'pf-hde-info-grid__label'}, 'Heap:'),
m('span', row.heap),
...(row.isRoot
? [
m('span', {class: 'pf-hde-info-grid__label'}, 'Root Types:'),
m('span', row.rootTypeNames?.join(', ')),
]
: []),
]),
]),
m(
Section,
{title: 'Object Size'},
(() => {
let retainedJava = 0;
let retainedNative = 0;
for (const h of row.retainedByHeap) {
retainedJava += h.java;
retainedNative += h.native_;
}
const sizeRows: Row[] = [
{
metric: 'Shallow',
java: row.shallowJava,
native: row.shallowNative,
count: 1,
},
{
metric: 'Retained',
java: retainedJava,
native: retainedNative,
count: row.retainedCount,
},
{
metric: 'Reachable',
java: row.reachableSize,
native: row.reachableNative,
count: row.reachableCount,
},
];
return m(DataGrid, {
schema: SIZE_SCHEMA,
rootSchema: 'query',
data: sizeRows,
initialColumns: [
{id: 'metric', field: 'metric'},
{id: 'java', field: 'java'},
{id: 'native', field: 'native'},
{id: 'count', field: 'count'},
],
});
})(),
),
detail.isClassObj
? m(Section, {title: 'Class Info'}, [
m('div', {class: 'pf-hde-info-grid pf-hde-mb-3'}, [
m('span', {class: 'pf-hde-info-grid__label'}, 'Instance Size:'),
m('span', {class: 'pf-hde-mono'}, String(detail.instanceSize)),
]),
])
: null,
detail.classHierarchy.length > 0
? m(
Section,
{title: 'Class Hierarchy'},
renderClassHierarchy(detail.classHierarchy, navigate),
)
: null,
detail.isClassObj
? m(
Section,
{title: 'Static Fields'},
renderFieldsGrid(detail.staticFields, navigate),
)
: null,
detail.isClassInstance
? m(
Section,
{title: 'Fields'},
detail.instanceFields.length > 0
? renderFieldsGrid(detail.instanceFields, navigate)
: m('p', {class: 'pf-hde-muted'}, 'No instance fields.'),
)
: null,
detail.isArrayInstance
? m(
Section,
{title: `Array Elements (${detail.arrayLength})`},
renderArrayGrid(
detail.arrayElems,
detail.elemTypeName ?? 'Object',
navigate,
detail.elemTypeName === 'byte'
? () => {
queries
.getRawArrayBlob(vnode.attrs.engine, params.id)
.then((blob) => {
if (blob !== null) {
downloadBlob(
`array-${fmtHex(params.id)}.bin`,
blob,
);
}
})
.catch(console.error);
}
: undefined,
),
)
: null,
m(
Section,
{
title:
detail.reverseRefs.length > 0
? `Objects with References to this Object (${detail.reverseRefs.length})`
: 'Objects with References to this Object',
defaultOpen:
detail.reverseRefs.length > 0 && detail.reverseRefs.length < 50,
},
detail.reverseRefs.length > 0
? m(DataGrid, {
schema: makeInstanceSchema(navigate),
rootSchema: 'query',
data: detail.reverseRefs.map(instanceRowToRow),
initialColumns: [
{id: 'id', field: 'id'},
{id: 'cls', field: 'cls'},
{id: 'str', field: 'str'},
{id: 'self_size', field: 'self_size'},
{id: 'native_size', field: 'native_size'},
{id: 'retained', field: 'retained'},
{id: 'retained_native', field: 'retained_native'},
{id: 'retained_count', field: 'retained_count'},
{id: 'reachable_size', field: 'reachable_size'},
{id: 'reachable_native', field: 'reachable_native'},
{id: 'reachable_count', field: 'reachable_count'},
],
showExportButton: true,
})
: m('p', {class: 'pf-hde-muted'}, 'No references to this object.'),
),
m(
Section,
{
title:
detail.dominated.length > 0
? `Immediately Dominated Objects (${detail.dominated.length})`
: 'Immediately Dominated Objects',
defaultOpen:
detail.dominated.length > 0 && detail.dominated.length < 50,
},
detail.dominated.length > 0
? m(DataGrid, {
schema: makeInstanceSchema(navigate),
rootSchema: 'query',
data: detail.dominated.map(instanceRowToRow),
initialColumns: [
{id: 'id', field: 'id'},
{id: 'cls', field: 'cls'},
{id: 'str', field: 'str'},
{id: 'self_size', field: 'self_size'},
{id: 'native_size', field: 'native_size'},
{id: 'retained', field: 'retained'},
{id: 'retained_native', field: 'retained_native'},
{id: 'retained_count', field: 'retained_count'},
{id: 'reachable_size', field: 'reachable_size'},
{id: 'reachable_native', field: 'reachable_native'},
{id: 'reachable_count', field: 'reachable_count'},
{id: 'heap', field: 'heap'},
],
showExportButton: true,
})
: m(
'p',
{class: 'pf-hde-muted'},
'No immediately dominated objects.',
),
),
]);
},
};
}
function renderFieldsGrid(fields: FieldRow[], navigate: NavFn): m.Children {
if (fields.length === 0) {
return m('div', {class: 'pf-hde-info-grid__label'}, 'No fields');
}
return m(DataGrid, {
schema: makeFieldSchema(navigate),
rootSchema: 'query',
data: fields.map(fieldRowToRow),
initialColumns: [
{id: 'type_name', field: 'type_name'},
{id: 'name', field: 'name'},
{id: 'value_display', field: 'value_display'},
{id: 'shallow', field: 'shallow'},
{id: 'shallow_native', field: 'shallow_native'},
{id: 'retained', field: 'retained'},
{id: 'retained_native', field: 'retained_native'},
{id: 'reachable', field: 'reachable'},
{id: 'reachable_native', field: 'reachable_native'},
{id: 'reachable_count', field: 'reachable_count'},
{id: 'value_kind', field: 'value_kind'},
{id: 'ref_id', field: 'ref_id'},
{id: 'ref_str', field: 'ref_str'},
],
showExportButton: true,
});
}
function renderArrayGrid(
elems: ArrayElemRow[],
elemTypeName: string,
navigate: NavFn,
onDownloadBytes?: () => void,
): m.Children {
function copyTsv() {
const header = 'Index\tValue';
const lines = elems.map(
(e) =>
e.idx + '\t' + (e.value.kind === 'prim' ? e.value.v : e.value.display),
);
navigator.clipboard
.writeText(header + '\n' + lines.join('\n'))
.catch(console.error);
}
return m('div', [
onDownloadBytes || elems.length > 0
? m('div', {class: 'pf-hde-action-row pf-hde-mb-2'}, [
onDownloadBytes
? m(
'button',
{class: 'pf-hde-download-link', onclick: onDownloadBytes},
'Download bytes',
)
: null,
elems.length > 0
? m(
'button',
{class: 'pf-hde-download-link', onclick: copyTsv},
'Copy as TSV',
)
: null,
])
: null,
m(DataGrid, {
schema: makeArraySchema(navigate, elemTypeName),
rootSchema: 'query',
data: elems.map((e) => arrayElemToRow(e, elemTypeName)),
initialColumns: [
{id: 'idx', field: 'idx'},
{id: 'value_display', field: 'value_display'},
{id: 'shallow', field: 'shallow'},
{id: 'shallow_native', field: 'shallow_native'},
{id: 'retained', field: 'retained'},
{id: 'retained_native', field: 'retained_native'},
{id: 'reachable', field: 'reachable'},
{id: 'reachable_native', field: 'reachable_native'},
{id: 'reachable_count', field: 'reachable_count'},
{id: 'value_kind', field: 'value_kind'},
{id: 'ref_id', field: 'ref_id'},
{id: 'ref_str', field: 'ref_str'},
],
showExportButton: true,
}),
]);
}
// Look up the object's path_hash in the chosen tree (BFS or dominator)
// and open the flamegraph pivoted on it with the matching metric. The
// hash is tree-specific so the tree dictates both. No-op if the object
// has no entry (e.g. unreachable garbage).
async function openInFlamegraph(
engine: Engine,
id: number,
cls: string,
isDominator: boolean,
openFlamegraphPivotedAt: OpenFlamegraphPivotedAt,
): Promise<void> {
const moduleName = isDominator
? 'android.memory.heap_graph.dominator_class_tree'
: 'android.memory.heap_graph.class_tree';
const table = isDominator
? '_heap_graph_dominator_path_hashes'
: '_heap_graph_path_hashes';
await engine.query(`INCLUDE PERFETTO MODULE ${moduleName};`);
const res = await engine.query(
`SELECT CAST(path_hash AS TEXT) AS path_hash
FROM ${table} WHERE id = ${id} LIMIT 1`,
);
const it = res.iter({path_hash: STR});
if (!it.valid()) return;
openFlamegraphPivotedAt(it.path_hash, shortClassName(cls), isDominator);
}
// `java.lang.Class<Foo>` has no useful subclasses in heap_graph_class; the
// meaningful filter target is `Foo`.
const CLASS_OBJ_PREFIX = 'java.lang.Class<';
function subclassFilterTarget(className: string): string {
if (className.startsWith(CLASS_OBJ_PREFIX) && className.endsWith('>')) {
return className.slice(CLASS_OBJ_PREFIX.length, -1);
}
return className;
}
function classFilterLink(className: string, navigate: NavFn): m.Child {
return m(
'button',
{
class: 'pf-hde-link',
title: 'Open subclasses of this class',
onclick: () =>
navigate('classes', {rootClass: subclassFilterTarget(className)}),
},
className,
);
}
function renderClassHierarchy(
hierarchy: string[],
navigate: NavFn,
): m.Children {
const topDown = hierarchy.slice().reverse();
return m(
'div',
{class: 'pf-hde-view-stack--tight'},
topDown.map((className, i) =>
m(
'div',
{
key: className,
class: `pf-hde-path-entry${i === topDown.length - 1 ? ' pf-hde-semibold' : ''}`,
style: {'--pf-hde-depth': String(i)},
},
[
m('span', {class: 'pf-hde-path-arrow'}, i === 0 ? '' : '→'),
classFilterLink(className, navigate),
],
),
),
);
}
export default ObjectView;