blob: 3c9ab414e8a17644e124b42f40be5b9445834d0d [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 type {LiveSession, SnapshotData} from '../../sessions/live_session';
import {
billboardKb,
counterPoints,
formatKb,
maxSeriesKb,
niceKbInterval,
} from '../../utils';
import {Billboard} from '../../components/billboard';
import {Panel} from '../../components/panel';
import {Stack} from '../../../../widgets/stack';
function buildPageCacheTimeSeries(
data: SnapshotData,
t0: number,
): LineChartData | undefined {
const counterNames = ['Cached', 'Shmem', 'Active(file)', 'Inactive(file)'];
const byTs = new Map<number, Map<string, number>>();
for (const name of counterNames) {
const samples = data.systemCounters.get(name);
if (samples === undefined) continue;
for (const {ts, value} of samples) {
let row = byTs.get(ts);
if (row === undefined) {
row = new Map();
byTs.set(ts, row);
}
row.set(name, Math.round(value / 1024));
}
}
if (byTs.size < 2) return undefined;
const timestamps = [...byTs.keys()].sort((a, b) => a - b);
const activeFilePoints: {x: number; y: number}[] = [];
const inactiveFilePoints: {x: number; y: number}[] = [];
const shmemPoints: {x: number; y: number}[] = [];
for (const ts of timestamps) {
const row = byTs.get(ts)!;
const shmem = row.get('Shmem');
const activeFile = row.get('Active(file)');
const inactiveFile = row.get('Inactive(file)');
if (
shmem === undefined ||
activeFile === undefined ||
inactiveFile === undefined
) {
continue;
}
const x = (ts - t0) / 1e9;
activeFilePoints.push({x, y: activeFile});
inactiveFilePoints.push({x, y: inactiveFile});
shmemPoints.push({x, y: shmem});
}
if (activeFilePoints.length < 2) return undefined;
return {
series: [
{
name: 'Active(file)',
points: activeFilePoints,
color: 'var(--pf-color-success)',
},
{
name: 'Inactive(file)',
points: inactiveFilePoints,
color: 'var(--pf-color-warning)',
},
{
name: 'Shmem (tmpfs/ashmem)',
points: shmemPoints,
color: 'var(--pf-color-danger)',
},
],
};
}
function buildFileCacheBreakdownTimeSeries(
data: SnapshotData,
t0: number,
): LineChartData | undefined {
const counterNames = ['Active(file)', 'Inactive(file)', 'Mapped', 'Dirty'];
const byTs = new Map<number, Map<string, number>>();
for (const name of counterNames) {
const samples = data.systemCounters.get(name);
if (samples === undefined) continue;
for (const {ts, value} of samples) {
let row = byTs.get(ts);
if (row === undefined) {
row = new Map();
byTs.set(ts, row);
}
row.set(name, Math.round(value / 1024));
}
}
if (byTs.size < 2) return undefined;
const timestamps = [...byTs.keys()].sort((a, b) => a - b);
const mappedDirtyPts: {x: number; y: number}[] = [];
const mappedCleanPts: {x: number; y: number}[] = [];
const unmappedDirtyPts: {x: number; y: number}[] = [];
const unmappedCleanPts: {x: number; y: number}[] = [];
for (const ts of timestamps) {
const row = byTs.get(ts)!;
const activeFile = row.get('Active(file)');
const inactiveFile = row.get('Inactive(file)');
const mapped = row.get('Mapped');
const dirty = row.get('Dirty');
if (
activeFile === undefined ||
inactiveFile === undefined ||
mapped === undefined ||
dirty === undefined
) {
continue;
}
const fileCache = activeFile + inactiveFile;
if (fileCache === 0) continue;
const mappedDirty = (mapped * dirty) / fileCache;
const x = (ts - t0) / 1e9;
mappedDirtyPts.push({x, y: Math.round(mappedDirty)});
mappedCleanPts.push({x, y: Math.round(mapped - mappedDirty)});
unmappedDirtyPts.push({x, y: Math.round(dirty - mappedDirty)});
unmappedCleanPts.push({
x,
y: Math.max(0, Math.round(fileCache - mapped - dirty + mappedDirty)),
});
}
if (mappedDirtyPts.length < 2) return undefined;
return {
series: [
{name: 'Mapped + Dirty', points: mappedDirtyPts},
{name: 'Mapped + Clean', points: mappedCleanPts},
{name: 'Unmapped + Dirty', points: unmappedDirtyPts},
{name: 'Unmapped + Clean', points: unmappedCleanPts},
],
};
}
function buildFileCacheActivityTimeSeries(
data: SnapshotData,
t0: number,
): LineChartData | undefined {
// Rates (events/s) are computed in SQL — see extractSnapshotData in
// live_session.ts.
const series: LineChartSeries[] = [];
const refault = counterPoints(
data.systemCounters.get('workingset_refault_file'),
t0,
);
if (refault !== undefined) {
series.push({
name: 'Refaults (thrashing)',
points: refault,
color: 'var(--pf-color-danger)',
});
}
const steal = counterPoints(data.systemCounters.get('pgsteal_file'), t0);
if (steal !== undefined) {
series.push({
name: 'Stolen (reclaimed)',
points: steal,
color: 'var(--pf-color-warning)',
});
}
const scan = counterPoints(data.systemCounters.get('pgscan_file'), t0);
if (scan !== undefined) {
series.push({
name: 'Scanned',
points: scan,
color: 'var(--pf-color-success)',
});
}
if (series.length === 0) return undefined;
return {series};
}
function getPageCacheBillboards(
fileCacheBreakdownData?: LineChartData,
): {total: number; dirty: number; mapped: number} | undefined {
const data = fileCacheBreakdownData;
if (data === undefined || data.series.length < 4) return undefined;
// Series order: Mapped+Dirty, Mapped+Clean, Unmapped+Dirty, Unmapped+Clean
const last = (idx: number) => {
const pts = data.series[idx].points;
return pts.length > 0 ? pts[pts.length - 1].y : 0;
};
const mappedDirty = last(0);
const mappedClean = last(1);
const unmappedDirty = last(2);
const unmappedClean = last(3);
return {
total: mappedDirty + mappedClean + unmappedDirty + unmappedClean,
dirty: mappedDirty + unmappedDirty,
mapped: mappedDirty + mappedClean,
};
}
export function renderPageCacheTab(session: LiveSession): m.Children {
const data = session.data;
if (!data) return null;
const t0 = data.ts0;
const pageCacheChartData = buildPageCacheTimeSeries(data, t0);
const fileCacheBreakdownData = buildFileCacheBreakdownTimeSeries(data, t0);
const fileCacheActivityData = buildFileCacheActivityTimeSeries(data, t0);
const bb = getPageCacheBillboards(fileCacheBreakdownData);
return m(Stack, {spacing: 'large'}, [
bb !== undefined &&
m(
Stack,
{orientation: 'horizontal', spacing: 'large'},
m(Billboard, {
...billboardKb(bb.total),
label: 'Total Page Cache',
desc: 'Derived: Active(file) + Inactive(file) from /proc/meminfo',
}),
m(Billboard, {
...billboardKb(bb.dirty),
label: 'Dirty',
desc: 'Source: Dirty from /proc/meminfo',
}),
m(Billboard, {
...billboardKb(bb.mapped),
label: 'Mapped',
desc: 'Source: Mapped from /proc/meminfo',
}),
),
m(
Panel,
{
title: 'Page Cache',
subtitle:
'Source: /proc/meminfo counters Active(file), Inactive(file), Shmem. ' +
'Stacked: Active(file) + Inactive(file) + Shmem \u2248 Cached.',
},
pageCacheChartData
? m(LineChartSvg, {
data: pageCacheChartData,
height: 250,
xAxisLabel: 'Time (s)',
yAxisLabel: 'Cache',
showLegend: true,
showPoints: false,
stacked: true,
gridLines: 'both',
xAxisMin: data.xMin,
xAxisMax: data.xMax,
formatXValue: (v: number) => `${v.toFixed(0)}s`,
formatYValue: (v: number) => formatKb(v),
yAxisMinInterval: niceKbInterval(
maxSeriesKb(pageCacheChartData.series),
),
})
: m('.pf-memscope-placeholder', 'Waiting for data\u2026'),
),
m(
Panel,
{
title: 'Page Cache Activity',
subtitle:
'Source: /proc/vmstat counters, shown as rates (delta/s). ' +
'Refaults = workingset_refault_file (evicted pages needed again). ' +
'Stolen = pgsteal_file (pages reclaimed). ' +
'Scanned = pgscan_file (pages considered for reclaim).',
},
fileCacheActivityData
? m(LineChartSvg, {
data: fileCacheActivityData,
height: 200,
xAxisLabel: 'Time (s)',
yAxisLabel: 'Pages/s',
showLegend: true,
showPoints: false,
gridLines: 'both',
xAxisMin: data.xMin,
xAxisMax: data.xMax,
formatXValue: (v: number) => `${v.toFixed(0)}s`,
formatYValue: (v: number) => v.toLocaleString(),
})
: m('.pf-memscope-placeholder', 'Waiting for data\u2026'),
),
]);
}