@houwert/conductor 0.27.2 → 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 -47
- package/dist/android/device.js +94 -0
- package/dist/commands/debug.js +6 -6
- package/dist/commands/device-pool.js +24 -5
- package/dist/commands/focused.js +27 -0
- package/dist/commands/input-latency.js +348 -0
- package/dist/commands/list-apps.js +8 -1
- package/dist/commands/list-devices.js +30 -1
- 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/bootstrap.js +6 -1
- package/dist/drivers/flow-runner.js +32 -2
- package/dist/drivers/metro-cdp.js +28 -3
- package/dist/index.js +81 -6
- 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-device-setup/SKILL.md +19 -2
- package/skills/conductor-profiler/SKILL.md +115 -7
|
@@ -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) {
|
|
@@ -246,6 +246,11 @@ function findPackageRoot() {
|
|
|
246
246
|
}
|
|
247
247
|
const DRIVERS_CACHE_ROOT = path_1.default.join(os_1.default.homedir(), '.conductor', 'drivers');
|
|
248
248
|
const DRIVERS_DOWNLOAD_BASE = 'https://github.com/DouweBos/conductor/releases/download';
|
|
249
|
+
// Must match the tag `.github/workflows/release.yml` pushes — the two are only
|
|
250
|
+
// ever in sync because a release builds this file and cuts its tag from one
|
|
251
|
+
// commit. Releases up to v0.27.2 predate the prefix and keep their `v<version>`
|
|
252
|
+
// tags, which is fine — each published version only ever fetches its own tag.
|
|
253
|
+
const DRIVERS_TAG_PREFIX = 'cli-v';
|
|
249
254
|
const DRIVERS_LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes (download can be slow)
|
|
250
255
|
const DRIVERS_LOCK_POLL_MS = 500;
|
|
251
256
|
let _driversDirPromise = null;
|
|
@@ -331,7 +336,7 @@ async function ensureDriversCache(pkgRoot) {
|
|
|
331
336
|
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
332
337
|
fs_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
333
338
|
const tarball = path_1.default.join(tmpDir, 'drivers.tar.gz');
|
|
334
|
-
const url = `${DRIVERS_DOWNLOAD_BASE}
|
|
339
|
+
const url = `${DRIVERS_DOWNLOAD_BASE}/${DRIVERS_TAG_PREFIX}${version}/drivers.tar.gz`;
|
|
335
340
|
(0, verbose_js_1.log)(`Downloading conductor drivers v${version} from ${url}...`);
|
|
336
341
|
try {
|
|
337
342
|
await downloadToFile(url, tarball);
|
|
@@ -29,6 +29,23 @@ const utils_js_1 = require("../utils.js");
|
|
|
29
29
|
function fmtMs(ms) {
|
|
30
30
|
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
31
31
|
}
|
|
32
|
+
// tvOS remote key names (uppercased, as flow pressKey normalises them) → IOSDriver
|
|
33
|
+
// button values. Without this a flow's `pressKey: "Remote Dpad Up"` would fall
|
|
34
|
+
// through to the software-keyboard path and silently do nothing — tvOS has no
|
|
35
|
+
// keyboard. Mirrors TVOS_REMOTE_BUTTONS in commands/press-key.ts.
|
|
36
|
+
const TVOS_FLOW_BUTTONS = {
|
|
37
|
+
'REMOTE DPAD UP': 'up',
|
|
38
|
+
'REMOTE DPAD DOWN': 'down',
|
|
39
|
+
'REMOTE DPAD LEFT': 'left',
|
|
40
|
+
'REMOTE DPAD RIGHT': 'right',
|
|
41
|
+
'REMOTE DPAD CENTER': 'select',
|
|
42
|
+
'REMOTE MENU': 'menu',
|
|
43
|
+
'REMOTE MEDIA PLAY PAUSE': 'playPause',
|
|
44
|
+
ENTER: 'select',
|
|
45
|
+
RETURN: 'select',
|
|
46
|
+
ESCAPE: 'menu',
|
|
47
|
+
BACK: 'menu',
|
|
48
|
+
};
|
|
32
49
|
// vega (Amazon Fire TV) remote key names → VegaDriver button values, for flow pressKey.
|
|
33
50
|
const VEGA_FLOW_BUTTONS = {
|
|
34
51
|
BACK: 'back',
|
|
@@ -375,6 +392,7 @@ async function executeFlow(flow, driver, opts = {}) {
|
|
|
375
392
|
output: opts.output ?? {},
|
|
376
393
|
depth: opts.depth ?? 0,
|
|
377
394
|
benchmark: opts.benchmark,
|
|
395
|
+
benchmarkSink: opts.benchmarkSink,
|
|
378
396
|
};
|
|
379
397
|
const flowStart = opts.benchmark ? perf_hooks_1.performance.now() : 0;
|
|
380
398
|
if (flow.onFlowStart?.length) {
|
|
@@ -498,9 +516,11 @@ async function executeCommand(cmd, driver, opts) {
|
|
|
498
516
|
else {
|
|
499
517
|
process.stdout.write(`${indent}→ ${label} ... `);
|
|
500
518
|
}
|
|
501
|
-
const
|
|
519
|
+
const timing = opts.benchmark || opts.benchmarkSink;
|
|
520
|
+
const t0 = timing ? perf_hooks_1.performance.now() : 0;
|
|
502
521
|
try {
|
|
503
522
|
await executeCommandBody(key, resolvedVal, driver, opts);
|
|
523
|
+
opts.benchmarkSink?.({ label, depth: opts.depth, ms: perf_hooks_1.performance.now() - t0, ok: true });
|
|
504
524
|
const elapsed = opts.benchmark ? ` (${fmtMs(perf_hooks_1.performance.now() - t0)})` : '';
|
|
505
525
|
if (!isCompound) {
|
|
506
526
|
console.log(`ok${elapsed}`);
|
|
@@ -511,6 +531,7 @@ async function executeCommand(cmd, driver, opts) {
|
|
|
511
531
|
}
|
|
512
532
|
catch (err) {
|
|
513
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 });
|
|
514
535
|
const elapsed = opts.benchmark ? ` (${fmtMs(perf_hooks_1.performance.now() - t0)})` : '';
|
|
515
536
|
if (optional) {
|
|
516
537
|
// For compound commands the sub-command already printed its warning/failure
|
|
@@ -1055,13 +1076,20 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1055
1076
|
case 'pressKey': {
|
|
1056
1077
|
const keyName = val.toUpperCase();
|
|
1057
1078
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
1079
|
+
const tvosButton = driver.platform === 'tvos' ? TVOS_FLOW_BUTTONS[keyName] : undefined;
|
|
1058
1080
|
// Home and Lock are hardware buttons on iOS, not software keys
|
|
1059
|
-
if (
|
|
1081
|
+
if (tvosButton) {
|
|
1082
|
+
await driver.pressButton(tvosButton);
|
|
1083
|
+
}
|
|
1084
|
+
else if (keyName === 'HOME') {
|
|
1060
1085
|
await driver.pressButton('home');
|
|
1061
1086
|
}
|
|
1062
1087
|
else if (keyName === 'LOCK' || keyName === 'POWER') {
|
|
1063
1088
|
await driver.pressButton('lock');
|
|
1064
1089
|
}
|
|
1090
|
+
else if (driver.platform === 'tvos') {
|
|
1091
|
+
throw new Error(`pressKey: key "${val}" is not supported on tvOS`);
|
|
1092
|
+
}
|
|
1065
1093
|
else {
|
|
1066
1094
|
await driver.pressKey(mapIosKey(keyName));
|
|
1067
1095
|
}
|
|
@@ -1249,6 +1277,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1249
1277
|
output: opts.output,
|
|
1250
1278
|
depth: childDepth,
|
|
1251
1279
|
benchmark: opts.benchmark,
|
|
1280
|
+
benchmarkSink: opts.benchmarkSink,
|
|
1252
1281
|
});
|
|
1253
1282
|
}
|
|
1254
1283
|
else {
|
|
@@ -1269,6 +1298,7 @@ async function executeCommandBody(key, val, driver, opts) {
|
|
|
1269
1298
|
output: opts.output,
|
|
1270
1299
|
depth: childDepth,
|
|
1271
1300
|
benchmark: opts.benchmark,
|
|
1301
|
+
benchmarkSink: opts.benchmarkSink,
|
|
1272
1302
|
});
|
|
1273
1303
|
}
|
|
1274
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");
|
|
@@ -223,6 +225,10 @@ async function main() {
|
|
|
223
225
|
'force',
|
|
224
226
|
'yes',
|
|
225
227
|
'update',
|
|
228
|
+
'measure',
|
|
229
|
+
'report',
|
|
230
|
+
'timeline',
|
|
231
|
+
'baselines',
|
|
226
232
|
],
|
|
227
233
|
string: [
|
|
228
234
|
'device',
|
|
@@ -254,6 +260,9 @@ async function main() {
|
|
|
254
260
|
'source',
|
|
255
261
|
'level',
|
|
256
262
|
'save',
|
|
263
|
+
'save-baseline',
|
|
264
|
+
'sequence',
|
|
265
|
+
'settle',
|
|
257
266
|
'diff',
|
|
258
267
|
'vs',
|
|
259
268
|
'top',
|
|
@@ -655,6 +664,15 @@ async function main() {
|
|
|
655
664
|
exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName, {
|
|
656
665
|
longPress: argv['long-press'],
|
|
657
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'],
|
|
658
676
|
});
|
|
659
677
|
break;
|
|
660
678
|
}
|
|
@@ -901,7 +919,7 @@ async function main() {
|
|
|
901
919
|
const rawEnv = argv['env'];
|
|
902
920
|
const envPairs = Array.isArray(rawEnv) ? rawEnv : rawEnv ? [rawEnv] : [];
|
|
903
921
|
const flowEnv = Object.fromEntries(envPairs.map((e) => e.split('=', 2)));
|
|
904
|
-
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);
|
|
905
923
|
break;
|
|
906
924
|
}
|
|
907
925
|
case 'run-flow-inline': {
|
|
@@ -942,7 +960,8 @@ async function main() {
|
|
|
942
960
|
const release = argv['release'];
|
|
943
961
|
const releaseId = typeof argv['release'] === 'string' ? argv['release'] : rest[0];
|
|
944
962
|
const action = acquire ? 'acquire' : release || releaseId ? 'release' : 'list';
|
|
945
|
-
|
|
963
|
+
const owner = argv['owner'] ? Number(argv['owner']) : undefined;
|
|
964
|
+
exitCode = await (0, device_pool_js_1.devicePool)(action, releaseId, opts, argv['device'], owner);
|
|
946
965
|
break;
|
|
947
966
|
}
|
|
948
967
|
case 'run-sequence': {
|
|
@@ -979,12 +998,15 @@ async function main() {
|
|
|
979
998
|
const sub = (rest[0] ?? '').toLowerCase();
|
|
980
999
|
const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
|
|
981
1000
|
const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
|
|
1001
|
+
const top = argv['top'] !== undefined ? Number(argv['top']) : undefined;
|
|
982
1002
|
if (sub === 'cpu') {
|
|
983
1003
|
const durationSec = argv['duration'] !== undefined ? Number(argv['duration']) : 10;
|
|
984
1004
|
exitCode = await (0, profile_js_1.profileCpu)(opts, sessionName, {
|
|
985
1005
|
durationSec,
|
|
986
1006
|
out: argv['out'],
|
|
987
1007
|
appId: rest[1],
|
|
1008
|
+
report: argv['report'],
|
|
1009
|
+
top,
|
|
988
1010
|
});
|
|
989
1011
|
}
|
|
990
1012
|
else if (sub === 'memory') {
|
|
@@ -996,14 +1018,67 @@ async function main() {
|
|
|
996
1018
|
appId: rest[1],
|
|
997
1019
|
});
|
|
998
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
|
+
}
|
|
999
1068
|
else if (sub === 'react') {
|
|
1000
1069
|
const sub2 = (rest[1] ?? '').toLowerCase();
|
|
1001
|
-
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
|
+
};
|
|
1002
1077
|
if (sub2 === 'start') {
|
|
1003
|
-
exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName,
|
|
1078
|
+
exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName, reactOpts);
|
|
1004
1079
|
}
|
|
1005
1080
|
else if (sub2 === 'stop') {
|
|
1006
|
-
exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName,
|
|
1081
|
+
exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName, reactOpts, top ?? 20);
|
|
1007
1082
|
}
|
|
1008
1083
|
else {
|
|
1009
1084
|
console.error('Usage: conductor profile react <start|stop>');
|
|
@@ -1011,7 +1086,7 @@ async function main() {
|
|
|
1011
1086
|
}
|
|
1012
1087
|
}
|
|
1013
1088
|
else {
|
|
1014
|
-
console.error('Usage: conductor profile <cpu|memory|react> [args]');
|
|
1089
|
+
console.error('Usage: conductor profile <cpu|memory|frames|js|react> [args]');
|
|
1015
1090
|
exitCode = 1;
|
|
1016
1091
|
}
|
|
1017
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 |
|
|
@@ -14,7 +14,7 @@ nothing is booted yet.
|
|
|
14
14
|
conductor workspace info # detected project type, bundle IDs, devices, Metro port — best first call
|
|
15
15
|
conductor list-devices # booted + available devices
|
|
16
16
|
conductor foreground-app # bundle id of the app currently in front
|
|
17
|
-
conductor list-apps # installed app ids / package names
|
|
17
|
+
conductor list-apps # installed app ids / package names (--json adds appNames on iOS/tvOS)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Devices
|
|
@@ -100,11 +100,28 @@ Parallel agents each get their own `--session <name>` so they don't collide.
|
|
|
100
100
|
| `conductor daemon-status` | Show daemon status |
|
|
101
101
|
| `conductor daemon-stop [--all]` | Stop this session's daemon (`--all` = every session) |
|
|
102
102
|
| `conductor device-pool --list` | List devices + pool status |
|
|
103
|
-
| `conductor device-pool --acquire` | Claim a free device (prints id)
|
|
103
|
+
| `conductor device-pool --acquire` | Claim a free device (prints id); `--device <id>` claims that one, `--owner <pid>` holds the claim |
|
|
104
104
|
| `conductor device-pool --release <id>` | Release a device back to the pool |
|
|
105
105
|
|
|
106
106
|
Don't leave a daemon running when you're done — `daemon-stop` it.
|
|
107
107
|
|
|
108
|
+
### Reserving a device
|
|
109
|
+
|
|
110
|
+
Claim a device before driving it when other agents share the machine, so nobody
|
|
111
|
+
taps through your test half-way. A claim belongs to a **process**: conductor
|
|
112
|
+
frees any claim whose owner has exited, and the CLI exits immediately, so
|
|
113
|
+
`--acquire` on its own reserves nothing. Pass the PID that should hold it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
conductor device-pool --acquire --device <id> --owner $$ --json # claim it
|
|
117
|
+
conductor device-pool --list --json # who holds what
|
|
118
|
+
conductor device-pool --release <id> # give it back
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Acquiring a device someone else holds fails rather than stealing it. Always
|
|
122
|
+
release when you're done — a crash releases it for you, an abandoned shell
|
|
123
|
+
doesn't.
|
|
124
|
+
|
|
108
125
|
## Tips
|
|
109
126
|
|
|
110
127
|
- `--device <id>` / `--device-name <name>` targets a device; `--platform` scopes by platform.
|