mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 13:20:54 +02:00
176 lines
6.5 KiB
JavaScript
176 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const DEFAULT_PACKAGE = 'io.github.boof2015.astra';
|
|
|
|
function numberMatch(text, pattern, group = 1) {
|
|
const match = text.match(pattern);
|
|
return match ? Number(match[group]) : null;
|
|
}
|
|
|
|
function appSummaryBucket(text, name) {
|
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const match = text.match(new RegExp(`^\\s*${escaped}:\\s+(\\d+)(?:\\s+(\\d+))?`, 'm'));
|
|
return match ? { pssKb: Number(match[1]), rssKb: match[2] ? Number(match[2]) : null } : null;
|
|
}
|
|
|
|
export function parseMeminfo(text) {
|
|
const total = text.match(
|
|
/TOTAL PSS:\s*(\d+)\s+TOTAL RSS:\s*(\d+)\s+TOTAL SWAP PSS:\s*(\d+)/
|
|
);
|
|
const bitmapMalloced = text.match(/^\s*Bitmap \(malloced\):\s+(\d+)\s+(\d+)/m);
|
|
const bitmapNonmalloced = text.match(/^\s*Bitmap \(nonmalloced\):\s+(\d+)\s+(\d+)/m);
|
|
const mallocedKb = bitmapMalloced ? Number(bitmapMalloced[2]) : 0;
|
|
const nonmallocedKb = bitmapNonmalloced ? Number(bitmapNonmalloced[2]) : 0;
|
|
|
|
return {
|
|
totalPssKb: total ? Number(total[1]) : null,
|
|
totalRssKb: total ? Number(total[2]) : null,
|
|
totalSwapPssKb: total ? Number(total[3]) : null,
|
|
buckets: {
|
|
javaHeap: appSummaryBucket(text, 'Java Heap'),
|
|
nativeHeap: appSummaryBucket(text, 'Native Heap'),
|
|
graphics: appSummaryBucket(text, 'Graphics'),
|
|
privateOther: appSummaryBucket(text, 'Private Other'),
|
|
system: appSummaryBucket(text, 'System'),
|
|
},
|
|
mtrack: {
|
|
eglKb: numberMatch(text, /^\s*EGL mtrack\s+(\d+)/m),
|
|
glKb: numberMatch(text, /^\s*GL mtrack\s+(\d+)/m),
|
|
},
|
|
bitmaps: {
|
|
mallocedCount: bitmapMalloced ? Number(bitmapMalloced[1]) : 0,
|
|
mallocedKb,
|
|
nonmallocedCount: bitmapNonmalloced ? Number(bitmapNonmalloced[1]) : 0,
|
|
nonmallocedKb,
|
|
totalKb: mallocedKb + nonmallocedKb,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function parseGfxinfo(text) {
|
|
const textureViews = [...text.matchAll(/^TextureView:\s*(\d+)x(\d+)\s*$/gm)].map(
|
|
(match) => ({ width: Number(match[1]), height: Number(match[2]) })
|
|
);
|
|
return {
|
|
textureViewCount: textureViews.length,
|
|
textureViews,
|
|
graphicBufferAllocatedKb: numberMatch(
|
|
text,
|
|
/Total allocated by GraphicBufferAllocator \(estimate\):\s*([\d.]+) KB/
|
|
),
|
|
gpuMemoryBytes: numberMatch(
|
|
text,
|
|
/Total GPU memory usage:\s*\n\s*(\d+) bytes/
|
|
),
|
|
glLayerCount: numberMatch(text, /Layers Total\s+[\d.]+ KB \(numLayers = (\d+)\)/),
|
|
};
|
|
}
|
|
|
|
export function buildMemoryProfile(meminfo, gfxinfo, metadata = {}) {
|
|
const memory = parseMeminfo(meminfo);
|
|
const graphics = parseGfxinfo(gfxinfo);
|
|
return {
|
|
capturedAt: new Date().toISOString(),
|
|
package: metadata.package ?? DEFAULT_PACKAGE,
|
|
label: metadata.label ?? null,
|
|
serial: metadata.serial ?? null,
|
|
memory,
|
|
graphics,
|
|
acceptance: {
|
|
stretchPssAtOrBelow300Mb:
|
|
memory.totalPssKb !== null ? memory.totalPssKb <= 300 * 1024 : null,
|
|
hardPssBelow400Mb:
|
|
memory.totalPssKb !== null ? memory.totalPssKb < 400 * 1024 : null,
|
|
graphicsBelow150Mb:
|
|
memory.buckets.graphics?.pssKb !== undefined &&
|
|
memory.buckets.graphics?.pssKb !== null
|
|
? memory.buckets.graphics.pssKb < 150 * 1024
|
|
: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function mb(kb) {
|
|
return kb === null || kb === undefined ? 'n/a' : `${(kb / 1024).toFixed(1)} MB`;
|
|
}
|
|
|
|
function printHuman(profile) {
|
|
const { memory, graphics } = profile;
|
|
const rows = [
|
|
['Total PSS', mb(memory.totalPssKb)],
|
|
['Total RSS', mb(memory.totalRssKb)],
|
|
['Swap PSS', mb(memory.totalSwapPssKb)],
|
|
['Java heap PSS', mb(memory.buckets.javaHeap?.pssKb)],
|
|
['Native heap PSS', mb(memory.buckets.nativeHeap?.pssKb)],
|
|
['Graphics PSS', mb(memory.buckets.graphics?.pssKb)],
|
|
['EGL mtrack', mb(memory.mtrack.eglKb)],
|
|
['GL mtrack', mb(memory.mtrack.glKb)],
|
|
['Tracked bitmaps', mb(memory.bitmaps.totalKb)],
|
|
['TextureViews', String(graphics.textureViewCount)],
|
|
['Graphic buffers', mb(graphics.graphicBufferAllocatedKb)],
|
|
['GPU cache/layers', graphics.gpuMemoryBytes === null ? 'n/a' : mb(graphics.gpuMemoryBytes / 1024)],
|
|
];
|
|
const width = Math.max(...rows.map(([name]) => name.length));
|
|
|
|
console.log(`Astra Android memory profile${profile.label ? ` — ${profile.label}` : ''}`);
|
|
console.log(`${profile.package}${profile.serial ? ` on ${profile.serial}` : ''}`);
|
|
for (const [name, value] of rows) console.log(`${name.padEnd(width)} ${value}`);
|
|
if (graphics.textureViews.length > 0) {
|
|
console.log(`TextureView sizes${' '.repeat(Math.max(1, width - 16))} ${graphics.textureViews.map((v) => `${v.width}x${v.height}`).join(', ')}`);
|
|
}
|
|
console.log(
|
|
`Gates${' '.repeat(Math.max(1, width - 5))} stretch<=300MB=${profile.acceptance.stretchPssAtOrBelow300Mb} ` +
|
|
`hard<400MB=${profile.acceptance.hardPssBelow400Mb} graphics<150MB=${profile.acceptance.graphicsBelow150Mb}`
|
|
);
|
|
}
|
|
|
|
function parseArgs(args) {
|
|
const result = { package: DEFAULT_PACKAGE, label: null, serial: null, json: false };
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--json') result.json = true;
|
|
else if (arg === '--package') result.package = args[++i];
|
|
else if (arg === '--label') result.label = args[++i];
|
|
else if (arg === '--serial') result.serial = args[++i];
|
|
else if (arg === '--help' || arg === '-h') result.help = true;
|
|
else throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function adbOutput(serial, commandArgs) {
|
|
const serialArgs = serial ? ['-s', serial] : [];
|
|
return execFileSync('adb', [...serialArgs, 'shell', 'dumpsys', ...commandArgs], {
|
|
encoding: 'utf8',
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
});
|
|
}
|
|
|
|
export function main(args = process.argv.slice(2)) {
|
|
const options = parseArgs(args);
|
|
if (options.help) {
|
|
console.log('Usage: node scripts/android-memory-profile.mjs [--package id] [--serial id] [--label name] [--json]');
|
|
return;
|
|
}
|
|
const meminfo = adbOutput(options.serial, ['meminfo', options.package]);
|
|
if (!meminfo.includes('TOTAL PSS:')) {
|
|
throw new Error(`No running process found for ${options.package}`);
|
|
}
|
|
const gfxinfo = adbOutput(options.serial, ['gfxinfo', options.package]);
|
|
const profile = buildMemoryProfile(meminfo, gfxinfo, options);
|
|
if (options.json) console.log(JSON.stringify(profile, null, 2));
|
|
else printHuman(profile);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|