@houwert/conductor 0.28.0 → 0.29.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/README.md +71 -62
- package/dist/android/device.js +94 -0
- package/dist/commands/debug.js +6 -6
- package/dist/commands/focused.js +27 -0
- package/dist/commands/input-latency.js +348 -0
- package/dist/commands/metro.js +3 -1
- package/dist/commands/native-rn.js +2 -2
- package/dist/commands/network.js +2 -2
- package/dist/commands/press-key.js +106 -60
- package/dist/commands/profile-frames.js +793 -0
- package/dist/commands/profile-gc.js +90 -0
- package/dist/commands/profile-js.js +320 -0
- package/dist/commands/profile.js +391 -77
- package/dist/commands/run-flow.js +70 -4
- package/dist/drivers/flow-runner.js +7 -1
- package/dist/drivers/metro-cdp.js +28 -3
- package/dist/index.js +79 -43
- package/dist/stats.js +71 -0
- package/package.json +1 -1
- package/skills/conductor-create-flow/SKILL.md +1 -1
- package/skills/conductor-device-interact/SKILL.md +1 -1
- package/skills/conductor-profiler/SKILL.md +115 -7
- package/dist/commands/cases.js +0 -260
- package/skills/conductor-test-cases/SKILL.md +0 -65
|
@@ -4,15 +4,58 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
|
+
exports.aggregateRuns = aggregateRuns;
|
|
7
8
|
exports.runFlow = runFlow;
|
|
8
9
|
exports.HELP = ` run-flow <file> [--device <id>] Run a Maestro YAML flow file
|
|
9
10
|
--env KEY=VALUE Inject env var (repeatable; overrides flow env block)
|
|
10
|
-
--benchmark Print elapsed time for each command and total flow time
|
|
11
|
+
--benchmark Print elapsed time for each command and total flow time
|
|
12
|
+
--repeat <n> With --benchmark, run the flow n times and report p50/p90/σ
|
|
13
|
+
--json With --benchmark --repeat, emit the aggregate as JSON`;
|
|
11
14
|
const path_1 = __importDefault(require("path"));
|
|
12
15
|
const runner_js_1 = require("../runner.js");
|
|
13
16
|
const flow_runner_js_1 = require("../drivers/flow-runner.js");
|
|
14
17
|
const output_js_1 = require("../output.js");
|
|
15
|
-
|
|
18
|
+
const stats_js_1 = require("../stats.js");
|
|
19
|
+
/**
|
|
20
|
+
* Aggregate per-run timings by command label. Keyed on label rather than
|
|
21
|
+
* position so a flow whose branches differ between runs still lines up.
|
|
22
|
+
*/
|
|
23
|
+
function aggregateRuns(runs) {
|
|
24
|
+
const byLabel = new Map();
|
|
25
|
+
for (const run of runs) {
|
|
26
|
+
for (const entry of run.entries) {
|
|
27
|
+
const slot = byLabel.get(entry.label) ?? { times: [], failures: 0 };
|
|
28
|
+
slot.times.push(entry.ms);
|
|
29
|
+
if (!entry.ok)
|
|
30
|
+
slot.failures++;
|
|
31
|
+
byLabel.set(entry.label, slot);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
runs: runs.length,
|
|
36
|
+
total: (0, stats_js_1.describe)(runs.map((r) => r.totalMs)),
|
|
37
|
+
commands: [...byLabel.entries()]
|
|
38
|
+
.map(([label, v]) => ({
|
|
39
|
+
label,
|
|
40
|
+
runs: v.times.length,
|
|
41
|
+
failures: v.failures,
|
|
42
|
+
...(0, stats_js_1.describe)(v.times),
|
|
43
|
+
}))
|
|
44
|
+
.sort((a, b) => (b.p50Ms ?? 0) - (a.p50Ms ?? 0)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function printAggregate(agg) {
|
|
48
|
+
console.log(`\nBenchmark over ${agg.runs} run(s)`);
|
|
49
|
+
console.log(` total p50 ${(0, stats_js_1.fmt)(agg.total.p50Ms)} p90 ${(0, stats_js_1.fmt)(agg.total.p90Ms)} ` +
|
|
50
|
+
`σ ${(0, stats_js_1.fmt)(agg.total.stddevMs)} min ${(0, stats_js_1.fmt)(agg.total.minMs)} max ${(0, stats_js_1.fmt)(agg.total.maxMs)}`);
|
|
51
|
+
console.log(`\n ${'p50'.padStart(9)} ${'p90'.padStart(9)} ${'σ'.padStart(8)} n command`);
|
|
52
|
+
for (const c of agg.commands) {
|
|
53
|
+
console.log(` ${(0, stats_js_1.fmt)(c.p50Ms).padStart(9)} ${(0, stats_js_1.fmt)(c.p90Ms).padStart(9)} ` +
|
|
54
|
+
`${(0, stats_js_1.fmt)(c.stddevMs).padStart(8)} ${c.runs} ${c.label}` +
|
|
55
|
+
(c.failures > 0 ? ` (${c.failures} failed)` : ''));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function runFlow(file, opts = {}, sessionName = 'default', env = {}, benchmark = false, repeat = 1) {
|
|
16
59
|
if (!file) {
|
|
17
60
|
(0, output_js_1.printError)('run-flow requires <file>', opts);
|
|
18
61
|
return 1;
|
|
@@ -21,8 +64,31 @@ async function runFlow(file, opts = {}, sessionName = 'default', env = {}, bench
|
|
|
21
64
|
try {
|
|
22
65
|
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
23
66
|
const flow = await (0, flow_runner_js_1.parseFlowFile)(resolvedFile, env);
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
if (repeat <= 1) {
|
|
68
|
+
await (0, flow_runner_js_1.executeFlow)(flow, driver, { cwd: path_1.default.dirname(resolvedFile), env, benchmark });
|
|
69
|
+
(0, output_js_1.printSuccess)(`run-flow "${file}" — done`, opts);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const runs = [];
|
|
73
|
+
for (let i = 0; i < repeat; i++) {
|
|
74
|
+
const entries = [];
|
|
75
|
+
console.log(`\n── run ${i + 1}/${repeat} ─────────────────────────────`);
|
|
76
|
+
const started = performance.now();
|
|
77
|
+
await (0, flow_runner_js_1.executeFlow)(flow, driver, {
|
|
78
|
+
cwd: path_1.default.dirname(resolvedFile),
|
|
79
|
+
env,
|
|
80
|
+
benchmark,
|
|
81
|
+
benchmarkSink: (e) => entries.push(e),
|
|
82
|
+
});
|
|
83
|
+
runs.push({ totalMs: (0, stats_js_1.round)(performance.now() - started), entries });
|
|
84
|
+
}
|
|
85
|
+
const agg = aggregateRuns(runs);
|
|
86
|
+
if (opts.json)
|
|
87
|
+
(0, output_js_1.printData)({ status: 'ok', benchmark: agg }, opts);
|
|
88
|
+
else {
|
|
89
|
+
printAggregate(agg);
|
|
90
|
+
(0, output_js_1.printSuccess)(`run-flow "${file}" — ${repeat} runs done`, opts);
|
|
91
|
+
}
|
|
26
92
|
return 0;
|
|
27
93
|
}
|
|
28
94
|
catch (err) {
|
|
@@ -392,6 +392,7 @@ async function executeFlow(flow, driver, opts = {}) {
|
|
|
392
392
|
output: opts.output ?? {},
|
|
393
393
|
depth: opts.depth ?? 0,
|
|
394
394
|
benchmark: opts.benchmark,
|
|
395
|
+
benchmarkSink: opts.benchmarkSink,
|
|
395
396
|
};
|
|
396
397
|
const flowStart = opts.benchmark ? perf_hooks_1.performance.now() : 0;
|
|
397
398
|
if (flow.onFlowStart?.length) {
|
|
@@ -515,9 +516,11 @@ async function executeCommand(cmd, driver, opts) {
|
|
|
515
516
|
else {
|
|
516
517
|
process.stdout.write(`${indent}→ ${label} ... `);
|
|
517
518
|
}
|
|
518
|
-
const
|
|
519
|
+
const timing = opts.benchmark || opts.benchmarkSink;
|
|
520
|
+
const t0 = timing ? perf_hooks_1.performance.now() : 0;
|
|
519
521
|
try {
|
|
520
522
|
await executeCommandBody(key, resolvedVal, driver, opts);
|
|
523
|
+
opts.benchmarkSink?.({ label, depth: opts.depth, ms: perf_hooks_1.performance.now() - t0, ok: true });
|
|
521
524
|
const elapsed = opts.benchmark ? ` (${fmtMs(perf_hooks_1.performance.now() - t0)})` : '';
|
|
522
525
|
if (!isCompound) {
|
|
523
526
|
console.log(`ok${elapsed}`);
|
|
@@ -528,6 +531,7 @@ async function executeCommand(cmd, driver, opts) {
|
|
|
528
531
|
}
|
|
529
532
|
catch (err) {
|
|
530
533
|
const msg = err instanceof Error ? err.message : String(err);
|
|
534
|
+
opts.benchmarkSink?.({ label, depth: opts.depth, ms: perf_hooks_1.performance.now() - t0, ok: false });
|
|
531
535
|
const elapsed = opts.benchmark ? ` (${fmtMs(perf_hooks_1.performance.now() - t0)})` : '';
|
|
532
536
|
if (optional) {
|
|
533
537
|
// For compound commands the sub-command already printed its warning/failure
|
|
@@ -1273,6 +1277,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1273
1277
|
output: opts.output,
|
|
1274
1278
|
depth: childDepth,
|
|
1275
1279
|
benchmark: opts.benchmark,
|
|
1280
|
+
benchmarkSink: opts.benchmarkSink,
|
|
1276
1281
|
});
|
|
1277
1282
|
}
|
|
1278
1283
|
else {
|
|
@@ -1293,6 +1298,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1293
1298
|
output: opts.output,
|
|
1294
1299
|
depth: childDepth,
|
|
1295
1300
|
benchmark: opts.benchmark,
|
|
1301
|
+
benchmarkSink: opts.benchmarkSink,
|
|
1296
1302
|
});
|
|
1297
1303
|
}
|
|
1298
1304
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.MetroCdpClient = void 0;
|
|
7
7
|
exports.selectDebuggerUrl = selectDebuggerUrl;
|
|
8
8
|
exports.resolveDebuggerUrl = resolveDebuggerUrl;
|
|
9
|
+
exports.resolveMetroPort = resolveMetroPort;
|
|
9
10
|
exports.cdpCall = cdpCall;
|
|
10
11
|
/**
|
|
11
12
|
* One-shot CDP client to Metro's debugger endpoint.
|
|
@@ -62,14 +63,38 @@ function selectDebuggerUrl(targets, opts, displayName) {
|
|
|
62
63
|
* Throws with a clear message if Metro is unreachable or no target matches.
|
|
63
64
|
*/
|
|
64
65
|
async function resolveDebuggerUrl(opts) {
|
|
65
|
-
const port = opts
|
|
66
|
+
const port = await resolveMetroPort(opts);
|
|
66
67
|
const host = opts.host ?? 'localhost';
|
|
67
|
-
|
|
68
|
+
let targets;
|
|
69
|
+
try {
|
|
70
|
+
targets = await (0, metro_js_1.fetchTargets)(port, host);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
74
|
+
throw new Error(`Cannot reach Metro on ${host}:${port} — ${detail}.\n` +
|
|
75
|
+
`Pass --port <n> if Metro listens elsewhere (a project may set RCT_METRO_PORT); ` +
|
|
76
|
+
`\`conductor workspace info\` reports the port it can see.`);
|
|
77
|
+
}
|
|
68
78
|
let displayName;
|
|
69
79
|
if (opts.deviceId && opts.platform) {
|
|
70
80
|
displayName = (await (0, metro_discovery_js_1.getDeviceDisplayName)(opts.platform, opts.deviceId)) ?? undefined;
|
|
71
81
|
}
|
|
72
|
-
return selectDebuggerUrl(targets, opts, displayName);
|
|
82
|
+
return selectDebuggerUrl(targets, { ...opts, port }, displayName);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Metro's port for this device: the explicit --port, else whatever the device is
|
|
86
|
+
* actually connected to, else 8081. Auto-discovery matters because a project
|
|
87
|
+
* that sets RCT_METRO_PORT otherwise fails with an opaque connection error.
|
|
88
|
+
*/
|
|
89
|
+
async function resolveMetroPort(opts) {
|
|
90
|
+
if (opts.port !== undefined)
|
|
91
|
+
return opts.port;
|
|
92
|
+
if (opts.deviceId && opts.platform) {
|
|
93
|
+
const discovered = await (0, metro_discovery_js_1.discoverMetroPortForDevice)(opts.platform, opts.deviceId).catch(() => null);
|
|
94
|
+
if (discovered)
|
|
95
|
+
return discovered;
|
|
96
|
+
}
|
|
97
|
+
return 8081;
|
|
73
98
|
}
|
|
74
99
|
/**
|
|
75
100
|
* Open a short-lived CDP socket, send a single method, return the result.
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,8 @@ const debug_js_1 = require("./commands/debug.js");
|
|
|
42
42
|
const network_js_1 = require("./commands/network.js");
|
|
43
43
|
const flow_record_js_1 = require("./commands/flow-record.js");
|
|
44
44
|
const profile_js_1 = require("./commands/profile.js");
|
|
45
|
+
const profile_frames_js_1 = require("./commands/profile-frames.js");
|
|
46
|
+
const profile_js_js_1 = require("./commands/profile-js.js");
|
|
45
47
|
const crashes_js_1 = require("./commands/crashes.js");
|
|
46
48
|
const flow_recorder_js_1 = require("./drivers/flow-recorder.js");
|
|
47
49
|
const foreground_app_js_1 = require("./commands/foreground-app.js");
|
|
@@ -61,7 +63,6 @@ const start_device_js_1 = require("./commands/start-device.js");
|
|
|
61
63
|
const stop_device_js_1 = require("./commands/stop-device.js");
|
|
62
64
|
const delete_device_js_1 = require("./commands/delete-device.js");
|
|
63
65
|
const logs_js_1 = require("./commands/logs.js");
|
|
64
|
-
const cases_js_1 = require("./commands/cases.js");
|
|
65
66
|
const memory_js_1 = require("./commands/memory.js");
|
|
66
67
|
const metro_js_1 = require("./commands/metro.js");
|
|
67
68
|
const clipboard_js_1 = require("./commands/clipboard.js");
|
|
@@ -89,7 +90,6 @@ const COMMAND_HELP = {
|
|
|
89
90
|
'list-devices': list_devices_js_1.HELP,
|
|
90
91
|
'foreground-app': foreground_app_js_1.HELP,
|
|
91
92
|
'list-apps': list_apps_js_1.HELP,
|
|
92
|
-
cases: cases_js_1.HELP,
|
|
93
93
|
'copy-app': copy_app_js_1.HELP,
|
|
94
94
|
'download-app': download_app_js_1.HELP,
|
|
95
95
|
'install-app': install_app_js_1.HELP,
|
|
@@ -225,6 +225,10 @@ async function main() {
|
|
|
225
225
|
'force',
|
|
226
226
|
'yes',
|
|
227
227
|
'update',
|
|
228
|
+
'measure',
|
|
229
|
+
'report',
|
|
230
|
+
'timeline',
|
|
231
|
+
'baselines',
|
|
228
232
|
],
|
|
229
233
|
string: [
|
|
230
234
|
'device',
|
|
@@ -256,6 +260,9 @@ async function main() {
|
|
|
256
260
|
'source',
|
|
257
261
|
'level',
|
|
258
262
|
'save',
|
|
263
|
+
'save-baseline',
|
|
264
|
+
'sequence',
|
|
265
|
+
'settle',
|
|
259
266
|
'diff',
|
|
260
267
|
'vs',
|
|
261
268
|
'top',
|
|
@@ -294,13 +301,6 @@ async function main() {
|
|
|
294
301
|
'speed',
|
|
295
302
|
'threshold',
|
|
296
303
|
'reference',
|
|
297
|
-
'project',
|
|
298
|
-
'junit',
|
|
299
|
-
'verdict',
|
|
300
|
-
'note',
|
|
301
|
-
'column',
|
|
302
|
-
'build',
|
|
303
|
-
'environment',
|
|
304
304
|
],
|
|
305
305
|
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
306
306
|
});
|
|
@@ -664,6 +664,15 @@ async function main() {
|
|
|
664
664
|
exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName, {
|
|
665
665
|
longPress: argv['long-press'],
|
|
666
666
|
duration: durationArg !== undefined ? Number(durationArg) : undefined,
|
|
667
|
+
measure: argv['measure'],
|
|
668
|
+
repeat: argv['repeat'] !== undefined ? Number(argv['repeat']) : undefined,
|
|
669
|
+
timeoutMs: argv['timeout'] !== undefined ? Number(argv['timeout']) : undefined,
|
|
670
|
+
pollIntervalMs: argv['poll-interval'] !== undefined ? Number(argv['poll-interval']) : undefined,
|
|
671
|
+
settleMs: argv['settle'] !== undefined ? Number(argv['settle']) : undefined,
|
|
672
|
+
sequence: typeof argv['sequence'] === 'string'
|
|
673
|
+
? argv['sequence'].split(',')
|
|
674
|
+
: undefined,
|
|
675
|
+
appId: argv['app'],
|
|
667
676
|
});
|
|
668
677
|
break;
|
|
669
678
|
}
|
|
@@ -878,35 +887,6 @@ async function main() {
|
|
|
878
887
|
interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
|
|
879
888
|
});
|
|
880
889
|
break;
|
|
881
|
-
case 'cases': {
|
|
882
|
-
// Repo-scoped, not device-scoped: cases and their results are files.
|
|
883
|
-
const root = argv['project'] ?? process.cwd();
|
|
884
|
-
const sub = rest[0] ?? 'list';
|
|
885
|
-
if (sub === 'list') {
|
|
886
|
-
exitCode = await (0, cases_js_1.casesList)(root, opts);
|
|
887
|
-
}
|
|
888
|
-
else if (sub === 'report') {
|
|
889
|
-
exitCode = await (0, cases_js_1.casesReport)(root, argv['junit'] ?? '', {
|
|
890
|
-
...opts,
|
|
891
|
-
build: argv['build'],
|
|
892
|
-
environment: argv['environment'],
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
else if (sub === 'result') {
|
|
896
|
-
exitCode = await (0, cases_js_1.casesResult)(root, rest[1] ?? '', argv['verdict'] ?? 'passed', {
|
|
897
|
-
...opts,
|
|
898
|
-
note: argv['note'],
|
|
899
|
-
column: argv['column'],
|
|
900
|
-
build: argv['build'],
|
|
901
|
-
environment: argv['environment'],
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
console.error(`Unknown cases subcommand "${sub}". Use list, report or result.`);
|
|
906
|
-
exitCode = 1;
|
|
907
|
-
}
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
890
|
case 'logs':
|
|
911
891
|
exitCode = await (0, logs_js_1.logs)(opts, sessionName, {
|
|
912
892
|
source: argv['source'],
|
|
@@ -939,7 +919,7 @@ async function main() {
|
|
|
939
919
|
const rawEnv = argv['env'];
|
|
940
920
|
const envPairs = Array.isArray(rawEnv) ? rawEnv : rawEnv ? [rawEnv] : [];
|
|
941
921
|
const flowEnv = Object.fromEntries(envPairs.map((e) => e.split('=', 2)));
|
|
942
|
-
exitCode = await (0, run_flow_js_1.runFlow)(file, opts, sessionName, flowEnv, argv['benchmark']);
|
|
922
|
+
exitCode = await (0, run_flow_js_1.runFlow)(file, opts, sessionName, flowEnv, argv['benchmark'], argv['repeat'] !== undefined ? Number(argv['repeat']) : 1);
|
|
943
923
|
break;
|
|
944
924
|
}
|
|
945
925
|
case 'run-flow-inline': {
|
|
@@ -1018,12 +998,15 @@ async function main() {
|
|
|
1018
998
|
const sub = (rest[0] ?? '').toLowerCase();
|
|
1019
999
|
const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
|
|
1020
1000
|
const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
|
|
1001
|
+
const top = argv['top'] !== undefined ? Number(argv['top']) : undefined;
|
|
1021
1002
|
if (sub === 'cpu') {
|
|
1022
1003
|
const durationSec = argv['duration'] !== undefined ? Number(argv['duration']) : 10;
|
|
1023
1004
|
exitCode = await (0, profile_js_1.profileCpu)(opts, sessionName, {
|
|
1024
1005
|
durationSec,
|
|
1025
1006
|
out: argv['out'],
|
|
1026
1007
|
appId: rest[1],
|
|
1008
|
+
report: argv['report'],
|
|
1009
|
+
top,
|
|
1027
1010
|
});
|
|
1028
1011
|
}
|
|
1029
1012
|
else if (sub === 'memory') {
|
|
@@ -1035,14 +1018,67 @@ async function main() {
|
|
|
1035
1018
|
appId: rest[1],
|
|
1036
1019
|
});
|
|
1037
1020
|
}
|
|
1021
|
+
else if (sub === 'frames') {
|
|
1022
|
+
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
1023
|
+
if (sub2 === 'reset') {
|
|
1024
|
+
exitCode = await (0, profile_frames_js_1.profileFramesReset)(opts, sessionName, rest[2]);
|
|
1025
|
+
}
|
|
1026
|
+
else if (sub2 === 'report') {
|
|
1027
|
+
exitCode = await (0, profile_frames_js_1.profileFramesReport)(opts, sessionName, {
|
|
1028
|
+
appId: rest[2],
|
|
1029
|
+
trackSec: argv['track'] !== undefined ? Number(argv['track']) : undefined,
|
|
1030
|
+
intervalMs: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
|
|
1031
|
+
top,
|
|
1032
|
+
saveBaseline: argv['save-baseline'],
|
|
1033
|
+
diff: argv['diff'],
|
|
1034
|
+
listBaselines: argv['baselines'],
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
console.error('Usage: conductor profile frames <reset|report> [<appId>]');
|
|
1039
|
+
exitCode = 1;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
else if (sub === 'js') {
|
|
1043
|
+
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
1044
|
+
const jsOpts = {
|
|
1045
|
+
port,
|
|
1046
|
+
targetIndex,
|
|
1047
|
+
out: argv['out'],
|
|
1048
|
+
top,
|
|
1049
|
+
durationSec: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
|
|
1050
|
+
};
|
|
1051
|
+
if (sub2 === 'record') {
|
|
1052
|
+
exitCode = await (0, profile_js_js_1.profileJsRecord)(opts, sessionName, jsOpts);
|
|
1053
|
+
}
|
|
1054
|
+
else if (sub2 === 'start') {
|
|
1055
|
+
exitCode = await (0, profile_js_js_1.profileJsStart)(opts, sessionName, jsOpts);
|
|
1056
|
+
}
|
|
1057
|
+
else if (sub2 === 'stop') {
|
|
1058
|
+
exitCode = await (0, profile_js_js_1.profileJsStop)(opts, sessionName, jsOpts);
|
|
1059
|
+
}
|
|
1060
|
+
else if (sub2 === '_hold') {
|
|
1061
|
+
exitCode = await (0, profile_js_js_1.profileJsHold)(sessionName, jsOpts);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
console.error('Usage: conductor profile js <record|start|stop>');
|
|
1065
|
+
exitCode = 1;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1038
1068
|
else if (sub === 'react') {
|
|
1039
1069
|
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
1040
|
-
const
|
|
1070
|
+
const reactOpts = {
|
|
1071
|
+
port,
|
|
1072
|
+
targetIndex,
|
|
1073
|
+
maxCommits: argv['max-commits'] !== undefined ? Number(argv['max-commits']) : undefined,
|
|
1074
|
+
maxComponents: argv['max-components'] !== undefined ? Number(argv['max-components']) : undefined,
|
|
1075
|
+
timeline: argv['timeline'],
|
|
1076
|
+
};
|
|
1041
1077
|
if (sub2 === 'start') {
|
|
1042
|
-
exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName,
|
|
1078
|
+
exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName, reactOpts);
|
|
1043
1079
|
}
|
|
1044
1080
|
else if (sub2 === 'stop') {
|
|
1045
|
-
exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName,
|
|
1081
|
+
exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName, reactOpts, top ?? 20);
|
|
1046
1082
|
}
|
|
1047
1083
|
else {
|
|
1048
1084
|
console.error('Usage: conductor profile react <start|stop>');
|
|
@@ -1050,7 +1086,7 @@ async function main() {
|
|
|
1050
1086
|
}
|
|
1051
1087
|
}
|
|
1052
1088
|
else {
|
|
1053
|
-
console.error('Usage: conductor profile <cpu|memory|react> [args]');
|
|
1089
|
+
console.error('Usage: conductor profile <cpu|memory|frames|js|react> [args]');
|
|
1054
1090
|
exitCode = 1;
|
|
1055
1091
|
}
|
|
1056
1092
|
break;
|
package/dist/stats.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Percentile / dispersion helpers shared by the profiling commands.
|
|
4
|
+
*
|
|
5
|
+
* Percentiles use nearest-rank on a sorted copy so a p99 over a handful of
|
|
6
|
+
* samples still names a real observation rather than an interpolated one that
|
|
7
|
+
* never happened.
|
|
8
|
+
*
|
|
9
|
+
* With no samples every figure is `null`, never 0. A missing measurement that
|
|
10
|
+
* reads as `0ms` is a missing measurement that reads as a *perfect* one, and a
|
|
11
|
+
* consumer has to remember to check `count` to avoid believing it. `null`
|
|
12
|
+
* propagates through arithmetic and comparison loudly enough to notice.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.EMPTY_DISTRIBUTION = void 0;
|
|
16
|
+
exports.hasSamples = hasSamples;
|
|
17
|
+
exports.percentile = percentile;
|
|
18
|
+
exports.round = round;
|
|
19
|
+
exports.roundOrNull = roundOrNull;
|
|
20
|
+
exports.describe = describe;
|
|
21
|
+
exports.fmt = fmt;
|
|
22
|
+
exports.EMPTY_DISTRIBUTION = {
|
|
23
|
+
count: 0,
|
|
24
|
+
minMs: null,
|
|
25
|
+
maxMs: null,
|
|
26
|
+
meanMs: null,
|
|
27
|
+
stddevMs: null,
|
|
28
|
+
p50Ms: null,
|
|
29
|
+
p90Ms: null,
|
|
30
|
+
p95Ms: null,
|
|
31
|
+
p99Ms: null,
|
|
32
|
+
};
|
|
33
|
+
function hasSamples(d) {
|
|
34
|
+
return d !== undefined && d.count > 0;
|
|
35
|
+
}
|
|
36
|
+
function percentile(sorted, p) {
|
|
37
|
+
if (sorted.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
const rank = Math.ceil((p / 100) * sorted.length);
|
|
40
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
|
|
41
|
+
}
|
|
42
|
+
function round(n, digits = 2) {
|
|
43
|
+
const f = 10 ** digits;
|
|
44
|
+
return Math.round(n * f) / f;
|
|
45
|
+
}
|
|
46
|
+
/** `round` that passes `null` through, for optional measurements. */
|
|
47
|
+
function roundOrNull(n, digits = 2) {
|
|
48
|
+
return n === null ? null : round(n, digits);
|
|
49
|
+
}
|
|
50
|
+
function describe(values) {
|
|
51
|
+
if (values.length === 0)
|
|
52
|
+
return { ...exports.EMPTY_DISTRIBUTION };
|
|
53
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
54
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
55
|
+
const variance = sorted.reduce((acc, v) => acc + (v - mean) ** 2, 0) / sorted.length;
|
|
56
|
+
return {
|
|
57
|
+
count: sorted.length,
|
|
58
|
+
minMs: round(sorted[0]),
|
|
59
|
+
maxMs: round(sorted[sorted.length - 1]),
|
|
60
|
+
meanMs: round(mean),
|
|
61
|
+
stddevMs: round(Math.sqrt(variance)),
|
|
62
|
+
p50Ms: roundOrNull(percentile(sorted, 50)),
|
|
63
|
+
p90Ms: roundOrNull(percentile(sorted, 90)),
|
|
64
|
+
p95Ms: roundOrNull(percentile(sorted, 95)),
|
|
65
|
+
p99Ms: roundOrNull(percentile(sorted, 99)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Format a possibly-absent measurement for text output. */
|
|
69
|
+
function fmt(v, unit = 'ms') {
|
|
70
|
+
return v === null || v === undefined ? 'n/a' : `${v}${unit}`;
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ existing Maestro flows run unchanged. Use flows for repeatable journeys; use
|
|
|
14
14
|
|
|
15
15
|
| Command | Purpose |
|
|
16
16
|
|---|---|
|
|
17
|
-
| `conductor run-flow <file> [--env K=V] [--benchmark]` | Run a Maestro YAML flow file |
|
|
17
|
+
| `conductor run-flow <file> [--env K=V] [--benchmark] [--repeat <n>]` | Run a Maestro YAML flow file. `--benchmark --repeat <n>` reports per-command p50/p90/stddev across runs |
|
|
18
18
|
| `conductor run-flow-inline '<yaml>' [--benchmark]` | Run inline YAML from the command line |
|
|
19
19
|
| `conductor run-sequence [--file path.json]` | Run a JSON sequence of conductor commands serially; reads stdin if no `--file` |
|
|
20
20
|
| `conductor run-parallel --flows-dir <path>` | Shard a directory of flows across all booted devices |
|
|
@@ -41,7 +41,7 @@ conductor assert-visible "Dashboard"
|
|
|
41
41
|
| `conductor copy-text-from <element>` | Print an element's text (and copy to the iOS clipboard) |
|
|
42
42
|
| `conductor input-text <text>` | Type into the focused field |
|
|
43
43
|
| `conductor erase-text [n]` | Erase n characters (default 50) |
|
|
44
|
-
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`) for tvOS / Android TV / vega. `--long-press` / `--duration <seconds>` holds it |
|
|
44
|
+
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`) for tvOS / Android TV / vega. `--long-press` / `--duration <seconds>` holds it; `--measure` times the response (see `conductor-profiler`) |
|
|
45
45
|
| `conductor hide-keyboard` | Dismiss the on-screen keyboard |
|
|
46
46
|
| `conductor back` | Press back |
|
|
47
47
|
| `conductor scroll [--direction down\|up\|left\|right]` | Scroll |
|
|
@@ -1,21 +1,116 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-profiler
|
|
3
|
-
description: Profile a running app's CPU,
|
|
3
|
+
description: Profile a running app's frame timing, input latency, JS CPU, native CPU, memory and React renders with the conductor CLI, and read crash reports. Use when investigating jank or sluggishness, slow navigation, input lag on TV remotes, memory growth or leaks, GC pauses, excessive React re-renders, or when an app has crashed and you need the crash report.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — profiling & crashes
|
|
7
7
|
|
|
8
8
|
Measure a running app's performance and inspect crashes.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Pick the right instrument
|
|
11
|
+
|
|
12
|
+
| Symptom the user reports | Measure with |
|
|
13
|
+
|---|---|
|
|
14
|
+
| "Scrolling/navigation is janky" | `profile frames` (Android) — objective jank rate and per-phase attribution |
|
|
15
|
+
| "I press a key and it takes a moment" | `press-key <key> --measure --repeat 20` |
|
|
16
|
+
| "It's slow, where does the time go?" (JS) | `profile js record --duration 10` |
|
|
17
|
+
| "It's slow, where does the time go?" (native) | `profile cpu --duration 10 --report` |
|
|
18
|
+
| "Which component re-renders on every input?" | `profile react start` → interact → `profile react stop` |
|
|
19
|
+
| "Memory grows / it stutters periodically" | `profile memory --track 30` |
|
|
20
|
+
|
|
21
|
+
`profile frames`, `profile cpu` and `profile memory` work on **release builds**
|
|
22
|
+
and on real hardware. `profile js` and `profile react` attach over Metro, so
|
|
23
|
+
they need a dev/profiling build.
|
|
24
|
+
|
|
25
|
+
## Frame timing & jank (Android, incl. Fire TV / Android TV)
|
|
26
|
+
|
|
27
|
+
| Command | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `conductor profile frames reset [<appId>]` | Zero the gfxinfo counters |
|
|
30
|
+
| `conductor profile frames report [<appId>]` | Jank rate, p50–p99 frame times, per-phase breakdown |
|
|
31
|
+
| `conductor profile frames report --track <s> [--interval <ms>]` | Reset, sample for N seconds, then report |
|
|
32
|
+
| `conductor profile frames report --save-baseline <name>` / `--diff <name>` / `--baselines` | Save and compare runs |
|
|
33
|
+
|
|
34
|
+
Reads `dumpsys gfxinfo <pkg> framestats`, so it needs no instrumentation.
|
|
35
|
+
|
|
36
|
+
**On TV, be deliberate about who is driving.** `press-key` injects with
|
|
37
|
+
`adb shell input keyevent`, which spawns a JVM on the device (~713ms) beside
|
|
38
|
+
the frames you are measuring. Mobile's answer — fling and measure the momentum
|
|
39
|
+
scroll — does not exist on TV, where focus moves one step per press and stops.
|
|
40
|
+
So a navigation capture is always partly a measurement of the harness. Idle
|
|
41
|
+
screens, load settles, cold start and self-running animations are clean; for
|
|
42
|
+
navigation, prefer asking a human to drive the physical remote during the
|
|
43
|
+
window (the command announces the window on stderr for exactly this), and treat
|
|
44
|
+
a divergence between human-driven and automated numbers as the harness.
|
|
45
|
+
|
|
46
|
+
Not available on `vega` — but a **physical Fire TV Stick runs Fire OS
|
|
47
|
+
(Android)** over adb and works fine.
|
|
48
|
+
|
|
49
|
+
Attribute jank by comparing each phase's p95 against its p50: `vsyncDelay` means
|
|
50
|
+
the UI thread was blocked elsewhere, `traversal` means measure/layout,
|
|
51
|
+
`draw` means display-list recording, `issueDraw` means render-thread/GPU.
|
|
52
|
+
|
|
53
|
+
## Input latency
|
|
54
|
+
|
|
55
|
+
| Command | Purpose |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `conductor press-key <key> --measure` | Time the app's response to the press |
|
|
58
|
+
| `conductor press-key <key> --measure --repeat <n>` | Take n samples and report a distribution |
|
|
59
|
+
| `--sequence <k1,k2>` | Cycle keys so repeats oscillate instead of drifting across the UI |
|
|
60
|
+
| `--timeout <ms>` / `--poll-interval <ms>` / `--settle <ms>` | Give-up time; poll delay; render time to allow |
|
|
61
|
+
|
|
62
|
+
Read `outcome` per sample before the aggregates. `moved` is a measurement;
|
|
63
|
+
`unchanged` means focus queries kept working and the app simply declined to move
|
|
64
|
+
(a rail edge, or a key this screen ignores) and is **not** a hang;
|
|
65
|
+
`query-failed` is the one that suggests something is wedged.
|
|
66
|
+
|
|
67
|
+
`--repeat` is not repeated measurement of one event — pressing Right twenty
|
|
68
|
+
times walks twenty different transitions. Read `byTransition` before the
|
|
69
|
+
aggregate, or use `--sequence` to oscillate between two positions.
|
|
70
|
+
|
|
71
|
+
On Android `pressToFrame` is derived from device-side clocks and is the figure
|
|
72
|
+
to trust. `focusChange` is bounded by how long one hierarchy dump takes, and a
|
|
73
|
+
`round-trip-bound` note fires when that floor dominates. gfxinfo's own
|
|
74
|
+
`inputLatency` appears only when the device populates `NewestInputEvent` — it
|
|
75
|
+
does on a Fire TV Stick, it never does on an NVIDIA SHIELD — so treat it as a
|
|
76
|
+
bonus, never as something whose absence means no input occurred.
|
|
77
|
+
|
|
78
|
+
## JS CPU (Hermes sampling profiler)
|
|
79
|
+
|
|
80
|
+
| Command | Purpose |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `conductor profile js record --duration <s> [--top n] [--out <path>]` | Sample, then rank functions by self time with `file:line` |
|
|
83
|
+
| `conductor profile js start` / `conductor profile js stop [--top n]` | Same, bracketing a flow you drive in between |
|
|
84
|
+
|
|
85
|
+
Writes a raw `.cpuprofile` you can load in Chrome DevTools (Performance → Load
|
|
86
|
+
profile) or convert with `npx hermes-profile-transformer`.
|
|
87
|
+
|
|
88
|
+
## Native CPU
|
|
11
89
|
|
|
12
90
|
| Command | Purpose |
|
|
13
91
|
|---|---|
|
|
14
|
-
| `conductor profile cpu --duration <s> [--out <path>]` | Record a
|
|
15
|
-
| `conductor profile
|
|
16
|
-
| `conductor profile react start` / `profile react stop [--top N]` | Install a React commit-profiler hook, then summarize captured commits |
|
|
92
|
+
| `conductor profile cpu --duration <s> [--out <path>]` | Record a trace (iOS: xctrace, Android: simpleperf) |
|
|
93
|
+
| `conductor profile cpu --duration <s> --report [--top n]` | Android: also return a ranked symbol table |
|
|
17
94
|
|
|
18
|
-
|
|
95
|
+
Without `--report` on Android you get a binary `perf.data` you cannot read —
|
|
96
|
+
pass `--report` when you need the answer rather than the artefact.
|
|
97
|
+
|
|
98
|
+
## React renders
|
|
99
|
+
|
|
100
|
+
| Command | Purpose |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `conductor profile react start [--max-commits n] [--max-components n]` | Install the commit-profiler hook |
|
|
103
|
+
| `conductor profile react stop [--top n] [--timeline] [--json]` | Stop and rank components by self time |
|
|
104
|
+
|
|
105
|
+
Sort key is `selfMs`, which is additive across components; `totalMs` is
|
|
106
|
+
subtree-inclusive and double-counts parents. `--json` includes the per-commit
|
|
107
|
+
timeline (timestamps + durations) so you can line a jank spike up with an input;
|
|
108
|
+
`--timeline` adds per-commit component detail. If either buffer overflows the
|
|
109
|
+
output says `truncated: true` with the dropped counts — raise the limits rather
|
|
110
|
+
than trusting the tail. On a release build it fails with a clear message instead
|
|
111
|
+
of reporting zeroes.
|
|
112
|
+
|
|
113
|
+
## Memory & GC
|
|
19
114
|
|
|
20
115
|
| Command | Purpose |
|
|
21
116
|
|---|---|
|
|
@@ -24,10 +119,17 @@ Measure a running app's performance and inspect crashes.
|
|
|
24
119
|
| `conductor memory --leaks` | Run leak detection (iOS only; slow, can pause the app) |
|
|
25
120
|
| `conductor memory --save <name>` / `--diff <name>` / `--diff <name> --vs <other>` | Snapshot and diff memory reports |
|
|
26
121
|
| `conductor memory --filter <regex>` / `--growth-only` / `--top <n>` | Narrow object/class tables (great for leak-hunting) |
|
|
122
|
+
| `conductor profile memory --track <s> [--interval <ms>]` | Sample over a window; on Android also reports heap growth and ART GC pause counts/durations |
|
|
27
123
|
|
|
28
124
|
Typical leak hunt: `memory --save before`, exercise the screen, then
|
|
29
125
|
`memory --diff before --growth-only`.
|
|
30
126
|
|
|
127
|
+
## Repeatable benchmarking
|
|
128
|
+
|
|
129
|
+
`conductor run-flow <file> --benchmark --repeat <n> --json` runs the flow n
|
|
130
|
+
times and reports per-command p50/p90/stddev. Use it on TV, where single-run
|
|
131
|
+
variance is large enough to swamp the effect you're looking for.
|
|
132
|
+
|
|
31
133
|
## Crashes
|
|
32
134
|
|
|
33
135
|
| Command | Purpose |
|
|
@@ -39,4 +141,10 @@ Typical leak hunt: `memory --save before`, exercise the screen, then
|
|
|
39
141
|
## Tips
|
|
40
142
|
|
|
41
143
|
- Add `--json` to parse reports programmatically.
|
|
42
|
-
-
|
|
144
|
+
- `--port` is auto-detected from the device for Metro-backed commands; pass it
|
|
145
|
+
explicitly only if that fails.
|
|
146
|
+
- These commands can be slow or pause the app — scope them with `--duration` /
|
|
147
|
+
`--track` and avoid leaving `crashes tail` running.
|
|
148
|
+
- An Android TV emulator is far faster than a Fire TV Stick. Trust frame timing
|
|
149
|
+
only from real hardware; emulators are fine for counts (React commits, GC
|
|
150
|
+
collections, JS self-time ranking).
|