@houwert/conductor 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/sdk.js +149 -0
- package/dist/commands/delete-device.js +17 -5
- package/dist/commands/device-pool.js +2 -1
- package/dist/commands/download-app.js +3 -2
- package/dist/commands/foreground-app.js +1 -0
- package/dist/commands/hprof.js +422 -0
- package/dist/commands/install-app.js +2 -9
- package/dist/commands/list-apps.js +2 -1
- package/dist/commands/list-devices.js +27 -3
- package/dist/commands/memory.js +716 -63
- package/dist/commands/run-parallel.js +2 -1
- package/dist/commands/start-device.js +12 -14
- package/dist/commands/stop-device.js +2 -1
- package/dist/daemon/server.js +2 -0
- package/dist/daemon/web-server.js +38 -0
- package/dist/drivers/android.js +6 -3
- package/dist/drivers/bootstrap.js +48 -9
- package/dist/drivers/log-sources/android.js +6 -6
- package/dist/drivers/log-sources/metro-discovery.js +8 -2
- package/dist/drivers/web.js +16 -2
- package/dist/index.js +26 -1
- package/dist/runner.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findAndroidSdkRoot = findAndroidSdkRoot;
|
|
7
|
+
exports.resolveAndroidTool = resolveAndroidTool;
|
|
8
|
+
exports.androidToolEnv = androidToolEnv;
|
|
9
|
+
exports.androidSpawnEnv = androidSpawnEnv;
|
|
10
|
+
exports.ensureAndroidEnv = ensureAndroidEnv;
|
|
11
|
+
/**
|
|
12
|
+
* Android SDK tool resolver.
|
|
13
|
+
*
|
|
14
|
+
* Locates `emulator`, `adb`, `avdmanager`, and `sdkmanager` on disk so conductor
|
|
15
|
+
* works even when the SDK is installed but its bin dirs aren't on PATH (a common
|
|
16
|
+
* setup on macOS where Android Studio installs to ~/Library/Android/sdk and only
|
|
17
|
+
* adb tends to be linked, e.g. via Homebrew).
|
|
18
|
+
*
|
|
19
|
+
* Lookup order for the SDK root:
|
|
20
|
+
* 1. ANDROID_HOME
|
|
21
|
+
* 2. ANDROID_SDK_ROOT
|
|
22
|
+
* 3. ~/Library/Android/sdk (macOS default)
|
|
23
|
+
* 4. ~/Android/Sdk (Linux default)
|
|
24
|
+
* 5. %LOCALAPPDATA%/Android/Sdk (Windows default)
|
|
25
|
+
*
|
|
26
|
+
* Each tool lives in a conventional subdir (with legacy fallbacks for cmdline tools).
|
|
27
|
+
* If nothing is found on disk, the bare command name is returned so the OS can do a
|
|
28
|
+
* final PATH lookup.
|
|
29
|
+
*/
|
|
30
|
+
const fs_1 = __importDefault(require("fs"));
|
|
31
|
+
const os_1 = __importDefault(require("os"));
|
|
32
|
+
const path_1 = __importDefault(require("path"));
|
|
33
|
+
const isWindows = process.platform === 'win32';
|
|
34
|
+
function exeName(base, kind) {
|
|
35
|
+
if (!isWindows)
|
|
36
|
+
return base;
|
|
37
|
+
return kind === 'script' ? `${base}.bat` : `${base}.exe`;
|
|
38
|
+
}
|
|
39
|
+
/** Subdirectories (relative to the SDK root) and the executable name for each tool. */
|
|
40
|
+
function candidateSubpaths(tool) {
|
|
41
|
+
switch (tool) {
|
|
42
|
+
case 'emulator':
|
|
43
|
+
return [path_1.default.join('emulator', exeName('emulator', 'binary'))];
|
|
44
|
+
case 'adb':
|
|
45
|
+
return [path_1.default.join('platform-tools', exeName('adb', 'binary'))];
|
|
46
|
+
case 'avdmanager':
|
|
47
|
+
return [
|
|
48
|
+
path_1.default.join('cmdline-tools', 'latest', 'bin', exeName('avdmanager', 'script')),
|
|
49
|
+
path_1.default.join('tools', 'bin', exeName('avdmanager', 'script')),
|
|
50
|
+
];
|
|
51
|
+
case 'sdkmanager':
|
|
52
|
+
return [
|
|
53
|
+
path_1.default.join('cmdline-tools', 'latest', 'bin', exeName('sdkmanager', 'script')),
|
|
54
|
+
path_1.default.join('tools', 'bin', exeName('sdkmanager', 'script')),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Candidate SDK root directories, in priority order. */
|
|
59
|
+
function candidateSdkRoots() {
|
|
60
|
+
const roots = [];
|
|
61
|
+
const env = process.env;
|
|
62
|
+
if (env.ANDROID_HOME)
|
|
63
|
+
roots.push(env.ANDROID_HOME);
|
|
64
|
+
if (env.ANDROID_SDK_ROOT)
|
|
65
|
+
roots.push(env.ANDROID_SDK_ROOT);
|
|
66
|
+
const home = os_1.default.homedir();
|
|
67
|
+
if (process.platform === 'darwin') {
|
|
68
|
+
roots.push(path_1.default.join(home, 'Library', 'Android', 'sdk'));
|
|
69
|
+
}
|
|
70
|
+
else if (process.platform === 'win32') {
|
|
71
|
+
const localAppData = env.LOCALAPPDATA;
|
|
72
|
+
if (localAppData)
|
|
73
|
+
roots.push(path_1.default.join(localAppData, 'Android', 'Sdk'));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
roots.push(path_1.default.join(home, 'Android', 'Sdk'));
|
|
77
|
+
}
|
|
78
|
+
// De-dupe while preserving order
|
|
79
|
+
return Array.from(new Set(roots));
|
|
80
|
+
}
|
|
81
|
+
/** First SDK root that actually exists on disk, or undefined. */
|
|
82
|
+
function findAndroidSdkRoot() {
|
|
83
|
+
for (const root of candidateSdkRoots()) {
|
|
84
|
+
try {
|
|
85
|
+
if (fs_1.default.existsSync(root) && fs_1.default.statSync(root).isDirectory())
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Absolute path to the Android SDK tool, or the bare tool name if nothing was found
|
|
96
|
+
* on disk (so PATH lookup can still take a swing at it).
|
|
97
|
+
*/
|
|
98
|
+
function resolveAndroidTool(tool) {
|
|
99
|
+
const root = findAndroidSdkRoot();
|
|
100
|
+
if (root) {
|
|
101
|
+
for (const sub of candidateSubpaths(tool)) {
|
|
102
|
+
const full = path_1.default.join(root, sub);
|
|
103
|
+
if (fs_1.default.existsSync(full))
|
|
104
|
+
return full;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return tool;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Environment overrides to merge into a spawned child's env when shelling out to
|
|
111
|
+
* an Android tool. Sets ANDROID_HOME / ANDROID_SDK_ROOT if (a) we found an SDK
|
|
112
|
+
* root on disk and (b) the parent process didn't already export them. The
|
|
113
|
+
* `emulator` binary in particular sometimes refuses to start without these.
|
|
114
|
+
*/
|
|
115
|
+
function androidToolEnv() {
|
|
116
|
+
const root = findAndroidSdkRoot();
|
|
117
|
+
if (!root)
|
|
118
|
+
return {};
|
|
119
|
+
const out = {};
|
|
120
|
+
if (!process.env.ANDROID_HOME)
|
|
121
|
+
out.ANDROID_HOME = root;
|
|
122
|
+
if (!process.env.ANDROID_SDK_ROOT)
|
|
123
|
+
out.ANDROID_SDK_ROOT = root;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Convenience: env to pass directly as `spawn(..., { env: androidSpawnEnv() })`. */
|
|
127
|
+
function androidSpawnEnv(extra) {
|
|
128
|
+
return { ...process.env, ...androidToolEnv(), ...(extra ?? {}) };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Lazily patches `process.env` with ANDROID_HOME / ANDROID_SDK_ROOT (when
|
|
132
|
+
* unset) so any child process that inherits parent env picks them up. Idempotent.
|
|
133
|
+
*
|
|
134
|
+
* Call sites that resolve Android tools should invoke this so the spawned
|
|
135
|
+
* tool — `emulator` in particular — finds the SDK even when ANDROID_HOME isn't
|
|
136
|
+
* exported in the user's shell.
|
|
137
|
+
*/
|
|
138
|
+
let _envEnsured = false;
|
|
139
|
+
function ensureAndroidEnv() {
|
|
140
|
+
if (_envEnsured)
|
|
141
|
+
return;
|
|
142
|
+
_envEnsured = true;
|
|
143
|
+
const overrides = androidToolEnv();
|
|
144
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
145
|
+
if (v !== undefined && process.env[k] === undefined) {
|
|
146
|
+
process.env[k] = v;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.HELP = ` delete-device <name-or-id>
|
|
|
10
10
|
--all Delete all shutdown simulators / non-running AVDs / web sessions`;
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const runner_js_1 = require("../runner.js");
|
|
13
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
13
14
|
const output_js_1 = require("../output.js");
|
|
14
15
|
const client_js_1 = require("../daemon/client.js");
|
|
15
16
|
const protocol_js_1 = require("../daemon/protocol.js");
|
|
@@ -45,7 +46,7 @@ async function deleteSimulator(udid) {
|
|
|
45
46
|
}
|
|
46
47
|
// ── Android ──────────────────────────────────────────────────────────────────
|
|
47
48
|
async function listAVDs() {
|
|
48
|
-
const result = await (0, runner_js_1.spawnCommand)('emulator', ['-list-avds']);
|
|
49
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds']);
|
|
49
50
|
if (!result.success)
|
|
50
51
|
return [];
|
|
51
52
|
return result.stdout
|
|
@@ -56,7 +57,7 @@ async function listAVDs() {
|
|
|
56
57
|
/** Map running emulator serial → AVD name */
|
|
57
58
|
async function runningAVDs() {
|
|
58
59
|
const map = new Map();
|
|
59
|
-
const result = await (0, runner_js_1.spawnCommand)('adb', ['devices']);
|
|
60
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices']);
|
|
60
61
|
if (!result.success)
|
|
61
62
|
return map;
|
|
62
63
|
const serials = result.stdout
|
|
@@ -65,7 +66,13 @@ async function runningAVDs() {
|
|
|
65
66
|
.map((l) => l.trim().split(/\s+/)[0])
|
|
66
67
|
.filter((s) => s && s.startsWith('emulator-'));
|
|
67
68
|
for (const serial of serials) {
|
|
68
|
-
const name = await (0, runner_js_1.spawnCommand)('adb', [
|
|
69
|
+
const name = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), [
|
|
70
|
+
'-s',
|
|
71
|
+
serial,
|
|
72
|
+
'emu',
|
|
73
|
+
'avd',
|
|
74
|
+
'name',
|
|
75
|
+
]);
|
|
69
76
|
if (name.success) {
|
|
70
77
|
const avdName = name.stdout.trim().split('\n')[0];
|
|
71
78
|
if (avdName)
|
|
@@ -75,13 +82,18 @@ async function runningAVDs() {
|
|
|
75
82
|
return map;
|
|
76
83
|
}
|
|
77
84
|
async function killEmulator(serial) {
|
|
78
|
-
const result = await (0, runner_js_1.spawnCommand)('adb', ['-s', serial, 'emu', 'kill']);
|
|
85
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', serial, 'emu', 'kill']);
|
|
79
86
|
if (!result.success) {
|
|
80
87
|
throw new Error(`Failed to kill emulator ${serial}: ${result.stderr.trim()}`);
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
async function deleteAVD(name) {
|
|
84
|
-
const result = await (0, runner_js_1.spawnCommand)('avdmanager', [
|
|
91
|
+
const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('avdmanager'), [
|
|
92
|
+
'delete',
|
|
93
|
+
'avd',
|
|
94
|
+
'-n',
|
|
95
|
+
name,
|
|
96
|
+
]);
|
|
85
97
|
if (!result.success) {
|
|
86
98
|
throw new Error(`Failed to delete AVD "${name}": ${result.stderr.trim()}`);
|
|
87
99
|
}
|
|
@@ -22,6 +22,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
22
22
|
const path_1 = __importDefault(require("path"));
|
|
23
23
|
const fs_1 = __importDefault(require("fs"));
|
|
24
24
|
const child_process_1 = require("child_process");
|
|
25
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
25
26
|
const output_js_1 = require("../output.js");
|
|
26
27
|
function poolFilePath() {
|
|
27
28
|
return (process.env.__CONDUCTOR_POOL_FILE ?? path_1.default.join(os_1.default.homedir(), '.conductor', 'device-pool.json'));
|
|
@@ -73,7 +74,7 @@ async function discoverAllDevices() {
|
|
|
73
74
|
const devices = [];
|
|
74
75
|
// Android: adb devices
|
|
75
76
|
try {
|
|
76
|
-
const out = await spawnCapture('adb', ['devices', '-l']);
|
|
77
|
+
const out = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices', '-l']);
|
|
77
78
|
for (const line of out.split('\n').slice(1)) {
|
|
78
79
|
const id = line.trim().split(/\s+/)[0];
|
|
79
80
|
if (id && !line.includes('offline') && id !== '') {
|
|
@@ -8,6 +8,7 @@ exports.downloadApp = downloadApp;
|
|
|
8
8
|
exports.HELP = ` download-app <appId> --output <path> Download installed app binary from device`;
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const runner_js_1 = require("../runner.js");
|
|
11
|
+
const sdk_js_1 = require("../android/sdk.js");
|
|
11
12
|
const session_js_1 = require("../session.js");
|
|
12
13
|
const output_js_1 = require("../output.js");
|
|
13
14
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
@@ -63,7 +64,7 @@ async function downloadApp(appId, output, opts = {}, sessionName = 'default') {
|
|
|
63
64
|
}
|
|
64
65
|
else {
|
|
65
66
|
// Android: find the APK path, then pull it
|
|
66
|
-
const pmPath = await (0, runner_js_1.spawnCommand)('adb', ['-s', deviceId, 'shell', 'pm', 'path', appId]);
|
|
67
|
+
const pmPath = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', deviceId, 'shell', 'pm', 'path', appId], { env: (0, sdk_js_1.androidSpawnEnv)() });
|
|
67
68
|
if (!pmPath.success) {
|
|
68
69
|
(0, output_js_1.printError)(`Failed to locate app on device: ${pmPath.stderr}`, opts);
|
|
69
70
|
return 1;
|
|
@@ -75,7 +76,7 @@ async function downloadApp(appId, output, opts = {}, sessionName = 'default') {
|
|
|
75
76
|
return 1;
|
|
76
77
|
}
|
|
77
78
|
const dest = output ?? path_1.default.join(process.cwd(), `${appId}.apk`);
|
|
78
|
-
const pull = await (0, runner_js_1.spawnCommand)('adb', ['-s', deviceId, 'pull', apkPath, dest]);
|
|
79
|
+
const pull = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', deviceId, 'pull', apkPath, dest], { env: (0, sdk_js_1.androidSpawnEnv)() });
|
|
79
80
|
if (!pull.success) {
|
|
80
81
|
(0, output_js_1.printError)(`Failed to pull APK: ${pull.stderr}`, opts);
|
|
81
82
|
return 1;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HELP = void 0;
|
|
4
|
+
exports.getInstalledAppIds = getInstalledAppIds;
|
|
4
5
|
exports.foregroundApp = foregroundApp;
|
|
5
6
|
exports.HELP = ` foreground-app Print bundle ID / package of the foreground app`;
|
|
6
7
|
const runner_js_1 = require("../runner.js");
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Streaming-ish parser for Android/JVM HPROF binary heap dumps.
|
|
4
|
+
*
|
|
5
|
+
* Implements enough of the format to extract per-class instance counts and
|
|
6
|
+
* bytes — equivalent to the "Class List" view in Android Studio's Memory
|
|
7
|
+
* Profiler. Walks every record so the cursor stays correctly aligned, but
|
|
8
|
+
* skips field bodies and reference graphs (we don't need retainer paths).
|
|
9
|
+
*
|
|
10
|
+
* Spec references:
|
|
11
|
+
* - Standard HPROF: https://hg.openjdk.org/jdk6/jdk6/jdk/raw-file/tip/src/share/demo/jvmti/hprof/manual.html
|
|
12
|
+
* - Android extensions (ART): art/runtime/hprof/hprof.cc in AOSP
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseHprof = parseHprof;
|
|
16
|
+
// ── Top-level record tags ────────────────────────────────────────────────────
|
|
17
|
+
const TAG_STRING = 0x01;
|
|
18
|
+
const TAG_LOAD_CLASS = 0x02;
|
|
19
|
+
const TAG_HEAP_DUMP = 0x0c;
|
|
20
|
+
const TAG_HEAP_DUMP_SEGMENT = 0x1c;
|
|
21
|
+
// const TAG_HEAP_DUMP_END = 0x2c; // marker only, no body
|
|
22
|
+
// ── Sub-record tags inside HEAP_DUMP / HEAP_DUMP_SEGMENT ─────────────────────
|
|
23
|
+
// Standard root types (just an ID, sometimes with thread/frame metadata).
|
|
24
|
+
const SUB_ROOT_UNKNOWN = 0xff;
|
|
25
|
+
const SUB_ROOT_JNI_GLOBAL = 0x01;
|
|
26
|
+
const SUB_ROOT_JNI_LOCAL = 0x02;
|
|
27
|
+
const SUB_ROOT_JAVA_FRAME = 0x03;
|
|
28
|
+
const SUB_ROOT_NATIVE_STACK = 0x04;
|
|
29
|
+
const SUB_ROOT_STICKY_CLASS = 0x05;
|
|
30
|
+
const SUB_ROOT_THREAD_BLOCK = 0x06;
|
|
31
|
+
const SUB_ROOT_MONITOR_USED = 0x07;
|
|
32
|
+
const SUB_ROOT_THREAD_OBJECT = 0x08;
|
|
33
|
+
// Object-bearing sub-records.
|
|
34
|
+
const SUB_CLASS_DUMP = 0x20;
|
|
35
|
+
const SUB_INSTANCE_DUMP = 0x21;
|
|
36
|
+
const SUB_OBJECT_ARRAY_DUMP = 0x22;
|
|
37
|
+
const SUB_PRIMITIVE_ARRAY_DUMP = 0x23;
|
|
38
|
+
// Android-specific extensions.
|
|
39
|
+
const SUB_HEAP_DUMP_INFO = 0xfe;
|
|
40
|
+
const SUB_ROOT_INTERNED_STRING = 0x89;
|
|
41
|
+
const SUB_ROOT_FINALIZING = 0x8a;
|
|
42
|
+
const SUB_ROOT_DEBUGGER = 0x8b;
|
|
43
|
+
const SUB_ROOT_REFERENCE_CLEANUP = 0x8c;
|
|
44
|
+
const SUB_ROOT_VM_INTERNAL = 0x8d;
|
|
45
|
+
const SUB_ROOT_JNI_MONITOR = 0x8e;
|
|
46
|
+
const SUB_UNREACHABLE = 0x90;
|
|
47
|
+
const SUB_PRIMITIVE_ARRAY_NODATA = 0xc3;
|
|
48
|
+
// ── HPROF basic types ────────────────────────────────────────────────────────
|
|
49
|
+
const T_OBJECT = 2;
|
|
50
|
+
const T_BOOLEAN = 4;
|
|
51
|
+
const T_CHAR = 5;
|
|
52
|
+
const T_FLOAT = 6;
|
|
53
|
+
const T_DOUBLE = 7;
|
|
54
|
+
const T_BYTE = 8;
|
|
55
|
+
const T_SHORT = 9;
|
|
56
|
+
const T_INT = 10;
|
|
57
|
+
const T_LONG = 11;
|
|
58
|
+
function valueSize(t, idSize) {
|
|
59
|
+
switch (t) {
|
|
60
|
+
case T_OBJECT:
|
|
61
|
+
return idSize;
|
|
62
|
+
case T_BOOLEAN:
|
|
63
|
+
case T_BYTE:
|
|
64
|
+
return 1;
|
|
65
|
+
case T_CHAR:
|
|
66
|
+
case T_SHORT:
|
|
67
|
+
return 2;
|
|
68
|
+
case T_FLOAT:
|
|
69
|
+
case T_INT:
|
|
70
|
+
return 4;
|
|
71
|
+
case T_DOUBLE:
|
|
72
|
+
case T_LONG:
|
|
73
|
+
return 8;
|
|
74
|
+
default:
|
|
75
|
+
// Some Android variants use non-standard type ids in constant pools.
|
|
76
|
+
// Conservatively assume 1 — we only use this to advance the cursor and
|
|
77
|
+
// bad alignment will surface as a parse error further down.
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function primArrayName(t) {
|
|
82
|
+
switch (t) {
|
|
83
|
+
case T_BOOLEAN:
|
|
84
|
+
return 'boolean[]';
|
|
85
|
+
case T_CHAR:
|
|
86
|
+
return 'char[]';
|
|
87
|
+
case T_FLOAT:
|
|
88
|
+
return 'float[]';
|
|
89
|
+
case T_DOUBLE:
|
|
90
|
+
return 'double[]';
|
|
91
|
+
case T_BYTE:
|
|
92
|
+
return 'byte[]';
|
|
93
|
+
case T_SHORT:
|
|
94
|
+
return 'short[]';
|
|
95
|
+
case T_INT:
|
|
96
|
+
return 'int[]';
|
|
97
|
+
case T_LONG:
|
|
98
|
+
return 'long[]';
|
|
99
|
+
default:
|
|
100
|
+
return `prim<${t}>[]`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Convert a JNI-encoded type name to a Java-source-style name.
|
|
105
|
+
* Handles both the bare class names ART writes (`java.lang.String`) and
|
|
106
|
+
* canonical JNI names (`Ljava/lang/String;`, `[I`, `[[Ljava/util/Map$Entry;`).
|
|
107
|
+
*/
|
|
108
|
+
function prettyClassName(raw) {
|
|
109
|
+
if (!raw)
|
|
110
|
+
return '<unknown>';
|
|
111
|
+
// ART often emits already-pretty names like "java.lang.String" or "byte[]".
|
|
112
|
+
// Detect the JNI-style encoding by leading "[" or "L...;".
|
|
113
|
+
if (raw[0] !== '[' && !(raw[0] === 'L' && raw.endsWith(';'))) {
|
|
114
|
+
// HotSpot HPROF writes instance class names with slashes (e.g.
|
|
115
|
+
// "java/lang/String"); ART writes them dotted. Normalise to dots.
|
|
116
|
+
return raw.includes('/') ? raw.replace(/\//g, '.') : raw;
|
|
117
|
+
}
|
|
118
|
+
let dims = 0;
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < raw.length && raw[i] === '[') {
|
|
121
|
+
dims++;
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
let base;
|
|
125
|
+
if (i >= raw.length) {
|
|
126
|
+
base = raw;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const c = raw[i];
|
|
130
|
+
switch (c) {
|
|
131
|
+
case 'L': {
|
|
132
|
+
const end = raw.endsWith(';') ? raw.length - 1 : raw.length;
|
|
133
|
+
base = raw.slice(i + 1, end).replace(/\//g, '.');
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'B':
|
|
137
|
+
base = 'byte';
|
|
138
|
+
break;
|
|
139
|
+
case 'C':
|
|
140
|
+
base = 'char';
|
|
141
|
+
break;
|
|
142
|
+
case 'D':
|
|
143
|
+
base = 'double';
|
|
144
|
+
break;
|
|
145
|
+
case 'F':
|
|
146
|
+
base = 'float';
|
|
147
|
+
break;
|
|
148
|
+
case 'I':
|
|
149
|
+
base = 'int';
|
|
150
|
+
break;
|
|
151
|
+
case 'J':
|
|
152
|
+
base = 'long';
|
|
153
|
+
break;
|
|
154
|
+
case 'S':
|
|
155
|
+
base = 'short';
|
|
156
|
+
break;
|
|
157
|
+
case 'Z':
|
|
158
|
+
base = 'boolean';
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
base = raw.slice(i);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return base + '[]'.repeat(dims);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse an HPROF file buffer and return per-class instance counts/bytes.
|
|
169
|
+
*
|
|
170
|
+
* "Bytes" is the sum of instance-data sizes (object body, array contents) —
|
|
171
|
+
* not retained size. This matches what Android Studio's Memory Profiler shows
|
|
172
|
+
* in its "Shallow Size" column.
|
|
173
|
+
*/
|
|
174
|
+
function parseHprof(buf) {
|
|
175
|
+
let pos = 0;
|
|
176
|
+
// Header: NUL-terminated ASCII string ("JAVA PROFILE 1.0.X")
|
|
177
|
+
const headerEnd = buf.indexOf(0, pos);
|
|
178
|
+
if (headerEnd < 0)
|
|
179
|
+
throw new Error('Invalid HPROF: missing header NUL terminator');
|
|
180
|
+
const header = buf.slice(pos, headerEnd).toString('ascii');
|
|
181
|
+
if (!header.startsWith('JAVA PROFILE')) {
|
|
182
|
+
throw new Error(`Not an HPROF file (header: ${JSON.stringify(header)})`);
|
|
183
|
+
}
|
|
184
|
+
pos = headerEnd + 1;
|
|
185
|
+
if (pos + 12 > buf.length)
|
|
186
|
+
throw new Error('HPROF truncated in header');
|
|
187
|
+
const idSize = buf.readUInt32BE(pos);
|
|
188
|
+
pos += 4;
|
|
189
|
+
if (idSize !== 4 && idSize !== 8) {
|
|
190
|
+
throw new Error(`Unsupported HPROF identifier size: ${idSize}`);
|
|
191
|
+
}
|
|
192
|
+
pos += 8; // timestamp: 2x u4 (high, low) — unused
|
|
193
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
194
|
+
const strings = new Map(); // string id → utf8
|
|
195
|
+
const classNameByObjId = new Map(); // class object id → class name string
|
|
196
|
+
const stats = new Map();
|
|
197
|
+
const heapStats = new Map();
|
|
198
|
+
let currentHeap = 'default';
|
|
199
|
+
let totalCount = 0;
|
|
200
|
+
let totalBytes = 0;
|
|
201
|
+
// ── Helpers (closures over `pos` / `idSize`) ───────────────────────────────
|
|
202
|
+
const readId = () => {
|
|
203
|
+
if (idSize === 4) {
|
|
204
|
+
const v = buf.readUInt32BE(pos);
|
|
205
|
+
pos += 4;
|
|
206
|
+
return v.toString();
|
|
207
|
+
}
|
|
208
|
+
const v = buf.readBigUInt64BE(pos);
|
|
209
|
+
pos += 8;
|
|
210
|
+
return v.toString();
|
|
211
|
+
};
|
|
212
|
+
const skipId = () => {
|
|
213
|
+
pos += idSize;
|
|
214
|
+
};
|
|
215
|
+
const readU4 = () => {
|
|
216
|
+
const v = buf.readUInt32BE(pos);
|
|
217
|
+
pos += 4;
|
|
218
|
+
return v;
|
|
219
|
+
};
|
|
220
|
+
const readU2 = () => {
|
|
221
|
+
const v = buf.readUInt16BE(pos);
|
|
222
|
+
pos += 2;
|
|
223
|
+
return v;
|
|
224
|
+
};
|
|
225
|
+
const readU1 = () => buf[pos++];
|
|
226
|
+
const bump = (rawClassName, bytes) => {
|
|
227
|
+
const name = prettyClassName(rawClassName);
|
|
228
|
+
const cur = stats.get(name);
|
|
229
|
+
if (cur) {
|
|
230
|
+
cur.count++;
|
|
231
|
+
cur.bytes += bytes;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
stats.set(name, { count: 1, bytes });
|
|
235
|
+
}
|
|
236
|
+
totalCount++;
|
|
237
|
+
totalBytes += bytes;
|
|
238
|
+
const h = heapStats.get(currentHeap);
|
|
239
|
+
if (h) {
|
|
240
|
+
h.count++;
|
|
241
|
+
h.bytes += bytes;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
heapStats.set(currentHeap, { count: 1, bytes });
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const classNameFor = (classObjId) => classNameByObjId.get(classObjId) ?? `<class@${classObjId}>`;
|
|
248
|
+
// ── Heap dump body parser (one pass over sub-records) ──────────────────────
|
|
249
|
+
const parseHeapDumpBody = (end) => {
|
|
250
|
+
while (pos < end) {
|
|
251
|
+
const sub = readU1();
|
|
252
|
+
switch (sub) {
|
|
253
|
+
// Roots that are just a single ID.
|
|
254
|
+
case SUB_ROOT_UNKNOWN:
|
|
255
|
+
case SUB_ROOT_STICKY_CLASS:
|
|
256
|
+
case SUB_ROOT_MONITOR_USED:
|
|
257
|
+
case SUB_ROOT_INTERNED_STRING:
|
|
258
|
+
case SUB_ROOT_FINALIZING:
|
|
259
|
+
case SUB_ROOT_DEBUGGER:
|
|
260
|
+
case SUB_ROOT_REFERENCE_CLEANUP:
|
|
261
|
+
case SUB_ROOT_VM_INTERNAL:
|
|
262
|
+
case SUB_UNREACHABLE:
|
|
263
|
+
skipId();
|
|
264
|
+
break;
|
|
265
|
+
case SUB_ROOT_JNI_GLOBAL:
|
|
266
|
+
skipId(); // object id
|
|
267
|
+
skipId(); // jni global ref id
|
|
268
|
+
break;
|
|
269
|
+
case SUB_ROOT_JNI_LOCAL:
|
|
270
|
+
case SUB_ROOT_JAVA_FRAME:
|
|
271
|
+
case SUB_ROOT_JNI_MONITOR:
|
|
272
|
+
skipId();
|
|
273
|
+
pos += 4 + 4; // thread serial + frame number
|
|
274
|
+
break;
|
|
275
|
+
case SUB_ROOT_NATIVE_STACK:
|
|
276
|
+
case SUB_ROOT_THREAD_BLOCK:
|
|
277
|
+
skipId();
|
|
278
|
+
pos += 4; // thread serial
|
|
279
|
+
break;
|
|
280
|
+
case SUB_ROOT_THREAD_OBJECT:
|
|
281
|
+
skipId();
|
|
282
|
+
pos += 4 + 4; // thread serial + stack trace serial
|
|
283
|
+
break;
|
|
284
|
+
case SUB_HEAP_DUMP_INFO: {
|
|
285
|
+
// Switches the "current heap" context for subsequent records.
|
|
286
|
+
// Layout: u4 heap_id, ID heap_name_string_id
|
|
287
|
+
pos += 4;
|
|
288
|
+
const nameId = readId();
|
|
289
|
+
currentHeap = strings.get(nameId) ?? `heap${nameId}`;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case SUB_CLASS_DUMP: {
|
|
293
|
+
// Variable-length: header + constant pool + static fields + instance fields.
|
|
294
|
+
skipId(); // class object id
|
|
295
|
+
pos += 4; // stack trace serial
|
|
296
|
+
skipId(); // super class id
|
|
297
|
+
skipId(); // classloader id
|
|
298
|
+
skipId(); // signers id
|
|
299
|
+
skipId(); // protection domain id
|
|
300
|
+
skipId(); // reserved
|
|
301
|
+
skipId(); // reserved
|
|
302
|
+
pos += 4; // instance size
|
|
303
|
+
// Constant pool: u2 count, then [u2 idx, u1 type, value]
|
|
304
|
+
const cpCount = readU2();
|
|
305
|
+
for (let i = 0; i < cpCount; i++) {
|
|
306
|
+
pos += 2; // constant pool index
|
|
307
|
+
const t = readU1();
|
|
308
|
+
pos += valueSize(t, idSize);
|
|
309
|
+
}
|
|
310
|
+
// Static fields: u2 count, then [ID name, u1 type, value]
|
|
311
|
+
const staticCount = readU2();
|
|
312
|
+
for (let i = 0; i < staticCount; i++) {
|
|
313
|
+
skipId(); // name string id
|
|
314
|
+
const t = readU1();
|
|
315
|
+
pos += valueSize(t, idSize);
|
|
316
|
+
}
|
|
317
|
+
// Instance field declarations: u2 count, then [ID name, u1 type]
|
|
318
|
+
const instanceCount = readU2();
|
|
319
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
320
|
+
skipId(); // name string id
|
|
321
|
+
pos += 1; // type
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case SUB_INSTANCE_DUMP: {
|
|
326
|
+
skipId(); // object id
|
|
327
|
+
pos += 4; // stack trace serial
|
|
328
|
+
const classObjId = readId();
|
|
329
|
+
const dataSize = readU4();
|
|
330
|
+
// Field bytes follow; we don't care about the values.
|
|
331
|
+
pos += dataSize;
|
|
332
|
+
// Account 16 bytes of object header on top of the field area —
|
|
333
|
+
// matches the convention Android Studio uses.
|
|
334
|
+
bump(classNameFor(classObjId), dataSize + 16);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case SUB_OBJECT_ARRAY_DUMP: {
|
|
338
|
+
skipId(); // array object id
|
|
339
|
+
pos += 4; // stack trace serial
|
|
340
|
+
const numElements = readU4();
|
|
341
|
+
const arrayClassId = readId();
|
|
342
|
+
pos += numElements * idSize; // element ids
|
|
343
|
+
// Object[] payload size = elements * idSize, plus 16-byte array header.
|
|
344
|
+
bump(classNameFor(arrayClassId), numElements * idSize + 16);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case SUB_PRIMITIVE_ARRAY_DUMP: {
|
|
348
|
+
skipId(); // array object id
|
|
349
|
+
pos += 4; // stack trace serial
|
|
350
|
+
const numElements = readU4();
|
|
351
|
+
const elemType = readU1();
|
|
352
|
+
const elemSize = valueSize(elemType, idSize);
|
|
353
|
+
pos += numElements * elemSize; // primitive bytes
|
|
354
|
+
bump(primArrayName(elemType), numElements * elemSize + 16);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case SUB_PRIMITIVE_ARRAY_NODATA: {
|
|
358
|
+
// Same shape as primitive array minus the actual data bytes.
|
|
359
|
+
skipId();
|
|
360
|
+
pos += 4;
|
|
361
|
+
const numElements = readU4();
|
|
362
|
+
const elemType = readU1();
|
|
363
|
+
bump(primArrayName(elemType), numElements * valueSize(elemType, idSize) + 16);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
default: {
|
|
367
|
+
// Unknown sub-record — we can't safely advance, so abort the segment.
|
|
368
|
+
throw new Error(`Unknown HPROF sub-record 0x${sub.toString(16)} at offset ${pos - 1}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
// ── Main record loop ───────────────────────────────────────────────────────
|
|
374
|
+
while (pos < buf.length) {
|
|
375
|
+
if (pos + 9 > buf.length)
|
|
376
|
+
break; // not enough for a record header
|
|
377
|
+
const tag = readU1();
|
|
378
|
+
pos += 4; // time delta (unused)
|
|
379
|
+
const len = readU4();
|
|
380
|
+
const recordEnd = pos + len;
|
|
381
|
+
if (recordEnd > buf.length)
|
|
382
|
+
break; // truncated tail
|
|
383
|
+
switch (tag) {
|
|
384
|
+
case TAG_STRING: {
|
|
385
|
+
const id = readId();
|
|
386
|
+
const str = buf.slice(pos, recordEnd).toString('utf8');
|
|
387
|
+
strings.set(id, str);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case TAG_LOAD_CLASS: {
|
|
391
|
+
pos += 4; // class serial
|
|
392
|
+
const classObjId = readId();
|
|
393
|
+
pos += 4; // stack trace serial
|
|
394
|
+
const nameStrId = readId();
|
|
395
|
+
const name = strings.get(nameStrId);
|
|
396
|
+
if (name !== undefined)
|
|
397
|
+
classNameByObjId.set(classObjId, name);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case TAG_HEAP_DUMP:
|
|
401
|
+
case TAG_HEAP_DUMP_SEGMENT:
|
|
402
|
+
parseHeapDumpBody(recordEnd);
|
|
403
|
+
break;
|
|
404
|
+
default:
|
|
405
|
+
// Unknown / uninteresting top-level record (UNLOAD_CLASS, STACK_FRAME,
|
|
406
|
+
// STACK_TRACE, ALLOC_SITES, ...). Skip the body via recordEnd.
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
pos = recordEnd;
|
|
410
|
+
}
|
|
411
|
+
const classes = [...stats.entries()]
|
|
412
|
+
.map(([cls, v]) => ({ class: cls, count: v.count, bytes: v.bytes }))
|
|
413
|
+
.sort((a, b) => b.bytes - a.bytes);
|
|
414
|
+
const heaps = {};
|
|
415
|
+
for (const [h, v] of heapStats)
|
|
416
|
+
heaps[h] = v;
|
|
417
|
+
return {
|
|
418
|
+
classes,
|
|
419
|
+
totals: { count: totalCount, bytes: totalBytes },
|
|
420
|
+
heaps: Object.keys(heaps).length > 0 ? heaps : undefined,
|
|
421
|
+
};
|
|
422
|
+
}
|