blob: f6738628af07f1335ff387d975373510d163179e [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 {
type CellRenderResult,
type SchemaRegistry,
} from '../../components/widgets/datagrid/datagrid_schema';
import {type SqlValue} from '../../trace_processor/query_result';
import {
type SmapsAggregated,
type SmapsEntry,
type VmaString,
} from './smaps_connection';
// ── Formatters ──────────────────────────────────────────────────────────────
export function fmtSize(bytes: number): string {
if (bytes === 0) return '\u2014';
if (bytes < 1024) return `${bytes} B`;
const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(kb < 10 ? 1 : 0)} KiB`;
const mb = kb / 1024;
if (mb < 1024) return `${mb.toFixed(mb < 10 ? 1 : 0)} MiB`;
const gb = mb / 1024;
return `${gb.toFixed(1)} GiB`;
}
export function sizeRenderer(value: SqlValue): CellRenderResult {
const n = Number(value);
return {
content: n > 0 ? fmtSize(n * 1024) : '\u2014',
align: 'right',
nullish: n === 0,
};
}
export function hexAddrRenderer(value: SqlValue): CellRenderResult {
const n = Number(value);
return {
content: n.toString(16).padStart(n > 0xffffffff ? 12 : 8, '0'),
align: 'left',
};
}
// ── VMA filters ─────────────────────────────────────────────────────────────
export type VmaType = 'all' | 'file' | 'anon';
export function classifyEntry(e: SmapsEntry): 'file' | 'anon' {
if (e.dev !== '00:00' && e.inode !== 0) return 'file';
return 'anon';
}
export interface VmaFilters {
type: VmaType;
r: boolean | null;
w: boolean | null;
x: boolean | null;
}
export function matchesFilters(e: SmapsEntry, f: VmaFilters): boolean {
if (f.type !== 'all' && classifyEntry(e) !== f.type) return false;
if (f.r !== null && (e.perms[0] === 'r') !== f.r) return false;
if (f.w !== null && (e.perms[1] === 'w') !== f.w) return false;
if (f.x !== null && (e.perms[2] === 'x') !== f.x) return false;
return true;
}
export function filterAggregated(
aggregated: SmapsAggregated[],
filters: VmaFilters,
): SmapsAggregated[] {
if (
filters.type === 'all' &&
filters.r === null &&
filters.w === null &&
filters.x === null
) {
return aggregated;
}
return aggregated
.map((g) => {
const entries = g.entries.filter((e) => matchesFilters(e, filters));
if (entries.length === 0) return null;
if (entries.length === g.entries.length) return g;
const agg: SmapsAggregated = {
name: g.name,
count: entries.length,
sizeKb: 0,
rssKb: 0,
pssKb: 0,
sharedCleanKb: 0,
sharedDirtyKb: 0,
privateCleanKb: 0,
privateDirtyKb: 0,
swapKb: 0,
swapPssKb: 0,
entries,
};
for (const e of entries) {
agg.sizeKb += e.sizeKb;
agg.rssKb += e.rssKb;
agg.pssKb += e.pssKb;
agg.sharedCleanKb += e.sharedCleanKb;
agg.sharedDirtyKb += e.sharedDirtyKb;
agg.privateCleanKb += e.privateCleanKb;
agg.privateDirtyKb += e.privateDirtyKb;
agg.swapKb += e.swapKb;
agg.swapPssKb += e.swapPssKb;
}
return agg;
})
.filter((g): g is SmapsAggregated => g !== null);
}
// ── Duplicate string computation ────────────────────────────────────────────
export interface DuplicateGroup {
value: string;
count: number;
totalBytes: number;
vmaCount: number;
}
export function computeDuplicates(strings: VmaString[]): DuplicateGroup[] {
const groups = new Map<
string,
{count: number; totalBytes: number; vmaIndices: Set<number>}
>();
for (const s of strings) {
const existing = groups.get(s.str);
if (existing !== undefined) {
existing.count++;
existing.totalBytes += s.str.length;
existing.vmaIndices.add(s.vmaIndex);
} else {
groups.set(s.str, {
count: 1,
totalBytes: s.str.length,
vmaIndices: new Set([s.vmaIndex]),
});
}
}
const result: DuplicateGroup[] = [];
for (const [value, g] of groups) {
if (g.count < 2) continue;
result.push({
value,
count: g.count,
totalBytes: g.totalBytes,
vmaCount: g.vmaIndices.size,
});
}
result.sort((a, b) => b.totalBytes - a.totalBytes);
return result;
}
// ── VMA-centric aggregation ─────────────────────────────────────────────────
export interface VmaCrossProcess {
name: string;
perms: string;
processCount: number;
totalPssKb: number;
totalRssKb: number;
totalSizeKb: number;
totalPrivDirtyKb: number;
totalPrivCleanKb: number;
totalSwapKb: number;
pids: number[];
}
export function aggregateVmasCrossProcess(
smapsData: Map<number, SmapsAggregated[]>,
filters: VmaFilters,
): VmaCrossProcess[] {
const byKey = new Map<
string,
{
perms: string;
pids: Set<number>;
pss: number;
rss: number;
size: number;
privDirty: number;
privClean: number;
swap: number;
}
>();
for (const [pid, aggregated] of smapsData) {
for (const g of aggregated) {
for (const e of g.entries) {
if (!matchesFilters(e, filters)) continue;
const key = `${e.name}|${e.perms}`;
const existing = byKey.get(key);
if (existing !== undefined) {
existing.pids.add(pid);
existing.pss += e.pssKb;
existing.rss += e.rssKb;
existing.size += e.sizeKb;
existing.privDirty += e.privateDirtyKb;
existing.privClean += e.privateCleanKb;
existing.swap += e.swapKb;
} else {
byKey.set(key, {
perms: e.perms,
pids: new Set([pid]),
pss: e.pssKb,
rss: e.rssKb,
size: e.sizeKb,
privDirty: e.privateDirtyKb,
privClean: e.privateCleanKb,
swap: e.swapKb,
});
}
}
}
}
const result: VmaCrossProcess[] = [];
for (const [key, data] of byKey) {
const name = key.split('|')[0];
result.push({
name: name || '[anonymous]',
perms: data.perms,
processCount: data.pids.size,
totalPssKb: data.pss,
totalRssKb: data.rss,
totalSizeKb: data.size,
totalPrivDirtyKb: data.privDirty,
totalPrivCleanKb: data.privClean,
totalSwapKb: data.swap,
pids: [...data.pids],
});
}
result.sort((a, b) => b.totalPssKb - a.totalPssKb);
return result;
}
// ── Static DataGrid schemas ─────────────────────────────────────────────────
export const ALL_STRINGS_SCHEMA: SchemaRegistry = {
string: {
vmaAddr: {
title: 'Address',
columnType: 'quantitative',
cellRenderer: (v: SqlValue) => ({
content: Number(v).toString(16).padStart(8, '0'),
align: 'left' as const,
}),
},
vmaName: {title: 'VMA', columnType: 'text'},
str: {title: 'String', columnType: 'text'},
},
};
export const VMAS_CROSS_SCHEMA: SchemaRegistry = {
vma: {
name: {title: 'Mapping', columnType: 'text'},
perms: {title: 'Perms', columnType: 'text'},
processCount: {title: 'Processes', columnType: 'quantitative'},
totalPssKb: {
title: 'PSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
totalRssKb: {
title: 'RSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
totalPrivDirtyKb: {
title: 'Priv Dirty',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
totalPrivCleanKb: {
title: 'Priv Clean',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
totalSwapKb: {
title: 'Swap',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
totalSizeKb: {
title: 'VSS',
columnType: 'quantitative',
cellRenderer: sizeRenderer,
},
},
};