@memlab/cli 1.0.10 → 1.0.12

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/bin/memlab.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S node --expose-gc --max-old-space-size=4096
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -6,8 +6,8 @@
6
6
  * This source code is licensed under the MIT license found in the
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  *
9
- * @emails oncall+ws_labs
10
9
  * @format
10
+ * @oncall ws_labs
11
11
  */
12
12
 
13
13
  // eslint-disable-next-line no-var
@@ -7,9 +7,8 @@
7
7
  * @format
8
8
  * @oncall ws_labs
9
9
  */
10
- import type { CLIOptions } from '@memlab/core';
10
+ import type { BaseOption, CLIOptions } from '@memlab/core';
11
11
  import BaseCommand, { CommandCategory } from '../../../BaseCommand';
12
- import { BaseOption } from '@memlab/core';
13
12
  export default class InteractiveHeapViewCommand extends BaseCommand {
14
13
  getCommandName(): string;
15
14
  getDescription(): string;
@@ -45,14 +45,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  };
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
47
  const fs_extra_1 = __importDefault(require("fs-extra"));
48
- const BaseCommand_1 = __importStar(require("../../../BaseCommand"));
49
48
  const core_1 = require("@memlab/core");
49
+ const heap_analysis_1 = require("@memlab/heap-analysis");
50
+ const BaseCommand_1 = __importStar(require("../../../BaseCommand"));
50
51
  const SnapshotFileOption_1 = __importDefault(require("../../../options/heap/SnapshotFileOption"));
51
52
  const JSEngineOption_1 = __importDefault(require("../../../options/heap/JSEngineOption"));
52
- const core_2 = require("@memlab/core");
53
- const heap_analysis_1 = require("@memlab/heap-analysis");
54
53
  const CliScreen_1 = __importDefault(require("./ui-components/CliScreen"));
55
54
  const HeapNodeIdOption_1 = __importDefault(require("../../../options/HeapNodeIdOption"));
55
+ const MLClusteringOption_1 = __importDefault(require("../../../options/MLClusteringOption"));
56
56
  class InteractiveHeapViewCommand extends BaseCommand_1.default {
57
57
  getCommandName() {
58
58
  return 'view-heap';
@@ -71,6 +71,7 @@ class InteractiveHeapViewCommand extends BaseCommand_1.default {
71
71
  new SnapshotFileOption_1.default(),
72
72
  new JSEngineOption_1.default(),
73
73
  new HeapNodeIdOption_1.default(),
74
+ new MLClusteringOption_1.default(),
74
75
  ];
75
76
  }
76
77
  // get the heap snapshot to view
@@ -89,18 +90,34 @@ class InteractiveHeapViewCommand extends BaseCommand_1.default {
89
90
  });
90
91
  }
91
92
  getNodesToFocus(heap) {
92
- const nodes = this.getNodesWithLargestRetainedSize(heap);
93
- nodes.push(...this.getDetachedNodes(heap));
94
- return nodes;
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ const ret = new Map();
95
+ const nodes = this.getNodesWithLargestRetainedSize(heap);
96
+ ret.set('large-object', nodes);
97
+ const detachedNodes = yield this.getDetachedNodes(heap);
98
+ ret.set('detached', detachedNodes);
99
+ return ret;
100
+ });
95
101
  }
96
102
  getDetachedNodes(heap) {
97
- const ret = [];
98
- heap.nodes.forEach(node => {
99
- if (core_1.utils.isDetachedDOMNode(node) || core_1.utils.isDetachedFiberNode(node)) {
100
- ret.push({ tag: 'Detached', heapObject: node });
101
- }
103
+ return __awaiter(this, void 0, void 0, function* () {
104
+ const ret = [];
105
+ const idSet = new Set();
106
+ heap.nodes.forEach(node => {
107
+ if (core_1.utils.isDetachedDOMNode(node) || core_1.utils.isDetachedFiberNode(node)) {
108
+ idSet.add(node.id);
109
+ }
110
+ });
111
+ // get a minimal set of objects to represent all the detached DOM elements
112
+ const dominatorIds = core_1.utils.getConditionalDominatorIds(idSet, heap, () => true);
113
+ dominatorIds.forEach(id => {
114
+ const node = heap.getNodeById(id);
115
+ if (node) {
116
+ ret.push({ tag: 'Detached', heapObject: node });
117
+ }
118
+ });
119
+ return ret;
102
120
  });
103
- return ret;
104
121
  }
105
122
  getNodesWithLargestRetainedSize(heap) {
106
123
  const sizeThreshold = 2 * 1024 * 1024; // 2MB
@@ -117,27 +134,36 @@ class InteractiveHeapViewCommand extends BaseCommand_1.default {
117
134
  }
118
135
  // get heap node to focus on
119
136
  getHeapNodes(heap) {
120
- if (core_1.config.focusFiberNodeId >= 0) {
121
- const node = heap.getNodeById(core_1.config.focusFiberNodeId);
122
- if (node) {
123
- return [{ heapObject: node }];
137
+ return __awaiter(this, void 0, void 0, function* () {
138
+ if (core_1.config.focusFiberNodeId >= 0) {
139
+ const node = heap.getNodeById(core_1.config.focusFiberNodeId);
140
+ if (node) {
141
+ const map = new Map();
142
+ map.set('Chosen', [
143
+ {
144
+ tag: 'Chosen',
145
+ heapObject: node,
146
+ },
147
+ ]);
148
+ return map;
149
+ }
150
+ }
151
+ const category = yield this.getNodesToFocus(heap);
152
+ if (category.size === 0) {
153
+ throw core_1.utils.haltOrThrow('please specify a heap node ' +
154
+ `via --${new HeapNodeIdOption_1.default().getOptionName()}`);
124
155
  }
125
- }
126
- const nodes = this.getNodesToFocus(heap);
127
- if (nodes.length === 0) {
128
- throw core_1.utils.haltOrThrow('please specify a heap node ' +
129
- `via --${new HeapNodeIdOption_1.default().getOptionName()}`);
130
- }
131
- return nodes;
156
+ return category;
157
+ });
132
158
  }
133
159
  run(options) {
134
160
  var _a;
135
161
  return __awaiter(this, void 0, void 0, function* () {
136
162
  const workDir = (_a = options.configFromOptions) === null || _a === void 0 ? void 0 : _a.workDir;
137
- const reportOutDir = core_2.fileManager.getReportOutDir({ workDir });
163
+ const reportOutDir = core_1.fileManager.getReportOutDir({ workDir });
138
164
  fs_extra_1.default.emptyDirSync(reportOutDir);
139
165
  const heap = yield this.getHeap(options);
140
- const nodes = this.getHeapNodes(heap);
166
+ const nodes = yield this.getHeapNodes(heap);
141
167
  new CliScreen_1.default('memlab heap viewer', heap, nodes).start();
142
168
  });
143
169
  }
@@ -3,7 +3,7 @@ import { ComponentDataItem } from './HeapViewUtils';
3
3
  export default class CliScreen {
4
4
  private screen;
5
5
  private objectBox;
6
- private parentObjectBox;
6
+ private clusteredObjectBox;
7
7
  private referrerBox;
8
8
  private referenceBox;
9
9
  private objectPropertyBox;
@@ -11,7 +11,8 @@ export default class CliScreen {
11
11
  private currentFocuseKey;
12
12
  private keyToComponent;
13
13
  private heapController;
14
- constructor(title: string, heap: IHeapSnapshot, nodes: ComponentDataItem[]);
14
+ constructor(title: string, heap: IHeapSnapshot, objectCategory: Map<string, ComponentDataItem[]>);
15
+ private setFirstObjectAsCurrrent;
15
16
  private initScreen;
16
17
  private initCallbacks;
17
18
  start(): void;
@@ -21,9 +22,8 @@ export default class CliScreen {
21
22
  private registerKeys;
22
23
  private addComponentToFocusKeyMap;
23
24
  private getNextFocusKey;
24
- private getLabel;
25
- private initParentObjectBox;
26
- private getParentObjectBoxSize;
25
+ private initClusteredObjectBox;
26
+ private getClusteredObjectBoxSize;
27
27
  private initReferrerBox;
28
28
  private getReferrerBoxSize;
29
29
  private initObjectBox;
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const HeapViewUtils_1 = require("./HeapViewUtils");
7
- const chalk_1 = __importDefault(require("chalk"));
8
7
  const blessed_1 = __importDefault(require("blessed"));
9
8
  const ListComponent_1 = __importDefault(require("./ListComponent"));
10
9
  const HeapViewController_1 = __importDefault(require("./HeapViewController"));
@@ -16,7 +15,7 @@ function positionToNumber(info) {
16
15
  * all the UI components in CLI.
17
16
  *
18
17
  * Screen Layout:
19
- * ┌─Referrers of [*] ─┐┌─Objects ──────────┐┌─References ───────┐
18
+ * ┌─Referrers ────────┐┌─Clustered Objects ┐┌─References ───────┐
20
19
  * │ ││ ││ │
21
20
  * │ ││ ││ │
22
21
  * │ ││ ││ │
@@ -26,7 +25,7 @@ function positionToNumber(info) {
26
25
  * │ ││ ││ │
27
26
  * │ ││ ││ │
28
27
  * └───────────────────┘│ ││ │
29
- * ┌─Referrers ────────┐│ ││ │
28
+ * ┌─Objects ──────────┐│ ││ │
30
29
  * │ ││ ││ │
31
30
  * │ │└───────────────────┘│ │
32
31
  * │ │┌─Object Detail ────┐│ │
@@ -38,18 +37,18 @@ function positionToNumber(info) {
38
37
  * └───────────────────┘└───────────────────┘└───────────────────┘
39
38
  */
40
39
  class CliScreen {
41
- constructor(title, heap, nodes) {
40
+ constructor(title, heap, objectCategory) {
42
41
  this.currentFocuseKey = 1;
43
- this.heapController = new HeapViewController_1.default(heap, nodes);
42
+ this.heapController = new HeapViewController_1.default(heap, objectCategory);
44
43
  this.screen = this.initScreen(title);
45
44
  const callbacks = this.initCallbacks(this.heapController, this.screen);
46
45
  this.keyToComponent = new Map();
47
46
  this.referrerBox = this.initReferrerBox(callbacks);
48
47
  this.heapController.setReferrerBox(this.referrerBox);
49
- this.parentObjectBox = this.initParentObjectBox(callbacks);
50
- this.heapController.setParentBox(this.parentObjectBox);
51
48
  this.objectBox = this.initObjectBox(callbacks);
52
49
  this.heapController.setObjectBox(this.objectBox);
50
+ this.clusteredObjectBox = this.initClusteredObjectBox(callbacks);
51
+ this.heapController.setClusteredBox(this.clusteredObjectBox);
53
52
  this.referenceBox = this.initReferenceBox(callbacks);
54
53
  this.heapController.setReferenceBox(this.referenceBox);
55
54
  this.objectPropertyBox = this.initObjectPropertyBox(callbacks);
@@ -57,6 +56,17 @@ class CliScreen {
57
56
  this.retainerTraceBox = this.initRetainerTraceBox(callbacks);
58
57
  this.heapController.setRetainerTraceBox(this.retainerTraceBox);
59
58
  this.registerEvents();
59
+ this.setFirstObjectAsCurrrent(objectCategory);
60
+ }
61
+ setFirstObjectAsCurrrent(objectCategory) {
62
+ const keys = Array.from(objectCategory.keys());
63
+ if (keys.length === 0) {
64
+ return;
65
+ }
66
+ const nodes = objectCategory.get(keys[0]);
67
+ if (!nodes || nodes.length === 0) {
68
+ return;
69
+ }
60
70
  this.heapController.setCurrentHeapObject((0, HeapViewUtils_1.getHeapObjectAt)(nodes, 0));
61
71
  }
62
72
  initScreen(title) {
@@ -97,7 +107,7 @@ class CliScreen {
97
107
  const screen = this.screen;
98
108
  screen.on('resize', () => {
99
109
  // all boxes/lists needs to resize
100
- this.updateComponentSize(this.parentObjectBox, this.getParentObjectBoxSize());
110
+ this.updateComponentSize(this.clusteredObjectBox, this.getClusteredObjectBoxSize());
101
111
  this.updateComponentSize(this.referrerBox, this.getReferrerBoxSize());
102
112
  this.updateComponentSize(this.objectBox, this.getObjectBoxSize());
103
113
  this.updateComponentSize(this.objectPropertyBox, this.getObjectPropertyBoxSize());
@@ -138,26 +148,26 @@ class CliScreen {
138
148
  getNextFocusKey() {
139
149
  return `${this.currentFocuseKey}`;
140
150
  }
141
- getLabel(text, key) {
142
- return `${text}` + chalk_1.default.grey(` (press ${chalk_1.default.inverse(key)} to focus)`);
143
- }
144
- initParentObjectBox(callbacks) {
145
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getParentObjectBoxSize()), { label: this.getLabel('Referrers of Current', this.getNextFocusKey()) }));
151
+ initClusteredObjectBox(callbacks) {
152
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getClusteredObjectBoxSize()));
153
+ box.setFocusKey(this.getNextFocusKey());
154
+ box.setLabel('Clustered Objects');
146
155
  this.screen.append(box.element);
147
156
  this.addComponentToFocusKeyMap(box);
148
157
  return box;
149
158
  }
150
- getParentObjectBoxSize() {
159
+ getClusteredObjectBoxSize() {
151
160
  return {
152
161
  width: Math.floor(positionToNumber(this.screen.width) / 3),
153
- height: positionToNumber(this.screen.height) -
154
- Math.floor(positionToNumber(this.screen.height) / 2),
155
- top: Math.floor(positionToNumber(this.screen.height) / 2),
156
- left: 0,
162
+ height: Math.floor((2 * positionToNumber(this.screen.height)) / 3),
163
+ top: 0,
164
+ left: Math.floor(positionToNumber(this.screen.width) / 3),
157
165
  };
158
166
  }
159
167
  initReferrerBox(callbacks) {
160
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getReferrerBoxSize()), { label: this.getLabel('Referrers', this.getNextFocusKey()) }));
168
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getReferrerBoxSize()));
169
+ box.setFocusKey(this.getNextFocusKey());
170
+ box.setLabel('Referrers');
161
171
  this.screen.append(box.element);
162
172
  this.addComponentToFocusKeyMap(box);
163
173
  return box;
@@ -171,7 +181,9 @@ class CliScreen {
171
181
  };
172
182
  }
173
183
  initObjectBox(callbacks) {
174
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getObjectBoxSize()), { label: this.getLabel('Objects', this.getNextFocusKey()) }));
184
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getObjectBoxSize()));
185
+ box.setFocusKey(this.getNextFocusKey());
186
+ box.setLabel('Objects');
175
187
  this.screen.append(box.element);
176
188
  this.addComponentToFocusKeyMap(box);
177
189
  return box;
@@ -179,13 +191,16 @@ class CliScreen {
179
191
  getObjectBoxSize() {
180
192
  return {
181
193
  width: Math.floor(positionToNumber(this.screen.width) / 3),
182
- height: Math.floor((2 * positionToNumber(this.screen.height)) / 3),
183
- top: 0,
184
- left: Math.floor(positionToNumber(this.screen.width) / 3),
194
+ height: positionToNumber(this.screen.height) -
195
+ Math.floor(positionToNumber(this.screen.height) / 2),
196
+ top: Math.floor(positionToNumber(this.screen.height) / 2),
197
+ left: 0,
185
198
  };
186
199
  }
187
200
  initObjectPropertyBox(callbacks) {
188
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getObjectPropertyBoxSize()), { label: this.getLabel('Object Detail', this.getNextFocusKey()) }));
201
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getObjectPropertyBoxSize()));
202
+ box.setFocusKey(this.getNextFocusKey());
203
+ box.setLabel('Objects Detail');
189
204
  this.screen.append(box.element);
190
205
  this.addComponentToFocusKeyMap(box);
191
206
  return box;
@@ -200,7 +215,9 @@ class CliScreen {
200
215
  };
201
216
  }
202
217
  initReferenceBox(callbacks) {
203
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getReferenceBoxSize()), { label: this.getLabel('References', this.getNextFocusKey()) }));
218
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getReferenceBoxSize()));
219
+ box.setFocusKey(this.getNextFocusKey());
220
+ box.setLabel('References');
204
221
  this.screen.append(box.element);
205
222
  this.addComponentToFocusKeyMap(box);
206
223
  return box;
@@ -215,7 +232,9 @@ class CliScreen {
215
232
  };
216
233
  }
217
234
  initRetainerTraceBox(callbacks) {
218
- const box = new ListComponent_1.default([], callbacks, Object.assign(Object.assign({}, this.getRetainerTraceBoxSize()), { label: this.getLabel('Retainer Trace', this.getNextFocusKey()) }));
235
+ const box = new ListComponent_1.default([], callbacks, Object.assign({}, this.getRetainerTraceBoxSize()));
236
+ box.setFocusKey(this.getNextFocusKey());
237
+ box.setLabel('Retainer Trace');
219
238
  this.screen.append(box.element);
220
239
  this.addComponentToFocusKeyMap(box);
221
240
  return box;
@@ -15,7 +15,9 @@ declare type SelectHeapObjectOption = {
15
15
  noChangeInReferrerBox?: boolean;
16
16
  noChangeInRetainerTraceBox?: boolean;
17
17
  noChangeInObjectPropertyBox?: boolean;
18
+ componentDataItem?: ComponentDataItem;
18
19
  };
20
+ export declare type ObjectCategory = Map<string, ComponentDataItem[]>;
19
21
  /**
20
22
  * HeapViewController managers all the data associated with each
21
23
  * UI components in CLI and coordinates the events/interaction
@@ -25,18 +27,24 @@ export default class HeapViewController {
25
27
  private currentHeapObject;
26
28
  private selectedHeapObject;
27
29
  private currentHeapObjectsInfo;
30
+ private currentClusteredObjectsInfo;
28
31
  private componentIdToDataMap;
29
32
  private componentIdToComponentMap;
30
33
  private heap;
31
- private parentBox;
34
+ private clusteredBox;
32
35
  private referrerBox;
33
36
  private objectBox;
34
37
  private referenceBox;
35
38
  private objectPropertyBox;
36
39
  private retainerTracePropertyBox;
37
- constructor(heap: IHeapSnapshot, nodes: ComponentDataItem[]);
40
+ constructor(heap: IHeapSnapshot, objectCategory: ObjectCategory);
41
+ private getFlattenHeapObjectsInfo;
42
+ private getFlattenClusteredObjectsInfo;
43
+ private shouldClusterCategory;
44
+ private clusterComponentDataItems;
38
45
  getContent(componentId: number): string[];
39
- setParentBox(component: ListComponent): void;
46
+ setClusteredBox(component: ListComponent): void;
47
+ getClusteredBoxData(): ComponentData;
40
48
  setReferrerBox(component: ListComponent): void;
41
49
  getReferrerBoxData(node?: IHeapNode): ComponentData;
42
50
  setObjectBox(component: ListComponent): void;
@@ -44,12 +52,19 @@ export default class HeapViewController {
44
52
  setReferenceBox(component: ListComponent): void;
45
53
  getReferenceBoxData(): ComponentData;
46
54
  setObjectPropertyBox(component: ListComponent): void;
47
- getObjectPropertyData(): ComponentData;
55
+ getObjectPropertyData(options?: {
56
+ details?: Map<string, string>;
57
+ }): ComponentData;
58
+ private getReadableString;
48
59
  private getKeyValuePairString;
49
60
  setRetainerTraceBox(component: ListComponent): void;
50
61
  getRetainerTraceData(): ComponentData;
51
- setCurrentHeapObjectFromComponent(componentId: number, itemIndex: number): void;
52
- setCurrentHeapObject(node: IHeapNode): void;
62
+ setCurrentHeapObjectFromComponent(componentId: number, itemIndex: number, options?: {
63
+ skipFocus?: boolean;
64
+ }): void;
65
+ setCurrentHeapObject(node: IHeapNode, options?: {
66
+ skipFocus?: boolean;
67
+ }): void;
53
68
  focusOnComponent(componentId: number): void;
54
69
  setSelectedHeapObjectFromComponent(componentId: number, itemIndex: number): void;
55
70
  setSelectedHeapObject(node: IHeapNode, options?: SelectHeapObjectOption): void;
@@ -12,13 +12,60 @@ const HeapViewUtils_1 = require("./HeapViewUtils");
12
12
  * among all UI components.
13
13
  */
14
14
  class HeapViewController {
15
- constructor(heap, nodes) {
15
+ constructor(heap, objectCategory) {
16
16
  this.heap = heap;
17
- this.currentHeapObject = (0, HeapViewUtils_1.getHeapObjectAt)(nodes, 0);
18
- this.currentHeapObjectsInfo = nodes;
17
+ this.currentHeapObjectsInfo =
18
+ this.getFlattenHeapObjectsInfo(objectCategory);
19
+ this.currentClusteredObjectsInfo =
20
+ this.getFlattenClusteredObjectsInfo(objectCategory);
21
+ this.currentHeapObject = (0, HeapViewUtils_1.getHeapObjectAt)(this.currentHeapObjectsInfo, 0);
19
22
  this.componentIdToDataMap = new Map();
20
23
  this.componentIdToComponentMap = new Map();
21
24
  }
25
+ getFlattenHeapObjectsInfo(objectCategory) {
26
+ let ret = [];
27
+ for (const category of objectCategory.keys()) {
28
+ const nodes = objectCategory.get(category);
29
+ ret = [...ret, ...nodes];
30
+ }
31
+ return ret;
32
+ }
33
+ getFlattenClusteredObjectsInfo(objectCategory) {
34
+ let ret = [];
35
+ for (const category of objectCategory.keys()) {
36
+ let nodes = objectCategory.get(category);
37
+ if (this.shouldClusterCategory(category)) {
38
+ nodes = this.clusterComponentDataItems(nodes);
39
+ }
40
+ ret = [...ret, ...nodes];
41
+ }
42
+ return ret;
43
+ }
44
+ shouldClusterCategory(category) {
45
+ return category === 'detached';
46
+ }
47
+ clusterComponentDataItems(nodes) {
48
+ const ret = [];
49
+ const nodeIds = new Set(nodes
50
+ .filter(node => node.heapObject)
51
+ .map(node => { var _a, _b; return (_b = (_a = node.heapObject) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : -1; }));
52
+ const clusters = core_1.analysis.clusterHeapObjects(nodeIds, this.heap);
53
+ clusters.forEach(cluster => {
54
+ var _a;
55
+ const id = (_a = cluster.id) !== null && _a !== void 0 ? _a : -1;
56
+ let node = null;
57
+ node = this.heap.getNodeById(id);
58
+ const details = new Map();
59
+ if (cluster.count) {
60
+ details.set('# of clusters', `${cluster.count}`);
61
+ }
62
+ if (cluster.retainedSize) {
63
+ details.set('aggregated retained size', `${core_1.utils.getReadableBytes(cluster.retainedSize)}`);
64
+ }
65
+ ret.push({ tag: 'Cluster', heapObject: node, details });
66
+ });
67
+ return ret;
68
+ }
22
69
  getContent(componentId) {
23
70
  const ret = [];
24
71
  const data = this.componentIdToDataMap.get(componentId);
@@ -29,11 +76,17 @@ class HeapViewController {
29
76
  }
30
77
  return ret;
31
78
  }
32
- setParentBox(component) {
79
+ setClusteredBox(component) {
33
80
  this.componentIdToComponentMap.set(component.id, component);
34
- this.parentBox = component;
81
+ this.clusteredBox = component;
35
82
  this.componentIdToDataMap.set(component.id, new HeapViewUtils_1.ComponentData());
36
83
  }
84
+ getClusteredBoxData() {
85
+ const data = new HeapViewUtils_1.ComponentData();
86
+ data.selectedIdx = 0;
87
+ data.items = this.currentClusteredObjectsInfo;
88
+ return data;
89
+ }
37
90
  setReferrerBox(component) {
38
91
  this.componentIdToComponentMap.set(component.id, component);
39
92
  this.referrerBox = component;
@@ -82,6 +135,7 @@ class HeapViewController {
82
135
  data.items.push({ referrerEdge: ref, heapObject: ref.toNode });
83
136
  return { stop: false };
84
137
  });
138
+ data.items.sort((i1, i2) => { var _a, _b, _c, _d; return ((_b = (_a = i2.heapObject) === null || _a === void 0 ? void 0 : _a.retainedSize) !== null && _b !== void 0 ? _b : 0) - ((_d = (_c = i1.heapObject) === null || _c === void 0 ? void 0 : _c.retainedSize) !== null && _d !== void 0 ? _d : 0); });
85
139
  data.selectedIdx = data.items.length > 0 ? 0 : -1;
86
140
  return data;
87
141
  }
@@ -90,7 +144,8 @@ class HeapViewController {
90
144
  this.objectPropertyBox = component;
91
145
  this.componentIdToDataMap.set(component.id, new HeapViewUtils_1.ComponentData());
92
146
  }
93
- getObjectPropertyData() {
147
+ getObjectPropertyData(options = {}) {
148
+ var _a, _b;
94
149
  const data = new HeapViewUtils_1.ComponentData();
95
150
  const node = this.selectedHeapObject;
96
151
  data.items.push({
@@ -109,10 +164,10 @@ class HeapViewController {
109
164
  stringContent: this.getKeyValuePairString('retained size', core_1.utils.getReadableBytes(node.retainedSize)),
110
165
  });
111
166
  data.items.push({
112
- stringContent: this.getKeyValuePairString('# of references', core_1.utils.getReadableBytes(node.edge_count)),
167
+ stringContent: this.getKeyValuePairString('# of references', node.edge_count),
113
168
  });
114
169
  data.items.push({
115
- stringContent: this.getKeyValuePairString('# of referrers', core_1.utils.getReadableBytes(node.referrers.length)),
170
+ stringContent: this.getKeyValuePairString('# of referrers', node.referrers.length),
116
171
  });
117
172
  if (node.dominatorNode) {
118
173
  data.items.push({
@@ -120,9 +175,46 @@ class HeapViewController {
120
175
  heapObject: node.dominatorNode,
121
176
  });
122
177
  }
178
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
179
+ const self = this;
180
+ (_a = options.details) === null || _a === void 0 ? void 0 : _a.forEach((value, key) => data.items.push({
181
+ stringContent: self.getKeyValuePairString(key, value),
182
+ }));
183
+ // inject additional node information
184
+ if (node.isString) {
185
+ const stringNode = node.toStringNode();
186
+ if (stringNode) {
187
+ const value = this.getReadableString(stringNode.stringValue);
188
+ data.items.push({
189
+ stringContent: this.getKeyValuePairString('string value', value),
190
+ });
191
+ }
192
+ }
193
+ if (node.type === 'number') {
194
+ data.items.push({
195
+ stringContent: this.getKeyValuePairString('numeric value', (_b = core_1.utils.getNumberNodeValue(node)) !== null && _b !== void 0 ? _b : '<error>'),
196
+ });
197
+ }
198
+ if (node.type === 'closure') {
199
+ const contextNode = node.getReferenceNode('context', 'internal');
200
+ if (contextNode) {
201
+ contextNode.forEachReference(edge => {
202
+ data.items.push({
203
+ tag: chalk_1.default.grey('Scope Variable'),
204
+ referrerEdge: edge,
205
+ heapObject: edge.toNode,
206
+ });
207
+ });
208
+ }
209
+ }
123
210
  data.selectedIdx = data.items.length > 0 ? 0 : -1;
124
211
  return data;
125
212
  }
213
+ getReadableString(value) {
214
+ return value.length > 300
215
+ ? value.substring(0, 300) + chalk_1.default.grey('...')
216
+ : value;
217
+ }
126
218
  getKeyValuePairString(key, value) {
127
219
  return key + chalk_1.default.grey(': ') + chalk_1.default.green(value);
128
220
  }
@@ -150,7 +242,7 @@ class HeapViewController {
150
242
  data.selectedIdx = data.items.length > 0 ? 0 : -1;
151
243
  return data;
152
244
  }
153
- setCurrentHeapObjectFromComponent(componentId, itemIndex) {
245
+ setCurrentHeapObjectFromComponent(componentId, itemIndex, options = {}) {
154
246
  const data = this.componentIdToDataMap.get(componentId);
155
247
  if (!data) {
156
248
  return;
@@ -163,27 +255,33 @@ class HeapViewController {
163
255
  if (!heapObject) {
164
256
  return;
165
257
  }
166
- this.setCurrentHeapObject(heapObject);
258
+ this.setCurrentHeapObject(heapObject, options);
167
259
  }
168
- setCurrentHeapObject(node) {
260
+ setCurrentHeapObject(node, options = {}) {
169
261
  this.currentHeapObject = node;
170
262
  // set parent box's data and content
171
- const parentBoxData = this.getReferrerBoxData(this.currentHeapObject);
172
- this.componentIdToDataMap.set(this.parentBox.id, parentBoxData);
173
- this.parentBox.setContent(this.getContent(this.parentBox.id));
174
- this.parentBox.selectIndex(parentBoxData.selectedIdx);
263
+ const clusteredBoxData = this.getClusteredBoxData();
264
+ this.componentIdToDataMap.set(this.clusteredBox.id, clusteredBoxData);
265
+ this.clusteredBox.setContent(this.getContent(this.clusteredBox.id));
266
+ this.clusteredBox.selectIndex(clusteredBoxData.selectedIdx);
175
267
  // set object box's data and content
176
268
  const objectBoxData = this.getObjectBoxData();
177
269
  this.componentIdToDataMap.set(this.objectBox.id, objectBoxData);
178
270
  this.objectBox.setContent(this.getContent(this.objectBox.id));
179
271
  this.objectBox.selectIndex(objectBoxData.selectedIdx);
180
272
  this.setSelectedHeapObject(node);
181
- this.focusOnComponent(this.objectBox.id);
273
+ if (!options.skipFocus) {
274
+ this.focusOnComponent(this.objectBox.id);
275
+ }
182
276
  }
183
277
  focusOnComponent(componentId) {
278
+ var _a;
184
279
  for (const component of this.componentIdToComponentMap.values()) {
185
280
  if (component.id === componentId) {
186
281
  component.focus();
282
+ const data = this.componentIdToDataMap.get(componentId);
283
+ const selectIndex = (_a = (data && data.selectedIdx)) !== null && _a !== void 0 ? _a : -1;
284
+ this.setSelectedHeapObjectFromComponent(componentId, selectIndex);
187
285
  }
188
286
  else {
189
287
  component.loseFocus();
@@ -214,9 +312,11 @@ class HeapViewController {
214
312
  noChangeInReferrerBox,
215
313
  noChangeInRetainerTraceBox,
216
314
  noChangeInObjectPropertyBox,
315
+ componentDataItem: item,
217
316
  });
218
317
  }
219
318
  setSelectedHeapObject(node, options = {}) {
319
+ var _a;
220
320
  this.selectedHeapObject = node;
221
321
  // set referrer box's data and content
222
322
  if (!options.noChangeInReferrerBox) {
@@ -224,6 +324,7 @@ class HeapViewController {
224
324
  this.componentIdToDataMap.set(this.referrerBox.id, data);
225
325
  this.referrerBox.setContent(this.getContent(this.referrerBox.id));
226
326
  this.referrerBox.selectIndex(data.selectedIdx);
327
+ this.referrerBox.setLabel(`Referrers of @${node.id}`);
227
328
  }
228
329
  // set reference box's data and content
229
330
  if (!options.noChangeInReferenceBox) {
@@ -231,13 +332,18 @@ class HeapViewController {
231
332
  this.componentIdToDataMap.set(this.referenceBox.id, data);
232
333
  this.referenceBox.setContent(this.getContent(this.referenceBox.id));
233
334
  this.referenceBox.selectIndex(data.selectedIdx);
335
+ this.referenceBox.setLabel(`References of @${node.id}`);
234
336
  }
235
337
  // set object property box's data and content
236
338
  if (!options.noChangeInObjectPropertyBox) {
237
- const data = this.getObjectPropertyData();
339
+ const propertyOption = ((_a = options === null || options === void 0 ? void 0 : options.componentDataItem) === null || _a === void 0 ? void 0 : _a.details)
340
+ ? { details: options.componentDataItem.details }
341
+ : {};
342
+ const data = this.getObjectPropertyData(propertyOption);
238
343
  this.componentIdToDataMap.set(this.objectPropertyBox.id, data);
239
344
  this.objectPropertyBox.setContent(this.getContent(this.objectPropertyBox.id));
240
345
  this.objectPropertyBox.selectIndex(data.selectedIdx);
346
+ this.objectPropertyBox.setLabel(`Object: @${node.id}`);
241
347
  }
242
348
  // set retainer trace box's data and content
243
349
  if (!options.noChangeInRetainerTraceBox) {
@@ -245,6 +351,7 @@ class HeapViewController {
245
351
  this.componentIdToDataMap.set(this.retainerTracePropertyBox.id, data);
246
352
  this.retainerTracePropertyBox.setContent(this.getContent(this.retainerTracePropertyBox.id));
247
353
  this.retainerTracePropertyBox.selectIndex(data.selectedIdx);
354
+ this.retainerTracePropertyBox.setLabel(`Retainer Trace of @${node.id}`);
248
355
  }
249
356
  }
250
357
  }
@@ -4,8 +4,8 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @emails oncall+ws_labs
8
7
  * @format
8
+ * @oncall ws_labs
9
9
  */
10
10
  import type { IHeapEdge, IHeapNode } from '@memlab/core';
11
11
  export declare class ComponentDataItem {
@@ -15,7 +15,9 @@ export declare class ComponentDataItem {
15
15
  heapObject?: IHeapNode;
16
16
  referenceEdge?: IHeapEdge;
17
17
  type?: string;
18
+ details?: Map<string, string>;
18
19
  static getTextForDisplay(data: ComponentDataItem): string;
20
+ private static getTextContent;
19
21
  }
20
22
  export declare class ComponentData {
21
23
  selectedIdx: number;
@@ -23,6 +25,7 @@ export declare class ComponentData {
23
25
  }
24
26
  export declare function throwIfNodesEmpty(nodes: ComponentDataItem[]): boolean;
25
27
  export declare function getHeapObjectAt(nodes: ComponentDataItem[], index: number): IHeapNode;
28
+ export declare function substringWithColor(input: string, begin: number): string;
26
29
  export declare type DebounceCallback = () => void;
27
30
  export declare type DebounceFunction = (callback: DebounceCallback) => void;
28
31
  export declare function debounce(timeInMs: number): DebounceFunction;
@@ -3,11 +3,70 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.debounce = exports.getHeapObjectAt = exports.throwIfNodesEmpty = exports.ComponentData = exports.ComponentDataItem = void 0;
6
+ exports.debounce = exports.substringWithColor = exports.getHeapObjectAt = exports.throwIfNodesEmpty = exports.ComponentData = exports.ComponentDataItem = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const core_1 = require("@memlab/core");
9
+ const lessUsefulEdgeTypeForDebugging = new Set([
10
+ 'internal',
11
+ 'hidden',
12
+ 'shortcut',
13
+ 'weak',
14
+ ]);
15
+ const reactEdgeNames = new Set([
16
+ 'alternate',
17
+ 'firstEffect',
18
+ 'lastEffect',
19
+ 'concurrentQueues',
20
+ 'child',
21
+ 'return',
22
+ 'sibling',
23
+ ]);
24
+ function isUsefulEdgeForDebugging(edge) {
25
+ if (lessUsefulEdgeTypeForDebugging.has(edge.type)) {
26
+ return false;
27
+ }
28
+ const edgeStr = `${edge.name_or_index}`;
29
+ if (reactEdgeNames.has(edgeStr)) {
30
+ if (core_1.utils.isFiberNode(edge.fromNode) || core_1.utils.isFiberNode(edge.toNode)) {
31
+ return false;
32
+ }
33
+ }
34
+ if (edgeStr.startsWith('__reactProps$')) {
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ const lessUsefulObjectTypeForDebugging = new Set([
40
+ 'native',
41
+ 'hidden',
42
+ 'array',
43
+ 'code',
44
+ 'synthetic',
45
+ ]);
46
+ function isUsefulObjectForDebugging(object) {
47
+ if (lessUsefulObjectTypeForDebugging.has(object.type)) {
48
+ return false;
49
+ }
50
+ return !core_1.utils.isFiberNode(object);
51
+ }
9
52
  class ComponentDataItem {
10
53
  static getTextForDisplay(data) {
54
+ const content = ComponentDataItem.getTextContent(data);
55
+ if (data.referrerEdge && isUsefulEdgeForDebugging(data.referrerEdge)) {
56
+ return content;
57
+ }
58
+ if (data.referenceEdge && isUsefulEdgeForDebugging(data.referenceEdge)) {
59
+ return content;
60
+ }
61
+ if (data.heapObject && isUsefulObjectForDebugging(data.heapObject)) {
62
+ return content;
63
+ }
64
+ if (!data.referenceEdge && !data.heapObject && !data.referrerEdge) {
65
+ return content;
66
+ }
67
+ return chalk_1.default.grey(content);
68
+ }
69
+ static getTextContent(data) {
11
70
  let ret = '';
12
71
  if (data.tag) {
13
72
  ret += `[${data.tag}] `;
@@ -28,7 +87,11 @@ class ComponentDataItem {
28
87
  const size = core_1.utils.getReadableBytes(data.heapObject.retainedSize);
29
88
  const sizeInfo = chalk_1.default.grey(' [') + chalk_1.default.bold(chalk_1.default.blue(size)) + chalk_1.default.grey(']');
30
89
  ret +=
31
- chalk_1.default.green(`[${data.heapObject.name}]`) +
90
+ chalk_1.default.green('[') +
91
+ (isUsefulObjectForDebugging(data.heapObject)
92
+ ? chalk_1.default.green(data.heapObject.name)
93
+ : chalk_1.default.bold(chalk_1.default.grey(data.heapObject.name))) +
94
+ chalk_1.default.green(']') +
32
95
  objectType +
33
96
  objectId +
34
97
  sizeInfo;
@@ -69,6 +132,64 @@ function getHeapObjectAt(nodes, index) {
69
132
  return nodes[index].heapObject;
70
133
  }
71
134
  exports.getHeapObjectAt = getHeapObjectAt;
135
+ // eslint-disable-next-line no-control-regex
136
+ const colorBegin = /^\u001b\[(\d+)m/;
137
+ // eslint-disable-next-line no-control-regex
138
+ const colorEnd = /^\u001b\](\d+)m/;
139
+ function stripColorCodeIfAny(input) {
140
+ const matchBegin = input.match(colorBegin);
141
+ const matchEnd = input.match(colorEnd);
142
+ const match = matchBegin || matchEnd;
143
+ if (!match) {
144
+ return { str: input, code: -1, isBegin: false };
145
+ }
146
+ const isBegin = !!matchBegin;
147
+ const code = parseInt(match[1], 10);
148
+ const str = input.substring(match[0].length);
149
+ return { str, code, isBegin };
150
+ }
151
+ function toColorControlChar(code, isBegin) {
152
+ const colorSpecialChar = '\u001b';
153
+ return colorSpecialChar + (isBegin ? '[' : ']') + code + 'm';
154
+ }
155
+ function substringWithColor(input, begin) {
156
+ const codeQueue = [];
157
+ let curIndex = 0;
158
+ let curStr = input;
159
+ while (curIndex < begin) {
160
+ // enqueue all control characters
161
+ let strip;
162
+ do {
163
+ strip = stripColorCodeIfAny(curStr);
164
+ curStr = strip.str;
165
+ if (strip.code >= 0) {
166
+ // pop if control begin meets control ends
167
+ const last = codeQueue[codeQueue.length - 1];
168
+ if (!last ||
169
+ last.code !== strip.code ||
170
+ strip.isBegin === true ||
171
+ last.isBegin === false) {
172
+ codeQueue.push({ code: strip.code, isBegin: strip.isBegin });
173
+ }
174
+ else {
175
+ codeQueue.pop();
176
+ }
177
+ }
178
+ } while (strip.code >= 0);
179
+ // strip one actual content character
180
+ curStr = curStr.substring(1);
181
+ ++curIndex;
182
+ }
183
+ // prepend control characters
184
+ while (codeQueue.length > 0) {
185
+ const last = codeQueue.pop();
186
+ if (last) {
187
+ curStr = toColorControlChar(last === null || last === void 0 ? void 0 : last.code, last === null || last === void 0 ? void 0 : last.isBegin) + curStr;
188
+ }
189
+ }
190
+ return curStr;
191
+ }
192
+ exports.substringWithColor = substringWithColor;
72
193
  function debounce(timeInMs) {
73
194
  let id = null;
74
195
  return (callback) => {
@@ -13,7 +13,7 @@ export declare type ListComponentOption = {
13
13
  height: number;
14
14
  left: number;
15
15
  top: number;
16
- label: string;
16
+ label?: string;
17
17
  };
18
18
  export declare type ListItemSelectInfo = {
19
19
  keyName: string;
@@ -38,6 +38,7 @@ export default class ListComponent {
38
38
  private horizonScrollPositionMap;
39
39
  private displayedItems;
40
40
  private moreEntryIndex;
41
+ private focusKey;
41
42
  private static readonly ListContentLimit;
42
43
  private static readonly loadMore;
43
44
  private static nextComponentId;
@@ -51,6 +52,8 @@ export default class ListComponent {
51
52
  focus(): void;
52
53
  loseFocus(): void;
53
54
  selectIndex(index: number): void;
55
+ setFocusKey(key: string): void;
56
+ setLabel(label: string): void;
54
57
  setContent(content: string[]): void;
55
58
  loadMoreContent(): void;
56
59
  private removeDisplayMoreEntry;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const blessed_1 = __importDefault(require("blessed"));
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const string_width_1 = __importDefault(require("string-width"));
9
+ const HeapViewUtils_1 = require("./HeapViewUtils");
9
10
  /**
10
11
  * A ListComponent is an UI list component in CLI.
11
12
  * It managers all the UI events related to the
@@ -16,11 +17,12 @@ class ListComponent {
16
17
  this.listIndex = 0;
17
18
  this.content = [];
18
19
  this.moreEntryIndex = -1;
20
+ this.focusKey = '';
19
21
  this.id = ListComponent.nextId();
20
22
  this.horizonScrollPositionMap = new Map();
21
23
  this.callbacks = callbacks;
22
24
  // init list element
23
- this.element = blessed_1.default.list(Object.assign(Object.assign({}, options), { tags: true, scrollable: true, keys: true, border: {
25
+ this.element = blessed_1.default.list(Object.assign(Object.assign({}, options), { tags: false, scrollable: true, keys: true, border: {
24
26
  fg: 'grey',
25
27
  type: 'line',
26
28
  }, style: {
@@ -106,7 +108,7 @@ class ListComponent {
106
108
  return;
107
109
  }
108
110
  this.horizonScrollPositionMap.set(this.listIndex, --offset);
109
- let newContent = selectedContent.substring(offset);
111
+ let newContent = (0, HeapViewUtils_1.substringWithColor)(selectedContent, offset);
110
112
  if (offset > 0) {
111
113
  newContent = chalk_1.default.grey('...') + newContent;
112
114
  }
@@ -124,7 +126,7 @@ class ListComponent {
124
126
  offset = (_a = this.horizonScrollPositionMap.get(this.listIndex)) !== null && _a !== void 0 ? _a : 0;
125
127
  }
126
128
  this.horizonScrollPositionMap.set(this.listIndex, ++offset);
127
- let newContent = selectedContent.substring(offset);
129
+ let newContent = (0, HeapViewUtils_1.substringWithColor)(selectedContent, offset);
128
130
  if (offset > 0) {
129
131
  newContent = chalk_1.default.grey('...') + newContent;
130
132
  }
@@ -134,10 +136,18 @@ class ListComponent {
134
136
  focus() {
135
137
  this.element.focus();
136
138
  this.element.style.border.fg = 'white';
139
+ this.element.style.selected = {
140
+ bg: 'grey',
141
+ bold: true,
142
+ };
137
143
  this.getFocus();
138
144
  }
139
145
  loseFocus() {
140
146
  this.element.style.border.fg = 'grey';
147
+ this.element.style.selected = {
148
+ bg: 'black',
149
+ bold: false,
150
+ };
141
151
  }
142
152
  selectIndex(index) {
143
153
  while (this.displayedItems <= index &&
@@ -147,6 +157,13 @@ class ListComponent {
147
157
  this.listIndex = index;
148
158
  this.element.select(index);
149
159
  }
160
+ setFocusKey(key) {
161
+ this.focusKey = key;
162
+ }
163
+ setLabel(label) {
164
+ const componentLabel = label + chalk_1.default.grey(` (press ${chalk_1.default.inverse(this.focusKey)} to focus)`);
165
+ this.element.setLabel(componentLabel);
166
+ }
150
167
  setContent(content) {
151
168
  const oldContent = this.content;
152
169
  this.element.clearItems();
@@ -216,5 +233,5 @@ class ListComponent {
216
233
  }
217
234
  exports.default = ListComponent;
218
235
  ListComponent.ListContentLimit = 100;
219
- ListComponent.loadMore = 20;
236
+ ListComponent.loadMore = 100;
220
237
  ListComponent.nextComponentId = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "license": "MIT",
5
5
  "description": "command line interface for memlab",
6
6
  "author": "Liang Gong <lgong@fb.com>",
@@ -24,10 +24,10 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@memlab/api": "^1.0.6",
28
- "@memlab/core": "^1.1.6",
29
- "@memlab/e2e": "^1.0.7",
30
- "@memlab/heap-analysis": "^1.0.5",
27
+ "@memlab/api": "^1.0.11",
28
+ "@memlab/core": "^1.1.10",
29
+ "@memlab/e2e": "^1.0.11",
30
+ "@memlab/heap-analysis": "^1.0.7",
31
31
  "ansi": "^0.3.1",
32
32
  "babar": "^0.2.0",
33
33
  "blessed": "^0.1.81",