| // 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 {SqlValue, Row} from '../../../trace_processor/query_result'; |
| import {DataGrid} from '../../../components/widgets/datagrid/datagrid'; |
| import type {SchemaRegistry} from '../../../components/widgets/datagrid/datagrid_schema'; |
| import type {OverviewData} from '../types'; |
| import {fmtSize} from '../format'; |
| import type {NavState} from '../nav_state'; |
| import {type NavFn, sizeRenderer} from '../components'; |
| import type {HeapDump} from '../queries'; |
| import {Callout} from '../../../widgets/callout'; |
| import {Button} from '../../../widgets/button'; |
| |
| const HEAP_SCHEMA: SchemaRegistry = { |
| query: { |
| heap: { |
| title: 'Heap', |
| columnType: 'text', |
| }, |
| java_size: { |
| title: 'Java Size', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| native_size: { |
| title: 'Native Size', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| total_size: { |
| title: 'Total Size', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| }, |
| }; |
| |
| const INFO_SCHEMA: SchemaRegistry = { |
| query: { |
| property: { |
| title: 'Property', |
| columnType: 'text', |
| }, |
| value: { |
| title: 'Value', |
| columnType: 'text', |
| }, |
| }, |
| }; |
| |
| function makeDuplicateBitmapSchema(navigate: NavFn): SchemaRegistry { |
| return { |
| query: { |
| dimensions: { |
| title: 'Dimensions', |
| columnType: 'text', |
| }, |
| copies: { |
| title: 'Copies', |
| columnType: 'quantitative', |
| cellRenderer: (value: SqlValue, row) => |
| m( |
| 'button', |
| { |
| class: 'pf-hde-link', |
| onclick: () => |
| navigate('bitmaps', { |
| filterKey: String(row.groupKey ?? ''), |
| }), |
| }, |
| String(value), |
| ), |
| }, |
| total_bytes: { |
| title: 'Total', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| wasted_bytes: { |
| title: 'Wasted', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| }, |
| }; |
| } |
| |
| function makeDuplicateArraySchema(navigate: NavFn): SchemaRegistry { |
| return { |
| query: { |
| className: { |
| title: 'Array Type', |
| columnType: 'text', |
| cellRenderer: (value: SqlValue) => |
| m( |
| 'button', |
| { |
| class: 'pf-hde-link', |
| onclick: () => navigate('objects', {cls: String(value ?? '')}), |
| }, |
| String(value ?? ''), |
| ), |
| }, |
| arrayHash: { |
| title: 'Hash', |
| columnType: 'text', |
| }, |
| copies: { |
| title: 'Copies', |
| columnType: 'quantitative', |
| cellRenderer: (value: SqlValue, row) => |
| m( |
| 'button', |
| { |
| class: 'pf-hde-link', |
| onclick: () => |
| navigate('arrays', { |
| arrayHash: String(row.arrayHash ?? ''), |
| }), |
| }, |
| String(value), |
| ), |
| }, |
| total_bytes: { |
| title: 'Total', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| wasted_bytes: { |
| title: 'Wasted', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| }, |
| }; |
| } |
| |
| function makeDuplicateStringSchema(navigate: NavFn): SchemaRegistry { |
| return { |
| query: { |
| value: { |
| title: 'Value', |
| columnType: 'text', |
| cellRenderer: (value: SqlValue) => |
| m( |
| 'button', |
| { |
| class: |
| 'pf-hde-link pf-hde-mono pf-hde-break-all pf-hde-str-color', |
| onclick: () => |
| navigate('strings', { |
| q: String(value ?? ''), |
| }), |
| }, |
| '"' + |
| (String(value ?? '').length > 200 |
| ? String(value).slice(0, 200) + '\u2026' |
| : String(value ?? '')) + |
| '"', |
| ), |
| }, |
| copies: { |
| title: 'Copies', |
| columnType: 'quantitative', |
| cellRenderer: (value: SqlValue, row) => |
| m( |
| 'button', |
| { |
| class: 'pf-hde-link', |
| onclick: () => navigate('strings', {q: String(row.value ?? '')}), |
| }, |
| String(value), |
| ), |
| }, |
| total_bytes: { |
| title: 'Total', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| wasted_bytes: { |
| title: 'Wasted', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| }, |
| }; |
| } |
| |
| function renderDuplicateSection( |
| title: string, |
| groupCount: number, |
| totalWasted: number, |
| targetView: string, |
| linkLabel: string, |
| navigate: NavFn, |
| schema: SchemaRegistry, |
| data: Row[], |
| columns: Array<{id: string; field: string}>, |
| ): m.Children { |
| return m('div', {class: 'pf-hde-card pf-hde-mt-4'}, [ |
| m('h3', {class: 'pf-hde-sub-heading'}, title), |
| m('p', {class: 'pf-hde-desc'}, [ |
| groupCount + |
| ' group' + |
| (groupCount > 1 ? 's' : '') + |
| ' detected, wasting ', |
| m('span', {class: 'pf-hde-mono pf-hde-semibold'}, fmtSize(totalWasted)), |
| '. ', |
| m( |
| 'button', |
| { |
| class: 'pf-hde-link--alt', |
| onclick: () => navigate(targetView as NavState['view']), |
| }, |
| linkLabel, |
| ), |
| ]), |
| m('div', {class: 'pf-hde-dup-grid-container'}, [ |
| m(DataGrid, { |
| schema, |
| rootSchema: 'query', |
| data, |
| initialColumns: columns, |
| fillHeight: true, |
| }), |
| ]), |
| ]); |
| } |
| |
| interface OverviewViewAttrs { |
| readonly overview: OverviewData; |
| readonly activeDump: HeapDump; |
| readonly navigate: NavFn; |
| readonly showDefaultChangedHint: boolean; |
| readonly onBackToTimeline: () => void; |
| readonly onDismissDefaultChangedHint: () => void; |
| } |
| function OverviewView(): m.Component<OverviewViewAttrs> { |
| return { |
| view(vnode) { |
| const { |
| overview, |
| activeDump, |
| navigate, |
| showDefaultChangedHint, |
| onBackToTimeline, |
| onDismissDefaultChangedHint, |
| } = vnode.attrs; |
| const showHint = showDefaultChangedHint; |
| const heapIndices: number[] = []; |
| for (let i = 0; i < overview.heaps.length; i++) { |
| const h = overview.heaps[i]; |
| if (h.java + h.native_ > 0) { |
| heapIndices.push(i); |
| } |
| } |
| const heaps = heapIndices.map((i) => overview.heaps[i]); |
| const totalJava = heaps.reduce((a, h) => a + h.java, 0); |
| const totalNative = heaps.reduce((a, h) => a + h.native_, 0); |
| |
| const heapRows: Row[] = [ |
| { |
| heap: 'Total', |
| java_size: totalJava, |
| native_size: totalNative, |
| total_size: totalJava + totalNative, |
| }, |
| ...heaps.map((h) => ({ |
| heap: h.name, |
| java_size: h.java, |
| native_size: h.native_, |
| total_size: h.java + h.native_, |
| })), |
| ]; |
| |
| const processLabel = |
| (activeDump.processName ?? '<unknown>') + |
| (activeDump.pid ? ` (pid ${activeDump.pid})` : ''); |
| const infoRows: Row[] = [ |
| {property: 'Process', value: processLabel}, |
| {property: 'Classes', value: overview.classCount.toLocaleString()}, |
| { |
| property: 'Reachable instances', |
| value: overview.reachableInstanceCount.toLocaleString(), |
| }, |
| { |
| property: 'Unreachable instances', |
| value: overview.unreachableInstanceCount.toLocaleString(), |
| }, |
| ]; |
| |
| return m('div', {class: 'pf-hde-view-scroll'}, [ |
| m('h2', {class: 'pf-hde-view-heading'}, 'Overview'), |
| showHint |
| ? m( |
| Callout, |
| { |
| className: 'pf-hde-default-changed-callout', |
| icon: 'info', |
| dismissible: true, |
| onDismiss: onDismissDefaultChangedHint, |
| }, |
| m('p', [ |
| m( |
| 'span', |
| 'Heapdump Explorer is now the default view for traces ' + |
| 'with heap-graph data.', |
| ), |
| m(Button, { |
| label: 'Back to Timeline', |
| icon: 'arrow_back', |
| compact: true, |
| onclick: onBackToTimeline, |
| }), |
| ]), |
| ) |
| : null, |
| |
| m('div', {class: 'pf-hde-card pf-hde-mb-4'}, [ |
| m('h3', {class: 'pf-hde-sub-heading'}, 'General Information'), |
| m(DataGrid, { |
| schema: INFO_SCHEMA, |
| rootSchema: 'query', |
| data: infoRows, |
| initialColumns: [ |
| {id: 'property', field: 'property'}, |
| {id: 'value', field: 'value'}, |
| ], |
| }), |
| ]), |
| m('div', {class: 'pf-hde-card'}, [ |
| m('h3', {class: 'pf-hde-sub-heading'}, 'Bytes Retained by Heap'), |
| m(DataGrid, { |
| schema: HEAP_SCHEMA, |
| rootSchema: 'query', |
| data: heapRows, |
| initialColumns: [ |
| {id: 'heap', field: 'heap'}, |
| {id: 'java_size', field: 'java_size'}, |
| {id: 'native_size', field: 'native_size'}, |
| {id: 'total_size', field: 'total_size'}, |
| ], |
| }), |
| ]), |
| overview.duplicateBitmaps && overview.duplicateBitmaps.length > 0 |
| ? renderDuplicateSection( |
| 'Duplicate Bitmaps', |
| overview.duplicateBitmaps.length, |
| overview.duplicateBitmaps.reduce((a, g) => a + g.wastedBytes, 0), |
| 'bitmaps', |
| 'View Bitmaps', |
| navigate, |
| makeDuplicateBitmapSchema(navigate), |
| overview.duplicateBitmaps.map((g) => ({ |
| dimensions: `${g.width} \u00d7 ${g.height}`, |
| groupKey: g.groupKey, |
| copies: g.count, |
| total_bytes: g.totalBytes, |
| wasted_bytes: g.wastedBytes, |
| })), |
| [ |
| {id: 'dimensions', field: 'dimensions'}, |
| {id: 'groupKey', field: 'groupKey'}, |
| {id: 'copies', field: 'copies'}, |
| {id: 'total_bytes', field: 'total_bytes'}, |
| {id: 'wasted_bytes', field: 'wasted_bytes'}, |
| ], |
| ) |
| : overview.hasFieldValues |
| ? m( |
| 'div', |
| {class: 'pf-hde-card pf-hde-mt-4 pf-hde-mb-4'}, |
| m('p', {class: 'pf-hde-muted'}, 'No duplicate bitmaps found.'), |
| ) |
| : null, |
| overview.duplicateStrings && overview.duplicateStrings.length > 0 |
| ? renderDuplicateSection( |
| 'Duplicate Strings', |
| overview.duplicateStrings.length, |
| overview.duplicateStrings.reduce((a, g) => a + g.wastedBytes, 0), |
| 'strings', |
| 'View Strings', |
| navigate, |
| makeDuplicateStringSchema(navigate), |
| overview.duplicateStrings.map((g) => ({ |
| value: g.value, |
| copies: g.count, |
| total_bytes: g.totalBytes, |
| wasted_bytes: g.wastedBytes, |
| })), |
| [ |
| {id: 'value', field: 'value'}, |
| {id: 'copies', field: 'copies'}, |
| {id: 'total_bytes', field: 'total_bytes'}, |
| {id: 'wasted_bytes', field: 'wasted_bytes'}, |
| ], |
| ) |
| : overview.hasFieldValues |
| ? m( |
| 'div', |
| {class: 'pf-hde-card pf-hde-mb-4'}, |
| m('p', {class: 'pf-hde-muted'}, 'No duplicate strings found.'), |
| ) |
| : null, |
| overview.duplicateArrays && overview.duplicateArrays.length > 0 |
| ? renderDuplicateSection( |
| 'Duplicate Primitive Arrays', |
| overview.duplicateArrays.length, |
| overview.duplicateArrays.reduce((a, g) => a + g.wastedBytes, 0), |
| 'arrays', |
| 'View Arrays', |
| navigate, |
| makeDuplicateArraySchema(navigate), |
| overview.duplicateArrays.map((g) => ({ |
| className: g.className, |
| arrayHash: g.arrayHash, |
| copies: g.count, |
| total_bytes: g.totalBytes, |
| wasted_bytes: g.wastedBytes, |
| })), |
| [ |
| {id: 'className', field: 'className'}, |
| {id: 'arrayHash', field: 'arrayHash'}, |
| {id: 'copies', field: 'copies'}, |
| {id: 'total_bytes', field: 'total_bytes'}, |
| {id: 'wasted_bytes', field: 'wasted_bytes'}, |
| ], |
| ) |
| : null, |
| ]); |
| }, |
| }; |
| } |
| |
| export default OverviewView; |