blob: 54686ca6b642df6f9d8bc55878119b85420bcafe [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 {
LineChartSvg,
type LineChartData,
type LineChartSeries,
} from '../../../../components/widgets/charts_svg/line_chart_svg';
import {Button, ButtonVariant} from '../../../../widgets/button';
import {Intent} from '../../../../widgets/common';
import {MenuDivider, MenuItem, PopupMenu} from '../../../../widgets/menu';
import {PopupPosition} from '../../../../widgets/popup';
import {
Grid,
GridCell,
type GridColumn,
GridHeaderCell,
renderSortMenuItems,
type SortDirection,
} from '../../../../widgets/grid';
import {TextInput} from '../../../../widgets/text_input';
import {RadioGroup} from '../../../../widgets/radio_group';
import type {
LiveSession,
ProcessInfo,
SnapshotData,
} from '../../sessions/live_session';
import {
categorizeProcess,
CATEGORIES,
type CategoryId,
} from '../../process_categories';
import {Billboard} from '../../components/billboard';
import {ColorChip, chipColor} from '../../components/color_chip';
import {billboardKb, formatKb, maxSeriesKb, niceKbInterval} from '../../utils';
import {
type ProcessGrouping,
type ProcessMetric,
type ProcessMemoryRow,
PROCESS_METRIC_OPTIONS,
OOM_SCORE_BUCKETS,
} from '../../process_data';
import {Stack} from '../../../../widgets/stack';
export type {ProcessGrouping, ProcessMetric, ProcessMemoryRow};
export {PROCESS_METRIC_OPTIONS, OOM_SCORE_BUCKETS};
// Tiny SVG sparkline for the trend column. Min/max are derived from the
// values themselves (so a flat series renders as a flat line at mid-height).
function sparkline(
values: ReadonlyArray<number>,
width = 64,
height = 16,
): m.Children {
if (values.length < 2) {
return m('span.pf-memscope-sparkline-empty', '—');
}
let min = values[0];
let max = values[0];
for (const v of values) {
if (v < min) min = v;
if (v > max) max = v;
}
const range = max - min || 1;
const stepX = width / (values.length - 1);
const padY = 2;
const drawH = height - padY * 2;
const points = values
.map((v, i) => {
const x = (i * stepX).toFixed(1);
const y = (padY + drawH - ((v - min) / range) * drawH).toFixed(1);
return `${x},${y}`;
})
.join(' ');
const last = values[values.length - 1];
const first = values[0];
const stroke =
last > first
? 'var(--pf-color-danger)'
: last < first
? 'var(--pf-color-success)'
: 'var(--pf-color-text-muted)';
return m(
'svg.pf-memscope-sparkline',
{
width,
height,
viewBox: `0 0 ${width} ${height}`,
preserveAspectRatio: 'none',
},
m('polyline', {
points,
'fill': 'none',
stroke,
'stroke-width': 1.25,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
}),
);
}
// ---------------------------------------------------------------------------
// Chart builders (inlined from chart_builders.ts)
// ---------------------------------------------------------------------------
function buildCategoryTimeSeries(
data: SnapshotData,
t0: number,
counters: readonly string[],
): LineChartData | undefined {
const catIds = Object.keys(CATEGORIES) as CategoryId[];
const tsSet = new Set<number>();
const byCatTs = new Map<number, Map<CategoryId, number>>();
for (const [upid, counterMap] of data.processCountersByUpid) {
const info = data.processInfo.get(upid);
if (info === undefined) continue;
const cat = categorizeProcess(info.processName);
const id = catIds.find((k) => CATEGORIES[k].name === cat.name)!;
for (const counterName of counters) {
const samples = counterMap.get(counterName);
if (samples === undefined) continue;
for (const {ts, value} of samples) {
tsSet.add(ts);
let catMap = byCatTs.get(ts);
if (catMap === undefined) {
catMap = new Map();
byCatTs.set(ts, catMap);
}
catMap.set(id, (catMap.get(id) ?? 0) + Math.round(value / 1024));
}
}
}
const timestamps = [...tsSet].sort((a, b) => a - b);
if (timestamps.length < 2) return undefined;
const pointsByCategory = new Map<CategoryId, {x: number; y: number}[]>();
for (const id of catIds) pointsByCategory.set(id, []);
for (const ts of timestamps) {
const x = (ts - t0) / 1e9;
const catMap = byCatTs.get(ts)!;
for (const id of catIds) {
pointsByCategory.get(id)!.push({x, y: catMap.get(id) ?? 0});
}
}
const series: LineChartSeries[] = [];
for (const id of catIds) {
const points = pointsByCategory.get(id)!;
if (points.some((p) => p.y > 0)) {
const cat = CATEGORIES[id];
series.push({name: cat.name, points, color: chipColor(cat.color)});
}
}
if (series.length === 0) return undefined;
return {series};
}
function buildOomScoreTimeSeries(
data: SnapshotData,
t0: number,
counters: readonly string[],
): LineChartData | undefined {
// Latest oom_score_adj per upid.
const oomByUpid = new Map<number, number>();
for (const [upid, counterMap] of data.processCountersByUpid) {
const samples = counterMap.get('oom_score_adj');
if (samples === undefined || samples.length === 0) continue;
let latestTs = -1;
let latestVal = 0;
for (const {ts, value} of samples) {
if (ts >= latestTs) {
latestTs = ts;
latestVal = value;
}
}
oomByUpid.set(upid, latestVal);
}
const tsSet = new Set<number>();
const byBucketTs = new Map<number, Map<number, number>>();
for (const [upid, counterMap] of data.processCountersByUpid) {
const oomScore = oomByUpid.get(upid) ?? 0;
const bucketIdx = OOM_SCORE_BUCKETS.findIndex(
(b) => oomScore >= b.minScore && oomScore <= b.maxScore,
);
const idx = bucketIdx !== -1 ? bucketIdx : OOM_SCORE_BUCKETS.length - 1;
for (const counterName of counters) {
const samples = counterMap.get(counterName);
if (samples === undefined) continue;
for (const {ts, value} of samples) {
tsSet.add(ts);
let bucketMap = byBucketTs.get(ts);
if (bucketMap === undefined) {
bucketMap = new Map();
byBucketTs.set(ts, bucketMap);
}
bucketMap.set(
idx,
(bucketMap.get(idx) ?? 0) + Math.round(value / 1024),
);
}
}
}
const timestamps = [...tsSet].sort((a, b) => a - b);
if (timestamps.length < 2) return undefined;
const pointsByBucket = new Map<number, {x: number; y: number}[]>();
for (let i = 0; i < OOM_SCORE_BUCKETS.length; i++) pointsByBucket.set(i, []);
for (const ts of timestamps) {
const x = (ts - t0) / 1e9;
const bucketMap = byBucketTs.get(ts)!;
for (let i = 0; i < OOM_SCORE_BUCKETS.length; i++) {
pointsByBucket.get(i)!.push({x, y: bucketMap.get(i) ?? 0});
}
}
const series: LineChartSeries[] = [];
for (let i = 0; i < OOM_SCORE_BUCKETS.length; i++) {
const points = pointsByBucket.get(i)!;
if (points.some((p) => p.y > 0)) {
const bucket = OOM_SCORE_BUCKETS[i];
series.push({name: bucket.name, points, color: chipColor(bucket.color)});
}
}
if (series.length === 0) return undefined;
return {series};
}
function buildProcessDrilldown(
data: SnapshotData,
t0: number,
counters: readonly string[],
filter: (info: ProcessInfo) => boolean,
): LineChartData | undefined {
const tsSet = new Set<number>();
const byProcTs = new Map<number, Map<string, number>>();
for (const [upid, counterMap] of data.processCountersByUpid) {
const info = data.processInfo.get(upid);
if (info === undefined || !filter(info)) continue;
// Disambiguate processes that share a name by appending the PID.
const seriesKey = `${info.processName} (${info.pid})`;
for (const counterName of counters) {
const samples = counterMap.get(counterName);
if (samples === undefined) continue;
for (const {ts, value} of samples) {
tsSet.add(ts);
let procMap = byProcTs.get(ts);
if (procMap === undefined) {
procMap = new Map();
byProcTs.set(ts, procMap);
}
procMap.set(
seriesKey,
(procMap.get(seriesKey) ?? 0) + Math.round(value / 1024),
);
}
}
}
const timestamps = [...tsSet].sort((a, b) => a - b);
if (timestamps.length < 2) return undefined;
const allNames = new Set<string>();
for (const procMap of byProcTs.values()) {
for (const name of procMap.keys()) allNames.add(name);
}
const pointsByProc = new Map<string, {x: number; y: number}[]>();
for (const name of allNames) pointsByProc.set(name, []);
for (const ts of timestamps) {
const x = (ts - t0) / 1e9;
const procMap = byProcTs.get(ts)!;
for (const name of allNames) {
pointsByProc.get(name)!.push({x, y: procMap.get(name) ?? 0});
}
}
const ranked = [...allNames]
.map((name) => ({
name,
points: pointsByProc.get(name)!,
total: pointsByProc.get(name)!.reduce((s, p) => s + p.y, 0),
}))
.sort((a, b) => b.total - a.total);
const TOP_N = 15;
const top = ranked.slice(0, TOP_N);
const rest = ranked.slice(TOP_N);
const series: LineChartSeries[] = top.map((r) => ({
name: r.name,
points: r.points,
}));
if (rest.length > 0) {
const otherPoints = timestamps.map((ts, i) => ({
x: (ts - t0) / 1e9,
y: rest.reduce((sum, r) => sum + r.points[i].y, 0),
}));
series.push({
name: `Other (${rest.length} processes)`,
points: otherPoints,
color: 'var(--pf-chart-color-neutral)',
});
}
if (series.length === 0) return undefined;
return {series};
}
function buildCategoryDrilldown(
data: SnapshotData,
categoryId: CategoryId,
t0: number,
counters: readonly string[],
): LineChartData | undefined {
const targetCat = CATEGORIES[categoryId];
return buildProcessDrilldown(
data,
t0,
counters,
(info) => categorizeProcess(info.processName).name === targetCat.name,
);
}
function buildOomDrilldown(
data: SnapshotData,
bucketIdx: number,
t0: number,
counters: readonly string[],
): LineChartData | undefined {
const bucket = OOM_SCORE_BUCKETS[bucketIdx];
const oomByUpid = new Map<number, number>();
for (const [upid, counterMap] of data.processCountersByUpid) {
const samples = counterMap.get('oom_score_adj');
if (samples === undefined || samples.length === 0) continue;
let latestTs = -1;
let latestVal = 0;
for (const {ts, value} of samples) {
if (ts >= latestTs) {
latestTs = ts;
latestVal = value;
}
}
oomByUpid.set(upid, latestVal);
}
return buildProcessDrilldown(data, t0, counters, (info) => {
const score = oomByUpid.get(info.upid) ?? 0;
return score >= bucket.minScore && score <= bucket.maxScore;
});
}
function buildLatestProcessMemory(data: SnapshotData): ProcessMemoryRow[] {
const rows: ProcessMemoryRow[] = [];
let maxTs = 0;
for (const counterMap of data.processCountersByUpid.values()) {
for (const samples of counterMap.values()) {
for (const {ts} of samples) {
if (ts > maxTs) maxTs = ts;
}
}
}
for (const [upid, counterMap] of data.processCountersByUpid) {
const info = data.processInfo.get(upid);
if (info === undefined) continue;
const getLatestRaw = (counterName: string): number => {
const samples = counterMap.get(counterName);
if (samples === undefined || samples.length === 0) return 0;
let latestTs = -1;
let latestValue = 0;
for (const {ts, value} of samples) {
if (ts >= latestTs) {
latestTs = ts;
latestValue = value;
}
}
return latestValue;
};
const rssKb = Math.round(getLatestRaw('mem.rss') / 1024);
if (rssKb === 0) continue;
const rssSamples = counterMap.get('mem.rss');
const rssTrendKb =
rssSamples !== undefined
? [...rssSamples]
.sort((a, b) => a.ts - b.ts)
.map(({value}) => Math.round(value / 1024))
: [];
rows.push({
processName: info.processName,
pid: info.pid,
rssKb,
anonKb: Math.round(getLatestRaw('mem.rss.anon') / 1024),
fileKb: Math.round(getLatestRaw('mem.rss.file') / 1024),
shmemKb: Math.round(getLatestRaw('mem.rss.shmem') / 1024),
swapKb: Math.round(getLatestRaw('mem.swap') / 1024),
dmabufKb: Math.round(getLatestRaw('mem.dmabuf_rss') / 1024),
oomScore: getLatestRaw('oom_score_adj'),
debuggable: info.debuggable,
ageSeconds: info.startTs !== null ? (maxTs - info.startTs) / 1e9 : null,
rssTrendKb,
});
}
rows.sort((a, b) => b.rssKb - a.rssKb);
return rows;
}
// Build a lookup from category name -> color for the cell renderer.
const CATEGORY_COLOR_MAP = new Map<string, string>(
(Object.values(CATEGORIES) as readonly {name: string; color: string}[]).map(
(c) => [c.name, c.color],
),
);
export interface ProcessesTabAttrs {
readonly session: LiveSession;
}
export class ProcessesTab implements m.ClassComponent<ProcessesTabAttrs> {
private grouping: ProcessGrouping = 'category';
private metric: ProcessMetric = 'anon_swap';
private selectedCategory?: CategoryId;
private selectedOomBucket?: number;
private processSearch: string = '';
view({attrs}: m.CVnode<ProcessesTabAttrs>): m.Children {
const data = attrs.session.data;
if (!data) return null;
const t0 = data.ts0;
const counters = PROCESS_METRIC_OPTIONS.find(
(o) => o.key === this.metric,
)!.counters;
const isDrilledDown =
this.grouping === 'category'
? this.selectedCategory !== undefined
: this.selectedOomBucket !== undefined;
const cat = this.selectedCategory
? CATEGORIES[this.selectedCategory]
: undefined;
const oomBucket =
this.selectedOomBucket !== undefined
? OOM_SCORE_BUCKETS[this.selectedOomBucket]
: undefined;
const chartData = isDrilledDown
? this.selectedCategory !== undefined
? buildCategoryDrilldown(data, this.selectedCategory, t0, counters)
: this.selectedOomBucket !== undefined
? buildOomDrilldown(data, this.selectedOomBucket, t0, counters)
: undefined
: this.grouping === 'category'
? buildCategoryTimeSeries(data, t0, counters)
: buildOomScoreTimeSeries(data, t0, counters);
// Pin x-axis to the exact data range so the plot edges line up with the
// first and last sample.
let chartXMin: number | undefined;
let chartXMax: number | undefined;
if (chartData !== undefined) {
for (const s of chartData.series) {
for (const p of s.points) {
if (chartXMin === undefined || p.x < chartXMin) chartXMin = p.x;
if (chartXMax === undefined || p.x > chartXMax) chartXMax = p.x;
}
}
}
const latestProcesses = buildLatestProcessMemory(data);
const processes =
this.grouping === 'category' && this.selectedCategory
? latestProcesses.filter(
(p: ProcessMemoryRow) =>
categorizeProcess(p.processName).name === cat!.name,
)
: this.grouping === 'oom_score' && oomBucket
? latestProcesses.filter(
(p: ProcessMemoryRow) =>
p.oomScore >= oomBucket.minScore &&
p.oomScore <= oomBucket.maxScore,
)
: latestProcesses;
const filteredProcesses =
this.processSearch.trim() === ''
? processes
: processes.filter((p) =>
p.processName
.toLowerCase()
.includes(this.processSearch.toLowerCase()),
);
const metricInfo = PROCESS_METRIC_OPTIONS.find(
(o) => o.key === this.metric,
)!;
const drillName = cat?.name ?? oomBucket?.name;
const groupLabel = this.grouping === 'category' ? 'Category' : 'OOM Score';
const title = drillName
? `Process Memory: ${drillName}`
: `Process Memory by ${groupLabel}`;
const subtitle = drillName
? `Stacked ${metricInfo.label} per process. Totals may exceed actual memory usage because RSS counts shared pages (COW, mmap) in every process that maps them.`
: `Stacked ${metricInfo.label} per ${groupLabel.toLowerCase()}. Click a ${groupLabel.toLowerCase()} to drill into individual processes.`;
// Compute billboard totals from all processes.
const totalAnonSwapKb = latestProcesses.reduce(
(s, p) => s + p.anonKb + p.swapKb,
0,
);
const totalFileKb = latestProcesses.reduce((s, p) => s + p.fileKb, 0);
const totalDmabufKb = latestProcesses.reduce((s, p) => s + p.dmabufKb, 0);
return m(Stack, {spacing: 'large'}, [
latestProcesses.length > 0 &&
m(
Stack,
{orientation: 'horizontal', spacing: 'large'},
m(Billboard, {
...billboardKb(totalAnonSwapKb),
label: 'Anon + Swap',
desc: 'Sum of anonymous RSS + swap across all processes',
}),
m(Billboard, {
...billboardKb(totalFileKb),
label: 'File',
desc: 'Sum of file-backed RSS across all processes',
}),
m(Billboard, {
...billboardKb(totalDmabufKb),
label: 'DMA-BUF',
desc: 'Sum of DMA-BUF heap RSS across all processes',
}),
),
m(
'.pf-memscope-panel',
m(
'.pf-memscope-panel__header',
m(
'.pf-memscope-panel__title-row',
isDrilledDown &&
m(Button, {
variant: ButtonVariant.Filled,
icon: 'arrow_back',
label:
this.grouping === 'category'
? 'All categories'
: 'All OOM buckets',
minimal: true,
onclick: () => {
this.selectedCategory = undefined;
this.selectedOomBucket = undefined;
},
}),
m('h2', title),
m(
'.pf-memscope-panel__controls',
m(
RadioGroup,
{
intent: Intent.Primary,
selectedValue: this.grouping,
onValueChange: (value) => {
const g = value as ProcessGrouping;
if (g === this.grouping) return;
this.grouping = g;
this.selectedCategory = undefined;
this.selectedOomBucket = undefined;
},
},
m(RadioGroup.Button, {value: 'category'}, 'By Category'),
m(RadioGroup.Button, {value: 'oom_score'}, 'By OOM Score'),
),
m(
RadioGroup,
{
intent: Intent.Primary,
selectedValue: this.metric,
onValueChange: (value) => {
const newMetric = value as ProcessMetric;
if (newMetric === this.metric) return;
this.metric = newMetric;
this.selectedCategory = undefined;
this.selectedOomBucket = undefined;
},
},
PROCESS_METRIC_OPTIONS.map((o) =>
m(RadioGroup.Button, {value: o.key}, o.label),
),
),
),
),
m('p', subtitle),
),
m(
'.pf-memscope-panel__body',
chartData
? m(LineChartSvg, {
data: chartData,
height: 350,
xAxisLabel: 'Time (s)',
yAxisLabel: 'RSS',
showLegend: true,
showPoints: false,
stacked: true,
gridLines: 'both',
xAxisMin: chartXMin,
xAxisMax: chartXMax,
formatXValue: (v: number) => `${v.toFixed(0)}s`,
formatYValue: (v: number) => formatKb(v),
yAxisMinInterval: niceKbInterval(maxSeriesKb(chartData.series)),
onSeriesClick: isDrilledDown
? undefined
: (seriesName: string) => this.onSeriesClick(seriesName),
})
: m('.pf-memscope-placeholder', 'Waiting for data\u2026'),
),
),
m(
'.pf-memscope-panel',
m(ProcessTable, {
processes: filteredProcesses,
isUserDebug: data.isUserDebug,
session: attrs.session,
searchQuery: this.processSearch,
onSearchChange: (q) => {
this.processSearch = q;
},
}),
),
]);
}
private onSeriesClick(seriesName: string) {
if (this.grouping === 'category') {
const catIds = Object.keys(CATEGORIES) as CategoryId[];
const id = catIds.find((k) => CATEGORIES[k].name === seriesName);
if (id) this.selectedCategory = id;
} else {
const idx = OOM_SCORE_BUCKETS.findIndex((b) => b.name === seriesName);
if (idx !== -1) this.selectedOomBucket = idx;
}
}
}
interface ProcessTableAttrs {
readonly processes: ProcessMemoryRow[];
readonly isUserDebug: boolean;
readonly session: LiveSession;
readonly searchQuery: string;
readonly onSearchChange: (q: string) => void;
}
class ProcessTable implements m.ClassComponent<ProcessTableAttrs> {
private sortKey: string = 'rss_kb';
private sortDir: SortDirection = 'DESC';
private showDebuggableOnly: boolean = false;
private oomBucketFilter: Set<number> = new Set();
private categoryFilter: Set<CategoryId> = new Set();
private headerCell(
key: string,
label: string,
hint: SortDirection = 'DESC',
): m.Children {
const current = this.sortKey === key ? this.sortDir : undefined;
const onSort = (dir: SortDirection | undefined) => {
this.sortKey = dir !== undefined ? key : 'rss_kb';
this.sortDir = dir ?? 'DESC';
};
return m(
GridHeaderCell,
{
sort: current,
onSort,
hintSortDirection: hint,
menuItems: renderSortMenuItems(current, onSort),
},
label,
);
}
view({attrs}: m.CVnode<ProcessTableAttrs>): m.Children {
const {processes, isUserDebug, searchQuery, onSearchChange} = attrs;
const visible = processes.filter((p) => {
if (this.showDebuggableOnly && !p.debuggable && !isUserDebug) {
return false;
}
if (this.oomBucketFilter.size > 0) {
const idx = OOM_SCORE_BUCKETS.findIndex(
(b) => p.oomScore >= b.minScore && p.oomScore <= b.maxScore,
);
if (!this.oomBucketFilter.has(idx)) return false;
}
if (this.categoryFilter.size > 0) {
const catIds = Object.keys(CATEGORIES) as CategoryId[];
const id = catIds.find(
(k) => CATEGORIES[k].name === categorizeProcess(p.processName).name,
);
if (id === undefined || !this.categoryFilter.has(id)) return false;
}
return true;
});
const mul = this.sortDir === 'ASC' ? 1 : -1;
const sorted = [...visible].sort((a, b) => {
switch (this.sortKey) {
case 'rss_kb':
return mul * (a.rssKb - b.rssKb);
case 'anon_swap_kb':
return mul * (a.anonKb + a.swapKb - b.anonKb - b.swapKb);
case 'file_kb':
return mul * (a.fileKb - b.fileKb);
case 'shmem_kb':
return mul * (a.shmemKb - b.shmemKb);
case 'pid':
return mul * (a.pid - b.pid);
case 'oom_score':
return mul * (a.oomScore - b.oomScore);
case 'age':
return mul * ((a.ageSeconds ?? -1) - (b.ageSeconds ?? -1));
case 'process':
return mul * a.processName.localeCompare(b.processName);
case 'category':
return (
mul *
categorizeProcess(a.processName).name.localeCompare(
categorizeProcess(b.processName).name,
)
);
case 'debuggable': {
const toNum = (p: ProcessMemoryRow) =>
p.debuggable ? 2 : isUserDebug ? 1 : 0;
return mul * (toNum(a) - toNum(b));
}
default:
return 0;
}
});
const columns: GridColumn[] = [
{
key: 'process',
header: this.headerCell('process', 'Process', 'ASC'),
maxInitialWidthPx: 400,
},
{key: 'category', header: this.headerCell('category', 'Category', 'ASC')},
{key: 'pid', header: this.headerCell('pid', 'PID')},
{key: 'oom_score', header: this.headerCell('oom_score', 'OOM Adj')},
{key: 'age', header: this.headerCell('age', 'Age')},
{key: 'rss_kb', header: this.headerCell('rss_kb', 'RSS')},
{key: 'trend', header: m(GridCell, 'RSS trend')},
{
key: 'anon_swap_kb',
header: this.headerCell('anon_swap_kb', 'Anon + Swap'),
},
{key: 'file_kb', header: this.headerCell('file_kb', 'File')},
{key: 'shmem_kb', header: this.headerCell('shmem_kb', 'Shmem')},
];
const rowData = sorted.map((p) => {
const cat = categorizeProcess(p.processName);
const color = CATEGORY_COLOR_MAP.get(cat.name);
const oomBucket = OOM_SCORE_BUCKETS.find(
(b) => p.oomScore >= b.minScore && p.oomScore <= b.maxScore,
);
const oomLabel = oomBucket
? `${p.oomScore} (${oomBucket.name.replace(/ \(.*\)$/, '')})`
: `${p.oomScore}`;
const secs = p.ageSeconds;
const ageStr = (() => {
if (secs === null || secs < 0) return '-';
const d = Math.floor(secs / 86400);
const h = Math.floor((secs % 86400) / 3600);
const mn = Math.floor((secs % 3600) / 60);
const s = Math.floor(secs % 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${mn}m`;
if (mn > 0) return `${mn}m ${s}s`;
return `${s}s`;
})();
return [
m(GridCell, p.processName),
m(GridCell, m(ColorChip, {color}, cat.name)),
m(GridCell, {align: 'right'}, `${p.pid}`),
m(
GridCell,
{align: 'right'},
oomBucket
? m(ColorChip, {color: oomBucket.color}, oomLabel)
: oomLabel,
),
m(GridCell, {align: 'right'}, ageStr),
m(GridCell, {align: 'right'}, formatKb(p.rssKb)),
m(GridCell, sparkline(p.rssTrendKb)),
m(
GridCell,
{align: 'right'},
p.anonKb + p.swapKb > 0 ? formatKb(p.anonKb + p.swapKb) : '-',
),
m(GridCell, {align: 'right'}, p.fileKb > 0 ? formatKb(p.fileKb) : '-'),
m(
GridCell,
{align: 'right'},
p.shmemKb > 0 ? formatKb(p.shmemKb) : '-',
),
];
});
return [
m(
'.pf-memscope-panel__header.pf-memscope-search-row',
m(TextInput, {
leftIcon: 'search',
placeholder: 'Filter processes\u2026',
value: searchQuery,
onInput: (v) => {
onSearchChange(v);
},
}),
m(Button, {
label: 'Debuggable only',
icon: 'bug_report',
variant: ButtonVariant.Filled,
intent: this.showDebuggableOnly ? Intent.Primary : Intent.None,
onclick: () => {
this.showDebuggableOnly = !this.showDebuggableOnly;
},
}),
m(
PopupMenu,
{
position: PopupPosition.Bottom,
trigger: m(Button, {
label: 'OOM Score',
icon: 'filter_list',
variant: ButtonVariant.Filled,
intent:
this.oomBucketFilter.size > 0 ? Intent.Primary : Intent.None,
}),
},
m(MenuItem, {
label: 'Clear all',
icon: 'close',
disabled: this.oomBucketFilter.size === 0,
onclick: () => this.oomBucketFilter.clear(),
}),
m(MenuDivider),
OOM_SCORE_BUCKETS.map((bucket, idx) =>
m(MenuItem, {
label: m('span.pf-memscope-oom-item', [
m(ColorChip, {color: bucket.color}),
bucket.name,
]),
active: this.oomBucketFilter.has(idx),
closePopupOnClick: false,
onclick: () => {
if (this.oomBucketFilter.has(idx)) {
this.oomBucketFilter.delete(idx);
} else {
this.oomBucketFilter.add(idx);
}
},
}),
),
),
m(
PopupMenu,
{
position: PopupPosition.Bottom,
trigger: m(Button, {
label: 'Category',
icon: 'filter_list',
variant: ButtonVariant.Filled,
intent:
this.categoryFilter.size > 0 ? Intent.Primary : Intent.None,
}),
},
m(MenuItem, {
label: 'Clear all',
icon: 'close',
disabled: this.categoryFilter.size === 0,
onclick: () => this.categoryFilter.clear(),
}),
m(MenuDivider),
(Object.keys(CATEGORIES) as CategoryId[]).map((id) => {
const cat = CATEGORIES[id];
return m(MenuItem, {
label: m('span.pf-memscope-oom-item', [
m(ColorChip, {color: cat.color}),
cat.name,
]),
active: this.categoryFilter.has(id),
closePopupOnClick: false,
onclick: () => {
if (this.categoryFilter.has(id)) {
this.categoryFilter.delete(id);
} else {
this.categoryFilter.add(id);
}
},
});
}),
),
),
m(Grid, {columns, rowData, fillHeight: false}),
];
}
}