blob: beade7e9284b622ba8bba3cca2c5f1e063a27c28 [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 App} from '../../public/app';
import {Anchor} from '../../widgets/anchor';
import {Button} from '../../widgets/button';
import {Spinner} from '../../widgets/spinner';
import {DataGrid} from '../../components/widgets/datagrid/datagrid';
import {type SchemaRegistry} from '../../components/widgets/datagrid/datagrid_schema';
import {type Row, type SqlValue} from '../../trace_processor/query_result';
import {Tabs, type TabsTab} from '../../widgets/tabs';
import {sizeRenderer, filterAggregated} from './data';
import {
TAB_PROCESSES,
TAB_INSPECT,
TAB_MAPPING,
TAB_STRINGS_ALL,
TAB_STRINGS_DUPS,
TAB_STRINGS_VMA,
procTabKey,
parseProcTabKey,
mapTabKey,
parseMapTabKey,
closeTab,
type ProcessTabState,
type MappingTabState,
type PageContext,
} from './state';
import {getDupsFor, buildStringsTabs, closeStringsTab} from './strings_view';
import {renderVmaFilterToolbar, renderMappingTab} from './mapping_view';
// ── Process View (outer tabs) ───────────────────────────────────────────────
export function renderProcessView(ctx: PageContext, app: App): m.Children {
const outerTabs: TabsTab[] = [];
outerTabs.push({
key: TAB_PROCESSES,
title: `Processes (${ctx.processes?.length ?? 0})`,
content: renderProcessesTab(ctx),
});
for (const pid of ctx.s.openProcessOrder) {
const ps = ctx.s.openProcesses.get(pid);
if (ps === undefined) continue;
const process = ctx.processes?.find((p) => p.pid === pid);
const pname = process?.name ?? `PID ${pid}`;
outerTabs.push({
key: procTabKey(pid),
title: pname,
closeButton: true,
content: renderProcessSubTabs(ctx, pid, ps, app),
});
}
const activeKey =
ctx.s.activeProcessPid !== null
? procTabKey(ctx.s.activeProcessPid)
: ctx.s.processTab;
return m(Tabs, {
tabs: outerTabs,
activeTabKey: activeKey,
onTabChange: (key) => {
if (key === TAB_PROCESSES) {
ctx.s.activeProcessPid = null;
ctx.s.processTab = TAB_PROCESSES;
} else {
const pid = parseProcTabKey(key);
if (pid !== undefined) {
ctx.s.activeProcessPid = pid;
ctx.s.processTab = key;
}
}
},
onTabClose: (key) => {
const pid = parseProcTabKey(key);
if (pid !== undefined) {
const next = closeTab(
ctx.s.openProcesses,
ctx.s.openProcessOrder,
ctx.s.activeProcessPid,
pid,
null,
);
ctx.s.activeProcessPid = next;
ctx.s.processTab = next !== null ? procTabKey(next) : TAB_PROCESSES;
}
},
});
}
// ── Per-process sub-tabs ────────────────────────────────────────────────────
function renderProcessSubTabs(
ctx: PageContext,
pid: number,
ps: ProcessTabState,
app: App,
): m.Children {
const subTabs: TabsTab[] = [];
subTabs.push({
key: TAB_INSPECT,
title: 'Mappings',
content: renderSmapsGrid(ctx, pid, ps, app),
});
for (const name of ps.openMappingOrder) {
const ms = ps.openMappings.get(name);
if (ms === undefined) continue;
subTabs.push({
key: mapTabKey(name),
title: name,
closeButton: true,
content: renderMappingSubTabs(ctx, pid, name, ms),
});
}
// Process-level strings tabs (from "All Strings" button)
if (ps.processStringsData !== null) {
const pss = ctx.getProcessStringsState(ps);
const dups = getDupsFor(pss, ps.processStringsData.strings);
subTabs.push(
...buildStringsTabs(
pss,
dups,
() => {
ps.activeMapping = null;
ps.subTab = TAB_STRINGS_ALL;
},
'All ',
'',
),
);
}
const activeKey =
ps.activeMapping !== null ? mapTabKey(ps.activeMapping) : ps.subTab;
return m(Tabs, {
tabs: subTabs,
activeTabKey: activeKey,
onTabChange: (key) => {
if (key === TAB_INSPECT) {
ps.activeMapping = null;
ps.subTab = TAB_INSPECT;
} else {
const name = parseMapTabKey(key);
if (name !== undefined) {
ps.activeMapping = name;
ps.subTab = key;
} else {
ps.activeMapping = null;
ps.subTab = key;
}
}
},
onTabClose: (key) => {
const name = parseMapTabKey(key);
if (name !== undefined) {
const next = closeTab(
ps.openMappings,
ps.openMappingOrder,
ps.activeMapping,
name,
null,
);
ps.activeMapping = next;
ps.subTab = next !== null ? mapTabKey(next) : TAB_INSPECT;
} else if (
key === TAB_STRINGS_ALL ||
key === TAB_STRINGS_DUPS ||
key === TAB_STRINGS_VMA
) {
const pss = ctx.getProcessStringsState(ps);
ps.subTab = closeStringsTab(pss, key, ps.subTab, TAB_INSPECT);
}
},
});
}
// ── Per-mapping sub-tabs ────────────────────────────────────────────────────
function renderMappingSubTabs(
ctx: PageContext,
pid: number,
mappingName: string,
ms: MappingTabState,
): m.Children {
const subTabs: TabsTab[] = [];
subTabs.push({
key: TAB_MAPPING,
title: 'VMAs',
content: renderMappingTab(ctx, pid, mappingName, ms),
});
if (ms.stringsData !== null) {
const dups = getDupsFor(ms, ms.stringsData.strings);
subTabs.push(
...buildStringsTabs(
ms,
dups,
() => {
ms.subTab = TAB_STRINGS_ALL;
},
'',
ms.stringsData.processName,
),
);
}
return m(Tabs, {
tabs: subTabs,
activeTabKey: ms.subTab,
onTabChange: (key) => {
ms.subTab = key;
},
onTabClose: (key) => {
if (
key === TAB_STRINGS_ALL ||
key === TAB_STRINGS_DUPS ||
key === TAB_STRINGS_VMA
) {
ms.subTab = closeStringsTab(ms, key, ms.subTab, TAB_MAPPING);
}
},
});
}
// ── Processes list DataGrid ─────────────────────────────────────────────────
function buildProcessSchema(ctx: PageContext): SchemaRegistry {
return {
process: {
pid: {title: 'PID', columnType: 'quantitative'},
name: {
title: 'Process',
columnType: 'text',
cellRenderer: (value: SqlValue, row: Row) => {
if (!ctx.isRoot) return String(value);
const pid = Number(row.pid);
return m(
Anchor,
{
onclick: () => ctx.inspectProcess(pid),
title: 'Inspect smaps',
},
String(value),
);
},
},
oomLabel: {title: 'State', columnType: 'text'},
pssKb: {
title: 'PSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
rssKb: {
title: 'RSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
privateDirtyKb: {
title: 'Priv Dirty',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
privateCleanKb: {
title: 'Priv Clean',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
swapKb: {
title: 'Swap',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
sizeKb: {
title: 'VSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
},
};
}
function renderProcessesTab(ctx: PageContext): m.Children {
if (ctx.processes === null) return null;
const rows: Row[] = ctx.processes.map((p) => {
const r = ctx.rollups.get(p.pid);
return {
pid: p.pid,
name: p.name,
oomLabel: p.oomLabel,
pssKb: r?.pssKb ?? 0,
rssKb: r?.rssKb ?? 0,
privateDirtyKb: r?.privateDirtyKb ?? 0,
privateCleanKb: r?.privateCleanKb ?? 0,
swapKb: r?.swapKb ?? 0,
sizeKb: r?.sizeKb ?? 0,
};
});
return m(DataGrid, {
key: ctx.enrichGeneration,
schema: buildProcessSchema(ctx),
rootSchema: 'process',
data: rows,
fillHeight: true,
initialColumns: [
{id: 'pid', field: 'pid'},
{id: 'name', field: 'name'},
{id: 'oomLabel', field: 'oomLabel'},
{id: 'pssKb', field: 'pssKb', sort: 'DESC' as const},
{id: 'rssKb', field: 'rssKb'},
{id: 'privateDirtyKb', field: 'privateDirtyKb'},
{id: 'privateCleanKb', field: 'privateCleanKb'},
{id: 'swapKb', field: 'swapKb'},
{id: 'sizeKb', field: 'sizeKb'},
],
});
}
// ── Aggregated mappings DataGrid (per-process) ──────────────────────────────
function buildAggMappingSchema(
ctx: PageContext,
ps: ProcessTabState,
): SchemaRegistry {
return {
mapping: {
name: {
title: 'Mapping',
columnType: 'text',
cellRenderer: (value: SqlValue) => {
const name = String(value);
return m(
Anchor,
{
onclick: () => ctx.openMapping(ps, name),
title: 'Show individual VMAs',
},
name,
);
},
},
count: {title: 'Count', columnType: 'quantitative'},
pssKb: {
title: 'PSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
rssKb: {
title: 'RSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
privateDirtyKb: {
title: 'Priv Dirty',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
privateCleanKb: {
title: 'Priv Clean',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
sharedDirtyKb: {
title: 'Shared Dirty',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
sharedCleanKb: {
title: 'Shared Clean',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
swapKb: {
title: 'Swap',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
sizeKb: {
title: 'VSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
},
};
}
function renderSmapsGrid(
ctx: PageContext,
pid: number,
ps: ProcessTabState,
app: App,
): m.Children {
if (ctx.loadingPid === pid) {
return m('.pf-smaps-explorer__loading', m(Spinner));
}
const rawAgg = ctx.smapsData.get(pid);
if (rawAgg === undefined) {
return m('.pf-smaps-explorer__loading--muted', 'Loading smaps\u2026');
}
const aggregated = filterAggregated(rawAgg, ctx.vmaFilters);
const process = ctx.processes?.find((p) => p.pid === pid);
const processName = process?.name ?? '';
const totalEntries = aggregated.reduce((n, g) => n + g.entries.length, 0);
const aggRows: Row[] = aggregated.map((g) => ({
name: g.name || '[anonymous]',
count: g.count,
sizeKb: g.sizeKb,
rssKb: g.rssKb,
pssKb: g.pssKb,
privateCleanKb: g.privateCleanKb,
privateDirtyKb: g.privateDirtyKb,
sharedCleanKb: g.sharedCleanKb,
sharedDirtyKb: g.sharedDirtyKb,
swapKb: g.swapKb,
}));
return m('.pf-smaps-explorer__panel', [
// Action bar
m('.pf-smaps-explorer__toolbar', [
m(
'span.pf-smaps-explorer__label',
`${aggregated.length} mappings \u00b7 ${totalEntries} VMAs`,
),
m('.pf-smaps-explorer__spacer'),
renderVmaFilterToolbar(ctx),
m(Button, {
label: 'All Strings',
icon: 'text_fields',
compact: true,
onclick: () => ctx.startStringsScan(pid, processName, ps),
}),
m(Button, {
label: 'Heap Dump',
icon: 'download',
compact: true,
onclick: () => ctx.captureHeap(pid, processName, app),
}),
]),
// Aggregated mappings DataGrid
m(
'.pf-smaps-explorer__grid-container',
m(DataGrid, {
key: ctx.smapsScanGeneration,
schema: buildAggMappingSchema(ctx, ps),
rootSchema: 'mapping',
data: aggRows,
fillHeight: true,
initialColumns: [
{id: 'name', field: 'name'},
{id: 'count', field: 'count'},
{id: 'pssKb', field: 'pssKb', sort: 'DESC' as const},
{id: 'rssKb', field: 'rssKb'},
{id: 'privateDirtyKb', field: 'privateDirtyKb'},
{id: 'privateCleanKb', field: 'privateCleanKb'},
{id: 'sharedDirtyKb', field: 'sharedDirtyKb'},
{id: 'sharedCleanKb', field: 'sharedCleanKb'},
{id: 'swapKb', field: 'swapKb'},
{id: 'sizeKb', field: 'sizeKb'},
],
}),
),
]);
}