@houwert/conductor 0.29.1 → 0.29.2

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.
@@ -7,6 +7,7 @@ exports.REACT_PROFILER_STOP = exports.REACT_PROFILER_READ = exports.REACT_PROFIL
7
7
  exports.parseSimpleperfReport = parseSimpleperfReport;
8
8
  exports.profileCpu = profileCpu;
9
9
  exports.heapGrowth = heapGrowth;
10
+ exports.inferCollections = inferCollections;
10
11
  exports.profileMemory = profileMemory;
11
12
  exports.profileReactStart = profileReactStart;
12
13
  exports.profileReactStop = profileReactStop;
@@ -228,6 +229,37 @@ function heapGrowth(samples) {
228
229
  function mb(bytes) {
229
230
  return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
230
231
  }
232
+ /**
233
+ * Infer collections from the heap series rather than from logcat.
234
+ *
235
+ * A heap that shrinks between two samples was collected between them — there is
236
+ * no other mechanism. This matters because ART's GC logging is not reliably on:
237
+ * measured on a Fire TV Stick (Fire OS, API 30), a window in which the Java heap
238
+ * fell 11.8MB produced no `art:I` lines at all. Absence of logged collections is
239
+ * therefore not absence of collection, and the deltas are the sounder signal.
240
+ */
241
+ function inferCollections(samples, series = 'javaHeapBytes') {
242
+ const values = [];
243
+ for (const s of samples) {
244
+ const app = s.data?.app;
245
+ const v = app?.[series];
246
+ if (typeof v === 'number')
247
+ values.push(v);
248
+ }
249
+ if (values.length < 2)
250
+ return undefined;
251
+ let collections = 0;
252
+ let reclaimedBytes = 0;
253
+ for (let i = 1; i < values.length; i++) {
254
+ const drop = values[i - 1] - values[i];
255
+ // Ignore sampling jitter; only count drops worth a collection.
256
+ if (drop > 256 * 1024) {
257
+ collections++;
258
+ reclaimedBytes += drop;
259
+ }
260
+ }
261
+ return { collections, reclaimedBytes, series };
262
+ }
231
263
  async function profileMemory(opts, sessionName, profileOpts) {
232
264
  const samples = [];
233
265
  const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined);
@@ -257,6 +289,7 @@ async function profileMemory(opts, sessionName, profileOpts) {
257
289
  }
258
290
  });
259
291
  const growth = heapGrowth(parsed);
292
+ const inferredGc = isAndroid ? inferCollections(parsed) : undefined;
260
293
  let gc;
261
294
  if (isAndroid) {
262
295
  const events = await (0, profile_gc_js_1.collectGcSince)(sessionName, gcSince);
@@ -264,7 +297,7 @@ async function profileMemory(opts, sessionName, profileOpts) {
264
297
  gc = (0, profile_gc_js_1.summariseGc)(events);
265
298
  }
266
299
  if (opts.json) {
267
- (0, output_js_1.printData)({ samples: parsed, durationMs: Date.now() - start, growth, gc }, opts);
300
+ (0, output_js_1.printData)({ samples: parsed, durationMs: Date.now() - start, growth, gc, inferredGc }, opts);
268
301
  }
269
302
  else {
270
303
  console.log(`profile memory — ${samples.length} samples over ${profileOpts.trackSec}s`);
@@ -293,8 +326,20 @@ async function profileMemory(opts, sessionName, profileOpts) {
293
326
  }
294
327
  }
295
328
  else if (isAndroid) {
296
- console.log('\n GC: no ART collections logged in the window. ART only logs collections it ' +
297
- 'considers noteworthy, so a quiet window here is a real signal.');
329
+ console.log('\n GC: no ART collections logged in the window.');
330
+ if (inferredGc && inferredGc.collections > 0) {
331
+ console.log(` But ${inferredGc.series} fell ${inferredGc.collections} time(s), reclaiming ` +
332
+ `${mb(inferredGc.reclaimedBytes)} — so collections did run and were not logged. ` +
333
+ `ART's logging is off or filtered on this build; trust the heap deltas, not logcat.`);
334
+ }
335
+ else {
336
+ console.log(' Absence of logged collections is not absence of collection — ART logging is ' +
337
+ 'off or filtered on some builds. The heap deltas above are the sounder signal.');
338
+ }
339
+ }
340
+ if (inferredGc && inferredGc.collections > 0 && gc) {
341
+ console.log(` (heap deltas independently imply ${inferredGc.collections} collection(s), ` +
342
+ `${mb(inferredGc.reclaimedBytes)} reclaimed)`);
298
343
  }
299
344
  }
300
345
  return 0;
@@ -131,7 +131,9 @@ async function discoverTarget(port, host, targetIndex) {
131
131
  const data = await fetchTargets(port, host);
132
132
  const withWs = data.filter((t) => t.webSocketDebuggerUrl);
133
133
  if (withWs.length === 0) {
134
- throw new Error('Metro returned no debugger targets. Is the app running on a device/simulator?');
134
+ throw new Error('Metro is reachable but has no debugger targets. Most often the app is a release ' +
135
+ 'build — release Hermes ships without the inspector and never connects to Metro. ' +
136
+ 'Otherwise, check the app is running and pointed at this Metro instance.');
135
137
  }
136
138
  if (targetIndex !== undefined) {
137
139
  if (targetIndex < 0 || targetIndex >= withWs.length) {
@@ -33,7 +33,17 @@ function selectDebuggerUrl(targets, opts, displayName) {
33
33
  const host = opts.host ?? 'localhost';
34
34
  const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
35
35
  if (withWs.length === 0) {
36
- throw new Error(`Metro on ${host}:${port} returned no debugger targets. Is an app running on a device/simulator?`);
36
+ // Metro is reachable but nothing registered. The usual cause is not a
37
+ // missing app — it is a release build: release Hermes ships without the
38
+ // inspector and never connects to Metro at all, so asking "is the app
39
+ // running?" sends people to check something that is already true.
40
+ throw new Error(`Metro on ${host}:${port} is reachable but has no debugger targets.\n` +
41
+ `Most often the app is a release build — release Hermes ships without the ` +
42
+ `inspector and never connects to Metro, so no amount of relaunching will help. ` +
43
+ `Use a debug or profiling build for anything that attaches over Metro ` +
44
+ `(\`profile js\`, \`profile react\`, \`debug\`, \`network\`).\n` +
45
+ `Otherwise: check the app is running and foreground, and that it is pointed at ` +
46
+ `this Metro instance rather than another one.`);
37
47
  }
38
48
  if (opts.targetIndex !== undefined) {
39
49
  if (opts.targetIndex < 0 || opts.targetIndex >= withWs.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.29.1",
3
+ "version": "0.29.2",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {