blob: 8919197190ce09fe14d1db0f84088438f7f61fe3 [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 {Time} from '../../base/time';
import {Spinner} from '../../widgets/spinner';
import {Button, ButtonVariant} from '../../widgets/button';
import {MenuItem, PopupMenu} from '../../widgets/menu';
import {Tabs} from '../../widgets/tabs';
import type {TabsTab} from '../../widgets/tabs';
import {formatDuration} from '../../components/time_utils';
import type {NavState, NavView} from './nav_state';
import type {OverviewData} from './types';
import type * as queries from './queries';
import {OverviewView} from './views/overview_view';
import {DominatorsView} from './views/dominators_view';
import {ObjectView} from './views/object_view';
import {AllObjectsView} from './views/all_objects_view';
import {BitmapGalleryView} from './views/bitmap_gallery_view';
import {ClassesView} from './views/classes_view';
import {StringsView} from './views/strings_view';
import {ArraysView} from './views/arrays_view';
import {FlamegraphObjectsView} from './views/flamegraph_objects_view';
import {FlamegraphView} from './views/flamegraph_view';
import {CallstackView} from './views/callstack_view';
import type {HeapDumpExplorerSession} from './session';
interface HeapDumpPageAttrs {
readonly session: HeapDumpExplorerSession;
readonly subpage: string | undefined;
}
const FG_KEY_PREFIX = 'fg-';
const INSTANCE_KEY_PREFIX = 'inst-';
function fgTabKey(pathHashes: string, isDominator: boolean): string {
return `${FG_KEY_PREFIX}${isDominator ? 'd' : 'n'}:${pathHashes}`;
}
function instanceTabKey(objId: number): string {
return `${INSTANCE_KEY_PREFIX}${objId}`;
}
function activeTabKey(session: HeapDumpExplorerSession): string {
const tabs = session.flamegraphTabs;
if (session.nav.view === 'flamegraph-objects' && tabs.length > 0) {
const active = session.activeFlamegraph;
const tab =
(active &&
tabs.find(
(t) =>
t.pathHashes === active.pathHashes &&
t.isDominator === active.isDominator,
)) ||
tabs[tabs.length - 1];
return fgTabKey(tab.pathHashes, tab.isDominator);
}
const objId = session.activeInstanceObjId;
if (objId !== null) {
return instanceTabKey(objId);
}
return session.nav.view;
}
// Per-tab select/close actions, looked up by key.
interface TabActions {
select(): void;
close?(): void;
}
function buildTabs(
session: HeapDumpExplorerSession,
activeDump: queries.HeapDump,
state: NavState,
overview: OverviewData,
): {tabs: TabsTab[]; actions: Map<string, TabActions>} {
const {engine, trace, navigateWithTabs, clearNavParam} = session;
const hideExplanationSetting = session.hideDefaultChangedHint;
const hideHint = hideExplanationSetting.get();
const actions = new Map<string, TabActions>();
const tabs: TabsTab[] = [
{
key: 'overview',
title: 'Overview',
content: m(OverviewView, {
overview,
activeDump,
navigate: navigateWithTabs,
showDefaultChangedHint: session.autoNavigated && !hideHint,
onBackToTimeline: () => trace.navigate('#!/viewer'),
onDismissDefaultChangedHint: () => hideExplanationSetting.set(true),
}),
},
{
key: 'flamegraph',
title: 'Flamegraph',
content: m(FlamegraphView, {
trace,
upid: activeDump.upid,
ts: activeDump.ts,
state: session.flamegraphPanelState,
onStateChange: session.setFlamegraphPanelState,
onShowObjects: (pathHashes, isDominator) =>
session.openFlamegraph({
pathHashes,
isDominator,
upid: activeDump.upid,
ts: activeDump.ts,
}),
}),
},
{
key: 'classes',
title: 'Classes',
content: m(ClassesView, {
engine,
activeDump,
navigate: navigateWithTabs,
clearNavParam,
initialRootClass:
state.view === 'classes' ? state.params.rootClass : undefined,
}),
},
{
key: 'objects',
title: 'Objects',
content: m(AllObjectsView, {
engine,
activeDump,
navigate: navigateWithTabs,
clearNavParam,
initialClass: state.view === 'objects' ? state.params.cls : undefined,
}),
},
{
key: 'dominators',
title: 'Dominators',
content: m(DominatorsView, {
engine,
activeDump,
navigate: navigateWithTabs,
}),
},
{
key: 'bitmaps',
title: 'Bitmaps',
content: m(BitmapGalleryView, {
engine,
activeDump,
navigate: navigateWithTabs,
clearNavParam,
hasFieldValues: overview.hasFieldValues,
filterKey:
state.view === 'bitmaps' ? state.params.filterKey : undefined,
}),
},
{
key: 'strings',
title: 'Strings',
content: m(StringsView, {
engine,
activeDump,
navigate: navigateWithTabs,
clearNavParam,
initialQuery: state.view === 'strings' ? state.params.q : undefined,
hasFieldValues: overview.hasFieldValues,
}),
},
{
key: 'arrays',
title: 'Arrays',
content: m(ArraysView, {
engine,
activeDump,
navigate: navigateWithTabs,
clearNavParam,
initialArrayHash:
state.view === 'arrays' ? state.params.arrayHash : undefined,
hasFieldValues: overview.hasFieldValues,
}),
},
{
key: 'callstack',
title: 'Callstack',
content: m(CallstackView, {
trace,
dump: activeDump,
state: session.callstackPanelState,
onStateChange: session.setCallstackPanelState,
}),
},
];
// Static tab keys are view names.
for (const tab of tabs) {
actions.set(tab.key, {select: () => session.navigate(tab.key as NavView)});
}
for (const fg of session.flamegraphTabs) {
const key = fgTabKey(fg.pathHashes, fg.isDominator);
tabs.push({
key,
title:
fg.count !== null
? `Flamegraph objects (${fg.count.toLocaleString()})`
: 'Flamegraph objects',
closeButton: true,
content: m(FlamegraphObjectsView, {
engine,
navigate: navigateWithTabs,
pathHashes: fg.pathHashes,
isDominator: fg.isDominator,
onBackToTimeline: () => trace.navigate('#!/viewer'),
}),
});
actions.set(key, {
select: () =>
session.navigate('flamegraph-objects', {
pathHashes: fg.pathHashes,
isDominator: fg.isDominator,
}),
close: () => session.closeFlamegraph(fg.pathHashes, fg.isDominator),
});
}
for (const obj of session.instanceTabs) {
const key = instanceTabKey(obj.objId);
tabs.push({
key,
title: obj.label,
closeButton: true,
content: m(ObjectView, {
engine,
activeDump,
heaps: overview.heaps,
navigate: navigateWithTabs,
openFlamegraphPivotedAt: session.openFlamegraphPivotedAt,
params: {id: obj.objId},
}),
});
actions.set(key, {
select: () => session.navigate('object', {id: obj.objId}),
close: () => session.closeInstanceTab(obj.objId),
});
}
return {tabs, actions};
}
function processLabel(d: queries.HeapDump): string {
return d.processName !== null
? `${d.processName} (pid ${d.pid})`
: `pid ${d.pid}`;
}
function renderDumpSelector(session: HeapDumpExplorerSession): m.Children {
const allDumps = session.dumps;
const active = session.activeDump;
if (allDumps.length <= 1 || active === null) return null;
return m(
'div',
{class: 'pf-hde-dump-selector'},
m('span', {class: 'pf-hde-dump-selector__label'}, 'Heap dump:'),
m(
PopupMenu,
{
trigger: m(Button, {
label: processLabel(active),
icon: 'memory',
rightIcon: 'arrow_drop_down',
variant: ButtonVariant.Outlined,
compact: true,
}),
},
allDumps.map((d) => {
const offset = Time.diff(d.ts, session.trace.traceInfo.start);
return m(MenuItem, {
label: `${processLabel(d)} — ${formatDuration(session.trace, offset)}`,
active: d === active,
onclick: () => session.selectDump(d),
});
}),
),
);
}
export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
oncreate({attrs}: m.VnodeDOM<HeapDumpPageAttrs>) {
attrs.session.setNavigateCallback((sub) => {
window.location.hash = `!/heapdump${sub ? '/' + sub : ''}`;
});
void attrs.session.loadOverview();
}
onremove({attrs}: m.VnodeDOM<HeapDumpPageAttrs>) {
attrs.session.setNavigateCallback(undefined);
}
view({attrs}: m.Vnode<HeapDumpPageAttrs>) {
const {session, subpage} = attrs;
session.syncFromSubpage(subpage);
session.syncInstanceTabFromNav();
session.syncFlamegraphTabFromNav();
const active = session.activeDump;
const overview = session.cachedOverview;
if (active === null || overview === null) {
return m(
'div',
{class: 'pf-hde-page'},
renderDumpSelector(session),
m('div', {class: 'pf-hde-loading'}, m(Spinner, {easing: true})),
);
}
// Keyed so Mithril remounts views (and their SQLDataSources) on
// dump switch.
const tabsKey = `${active.upid}:${active.ts}`;
const {tabs, actions} = buildTabs(session, active, session.nav, overview);
return m(
'div',
{class: 'pf-hde-page'},
renderDumpSelector(session),
m(
'main',
{class: 'pf-hde-main'},
m(Tabs, {
key: tabsKey,
tabs,
activeTabKey: activeTabKey(session),
onTabChange: (key: string) => actions.get(key)?.select(),
onTabClose: (key: string) => actions.get(key)?.close?.(),
}),
),
);
}
}