| // 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, Row} from '../../../trace_processor/query_result'; |
| import {Spinner} from '../../../widgets/spinner'; |
| import {EmptyState} from '../../../widgets/empty_state'; |
| 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 {StringListRow} from '../types'; |
| import {fmtSize, fmtHex} from '../format'; |
| import type {Filter} from '../../../components/widgets/datagrid/model'; |
| import { |
| type NavFn, |
| sizeRenderer, |
| countRenderer, |
| SQL_PREAMBLE, |
| RowCounter, |
| COL_INFO, |
| colHeader, |
| } from '../components'; |
| import * as queries from '../queries'; |
| import {dumpFilterSql, type HeapDump} from '../queries'; |
| |
| function buildQuery(activeDump: HeapDump): string { |
| return ` |
| SELECT base.*, |
| a.cumulative_size AS reachable_size, |
| a.cumulative_native_size AS reachable_native, |
| a.cumulative_count AS reachable_count |
| FROM ( |
| SELECT |
| o.id, |
| od.value_string AS value, |
| LENGTH(od.value_string) AS len, |
| o.self_size, |
| ifnull(d.dominated_size_bytes, o.self_size) |
| + ifnull(d.dominated_native_size_bytes, o.native_size) AS retained, |
| ifnull(o.heap_type, 'default') AS heap |
| FROM heap_graph_object o |
| JOIN heap_graph_class c ON o.type_id = c.id |
| LEFT JOIN heap_graph_object_data od ON o.object_data_id = od.id |
| LEFT JOIN heap_graph_dominator_tree d ON d.id = o.id |
| WHERE o.reachable != 0 |
| AND ${dumpFilterSql(activeDump, 'o')} |
| AND od.value_string IS NOT NULL |
| AND (c.name = 'java.lang.String' |
| OR c.deobfuscated_name = 'java.lang.String') |
| ) base |
| LEFT JOIN _heap_graph_object_tree_aggregation a ON a.id = base.id |
| `; |
| } |
| |
| function makeUiSchema(navigate: NavFn): SchemaRegistry { |
| return { |
| query: { |
| id: { |
| title: 'Object', |
| columnType: 'identifier', |
| cellRenderer: (value: SqlValue, row) => { |
| const id = Number(value); |
| const str = row.value != null ? String(row.value) : null; |
| const display = `String ${fmtHex(id)}`; |
| return m( |
| 'button', |
| { |
| class: 'pf-hde-link', |
| onclick: () => |
| navigate('object', { |
| id, |
| label: str |
| ? `"${str.length > 40 ? str.slice(0, 40) + '\u2026' : str}"` |
| : display, |
| }), |
| }, |
| m( |
| 'span', |
| { |
| class: 'pf-hde-mono pf-hde-break-all pf-hde-str-color', |
| }, |
| str |
| ? '"' + |
| (str.length > 300 ? str.slice(0, 300) + '\u2026' : str) + |
| '"' |
| : display, |
| ), |
| ); |
| }, |
| }, |
| retained: { |
| title: colHeader('Retained', COL_INFO.retained), |
| titleString: 'Retained', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| len: { |
| title: 'Length', |
| columnType: 'quantitative', |
| cellRenderer: countRenderer, |
| }, |
| heap: { |
| title: 'Heap', |
| columnType: 'text', |
| }, |
| value: { |
| title: 'Value', |
| columnType: 'text', |
| }, |
| self_size: { |
| title: colHeader('Shallow', COL_INFO.shallow), |
| titleString: 'Shallow', |
| columnType: 'quantitative', |
| cellRenderer: sizeRenderer, |
| }, |
| 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 count', COL_INFO.reachableCount), |
| titleString: 'Reachable count', |
| columnType: 'quantitative', |
| cellRenderer: countRenderer, |
| }, |
| }, |
| }; |
| } |
| |
| const SUMMARY_SCHEMA: SchemaRegistry = { |
| query: { |
| property: {title: 'Property', columnType: 'text'}, |
| value: {title: 'Value', columnType: 'text'}, |
| }, |
| }; |
| |
| // --- StringsView ------------------------------------------------------------- |
| |
| interface StringsViewAttrs { |
| readonly engine: Engine; |
| readonly activeDump: HeapDump; |
| readonly navigate: NavFn; |
| readonly clearNavParam: (key: string) => void; |
| readonly initialQuery?: string; |
| readonly hasFieldValues?: boolean; |
| } |
| |
| function StringsView(): m.Component<StringsViewAttrs> { |
| let allRows: StringListRow[] | null = null; |
| let alive = true; |
| let dataSource: SQLDataSource | null = null; |
| const counter = new RowCounter(); |
| let filters: Filter[] = []; |
| |
| function applyNavFilter( |
| q: string | undefined, |
| clearNavParam: (key: string) => void, |
| ) { |
| if (!q) return; |
| filters = [{field: 'value', op: '=' as const, value: q}]; |
| counter.onFiltersChanged(filters); |
| clearNavParam('q'); |
| } |
| |
| return { |
| oninit(vnode) { |
| const {engine, activeDump} = vnode.attrs; |
| const query = buildQuery(activeDump); |
| dataSource = new SQLDataSource({ |
| engine, |
| sqlSchema: createSimpleSchema(query), |
| rootSchemaName: 'query', |
| preamble: SQL_PREAMBLE, |
| }); |
| counter.init(engine, query, SQL_PREAMBLE); |
| applyNavFilter(vnode.attrs.initialQuery, vnode.attrs.clearNavParam); |
| queries |
| .getStringList(engine, activeDump) |
| .then((r) => { |
| if (!alive) return; |
| allRows = r; |
| m.redraw(); |
| }) |
| .catch(console.error); |
| }, |
| onupdate(vnode) { |
| applyNavFilter(vnode.attrs.initialQuery, vnode.attrs.clearNavParam); |
| }, |
| onremove() { |
| alive = false; |
| }, |
| view(vnode) { |
| const {navigate} = vnode.attrs; |
| |
| if (!allRows) { |
| return m('div', {class: 'pf-hde-loading'}, m(Spinner, {easing: true})); |
| } |
| |
| if (allRows.length === 0) { |
| return m(EmptyState, { |
| icon: 'text_fields', |
| title: |
| vnode.attrs.hasFieldValues === false |
| ? 'String values require an ART heap dump (.hprof)' |
| : 'No string data available', |
| fillHeight: true, |
| }); |
| } |
| |
| const totalRetained = allRows.reduce((s, r) => s + r.retainedSize, 0); |
| const uniqueCount = (() => { |
| const seen = new Set<string>(); |
| for (const r of allRows) seen.add(r.value); |
| return seen.size; |
| })(); |
| |
| const summaryRows: Row[] = [ |
| {property: 'Total strings', value: allRows.length.toLocaleString()}, |
| {property: 'Unique values', value: uniqueCount.toLocaleString()}, |
| {property: 'Total retained', value: fmtSize(totalRetained)}, |
| ]; |
| |
| return m('div', {class: 'pf-hde-view-content'}, [ |
| m('h2', {class: 'pf-hde-view-heading'}, counter.heading('Strings')), |
| |
| m('div', {class: 'pf-hde-card pf-hde-mb-4 pf-hde-flex-none'}, [ |
| m(DataGrid, { |
| schema: SUMMARY_SCHEMA, |
| rootSchema: 'query', |
| data: summaryRows, |
| initialColumns: [ |
| {id: 'property', field: 'property'}, |
| {id: 'value', field: 'value'}, |
| ], |
| }), |
| ]), |
| |
| dataSource |
| ? m(DataGrid, { |
| schema: makeUiSchema(navigate), |
| rootSchema: 'query', |
| data: dataSource, |
| fillHeight: true, |
| initialColumns: [ |
| {id: 'id', field: 'id'}, |
| {id: 'value', field: 'value'}, |
| {id: 'retained', field: 'retained'}, |
| {id: 'reachable_size', field: 'reachable_size'}, |
| {id: 'reachable_native', field: 'reachable_native'}, |
| {id: 'reachable_count', field: 'reachable_count'}, |
| {id: 'len', field: 'len'}, |
| {id: 'heap', field: 'heap'}, |
| ], |
| filters, |
| showExportButton: true, |
| onFiltersChanged: (f) => { |
| filters = [...f]; |
| counter.onFiltersChanged(f); |
| }, |
| }) |
| : null, |
| ]); |
| }, |
| }; |
| } |
| |
| export default StringsView; |