appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
486 lines • 22.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MEMINFO_TITLES = exports.SUPPORTED_PERFORMANCE_DATA_TYPES = exports.MEMORY_KEYS = exports.BATTERY_KEYS = exports.CPU_KEYS = exports.NETWORK_KEYS = void 0;
exports.getPerformanceDataTypes = getPerformanceDataTypes;
exports.getPerformanceData = getPerformanceData;
exports.mobileGetPerformanceData = mobileGetPerformanceData;
exports.getMemoryInfo = getMemoryInfo;
exports.getNetworkTrafficInfo = getNetworkTrafficInfo;
exports.getCPUInfo = getCPUInfo;
exports.getBatteryInfo = getBatteryInfo;
const asyncbox_1 = require("asyncbox");
const lodash_1 = __importDefault(require("lodash"));
exports.NETWORK_KEYS = [
[
'bucketStart',
'activeTime',
'rxBytes',
'rxPackets',
'txBytes',
'txPackets',
'operations',
'bucketDuration',
],
['st', 'activeTime', 'rb', 'rp', 'tb', 'tp', 'op', 'bucketDuration'],
];
exports.CPU_KEYS = ['user', 'kernel'];
exports.BATTERY_KEYS = ['power'];
exports.MEMORY_KEYS = [
'totalPrivateDirty',
'nativePrivateDirty',
'dalvikPrivateDirty',
'eglPrivateDirty',
'glPrivateDirty',
'totalPss',
'nativePss',
'dalvikPss',
'eglPss',
'glPss',
'nativeHeapAllocatedSize',
'nativeHeapSize',
'nativeRss',
'dalvikRss',
'totalRss',
];
exports.SUPPORTED_PERFORMANCE_DATA_TYPES = Object.freeze({
cpuinfo: 'the amount of cpu by user and kernel process - cpu information for applications on real devices and simulators',
memoryinfo: 'the amount of memory used by the process - memory information for applications on real devices and simulators',
batteryinfo: 'the remaining battery power - battery power information for applications on real devices and simulators',
networkinfo: 'the network statistics - network rx/tx information for applications on real devices and simulators',
});
exports.MEMINFO_TITLES = Object.freeze({
NATIVE: 'Native',
DALVIK: 'Dalvik',
EGL: 'EGL',
GL: 'GL',
MTRACK: 'mtrack',
TOTAL: 'TOTAL',
HEAP: 'Heap',
});
const RETRY_PAUSE_MS = 1000;
/**
* Retrieves the list of available performance data types.
*
* @returns An array of supported performance data type names.
* The possible values are: 'cpuinfo', 'memoryinfo', 'batteryinfo', 'networkinfo'.
*/
async function getPerformanceDataTypes() {
return lodash_1.default.keys(exports.SUPPORTED_PERFORMANCE_DATA_TYPES);
}
/**
* Retrieves performance data for the specified data type.
*
* @param packageName The package name of the application to get performance data for.
* Required for 'cpuinfo' and 'memoryinfo' data types.
* @param dataType The type of performance data to retrieve.
* Must be one of values returned by {@link getPerformanceDataTypes}.
* @param retries The number of retry attempts if data retrieval fails.
* @returns A two-dimensional array where the first row contains column names
* and subsequent rows contain the sampled data values.
* @throws {Error} If the data type is not supported or data retrieval fails.
*/
async function getPerformanceData(packageName, dataType, retries = 2) {
switch (lodash_1.default.toLower(dataType)) {
case 'batteryinfo':
return await getBatteryInfo.call(this, retries);
case 'cpuinfo':
return await getCPUInfo.call(this, packageName, retries);
case 'memoryinfo':
return await getMemoryInfo.call(this, packageName, retries);
case 'networkinfo':
return await getNetworkTrafficInfo.call(this, retries);
default:
throw new Error(`No performance data of type '${dataType}' found. ` +
`Only the following values are supported: ${JSON.stringify(exports.SUPPORTED_PERFORMANCE_DATA_TYPES, [' '], 2)}`);
}
}
/**
* Retrieves performance data about the given Android subsystem.
* The data is parsed from the output of the dumpsys utility.
*
* The output depends on the selected subsystem.
* It is orginized into a table, where the first row represent column names
* and the following rows represent the sampled data for each column.
* Example output for different data types:
* - batteryinfo: [[power], [23]]
* - memory info: [[totalPrivateDirty, nativePrivateDirty, dalvikPrivateDirty, eglPrivateDirty, glPrivateDirty, totalPss,
* nativePss, dalvikPss, eglPss, glPss, nativeHeapAllocatedSize, nativeHeapSize], [18360, 8296, 6132, null, null, 42588, 8406, 7024, null, null, 26519, 10344]]
* - networkinfo: [[bucketStart, activeTime, rxBytes, rxPackets, txBytes, txPackets, operations, bucketDuration,],
* [1478091600000, null, 1099075, 610947, 928, 114362, 769, 0, 3600000], [1478095200000, null, 1306300, 405997, 509, 46359, 370, 0, 3600000]]
*
* [[st, activeTime, rb, rp, tb, tp, op, bucketDuration], [1478088000, null, null, 32115296, 34291, 2956805, 25705, 0, 3600],
* [1478091600, null, null, 2714683, 11821, 1420564, 12650, 0, 3600], [1478095200, null, null, 10079213, 19962, 2487705, 20015, 0, 3600],
* [1478098800, null, null, 4444433, 10227, 1430356, 10493, 0, 3600]]
* - cpuinfo: [[user, kernel], [0.9, 1.3]]
*/
async function mobileGetPerformanceData(packageName, dataType) {
return await this.getPerformanceData(packageName, dataType);
}
// #region Internal helpers
/**
* API level between 18 and 30
* ['<System Type>', '<Memory Type>', <pss total>, <private dirty>, <private clean>, <swapPss dirty>, <heap size>, <heap alloc>, <heap free>]
* except 'TOTAL', which skips the second type name
* !!! valDict gets mutated
*/
function parseMeminfoForApi19To29(entries, valDict) {
const [type, subType] = entries;
if (type === exports.MEMINFO_TITLES.NATIVE && subType === exports.MEMINFO_TITLES.HEAP) {
[
,
,
valDict.nativePss,
valDict.nativePrivateDirty,
,
,
valDict.nativeHeapSize,
valDict.nativeHeapAllocatedSize,
] = entries;
}
else if (type === exports.MEMINFO_TITLES.DALVIK && subType === exports.MEMINFO_TITLES.HEAP) {
[, , valDict.dalvikPss, valDict.dalvikPrivateDirty] = entries;
}
else if (type === exports.MEMINFO_TITLES.EGL && subType === exports.MEMINFO_TITLES.MTRACK) {
[, , valDict.eglPss, valDict.eglPrivateDirty] = entries;
}
else if (type === exports.MEMINFO_TITLES.GL && subType === exports.MEMINFO_TITLES.MTRACK) {
[, , valDict.glPss, valDict.glPrivateDirty] = entries;
}
else if (type === exports.MEMINFO_TITLES.TOTAL && entries.length === 8) {
// there are two totals, and we only want the full listing, which has 8 entries
[, valDict.totalPss, valDict.totalPrivateDirty] = entries;
}
}
/**
* API level 30 and above
* ['<System Type>', '<Memory Type>', <pss total>, <private dirty>, <private clean>, <swapPss dirty>, <rss total>, <heap size>, <heap alloc>, <heap free>]
* !!! valDict gets mutated
*/
function parseMeminfoForApiAbove29(entries, valDict) {
const [type, subType] = entries;
if (type === exports.MEMINFO_TITLES.NATIVE && subType === exports.MEMINFO_TITLES.HEAP) {
[
,
,
valDict.nativePss,
valDict.nativePrivateDirty,
,
,
valDict.nativeRss,
valDict.nativeHeapSize,
valDict.nativeHeapAllocatedSize,
] = entries;
}
else if (type === exports.MEMINFO_TITLES.DALVIK && subType === exports.MEMINFO_TITLES.HEAP) {
[, , valDict.dalvikPss, valDict.dalvikPrivateDirty, , , valDict.dalvikRss] = entries;
}
else if (type === exports.MEMINFO_TITLES.EGL && subType === exports.MEMINFO_TITLES.MTRACK) {
[, , valDict.eglPss, valDict.eglPrivateDirty] = entries;
}
else if (type === exports.MEMINFO_TITLES.GL && subType === exports.MEMINFO_TITLES.MTRACK) {
[, , valDict.glPss, valDict.glPrivateDirty] = entries;
}
else if (type === exports.MEMINFO_TITLES.TOTAL && entries.length === 9) {
// has 9 entries
[, valDict.totalPss, valDict.totalPrivateDirty, , , valDict.totalRss] = entries;
}
}
/**
* Retrieves memory information for the specified application package.
*
* The data is parsed from the output of `dumpsys meminfo` command.
* The output format varies depending on the Android API level:
* - API 18-29: Contains PSS, private dirty, and heap information
* - API 30+: Additionally includes RSS information
*
* @param packageName The package name of the application to get memory information for.
* @param retries The number of retry attempts if data retrieval fails.
* @returns A two-dimensional array where the first row contains memory metric names
* (totalPrivateDirty, nativePrivateDirty, dalvikPrivateDirty, eglPrivateDirty,
* glPrivateDirty, totalPss, nativePss, dalvikPss, eglPss, glPss,
* nativeHeapAllocatedSize, nativeHeapSize, nativeRss, dalvikRss, totalRss)
* and the second row contains the corresponding values.
* @throws {Error} If memory data cannot be retrieved or parsed.
*/
async function getMemoryInfo(packageName, retries = 2) {
return (await (0, asyncbox_1.retryInterval)(retries, RETRY_PAUSE_MS, async () => {
const cmd = [
'dumpsys',
'meminfo',
`'${packageName}'`,
'|',
'grep',
'-E',
`'${exports.MEMINFO_TITLES.NATIVE}|${exports.MEMINFO_TITLES.DALVIK}|${exports.MEMINFO_TITLES.EGL}` +
`|${exports.MEMINFO_TITLES.GL}|${exports.MEMINFO_TITLES.TOTAL}'`,
];
const data = await this.adb.shell(cmd);
if (!data) {
throw new Error('No data from dumpsys');
}
const valDict = { totalPrivateDirty: '' };
const apiLevel = await this.adb.getApiLevel();
for (const line of data.split('\n')) {
const entries = line.trim().split(/\s+/).filter(Boolean);
if (apiLevel >= 30) {
parseMeminfoForApiAbove29(entries, valDict);
}
else {
parseMeminfoForApi19To29(entries, valDict);
}
}
if (valDict.totalPrivateDirty && valDict.totalPrivateDirty !== 'nodex') {
const headers = lodash_1.default.clone(exports.MEMORY_KEYS);
const values = headers.map((header) => valDict[header]);
return [headers, values];
}
throw new Error(`Unable to parse memory data: '${data}'`);
}));
}
/**
* Retrieves network traffic statistics from the device.
*
* The data is parsed from the output of `dumpsys netstats` command.
* The output format differs between emulators and real devices:
* - Emulators: Uses full key names (bucketStart, activeTime, rxBytes, etc.)
* - Real devices (Android 7.1+): Uses abbreviated keys (st, rb, rp, tb, tp, op)
*
* @param retries The number of retry attempts if data retrieval fails.
* @returns A two-dimensional array where the first row contains network metric names
* (bucketStart/st, activeTime, rxBytes/rb, rxPackets/rp, txBytes/tb, txPackets/tp,
* operations/op, bucketDuration) and subsequent rows contain the sampled data
* for each time bucket.
* @throws {Error} If network traffic data cannot be retrieved or parsed.
*/
async function getNetworkTrafficInfo(retries = 2) {
return (await (0, asyncbox_1.retryInterval)(retries, RETRY_PAUSE_MS, async () => {
const returnValue = [];
let bucketDuration;
let bucketStart;
let activeTime;
let rxBytes;
let rxPackets;
let txBytes;
let txPackets;
let operations;
const cmd = ['dumpsys', 'netstats'];
let data = await this.adb.shell(cmd);
if (!data)
throw new Error('No data from dumpsys'); //eslint-disable-line curly
// In case of network traffic information, it is different for the return data between emulator and real device.
// the return data of emulator
// Xt stats:
// Pending bytes: 39250
// History since boot:
// ident=[[type=WIFI, subType=COMBINED, networkId="WiredSSID"]] uid=-1 set=ALL tag=0x0
// NetworkStatsHistory: bucketDuration=3600000
// bucketStart=1478098800000 activeTime=31824 rxBytes=21502 rxPackets=78 txBytes=17748 txPackets=90 operations=0
//
// 7.1
// Xt stats:
// Pending bytes: 481487
// History since boot:
// ident=[{type=MOBILE, subType=COMBINED, subscriberId=310260..., metered=true}] uid=-1 set=ALL tag=0x0
// NetworkStatsHistory: bucketDuration=3600
// st=1483984800 rb=0 rp=0 tb=12031 tp=184 op=0
// st=1483988400 rb=0 rp=0 tb=38476 tp=587 op=0
// st=1483999200 rb=315616 rp=400 tb=94800 tp=362 op=0
// st=1484002800 rb=15826 rp=20 tb=4738 tp=16 op=0
//
// the return data of real device
// Xt stats:
// Pending bytes: 0
// History since boot:
// ident=[{type=MOBILE, subType=COMBINED, subscriberId=450050...}] uid=-1 set=ALL tag=0x0
// NetworkStatsHistory: bucketDuration=3600
// st=1478088000 rb=32115296 rp=34291 tb=2956805 tp=25705 op=0
// st=1478091600 rb=2714683 rp=11821 tb=1420564 tp=12650 op=0
// st=1478095200 rb=10079213 rp=19962 tb=2487705 tp=20015 op=0
// st=1478098800 rb=4444433 rp=10227 tb=1430356 tp=10493 op=0
let index = 0;
const fromXtstats = data.indexOf('Xt stats:');
let start = data.indexOf('Pending bytes:', fromXtstats);
let delimiter = data.indexOf(':', start + 1);
let end = data.indexOf('\n', delimiter + 1);
const pendingBytes = data.substring(delimiter + 1, end).trim();
if (end > delimiter) {
start = data.indexOf('bucketDuration', end + 1);
delimiter = data.indexOf('=', start + 1);
end = data.indexOf('\n', delimiter + 1);
bucketDuration = data.substring(delimiter + 1, end).trim();
}
if (start >= 0) {
data = data.substring(end + 1, data.length);
const arrayList = data.split('\n');
if (arrayList.length > 0) {
start = -1;
for (let j = 0; j < exports.NETWORK_KEYS.length; ++j) {
start = arrayList[0].indexOf(exports.NETWORK_KEYS[j][0]);
if (start >= 0) {
index = j;
returnValue[0] = [];
for (let k = 0; k < exports.NETWORK_KEYS[j].length; ++k) {
returnValue[0][k] = exports.NETWORK_KEYS[j][k];
}
break;
}
}
let returnIndex = 1;
for (const dataLine of arrayList) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][0]);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
bucketStart = dataLine.substring(delimiter + 1, end).trim();
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][1], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
activeTime = dataLine.substring(delimiter + 1, end).trim();
}
}
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][2], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
rxBytes = dataLine.substring(delimiter + 1, end).trim();
}
}
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][3], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
rxPackets = dataLine.substring(delimiter + 1, end).trim();
}
}
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][4], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
txBytes = dataLine.substring(delimiter + 1, end).trim();
}
}
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][5], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.indexOf(' ', delimiter + 1);
txPackets = dataLine.substring(delimiter + 1, end).trim();
}
}
if (end > delimiter) {
start = dataLine.indexOf(exports.NETWORK_KEYS[index][6], end + 1);
if (start >= 0) {
delimiter = dataLine.indexOf('=', start + 1);
end = dataLine.length;
operations = dataLine.substring(delimiter + 1, end).trim();
}
}
returnValue[returnIndex++] = [
bucketStart,
activeTime,
rxBytes,
rxPackets,
txBytes,
txPackets,
operations,
bucketDuration,
];
}
}
}
}
if (!lodash_1.default.isEqual(pendingBytes, '') &&
!lodash_1.default.isUndefined(pendingBytes) &&
!lodash_1.default.isEqual(pendingBytes, 'nodex')) {
return returnValue;
}
else {
throw new Error(`Unable to parse network traffic data: '${data}'`);
}
}));
}
/**
* Return the CPU information related to the given packageName.
* It raises an exception if the dumped CPU information did not include the given packageName
* or the format was wrong.
* The CPU information's sampling interval depends on the device under test.
* For example, some devices have 5 minutes interval. When you get the information
* from 2023-02-07 11:59:40.468 to 2023-02-07 12:04:40.556, then the next will be
* from 2023-02-07 12:04:40.556 to 2023-02-07 12:09:40.668. No process information
* exists in the result if the process was not running during the period.
*
* @param packageName The package name to get the CPU information.
* @param retries The number of retry count.
* @returns The array of the parsed CPU upsage percentages.
* e.g. ['cpuinfo', ['14.3', '28.2']]
* '14.3' is usage by the user (%), '28.2' is usage by the kernel (%)
* @throws {Error} If it failed to parse the result of dumpsys, or no package name exists.
*/
async function getCPUInfo(packageName, retries = 2) {
// TODO: figure out why this is
// sometimes, the function of 'adb.shell' fails. when I tested this function on the target of 'Galaxy Note5',
// adb.shell(dumpsys cpuinfo) returns cpu datas for other application packages, but I can't find the data for packageName.
// It usually fails 30 times and success for the next time,
// Since then, he has continued to succeed.
// @ts-expect-error retryInterval says it can return `null`, but it doesn't look like it actually can.
// FIXME: fix this in asyncbox
return await (0, asyncbox_1.retryInterval)(retries, RETRY_PAUSE_MS, async () => {
let output;
try {
output = await this.adb.shell(['dumpsys', 'cpuinfo']);
}
catch (e) {
const err = e;
if (err.stderr) {
this.log.info(err.stderr);
}
throw e;
}
// `output` will be something like
// +0% 2209/io.appium.android.apis: 0.1% user + 0.2% kernel / faults: 70 minor
const usagesPattern = new RegExp(`^.+\\/${lodash_1.default.escapeRegExp(packageName)}:\\D+([\\d.]+)%\\s+user\\s+\\+\\s+([\\d.]+)%\\s+kernel`, 'm');
const match = usagesPattern.exec(output);
if (!match) {
this.log.debug(output);
throw new Error(`Unable to parse cpu usage data for '${packageName}'. Check the server log for more details`);
}
const user = match[1];
const kernel = match[2];
return [exports.CPU_KEYS, [user, kernel]];
});
}
/**
* Retrieves battery level information from the device.
*
* The data is parsed from the output of `dumpsys battery` command.
*
* @param retries The number of retry attempts if data retrieval fails.
* @returns A two-dimensional array where the first row contains the metric name ['power']
* and the second row contains the battery level as a string (0-100).
* @throws {Error} If battery data cannot be retrieved or parsed.
*/
async function getBatteryInfo(retries = 2) {
return (await (0, asyncbox_1.retryInterval)(retries, RETRY_PAUSE_MS, async () => {
const cmd = ['dumpsys', 'battery', '|', 'grep', 'level'];
const data = await this.adb.shell(cmd);
if (!data)
throw new Error('No data from dumpsys'); //eslint-disable-line curly
const power = parseInt((data.split(':')[1] || '').trim(), 10);
if (!Number.isNaN(power)) {
return [lodash_1.default.clone(exports.BATTERY_KEYS), [power.toString()]];
}
else {
throw new Error(`Unable to parse battery data: '${data}'`);
}
}));
}
// #endregion
//# sourceMappingURL=performance.js.map