| // 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; |