@memlab/mcp-server 2.1.1 → 2.3.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.
Files changed (162) hide show
  1. package/README.md +73 -3
  2. package/dist/analysis-budget.d.ts +71 -0
  3. package/dist/analysis-budget.d.ts.map +1 -0
  4. package/dist/analysis-budget.js +118 -0
  5. package/dist/analysis-budget.js.map +1 -0
  6. package/dist/guardrail.d.ts +24 -0
  7. package/dist/guardrail.d.ts.map +1 -0
  8. package/dist/guardrail.js +65 -0
  9. package/dist/guardrail.js.map +1 -0
  10. package/dist/heap-state.d.ts +46 -1
  11. package/dist/heap-state.d.ts.map +1 -1
  12. package/dist/heap-state.js +142 -10
  13. package/dist/heap-state.js.map +1 -1
  14. package/dist/index.js +36 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/tools/array-group-by.d.ts +12 -0
  17. package/dist/tools/array-group-by.d.ts.map +1 -0
  18. package/dist/tools/array-group-by.js +191 -0
  19. package/dist/tools/array-group-by.js.map +1 -0
  20. package/dist/tools/auto-investigate.d.ts.map +1 -1
  21. package/dist/tools/auto-investigate.js +999 -37
  22. package/dist/tools/auto-investigate.js.map +1 -1
  23. package/dist/tools/cache-analysis.d.ts.map +1 -1
  24. package/dist/tools/cache-analysis.js +438 -21
  25. package/dist/tools/cache-analysis.js.map +1 -1
  26. package/dist/tools/check-health.d.ts.map +1 -1
  27. package/dist/tools/check-health.js +261 -1
  28. package/dist/tools/check-health.js.map +1 -1
  29. package/dist/tools/class-histogram.d.ts.map +1 -1
  30. package/dist/tools/class-histogram.js +142 -33
  31. package/dist/tools/class-histogram.js.map +1 -1
  32. package/dist/tools/closure-inspection.d.ts.map +1 -1
  33. package/dist/tools/closure-inspection.js +238 -25
  34. package/dist/tools/closure-inspection.js.map +1 -1
  35. package/dist/tools/detached-dom.d.ts.map +1 -1
  36. package/dist/tools/detached-dom.js +25 -3
  37. package/dist/tools/detached-dom.js.map +1 -1
  38. package/dist/tools/dev-artifacts.d.ts +33 -0
  39. package/dist/tools/dev-artifacts.d.ts.map +1 -0
  40. package/dist/tools/dev-artifacts.js +165 -0
  41. package/dist/tools/dev-artifacts.js.map +1 -0
  42. package/dist/tools/diff-snapshots.d.ts.map +1 -1
  43. package/dist/tools/diff-snapshots.js +113 -18
  44. package/dist/tools/diff-snapshots.js.map +1 -1
  45. package/dist/tools/duplicated-strings.d.ts.map +1 -1
  46. package/dist/tools/duplicated-strings.js +74 -12
  47. package/dist/tools/duplicated-strings.js.map +1 -1
  48. package/dist/tools/eval.d.ts.map +1 -1
  49. package/dist/tools/eval.js +189 -10
  50. package/dist/tools/eval.js.map +1 -1
  51. package/dist/tools/event-listener-leaks.d.ts.map +1 -1
  52. package/dist/tools/event-listener-leaks.js +235 -13
  53. package/dist/tools/event-listener-leaks.js.map +1 -1
  54. package/dist/tools/event-registry.d.ts +12 -0
  55. package/dist/tools/event-registry.d.ts.map +1 -0
  56. package/dist/tools/event-registry.js +183 -0
  57. package/dist/tools/event-registry.js.map +1 -0
  58. package/dist/tools/find-by-property.d.ts.map +1 -1
  59. package/dist/tools/find-by-property.js +113 -31
  60. package/dist/tools/find-by-property.js.map +1 -1
  61. package/dist/tools/find-by-shape.d.ts +12 -0
  62. package/dist/tools/find-by-shape.d.ts.map +1 -0
  63. package/dist/tools/find-by-shape.js +178 -0
  64. package/dist/tools/find-by-shape.js.map +1 -0
  65. package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
  66. package/dist/tools/find-nodes-by-class.js +50 -6
  67. package/dist/tools/find-nodes-by-class.js.map +1 -1
  68. package/dist/tools/for-each.d.ts.map +1 -1
  69. package/dist/tools/for-each.js +1 -1
  70. package/dist/tools/for-each.js.map +1 -1
  71. package/dist/tools/get-node.d.ts.map +1 -1
  72. package/dist/tools/get-node.js +6 -0
  73. package/dist/tools/get-node.js.map +1 -1
  74. package/dist/tools/get-string.d.ts +12 -0
  75. package/dist/tools/get-string.d.ts.map +1 -0
  76. package/dist/tools/get-string.js +223 -0
  77. package/dist/tools/get-string.js.map +1 -0
  78. package/dist/tools/get-value.d.ts +12 -0
  79. package/dist/tools/get-value.d.ts.map +1 -0
  80. package/dist/tools/get-value.js +152 -0
  81. package/dist/tools/get-value.js.map +1 -0
  82. package/dist/tools/global-variables.d.ts.map +1 -1
  83. package/dist/tools/global-variables.js +54 -34
  84. package/dist/tools/global-variables.js.map +1 -1
  85. package/dist/tools/growth-signals.d.ts +12 -0
  86. package/dist/tools/growth-signals.d.ts.map +1 -0
  87. package/dist/tools/growth-signals.js +211 -0
  88. package/dist/tools/growth-signals.js.map +1 -0
  89. package/dist/tools/intern-opportunities.d.ts +12 -0
  90. package/dist/tools/intern-opportunities.d.ts.map +1 -0
  91. package/dist/tools/intern-opportunities.js +289 -0
  92. package/dist/tools/intern-opportunities.js.map +1 -0
  93. package/dist/tools/load-snapshot.d.ts +10 -0
  94. package/dist/tools/load-snapshot.d.ts.map +1 -1
  95. package/dist/tools/load-snapshot.js +152 -15
  96. package/dist/tools/load-snapshot.js.map +1 -1
  97. package/dist/tools/object-cost-breakdown.d.ts +12 -0
  98. package/dist/tools/object-cost-breakdown.d.ts.map +1 -0
  99. package/dist/tools/object-cost-breakdown.js +439 -0
  100. package/dist/tools/object-cost-breakdown.js.map +1 -0
  101. package/dist/tools/object-shape.d.ts.map +1 -1
  102. package/dist/tools/object-shape.js +95 -2
  103. package/dist/tools/object-shape.js.map +1 -1
  104. package/dist/tools/pinch-points.d.ts.map +1 -1
  105. package/dist/tools/pinch-points.js +9 -9
  106. package/dist/tools/pinch-points.js.map +1 -1
  107. package/dist/tools/property-distribution.d.ts +12 -0
  108. package/dist/tools/property-distribution.d.ts.map +1 -0
  109. package/dist/tools/property-distribution.js +168 -0
  110. package/dist/tools/property-distribution.js.map +1 -0
  111. package/dist/tools/quick-diagnosis.d.ts.map +1 -1
  112. package/dist/tools/quick-diagnosis.js +56 -19
  113. package/dist/tools/quick-diagnosis.js.map +1 -1
  114. package/dist/tools/retainer-summary.d.ts.map +1 -1
  115. package/dist/tools/retainer-summary.js +90 -5
  116. package/dist/tools/retainer-summary.js.map +1 -1
  117. package/dist/tools/retainer-trace.d.ts.map +1 -1
  118. package/dist/tools/retainer-trace.js +74 -22
  119. package/dist/tools/retainer-trace.js.map +1 -1
  120. package/dist/tools/search-nodes.d.ts.map +1 -1
  121. package/dist/tools/search-nodes.js +10 -3
  122. package/dist/tools/search-nodes.js.map +1 -1
  123. package/dist/tools/search-strings.d.ts +12 -0
  124. package/dist/tools/search-strings.d.ts.map +1 -0
  125. package/dist/tools/search-strings.js +182 -0
  126. package/dist/tools/search-strings.js.map +1 -0
  127. package/dist/tools/sequence-analysis.d.ts +12 -0
  128. package/dist/tools/sequence-analysis.d.ts.map +1 -0
  129. package/dist/tools/sequence-analysis.js +234 -0
  130. package/dist/tools/sequence-analysis.js.map +1 -0
  131. package/dist/tools/server-status.d.ts +12 -0
  132. package/dist/tools/server-status.d.ts.map +1 -0
  133. package/dist/tools/server-status.js +41 -0
  134. package/dist/tools/server-status.js.map +1 -0
  135. package/dist/tools/shape-histogram.d.ts.map +1 -1
  136. package/dist/tools/shape-histogram.js +61 -16
  137. package/dist/tools/shape-histogram.js.map +1 -1
  138. package/dist/tools/sliced-strings.d.ts.map +1 -1
  139. package/dist/tools/sliced-strings.js +16 -11
  140. package/dist/tools/sliced-strings.js.map +1 -1
  141. package/dist/tools/snapshot-summary.d.ts.map +1 -1
  142. package/dist/tools/snapshot-summary.js +55 -13
  143. package/dist/tools/snapshot-summary.js.map +1 -1
  144. package/dist/tools/snapshots.d.ts +12 -0
  145. package/dist/tools/snapshots.d.ts.map +1 -0
  146. package/dist/tools/snapshots.js +93 -0
  147. package/dist/tools/snapshots.js.map +1 -0
  148. package/dist/tools/stale-collections.d.ts.map +1 -1
  149. package/dist/tools/stale-collections.js +248 -7
  150. package/dist/tools/stale-collections.js.map +1 -1
  151. package/dist/tools/trace-dominators.d.ts.map +1 -1
  152. package/dist/tools/trace-dominators.js +28 -1
  153. package/dist/tools/trace-dominators.js.map +1 -1
  154. package/dist/tools/weakmap-entries.d.ts +12 -0
  155. package/dist/tools/weakmap-entries.d.ts.map +1 -0
  156. package/dist/tools/weakmap-entries.js +156 -0
  157. package/dist/tools/weakmap-entries.js.map +1 -0
  158. package/dist/utils.d.ts +81 -2
  159. package/dist/utils.d.ts.map +1 -1
  160. package/dist/utils.js +303 -35
  161. package/dist/utils.js.map +1 -1
  162. package/package.json +3 -3
@@ -119,7 +119,8 @@ export function registerEval(server) {
119
119
  '`.toNode`, `.fromNode`.\n' +
120
120
  '**Iterating all nodes:** `snapshot.nodes.forEach(node => { ... })` — NOT for-of.\n' +
121
121
  '**Get node by ID:** `snapshot.getNodeById(id)` returns IHeapNode or null.\n' +
122
- '**String values:** `node.toStringNode()?.stringValue` for string nodes.\n\n' +
122
+ '**String values:** `node.toStringNode()?.stringValue` for string nodes.\n' +
123
+ '**Caveat — retained_size is unreliable here:** inside eval, `node.retained_size`/`.retainedSize` can read back ~0 for every node on some loads. Node counts, property/edge walks, and string values ARE trustworthy. For authoritative retained sizes call `helpers.retainedSize(id)` / `helpers.retainedSizes([ids])` (they re-resolve the node on the real snapshot), or use the dedicated tools (`memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, `memlab_object_shape`).\n\n' +
123
124
  '**Example — inspect Map entries:**\n' +
124
125
  '```\nconst map = snapshot.getNodeById(12345);\nconst entries = [];\n' +
125
126
  'for (const edge of map.references) {\n' +
@@ -127,23 +128,38 @@ export function registerEval(server) {
127
128
  ' for (const te of edge.toNode.references) {\n' +
128
129
  ' entries.push({name: te.toNode.name, type: te.toNode.type});\n' +
129
130
  ' }\n }\n}\nresult = entries.slice(0, 10);\n```', {
131
+ mode: z
132
+ .enum(['eval', 'describe_env'])
133
+ .optional()
134
+ .default('eval')
135
+ .describe('"eval" (default) runs `code`. "describe_env" ignores `code` and returns the in-scope globals, the IHeapNode/IHeapEdge API, and the required calling conventions (`result =`, `.forEach`) so you can self-correct before running.'),
130
136
  code: z
131
137
  .string()
138
+ .optional()
132
139
  .describe('JavaScript code to execute. Must assign the output to a `result` variable. ' +
133
140
  'Available globals: snapshot (IHeapSnapshot — use .nodes.forEach(), .getNodeById(), .edges.forEach()), ' +
134
141
  'utils (@memlab/core utils with aggregateDominatorMetrics, isFiberNode, isDetachedDOMNode), ' +
135
142
  'helpers ({ serializeNodeSummary, serializeNodeDetail, formatBytes, formatNumber, ' +
136
- 'markdownTable, isNodeWorthInspecting, filterLargestObjects, queryNodes }), ' +
143
+ 'markdownTable, isNodeWorthInspecting, filterLargestObjects, queryNodes, ' +
144
+ 'groupReferrersByEdge(nodeId), groupArrayElementsByProperty(arrayNodeId, propName), ' +
145
+ 'isOrphaned(nodeId, ownershipEdgeNames[]), countUniqueTargets(arrayNodeId, propName), ' +
146
+ 'retainedSize(id), retainedSizes(ids[]) }), ' +
137
147
  'and standard JS built-ins. ' +
138
148
  'Node traversal: use node.references (outgoing) and node.referrers (incoming) with for-of. ' +
139
149
  'Edge properties: .name_or_index, .type, .toNode, .fromNode.'),
140
150
  timeout_ms: z
141
151
  .number()
142
152
  .optional()
143
- .default(30000)
144
- .describe('Execution timeout in milliseconds (default 30000)'),
145
- }, async ({ code, timeout_ms }) => {
153
+ .default(60000)
154
+ .describe('Execution timeout in milliseconds (default 60000). Full-snapshot scans on large heaps may need 120000+.'),
155
+ }, async ({ mode, code, timeout_ms }) => {
146
156
  try {
157
+ if (mode === 'describe_env') {
158
+ return toolResult(describeEnv());
159
+ }
160
+ if (code == null || code.trim() === '') {
161
+ return errorResult(new Error('No code provided. Pass `code`, or use mode:"describe_env" to see the available globals and conventions.'));
162
+ }
147
163
  const snapshot = getSnapshot();
148
164
  const consoleOutput = [];
149
165
  const capturedConsole = {
@@ -152,6 +168,107 @@ export function registerEval(server) {
152
168
  error: (...args) => consoleOutput.push('[error] ' + args.map(String).join(' ')),
153
169
  info: (...args) => consoleOutput.push('[info] ' + args.map(String).join(' ')),
154
170
  };
171
+ const groupReferrersByEdge = (nodeId) => {
172
+ const target = snapshot.getNodeById(nodeId);
173
+ if (!target)
174
+ return {};
175
+ const groups = {};
176
+ for (const edge of target.referrers) {
177
+ const eName = String(edge.name_or_index);
178
+ const from = edge.fromNode;
179
+ if (!groups[eName])
180
+ groups[eName] = [];
181
+ if (groups[eName].length < 10) {
182
+ groups[eName].push({
183
+ fromName: from.name,
184
+ fromType: from.type,
185
+ fromId: from.id,
186
+ });
187
+ }
188
+ }
189
+ return groups;
190
+ };
191
+ const groupArrayElementsByProperty = (arrayNodeId, propertyName) => {
192
+ const arrNode = snapshot.getNodeById(arrayNodeId);
193
+ if (!arrNode)
194
+ return { error: 'Node not found' };
195
+ const groups = {};
196
+ let missing = 0;
197
+ let total = 0;
198
+ for (const edge of arrNode.references) {
199
+ if (edge.type !== 'element')
200
+ continue;
201
+ const elem = edge.toNode;
202
+ if (elem.id <= 3)
203
+ continue;
204
+ total++;
205
+ let found = false;
206
+ for (const propEdge of elem.references) {
207
+ if (String(propEdge.name_or_index) === propertyName) {
208
+ const target = propEdge.toNode;
209
+ const key = target.name;
210
+ if (!groups[key])
211
+ groups[key] = { count: 0, exampleId: target.id };
212
+ groups[key].count++;
213
+ found = true;
214
+ break;
215
+ }
216
+ }
217
+ if (!found)
218
+ missing++;
219
+ }
220
+ return { groups, total, missing };
221
+ };
222
+ const isOrphaned = (nodeId, ownershipEdgeNames) => {
223
+ const target = snapshot.getNodeById(nodeId);
224
+ if (!target)
225
+ return false;
226
+ const ownerSet = new Set(ownershipEdgeNames);
227
+ for (const edge of target.referrers) {
228
+ if (ownerSet.has(String(edge.name_or_index)))
229
+ return false;
230
+ }
231
+ return true;
232
+ };
233
+ // Authoritative retained sizes (Feedback round 3 §3b). Reading
234
+ // `.retainedSize` off proxied/iterated nodes inside eval can come back
235
+ // ~0; these helpers look the node up fresh on the real snapshot (the
236
+ // same path the dedicated tools use) so custom analyses can rank by
237
+ // retained size.
238
+ const retainedSize = (id) => {
239
+ const n = snapshot.getNodeById(id);
240
+ return n ? n.retainedSize : 0;
241
+ };
242
+ const retainedSizes = (ids) => {
243
+ const out = {};
244
+ for (const id of ids) {
245
+ const n = snapshot.getNodeById(id);
246
+ out[id] = n ? n.retainedSize : 0;
247
+ }
248
+ return out;
249
+ };
250
+ const countUniqueTargets = (arrayNodeId, propertyName) => {
251
+ const arrNode = snapshot.getNodeById(arrayNodeId);
252
+ if (!arrNode)
253
+ return { error: 'Node not found' };
254
+ const uniqueIds = new Set();
255
+ let total = 0;
256
+ for (const edge of arrNode.references) {
257
+ if (edge.type !== 'element')
258
+ continue;
259
+ const elem = edge.toNode;
260
+ if (elem.id <= 3)
261
+ continue;
262
+ total++;
263
+ for (const propEdge of elem.references) {
264
+ if (String(propEdge.name_or_index) === propertyName) {
265
+ uniqueIds.add(propEdge.toNode.id);
266
+ break;
267
+ }
268
+ }
269
+ }
270
+ return { uniqueCount: uniqueIds.size, totalElements: total };
271
+ };
155
272
  const helpers = {
156
273
  serializeNodeSummary,
157
274
  serializeNodeDetail,
@@ -161,6 +278,12 @@ export function registerEval(server) {
161
278
  isNodeWorthInspecting,
162
279
  filterLargestObjects,
163
280
  queryNodes,
281
+ groupReferrersByEdge,
282
+ groupArrayElementsByProperty,
283
+ isOrphaned,
284
+ countUniqueTargets,
285
+ retainedSize,
286
+ retainedSizes,
164
287
  };
165
288
  const sandbox = {
166
289
  snapshot: wrapSnapshot(snapshot),
@@ -197,6 +320,15 @@ export function registerEval(server) {
197
320
  const context = vm.createContext(sandbox);
198
321
  const script = new vm.Script(code, { filename: 'memlab_eval' });
199
322
  script.runInContext(context, { timeout: timeout_ms });
323
+ // Actionable hint when nothing was assigned to `result` (the #1 user
324
+ // error — code that `return`s a value or runs a value-returning IIFE
325
+ // never populates `result`, so output is silently "undefined").
326
+ if (sandbox.result === undefined && consoleOutput.length === 0) {
327
+ return toolResult('Your code ran without error but never assigned to `result`, so there is nothing to return.\n' +
328
+ 'Assign the value you want back to `result` (do NOT use `return` at the top level), e.g.:\n' +
329
+ ' `result = someValue;`\n' +
330
+ 'Use mode:"describe_env" to see the full calling convention.');
331
+ }
200
332
  let output;
201
333
  try {
202
334
  output = JSON.stringify(sandbox.result, null, 2) ?? 'undefined';
@@ -212,12 +344,59 @@ export function registerEval(server) {
212
344
  return toolResult(output);
213
345
  }
214
346
  catch (err) {
215
- if (err instanceof Error &&
216
- err.message.includes('Script execution timed out')) {
217
- return errorResult(new Error(`Execution timed out after ${timeout_ms}ms`));
218
- }
219
- return errorResult(err);
347
+ return errorResult(new Error(actionableEvalError(err, code)));
220
348
  }
221
349
  });
222
350
  }
351
+ // Map the opaque VM errors that the documented calling-convention mistakes
352
+ // produce into actionable guidance (Feedback §3).
353
+ function actionableEvalError(err, code) {
354
+ const msg = err instanceof Error ? err.message : String(err);
355
+ if (msg.includes('Script execution timed out')) {
356
+ return `Execution timed out. Increase timeout_ms, or narrow the scan (filter earlier, use a dedicated tool like memlab_find_by_property/memlab_property_distribution instead of a full snapshot.nodes walk).`;
357
+ }
358
+ if (msg.includes('Illegal return statement')) {
359
+ return `Illegal return statement: you cannot \`return\` at the top level here. Assign the value to \`result\` instead — e.g. \`result = ...;\`. (Use mode:"describe_env" for the convention.)`;
360
+ }
361
+ if (msg.includes('is not iterable')) {
362
+ return `${msg}\nHint: \`snapshot.nodes\` is not a for-of iterable. Use \`snapshot.nodes.forEach(node => { ... })\` (and \`snapshot.edges.forEach(...)\`). \`node.references\`/\`node.referrers\` ARE for-of iterable.`;
363
+ }
364
+ if (code &&
365
+ /\bfor\s*\(\s*(const|let|var)\b.*\bof\b.*\bsnapshot\.nodes\b/.test(code)) {
366
+ return `${msg}\nHint: iterate all nodes with \`snapshot.nodes.forEach(node => { ... })\`, not \`for...of\`.`;
367
+ }
368
+ return msg;
369
+ }
370
+ function describeEnv() {
371
+ return [
372
+ '# memlab_eval environment',
373
+ '',
374
+ '## Calling conventions (REQUIRED)',
375
+ '- Assign your output to `result` — do NOT use `return` at the top level (that throws "Illegal return statement").',
376
+ '- Iterate all nodes with `snapshot.nodes.forEach(node => { ... })` and all edges with `snapshot.edges.forEach(...)`. `snapshot.nodes` is NOT a for-of iterable.',
377
+ '- `node.references` (outgoing) and `node.referrers` (incoming) ARE for-of iterable.',
378
+ '',
379
+ '## In-scope globals',
380
+ '- `snapshot` — IHeapSnapshot: `.nodes.forEach(cb)`, `.edges.forEach(cb)`, `.getNodeById(id)`.',
381
+ '- `utils` — @memlab/core utils (e.g. `aggregateDominatorMetrics`, `isFiberNode`, `isDetachedDOMNode`).',
382
+ '- `helpers` — `serializeNodeSummary`, `serializeNodeDetail`, `formatBytes`, `formatNumber`, `markdownTable`, `isNodeWorthInspecting`, `filterLargestObjects`, `queryNodes`, `groupReferrersByEdge(nodeId)`, `groupArrayElementsByProperty(arrayNodeId, prop)`, `isOrphaned(nodeId, ownerEdges[])`, `countUniqueTargets(arrayNodeId, prop)`, `retainedSize(id)`, `retainedSizes(ids[])`.',
383
+ '- Standard JS built-ins (Array, Object, Map, Set, JSON, Math, RegExp, …). No require/process/fs/network.',
384
+ '',
385
+ '## IHeapNode API',
386
+ '`.id`, `.name`, `.type`, `.self_size`, `.retainedSize` (alias `.retained_size`), `.edge_count`, `.is_detached`, `.numOfReferrers` (alias `.referrer_count`), `.isString`, `.toStringNode()?.stringValue`, `.hasPathEdge`, `.pathEdge`, `.dominatorNode`, `.location` (`script_id`/`line`/`column`).',
387
+ '',
388
+ '## Caveat: retained_size',
389
+ 'Inside eval, `.retainedSize`/`.retained_size` can read back ~0 for every node on some loads. Counts, property/edge walks, and string values are reliable. For authoritative retained sizes call `helpers.retainedSize(id)` or `helpers.retainedSizes([ids])` (they re-resolve the node on the real snapshot, so you can rank custom analyses by retained size), or use `memlab_largest_objects`, `memlab_class_histogram`, `memlab_pinch_points`, or `memlab_object_shape`.',
390
+ '',
391
+ '## IHeapEdge API',
392
+ '`.name_or_index`, `.type` (property/element/context/internal/hidden/shortcut), `.toNode`, `.fromNode`.',
393
+ '',
394
+ '## Runnable example',
395
+ '```',
396
+ 'const counts = {};',
397
+ 'snapshot.nodes.forEach(node => { counts[node.type] = (counts[node.type] || 0) + 1; });',
398
+ 'result = counts;',
399
+ '```',
400
+ ].join('\n');
401
+ }
223
402
  //# sourceMappingURL=eval.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eval.js","sourceRoot":"","sources":["../../src/tools/eval.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"eval.js","sourceRoot":"","sources":["../../src/tools/eval.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-listener-leaks.d.ts","sourceRoot":"","sources":["../../src/tools/event-listener-leaks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AA0JvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2LlE"}
1
+ {"version":3,"file":"event-listener-leaks.d.ts","sourceRoot":"","sources":["../../src/tools/event-listener-leaks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AA+NvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAkZlE"}
@@ -102,14 +102,63 @@ function countZombies(listeners, snapshot) {
102
102
  }
103
103
  return zombies;
104
104
  }
105
+ function analyzeContextDistribution(listeners, snapshot) {
106
+ const callbackIds = new Set();
107
+ const contextIds = new Set();
108
+ const contextShapeMap = new Map();
109
+ let orphanedCount = 0;
110
+ for (const l of listeners) {
111
+ callbackIds.add(l.callbackId);
112
+ if (l.contextId != null) {
113
+ contextIds.add(l.contextId);
114
+ const ctxNode = snapshot.getNodeById(l.contextId);
115
+ if (ctxNode) {
116
+ const shapeName = ctxNode.name;
117
+ const existing = contextShapeMap.get(shapeName);
118
+ if (existing) {
119
+ existing.count++;
120
+ }
121
+ else {
122
+ contextShapeMap.set(shapeName, { count: 1, exampleId: ctxNode.id });
123
+ }
124
+ if (ctxNode.numOfReferrers <= 2) {
125
+ orphanedCount++;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ const contextShapes = [...contextShapeMap.entries()]
131
+ .map(([shapeName, info]) => ({
132
+ shapeName,
133
+ count: info.count,
134
+ exampleId: info.exampleId,
135
+ }))
136
+ .sort((a, b) => b.count - a.count)
137
+ .slice(0, 10);
138
+ return {
139
+ uniqueCallbacks: callbackIds.size,
140
+ uniqueContexts: contextIds.size,
141
+ totalListeners: listeners.length,
142
+ contextShapes,
143
+ orphanedContextCount: orphanedCount,
144
+ };
145
+ }
146
+ // A duplicate is the SAME callback function instance bound to the SAME context
147
+ // instance, registered more than once. We key by node ids (callbackId +
148
+ // contextId), NOT by name/shape — keying by name counted distinct closures that
149
+ // merely share a name across different hosts as "duplicates", which produced
150
+ // wildly inflated counts (e.g. 99.95%) on per-model registries that are really
151
+ // structural, one-listener-per-model (Feedback §8).
105
152
  function countDuplicateCallbacks(listeners) {
106
- const callbackContextPairs = new Map();
153
+ const pairs = new Map();
107
154
  for (const l of listeners) {
108
- const key = `${l.callbackName}\0${l.contextName ?? ''}`;
109
- callbackContextPairs.set(key, (callbackContextPairs.get(key) ?? 0) + 1);
155
+ if (l.callbackId <= 0)
156
+ continue;
157
+ const key = `${l.callbackId}\0${l.contextId ?? 0}`;
158
+ pairs.set(key, (pairs.get(key) ?? 0) + 1);
110
159
  }
111
160
  let duplicates = 0;
112
- for (const count of callbackContextPairs.values()) {
161
+ for (const count of pairs.values()) {
113
162
  if (count > 1)
114
163
  duplicates += count - 1;
115
164
  }
@@ -117,11 +166,14 @@ function countDuplicateCallbacks(listeners) {
117
166
  }
118
167
  export function registerEventListenerLeaks(server) {
119
168
  server.tool('memlab_event_listener_leaks', 'Detect EventEmitter-style listener accumulation — the #1 cause of memory leaks in ' +
120
- 'Backbone/Node.js/EventEmitter-based apps. Scans for objects with _events/_listeners ' +
121
- 'properties and flags: arrays with >N entries (accumulation), entries where the ' +
122
- 'context/ctx object has no other referrers (zombie listeners from missed .off() calls), ' +
123
- 'and multiple entries with the same callback but different contexts (mount/unmount leaks). ' +
124
- 'Use after memlab_auto_investigate flags subscription accumulation.', {
169
+ 'event-driven apps. Scans for: (1) objects with _events/_listeners properties (Backbone/Node.js), ' +
170
+ 'and (2) accumulations of {callback, context}-shaped objects (custom event systems like ' +
171
+ 'WAWebEventEmitter, Signal, etc.). Detects arrays with >N entries (accumulation), entries ' +
172
+ 'where the context/ctx object has no other referrers (zombie listeners), and multiple entries ' +
173
+ 'with the same callback (mount/unmount leaks). ' +
174
+ 'Use extra_event_properties to detect app-specific event container property names ' +
175
+ '(e.g., "#eventMap" for private class fields). ' +
176
+ '⚠ Full-heap scan — slow on very large heaps (millions of nodes).', {
125
177
  min_listeners: z
126
178
  .number()
127
179
  .optional()
@@ -137,10 +189,35 @@ export function registerEventListenerLeaks(server) {
137
189
  .optional()
138
190
  .default(true)
139
191
  .describe('Check for zombie listeners whose context objects have no other referrers (default true). Can be slow on very large snapshots.'),
140
- }, async ({ min_listeners, limit, check_zombies }) => {
192
+ extra_event_properties: z
193
+ .array(z.string())
194
+ .optional()
195
+ .describe('Additional property names to scan as event containers (e.g., ["#eventMap", "#listeningTo"] for private class fields). ' +
196
+ 'These are checked in addition to the built-in patterns (_events, _listeners, eventMap, etc.).'),
197
+ detect_shapes: z
198
+ .boolean()
199
+ .optional()
200
+ .default(true)
201
+ .describe('Also detect accumulations of {callback, context}-shaped objects regardless of parent property naming (default true). ' +
202
+ "Catches custom event systems that don't use standard _events/_listeners patterns."),
203
+ analyze_contexts: z
204
+ .boolean()
205
+ .optional()
206
+ .default(true)
207
+ .describe('For each accumulation, analyze the distribution of unique callbacks vs contexts, ' +
208
+ 'group context objects by shape/class, and detect orphaned contexts (default true). ' +
209
+ 'This is the key signal for identifying listener-based memory leaks where many ' +
210
+ 'orphaned objects register on the same event.'),
211
+ }, async ({ min_listeners, limit, check_zombies, extra_event_properties, detect_shapes, analyze_contexts, }) => {
141
212
  try {
142
213
  const snapshot = getSnapshot();
143
214
  const accumulations = [];
215
+ const effectiveEventProps = new Set(EVENT_PROPERTY_NAMES);
216
+ if (extra_event_properties) {
217
+ for (const p of extra_event_properties) {
218
+ effectiveEventProps.add(p);
219
+ }
220
+ }
144
221
  snapshot.nodes.forEach(node => {
145
222
  if (node.id <= 3)
146
223
  return;
@@ -148,7 +225,7 @@ export function registerEventListenerLeaks(server) {
148
225
  return;
149
226
  for (const edge of node.references) {
150
227
  const eName = String(edge.name_or_index);
151
- if (!EVENT_PROPERTY_NAMES.has(eName))
228
+ if (!effectiveEventProps.has(eName))
152
229
  continue;
153
230
  if (edge.toNode.id <= 3)
154
231
  continue;
@@ -161,6 +238,9 @@ export function registerEventListenerLeaks(server) {
161
238
  ? countZombies(allListeners, snapshot)
162
239
  : 0;
163
240
  const duplicateCallbackCount = countDuplicateCallbacks(allListeners);
241
+ const contextDist = analyze_contexts
242
+ ? analyzeContextDistribution(allListeners, snapshot)
243
+ : undefined;
164
244
  const entry = {
165
245
  hostNodeId: node.id,
166
246
  hostName: node.name,
@@ -179,6 +259,7 @@ export function registerEventListenerLeaks(server) {
179
259
  zombieCount,
180
260
  duplicateCallbackCount,
181
261
  totalRetainedSize: node.retainedSize,
262
+ contextDistribution: contextDist,
182
263
  };
183
264
  let inserted = false;
184
265
  for (let i = 0; i < accumulations.length; i++) {
@@ -195,14 +276,129 @@ export function registerEventListenerLeaks(server) {
195
276
  break;
196
277
  }
197
278
  });
279
+ // Shape-based detection: find accumulations of {callback, context}-shaped objects
280
+ if (detect_shapes) {
281
+ const CALLBACK_PROPS = new Set([
282
+ 'callback',
283
+ 'fn',
284
+ 'handler',
285
+ 'listener',
286
+ ]);
287
+ const CONTEXT_PROPS = new Set([
288
+ 'context',
289
+ 'ctx',
290
+ 'this',
291
+ 'target',
292
+ 'scope',
293
+ ]);
294
+ const shapeMap = new Map();
295
+ snapshot.nodes.forEach(node => {
296
+ if (node.id <= 3)
297
+ return;
298
+ if (node.type !== 'object' || node.name !== 'Object')
299
+ return;
300
+ const propNames = [];
301
+ let hasCallback = false;
302
+ let hasContext = false;
303
+ let callbackId = 0;
304
+ let callbackName = '';
305
+ let contextId = null;
306
+ let contextName = null;
307
+ for (const edge of node.references) {
308
+ if (edge.type !== 'property')
309
+ continue;
310
+ const pName = String(edge.name_or_index);
311
+ propNames.push(pName);
312
+ if (CALLBACK_PROPS.has(pName)) {
313
+ hasCallback = true;
314
+ callbackId = edge.toNode.id;
315
+ callbackName = edge.toNode.name;
316
+ }
317
+ if (CONTEXT_PROPS.has(pName)) {
318
+ hasContext = true;
319
+ contextId = edge.toNode.id;
320
+ contextName = edge.toNode.name;
321
+ }
322
+ }
323
+ if (!hasCallback || !hasContext)
324
+ return;
325
+ if (propNames.length === 0 || propNames.length > 10)
326
+ return;
327
+ propNames.sort();
328
+ const key = propNames.join(',');
329
+ const existing = shapeMap.get(key);
330
+ if (existing) {
331
+ existing.count++;
332
+ existing.totalRetained += node.retainedSize;
333
+ existing.allListeners.push({
334
+ callbackId,
335
+ callbackName,
336
+ contextId,
337
+ contextName,
338
+ });
339
+ }
340
+ else {
341
+ shapeMap.set(key, {
342
+ count: 1,
343
+ totalRetained: node.retainedSize,
344
+ properties: propNames,
345
+ exampleId: node.id,
346
+ allListeners: [
347
+ { callbackId, callbackName, contextId, contextName },
348
+ ],
349
+ });
350
+ }
351
+ });
352
+ for (const [, shape] of shapeMap) {
353
+ if (shape.count < min_listeners)
354
+ continue;
355
+ const contextDist = analyze_contexts
356
+ ? analyzeContextDistribution(shape.allListeners, snapshot)
357
+ : undefined;
358
+ const entry = {
359
+ hostNodeId: shape.exampleId,
360
+ hostName: `{${shape.properties.join(', ')}}`,
361
+ hostType: 'shape',
362
+ eventPropertyName: `(${shape.properties.join(', ')})`,
363
+ eventContainerId: shape.exampleId,
364
+ totalListeners: shape.count,
365
+ eventBreakdown: [
366
+ {
367
+ eventName: '(shape-based)',
368
+ listenerCount: shape.count,
369
+ sampleListeners: shape.allListeners.slice(0, 3),
370
+ },
371
+ ],
372
+ zombieCount: 0,
373
+ duplicateCallbackCount: countDuplicateCallbacks(shape.allListeners),
374
+ totalRetainedSize: shape.totalRetained,
375
+ contextDistribution: contextDist,
376
+ };
377
+ let inserted = false;
378
+ for (let i = 0; i < accumulations.length; i++) {
379
+ if (entry.totalListeners > accumulations[i].totalListeners) {
380
+ accumulations.splice(i, 0, entry);
381
+ inserted = true;
382
+ break;
383
+ }
384
+ }
385
+ if (!inserted)
386
+ accumulations.push(entry);
387
+ if (accumulations.length > limit)
388
+ accumulations.length = limit;
389
+ }
390
+ }
198
391
  if (accumulations.length === 0) {
199
392
  return toolResult(`No event listener accumulation found with >= ${min_listeners} listeners. ` +
200
- `Try lowering min_listeners or check \`memlab_find_by_property\` with property_name="_events".`);
393
+ `Try lowering min_listeners, or check \`memlab_find_by_property\` with property_name="callback", ` +
394
+ `or use \`memlab_shape_histogram\` with sort_by="count" to find high-count shapes.`);
201
395
  }
202
396
  const lines = [
203
397
  `## Event Listener Leak Detection`,
204
398
  `Found ${accumulations.length} object(s) with potential listener accumulation`,
205
399
  '',
400
+ '_"Duplicates" = the same callback instance bound to the same context, registered more than once (by node id, not by name/shape). One listener per distinct model/host is structural, not a leak._',
401
+ '',
206
402
  ];
207
403
  const headers = [
208
404
  'Host',
@@ -233,7 +429,33 @@ export function registerEventListenerLeaks(server) {
233
429
  lines.push('');
234
430
  }
235
431
  if (a.duplicateCallbackCount > 0) {
236
- lines.push(`**${formatNumber(a.duplicateCallbackCount)} duplicate callback(s)** — same callback registered multiple times (classic mount/unmount leak)`);
432
+ lines.push(`**${formatNumber(a.duplicateCallbackCount)} duplicate callback(s)** — the same callback *instance* bound to the same context registered more than once (classic mount/unmount leak). One listener per distinct model is structural, not a duplicate.`);
433
+ lines.push('');
434
+ }
435
+ if (a.contextDistribution) {
436
+ const cd = a.contextDistribution;
437
+ lines.push('**Context distribution:**');
438
+ lines.push(`- ${formatNumber(cd.uniqueCallbacks)} unique callback(s) across ${formatNumber(cd.totalListeners)} listeners`);
439
+ lines.push(`- ${formatNumber(cd.uniqueContexts)} unique context object(s) across ${formatNumber(cd.totalListeners)} listeners`);
440
+ if (cd.orphanedContextCount > 0) {
441
+ const orphanPct = cd.uniqueContexts > 0
442
+ ? ` (${((cd.orphanedContextCount / cd.uniqueContexts) * 100).toFixed(0)}%)`
443
+ : '';
444
+ lines.push(`- **${formatNumber(cd.orphanedContextCount)} orphaned context(s)**${orphanPct} — only retained by listener registrations`);
445
+ }
446
+ if (cd.contextShapes.length > 0) {
447
+ lines.push('- Context shapes:');
448
+ for (const cs of cd.contextShapes) {
449
+ lines.push(` - ${formatNumber(cs.count)}× \`${cs.shapeName}\` (example: @${cs.exampleId})`);
450
+ }
451
+ }
452
+ if (cd.uniqueCallbacks <= 3 &&
453
+ cd.uniqueContexts > 10 &&
454
+ cd.orphanedContextCount > cd.uniqueContexts * 0.5) {
455
+ lines.push('');
456
+ lines.push(`**⚠ LEAK PATTERN:** Few callbacks (${cd.uniqueCallbacks}) but many orphaned contexts (${formatNumber(cd.orphanedContextCount)}) — ` +
457
+ `classic listener-based memory leak where orphaned objects register on shared models but are never unregistered.`);
458
+ }
237
459
  lines.push('');
238
460
  }
239
461
  lines.push('Event breakdown:');