@houwert/conductor 0.29.2 → 0.29.3
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.
|
@@ -647,8 +647,10 @@ async function profileFramesReport(opts, sessionName, frameOpts) {
|
|
|
647
647
|
if (frameOpts.trackSec !== undefined && repeat === 1) {
|
|
648
648
|
notes.push({
|
|
649
649
|
code: 'single-window',
|
|
650
|
-
message: 'One window carries no run-to-run variance, so
|
|
651
|
-
'
|
|
650
|
+
message: 'One window carries no run-to-run variance, so nothing here can be called a change. ' +
|
|
651
|
+
'Measured on a Fire TV Stick, five identical captures of the same screen ranged ' +
|
|
652
|
+
'37.8-72.9% janky and 6.2-13.0ms issueDraw p50 — wide enough to invent a regression ' +
|
|
653
|
+
'or hide one. Treat --repeat 5 as the default on TV, not an option.',
|
|
652
654
|
});
|
|
653
655
|
}
|
|
654
656
|
const report = {
|
package/dist/commands/profile.js
CHANGED
|
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REACT_PROFILER_STOP = exports.REACT_PROFILER_READ = exports.REACT_PROFILER_INSTALL = exports.HELP = void 0;
|
|
7
|
+
exports.isUnsymbolised = isUnsymbolised;
|
|
8
|
+
exports.rollupByDso = rollupByDso;
|
|
9
|
+
exports.unsymbolisedPercent = unsymbolisedPercent;
|
|
7
10
|
exports.parseSimpleperfReport = parseSimpleperfReport;
|
|
8
11
|
exports.profileCpu = profileCpu;
|
|
9
12
|
exports.heapGrowth = heapGrowth;
|
|
@@ -37,9 +40,80 @@ const output_js_1 = require("../output.js");
|
|
|
37
40
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
38
41
|
const sdk_js_1 = require("../android/sdk.js");
|
|
39
42
|
const runner_js_1 = require("../runner.js");
|
|
43
|
+
const device_js_1 = require("../android/device.js");
|
|
40
44
|
const memory_js_1 = require("./memory.js");
|
|
41
45
|
const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
|
|
42
46
|
const profile_gc_js_1 = require("./profile-gc.js");
|
|
47
|
+
/** simpleperf renders an unresolved address as `libfoo.so[+1a62f8]`. */
|
|
48
|
+
function isUnsymbolised(symbol) {
|
|
49
|
+
return /\[\+[0-9a-fx]+\]\s*$/i.test(symbol);
|
|
50
|
+
}
|
|
51
|
+
function shortDso(dso) {
|
|
52
|
+
return dso.includes('/') ? dso.slice(dso.lastIndexOf('/') + 1) : dso;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sum a flat symbol table by library.
|
|
56
|
+
*
|
|
57
|
+
* A stripped app library shows up as a dozen `libfoo.so[+offset]` rows that a
|
|
58
|
+
* reader cannot tell apart from a dozen unrelated hotspots. Rolled up, one
|
|
59
|
+
* library dominating is immediately visible — which is the actual finding when
|
|
60
|
+
* a profile has no single hot symbol.
|
|
61
|
+
*/
|
|
62
|
+
function rollupByDso(entries) {
|
|
63
|
+
const byDso = new Map();
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
const key = shortDso(e.dso);
|
|
66
|
+
const slot = byDso.get(key) ?? { percent: 0, symbols: 0, unsym: 0 };
|
|
67
|
+
slot.percent += e.percent;
|
|
68
|
+
slot.symbols++;
|
|
69
|
+
if (isUnsymbolised(e.symbol))
|
|
70
|
+
slot.unsym++;
|
|
71
|
+
byDso.set(key, slot);
|
|
72
|
+
}
|
|
73
|
+
return [...byDso.entries()]
|
|
74
|
+
.map(([dso, v]) => ({
|
|
75
|
+
dso,
|
|
76
|
+
percent: Math.round(v.percent * 100) / 100,
|
|
77
|
+
symbols: v.symbols,
|
|
78
|
+
unsymbolised: v.unsym === v.symbols,
|
|
79
|
+
}))
|
|
80
|
+
.sort((a, b) => b.percent - a.percent);
|
|
81
|
+
}
|
|
82
|
+
/** Share of the sampled overhead that resolved to no function name. */
|
|
83
|
+
function unsymbolisedPercent(entries) {
|
|
84
|
+
const total = entries.reduce((a, e) => a + e.percent, 0);
|
|
85
|
+
if (total <= 0)
|
|
86
|
+
return 0;
|
|
87
|
+
const unsym = entries.filter((e) => isUnsymbolised(e.symbol)).reduce((a, e) => a + e.percent, 0);
|
|
88
|
+
return Math.round((unsym / total) * 10000) / 100;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turn a simpleperf failure into something actionable.
|
|
92
|
+
*
|
|
93
|
+
* simpleperf needs the target to be `android:debuggable` or to declare
|
|
94
|
+
* `<profileable android:shell="true"/>`. A stock release APK is neither, and
|
|
95
|
+
* the resulting error says only `exited with 1`.
|
|
96
|
+
*/
|
|
97
|
+
async function explainSimpleperfFailure(deviceId, appId, stderr) {
|
|
98
|
+
// Drop simpleperf's PMU probing chatter; it is present on every run.
|
|
99
|
+
const meaningful = stderr
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
.filter((l) => l.trim() && !/cannot read event type|Failed to read event type/i.test(l))
|
|
102
|
+
.slice(-4)
|
|
103
|
+
.join('\n');
|
|
104
|
+
if (appId) {
|
|
105
|
+
const pkg = await (0, device_js_1.adbShell)(deviceId, ['dumpsys', 'package', appId]);
|
|
106
|
+
if (pkg.success && /^\s*flags=\[/m.test(pkg.stdout) && !/\bDEBUGGABLE\b/.test(pkg.stdout)) {
|
|
107
|
+
return (`simpleperf cannot profile ${appId}: the installed APK is neither debuggable nor ` +
|
|
108
|
+
`profileable.\nsimpleperf needs android:debuggable, or ` +
|
|
109
|
+
`\`<profileable android:shell="true"/>\` inside <application> — the latter is a ` +
|
|
110
|
+
`one-line manifest change that keeps the build a release build and is the right fix ` +
|
|
111
|
+
`for a perf build.\n` +
|
|
112
|
+
(meaningful ? `simpleperf said:\n${meaningful}` : ''));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return `simpleperf record failed.\n${meaningful || '(no diagnostic output)'}`;
|
|
116
|
+
}
|
|
43
117
|
/**
|
|
44
118
|
* Parse `simpleperf report --sort dso,symbol`. The header row is located by its
|
|
45
119
|
* `Overhead` column rather than by line number, since simpleperf prefixes the
|
|
@@ -105,11 +179,13 @@ async function recordAndroidCpu(deviceId, appId, durationSec, out, report) {
|
|
|
105
179
|
else {
|
|
106
180
|
recordArgs.push('-a');
|
|
107
181
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
182
|
+
// Capture rather than inherit: simpleperf prints a wall of `cannot read event
|
|
183
|
+
// type` lines while probing PMU support, and those are not the error. Letting
|
|
184
|
+
// them through buries whatever actually went wrong.
|
|
185
|
+
const rec = await (0, runner_js_1.spawnCommand)(adb, recordArgs, { env });
|
|
186
|
+
if (!rec.success) {
|
|
187
|
+
throw new Error(await explainSimpleperfFailure(deviceId, appId, rec.stderr));
|
|
188
|
+
}
|
|
113
189
|
// Symbolize on-device before pulling: /system/bin/simpleperf resolves against
|
|
114
190
|
// the libraries actually loaded there, which a host-side report cannot do
|
|
115
191
|
// without a matching symfs.
|
|
@@ -182,17 +258,41 @@ async function profileCpu(opts, sessionName, profileOpts) {
|
|
|
182
258
|
(0, output_js_1.printError)(`profile cpu is not supported on platform ${platform ?? '(unknown)'}`, opts);
|
|
183
259
|
return 1;
|
|
184
260
|
}
|
|
261
|
+
const byDso = entries ? rollupByDso(entries) : undefined;
|
|
262
|
+
const unsymPercent = entries ? unsymbolisedPercent(entries) : undefined;
|
|
185
263
|
if (opts.json) {
|
|
186
|
-
(0, output_js_1.printData)({
|
|
264
|
+
(0, output_js_1.printData)({
|
|
265
|
+
out,
|
|
266
|
+
durationSec: profileOpts.durationSec,
|
|
267
|
+
platform,
|
|
268
|
+
symbols: entries,
|
|
269
|
+
byDso,
|
|
270
|
+
unsymbolisedPercent: unsymPercent,
|
|
271
|
+
}, opts);
|
|
187
272
|
}
|
|
188
273
|
else {
|
|
189
274
|
(0, output_js_1.printSuccess)(`profile cpu — recorded ${profileOpts.durationSec}s → ${out}`, opts);
|
|
275
|
+
if (byDso && byDso.length > 0) {
|
|
276
|
+
// Lead with the rollup: a flat profile has no hot symbol, and the real
|
|
277
|
+
// finding is usually which library the samples are spread across.
|
|
278
|
+
console.log(`\n by library`);
|
|
279
|
+
for (const d of byDso.slice(0, 10)) {
|
|
280
|
+
console.log(` ${`${d.percent}%`.padStart(9)} ${d.dso} (${d.symbols} symbol${d.symbols === 1 ? '' : 's'}` +
|
|
281
|
+
`${d.unsymbolised ? ', unsymbolised' : ''})`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
190
284
|
if (entries) {
|
|
191
|
-
console.log(
|
|
285
|
+
console.log(`\n ${'overhead'.padStart(9)} symbol`);
|
|
192
286
|
for (const e of entries) {
|
|
193
287
|
console.log(` ${`${e.percent}%`.padStart(9)} ${e.symbol} [${e.dso}]`);
|
|
194
288
|
}
|
|
195
289
|
}
|
|
290
|
+
if (unsymPercent !== undefined && unsymPercent > 10) {
|
|
291
|
+
console.log(`\n note: ${unsymPercent}% of sampled overhead resolved to a raw address rather ` +
|
|
292
|
+
`than a function — those libraries are stripped. Read the by-library rollup above ` +
|
|
293
|
+
`instead of the symbol table; a dozen \`lib.so[+offset]\` rows are one library, ` +
|
|
294
|
+
`not a dozen findings.`);
|
|
295
|
+
}
|
|
196
296
|
}
|
|
197
297
|
return 0;
|
|
198
298
|
}
|
package/package.json
CHANGED
|
@@ -18,9 +18,14 @@ Measure a running app's performance and inspect crashes.
|
|
|
18
18
|
| "Which component re-renders on every input?" | `profile react start` → interact → `profile react stop` |
|
|
19
19
|
| "Memory grows / it stutters periodically" | `profile memory --track 30` |
|
|
20
20
|
|
|
21
|
-
`profile frames
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
Only `profile frames` and `profile memory` work on a **stock release build**.
|
|
22
|
+
`profile cpu` needs `android:debuggable` or `<profileable android:shell="true"/>`
|
|
23
|
+
in the manifest; `profile js` and `profile react` attach over Metro, and release
|
|
24
|
+
Hermes ships without the inspector so there is no target to attach to.
|
|
25
|
+
|
|
26
|
+
On TV, run `profile frames --track` with `--repeat 5`. Identical captures of one
|
|
27
|
+
screen have been measured spanning 37.8-72.9% janky — a single window can invent
|
|
28
|
+
a regression or hide one, and cannot support a `--diff`.
|
|
24
29
|
|
|
25
30
|
## Frame timing & jank (Android, incl. Fire TV / Android TV)
|
|
26
31
|
|