@memlab/core 1.0.9 → 1.1.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.
@@ -36,7 +36,7 @@ test('Capture inserted object', () => __awaiter(void 0, void 0, void 0, function
36
36
  }
37
37
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
38
  const injected = new TestObject();
39
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
39
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
40
40
  expect(heap.hasObjectWithClassName('TestObject')).toBe(true);
41
41
  }), timeout);
42
42
  test('Does not capture transcient object', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -49,6 +49,6 @@ test('Does not capture transcient object', () => __awaiter(void 0, void 0, void
49
49
  let injected = new TestObject();
50
50
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
51
  injected = null;
52
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
52
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
53
53
  expect(heap.hasObjectWithClassName('TestObject')).toBe(false);
54
54
  }), timeout);
@@ -30,7 +30,7 @@ const timeout = 5 * 60 * 1000;
30
30
  test('Capture current node heap snapshot', () => __awaiter(void 0, void 0, void 0, function* () {
31
31
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
32
  const object = { 'memlab-test-heap-property': 'memlab-test-heap-value' };
33
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
33
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
34
34
  expect(heap.hasObjectWithPropertyName('memlab-test-heap-property')).toBe(true);
35
35
  }), timeout);
36
36
  test('Nullified Object should not exist in heap', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -39,7 +39,7 @@ test('Nullified Object should not exist in heap', () => __awaiter(void 0, void 0
39
39
  };
40
40
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
41
41
  object = null;
42
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
42
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
43
43
  expect(heap.hasObjectWithPropertyName('memlab-test-heap-property')).toBe(false);
44
44
  }), timeout);
45
45
  test('Strongly referenced object should exist in heap', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -59,7 +59,7 @@ test('Strongly referenced object should exist in heap', () => __awaiter(void 0,
59
59
  }
60
60
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
61
  const object = buildTest();
62
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
62
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
63
63
  expect(heap.hasObjectWithClassName('TestClass1')).toBe(true);
64
64
  expect(heap.hasObjectWithClassName('TestClass2')).toBe(true);
65
65
  }), timeout);
@@ -80,7 +80,7 @@ test('Weakly referenced object should not exist in heap', () => __awaiter(void 0
80
80
  }
81
81
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
82
82
  const object = buildTest();
83
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
83
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
84
84
  expect(heap.hasObjectWithClassName('TestClass3')).toBe(true);
85
85
  expect(heap.hasObjectWithClassName('TestClass4')).toBe(false);
86
86
  }), timeout);
@@ -90,7 +90,7 @@ test('Check annotated objects', () => __awaiter(void 0, void 0, void 0, function
90
90
  (0, NodeHeap_1.tagObject)(o1, 'memlab-mark-1');
91
91
  (0, NodeHeap_1.tagObject)(o2, 'memlab-mark-2');
92
92
  o2 = null;
93
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
93
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
94
94
  expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);
95
95
  expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);
96
96
  }), timeout);
@@ -41,7 +41,7 @@ test('String heap object APIs work', () => __awaiter(void 0, void 0, void 0, fun
41
41
  injected.complexConcatString += 'value_';
42
42
  injected.complexConcatString += 123;
43
43
  injected.complexConcatString += '_suffix';
44
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
44
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
45
45
  const testObject = heap.getAnyObjectWithClassName('TestObject');
46
46
  expect(testObject).not.toBe(null);
47
47
  // testObject.originalString === 'test'
@@ -74,7 +74,7 @@ test('Check getReference and getReferenceNode', () => __awaiter(void 0, void 0,
74
74
  });
75
75
  return detected;
76
76
  };
77
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
77
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
78
78
  expect(checker(heap)).toBe(true);
79
79
  }), timeout);
80
80
  test('Check getReferrers and getReferrerNodes', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -135,6 +135,6 @@ test('Check getReferrers and getReferrerNodes', () => __awaiter(void 0, void 0,
135
135
  }
136
136
  return true;
137
137
  };
138
- const heap = yield (0, NodeHeap_1.getCurrentNodeHeap)();
138
+ const heap = yield (0, NodeHeap_1.getNodeInnocentHeap)();
139
139
  expect(checker(heap)).toBe(true);
140
140
  }), timeout);
@@ -139,6 +139,7 @@ export declare class MemLabConfig {
139
139
  waitAfterOperation: number;
140
140
  waitAfterScrolling: number;
141
141
  waitAfterTyping: number;
142
+ waitForNetworkInDefaultScenario: number;
142
143
  stressTestRepeat: number;
143
144
  avoidLeakWithoutDetachedElements: boolean;
144
145
  hideBrowserLeak: boolean;
@@ -192,6 +192,8 @@ class MemLabConfig {
192
192
  this.waitAfterScrolling = 5000;
193
193
  // extra delay after typing
194
194
  this.waitAfterTyping = 1000;
195
+ // page load checker: default wait for network idle in scenario test
196
+ this.waitForNetworkInDefaultScenario = 10000;
195
197
  // default repeat for stress testing
196
198
  this.stressTestRepeat = 5;
197
199
  // only show leaks with detached HTML elements
@@ -61,6 +61,7 @@ declare class MemLabConsole {
61
61
  progress(cur: number, total: number, options?: {
62
62
  message?: string;
63
63
  }): void;
64
+ flush(): void;
64
65
  }
65
66
  declare const _default: MemLabConsole;
66
67
  export default _default;
@@ -340,5 +340,8 @@ class MemLabConsole {
340
340
  const progress = `${message}: |${bar}| ${percent}/100`;
341
341
  this.overwrite(progress, { level: 'top' });
342
342
  }
343
+ flush() {
344
+ this.clearPrevOverwriteMsg();
345
+ }
343
346
  }
344
347
  exports.default = MemLabConsole.getInstance();
@@ -43,6 +43,8 @@ export declare class FileManager {
43
43
  getAllFilesInDir(dir: string): string[];
44
44
  getDataOutDir(options?: FileOption): string;
45
45
  getCoreProjectBaseDir(): string;
46
+ getMonoRepoDir(): string;
47
+ getDocDir(): string;
46
48
  getReportOutDir(options?: FileOption): string;
47
49
  getPreviewReportDir(options?: FileOption): string;
48
50
  getLeakSummaryFile(options?: FileOption): string;
@@ -133,6 +133,12 @@ class FileManager {
133
133
  getCoreProjectBaseDir() {
134
134
  return path_1.default.join(__dirname, '..', '..');
135
135
  }
136
+ getMonoRepoDir() {
137
+ return path_1.default.join(this.getCoreProjectBaseDir(), '..', '..');
138
+ }
139
+ getDocDir() {
140
+ return path_1.default.join(this.getMonoRepoDir(), 'website', 'docs');
141
+ }
136
142
  getReportOutDir(options = {}) {
137
143
  return path_1.default.join(this.getPersistDataDir(options), 'reports');
138
144
  }
@@ -7,10 +7,69 @@
7
7
  * @emails oncall+ws_labs
8
8
  * @format
9
9
  */
10
- import type { AnyValue, IHeapSnapshot } from './Types';
11
- declare type AnyObject = Record<AnyValue, AnyValue>;
12
- export declare function tagObject(o: AnyObject, tag: string): AnyObject;
10
+ import type { IHeapSnapshot } from './Types';
11
+ /**
12
+ * Tags a string marker to an object instance, which can later be checked by
13
+ * {@link hasObjectWithTag}. This API does not modify the object instance in
14
+ * any way (e.g., no additional or hidden properties added to the tagged
15
+ * object).
16
+ *
17
+ * @param o specify the object instance you want to tag, you cannot tag a
18
+ * [primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
19
+ * @param tag marker name to tag on the object instance
20
+ * @returns returns the tagged object instance (same reference as
21
+ * the input argument `o`)
22
+ * * **Examples**:
23
+ * ```typescript
24
+ * import type {IHeapSnapshot, AnyValue} from '@memlab/core';
25
+ * import {config, getNodeInnocentHeap, tagObject} from '@memlab/core';
26
+ *
27
+ * test('memory test', async () => {
28
+ * config.muteConsole = true;
29
+ * const o1: AnyValue = {};
30
+ * let o2: AnyValue = {};
31
+ *
32
+ * // tag o1 with marker: "memlab-mark-1", does not modify o1 in any way
33
+ * tagObject(o1, 'memlab-mark-1');
34
+ * // tag o2 with marker: "memlab-mark-2", does not modify o2 in any way
35
+ * tagObject(o2, 'memlab-mark-2');
36
+ *
37
+ * o2 = null;
38
+ *
39
+ * const heap: IHeapSnapshot = await getNodeInnocentHeap();
40
+ *
41
+ * // expect object with marker "memlab-mark-1" exists
42
+ * expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);
43
+ *
44
+ * // expect object with marker "memlab-mark-2" can be GCed
45
+ * expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);
46
+ *
47
+ * }, 30000);
48
+ * ```
49
+ */
50
+ export declare function tagObject<T extends object>(o: T, tag: string): T;
13
51
  export declare function dumpNodeHeapSnapshot(): string;
14
- export declare function getCurrentNodeHeap(): Promise<IHeapSnapshot>;
15
- export {};
52
+ /**
53
+ * Take a heap snapshot of the current program state
54
+ * and parse it as {@link IHeapSnapshot}. Notice that
55
+ * this API does not calculate some heap analysis meta data
56
+ * for heap analysis. But this also means faster heap parsing.
57
+ *
58
+ * @returns heap representation without heap analysis meta data.
59
+ *
60
+ * If you need to get the heap snapshot with heap analysis meta data
61
+ * use {@link dumpNodeHeapSnapshot} and {@link getHeapFromFile},
62
+ * for example:
63
+ * ```typescript
64
+ * import type {IHeapSnapshot} from '@memlab/core';
65
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
66
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
67
+ *
68
+ * (async function () {
69
+ * const heapFile = dumpNodeHeapSnapshot();
70
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
71
+ * })();
72
+ * ```
73
+ */
74
+ export declare function getNodeInnocentHeap(): Promise<IHeapSnapshot>;
16
75
  //# sourceMappingURL=NodeHeap.d.ts.map
@@ -21,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.getCurrentNodeHeap = exports.dumpNodeHeapSnapshot = exports.tagObject = void 0;
24
+ exports.getNodeInnocentHeap = exports.dumpNodeHeapSnapshot = exports.tagObject = void 0;
25
25
  const fs_extra_1 = __importDefault(require("fs-extra"));
26
26
  const path_1 = __importDefault(require("path"));
27
27
  const v8_1 = __importDefault(require("v8"));
@@ -33,6 +33,45 @@ class MemLabTaggedStore {
33
33
  }
34
34
  }
35
35
  const store = new MemLabTaggedStore();
36
+ /**
37
+ * Tags a string marker to an object instance, which can later be checked by
38
+ * {@link hasObjectWithTag}. This API does not modify the object instance in
39
+ * any way (e.g., no additional or hidden properties added to the tagged
40
+ * object).
41
+ *
42
+ * @param o specify the object instance you want to tag, you cannot tag a
43
+ * [primitive](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
44
+ * @param tag marker name to tag on the object instance
45
+ * @returns returns the tagged object instance (same reference as
46
+ * the input argument `o`)
47
+ * * **Examples**:
48
+ * ```typescript
49
+ * import type {IHeapSnapshot, AnyValue} from '@memlab/core';
50
+ * import {config, getNodeInnocentHeap, tagObject} from '@memlab/core';
51
+ *
52
+ * test('memory test', async () => {
53
+ * config.muteConsole = true;
54
+ * const o1: AnyValue = {};
55
+ * let o2: AnyValue = {};
56
+ *
57
+ * // tag o1 with marker: "memlab-mark-1", does not modify o1 in any way
58
+ * tagObject(o1, 'memlab-mark-1');
59
+ * // tag o2 with marker: "memlab-mark-2", does not modify o2 in any way
60
+ * tagObject(o2, 'memlab-mark-2');
61
+ *
62
+ * o2 = null;
63
+ *
64
+ * const heap: IHeapSnapshot = await getNodeInnocentHeap();
65
+ *
66
+ * // expect object with marker "memlab-mark-1" exists
67
+ * expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);
68
+ *
69
+ * // expect object with marker "memlab-mark-2" can be GCed
70
+ * expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);
71
+ *
72
+ * }, 30000);
73
+ * ```
74
+ */
36
75
  function tagObject(o, tag) {
37
76
  if (!store.taggedObjects[tag]) {
38
77
  store.taggedObjects[tag] = new WeakSet();
@@ -47,7 +86,29 @@ function dumpNodeHeapSnapshot() {
47
86
  return file;
48
87
  }
49
88
  exports.dumpNodeHeapSnapshot = dumpNodeHeapSnapshot;
50
- function getCurrentNodeHeap() {
89
+ /**
90
+ * Take a heap snapshot of the current program state
91
+ * and parse it as {@link IHeapSnapshot}. Notice that
92
+ * this API does not calculate some heap analysis meta data
93
+ * for heap analysis. But this also means faster heap parsing.
94
+ *
95
+ * @returns heap representation without heap analysis meta data.
96
+ *
97
+ * If you need to get the heap snapshot with heap analysis meta data
98
+ * use {@link dumpNodeHeapSnapshot} and {@link getHeapFromFile},
99
+ * for example:
100
+ * ```typescript
101
+ * import type {IHeapSnapshot} from '@memlab/core';
102
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
103
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
104
+ *
105
+ * (async function () {
106
+ * const heapFile = dumpNodeHeapSnapshot();
107
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
108
+ * })();
109
+ * ```
110
+ */
111
+ function getNodeInnocentHeap() {
51
112
  return __awaiter(this, void 0, void 0, function* () {
52
113
  const file = dumpNodeHeapSnapshot();
53
114
  const snapshot = yield Utils_1.default.getSnapshotFromFile(file, {
@@ -59,4 +120,4 @@ function getCurrentNodeHeap() {
59
120
  return snapshot;
60
121
  });
61
122
  }
62
- exports.getCurrentNodeHeap = getCurrentNodeHeap;
123
+ exports.getNodeInnocentHeap = getNodeInnocentHeap;
@@ -10,6 +10,7 @@
10
10
  import { ParsedArgs } from 'minimist';
11
11
  import type { LaunchOptions, Page } from 'puppeteer';
12
12
  import type { ErrorHandling, MemLabConfig } from './Config';
13
+ /** @internal */
13
14
  export declare type AnyValue = any;
14
15
  /** @internal */
15
16
  export declare type RecordValue = string | number | boolean | null | RecordValue[] | {
@@ -141,23 +142,319 @@ export declare type QuickExperiment = {
141
142
  experiment: string;
142
143
  group: string;
143
144
  };
145
+ /**
146
+ * The type for defining custom leak-filtering logic.
147
+ * * **Examples**:
148
+ * ```typescript
149
+ * const scenario = {
150
+ *
151
+ * };
152
+ *
153
+ * let map = Object.create(null);
154
+ * const beforeLeakFilter = (snapshot: IHeapSnapshot, _leakedNodeIds: HeapNodeIdSet): void => {
155
+ * map = initializeMapUsingSnapshot(snapshot);
156
+ * };
157
+ *
158
+ * // duplicated string with size > 1KB as memory leak
159
+ * const leakFilter = (node: IHeapNode): boolean => {
160
+ * if (node.type !== 'string' || node.retainedSize < 1000) {
161
+ * return false;
162
+ * }
163
+ * const str = utils.getStringNodeValue(node);
164
+ * return map[str] > 1;
165
+ * };
166
+ *
167
+ * export default {beforeLeakFilter, leakFilter};
168
+ * ```
169
+ */
144
170
  export interface ILeakFilter {
145
171
  beforeLeakFilter?: InitLeakFilterCallback;
146
172
  leakFilter: LeakFilterCallback;
147
173
  }
174
+ /**
175
+ * Lifecycle function callback that is invoked initially once before calling any
176
+ * leak filter function.
177
+ *
178
+ * @param snaphost - heap snapshot see {@link IHeapSnapshot}
179
+ * @param leakedNodeIds - the set of leaked object (node) ids.
180
+ */
148
181
  export declare type InitLeakFilterCallback = (snapshot: IHeapSnapshot, leakedNodeIds: HeapNodeIdSet) => void;
182
+ /**
183
+ * Callback that can be used to define a logic to filter the
184
+ * leaked objects. The callback is only called for every node
185
+ * allocated but not released from the target interaction
186
+ * in the heap snapshot.
187
+ *
188
+ * @param node - the node that is kept alive in the memory in the heap snapshot
189
+ * @param snapshot - the snapshot of target interaction
190
+ * @param leakedNodeIds - the set of leaked node ids
191
+ *
192
+ * @returns the value indicating whether the given node in the snapshot
193
+ * should be considered as leaked.
194
+ * * **Examples**:
195
+ * ```javascript
196
+ * // any node in the heap snapshot that is greater than 1MB
197
+ * function leakFilter(node, _snapshot, _leakedNodeIds) {
198
+ * return node.retainedSize > 1000000;
199
+ * };
200
+ * ```
201
+ */
149
202
  export declare type LeakFilterCallback = (node: IHeapNode, snapshot: IHeapSnapshot, leakedNodeIds: HeapNodeIdSet) => boolean;
203
+ /**
204
+ * The callback defines browser interactions which are
205
+ * used by memlab to interact with the web app under test.
206
+ */
150
207
  export declare type InteractionsCallback = (page: Page, args?: OperationArgs) => Promise<void>;
208
+ /**
209
+ * Test scenario specifies how you want a E2E test to interact with a web browser.
210
+ * The test scenario can be saved as a `.js` file and passed to the `memlab
211
+ * run --scenario` command:
212
+ * ```javascript
213
+ * // save as test.js and use in terminal:
214
+ * // $ memlab run --scenario test.js
215
+ *
216
+ * module.exports = {
217
+ * url: () => 'https://www.npmjs.com/',
218
+ * action: async () => ... ,
219
+ * back: async () => ... ,
220
+ * };
221
+ * ```
222
+ *
223
+ * The test scenario instance can also be passed to the
224
+ * [`run` API](../modules/api_src#run) exported by `@memlab/api`.
225
+ * ```typescript
226
+ * const {run} = require('@memlab/api');
227
+ *
228
+ * (async function () {
229
+ * const scenario = {
230
+ * url: () => 'https://www.facebook.com',
231
+ * action: async () => ... ,
232
+ * back: async () => ... ,
233
+ * };
234
+ * const leaks = await run({scenario});
235
+ * })();
236
+ * ```
237
+ */
151
238
  export interface IScenario {
239
+ /** @internal */
152
240
  name?: () => string;
241
+ /** @internal */
153
242
  app?: () => string;
243
+ /**
244
+ * If the page you are running memlab against requires authentication or
245
+ * specific cookie(s) to be set, you can pass them as
246
+ * a list of <name, value> pairs.
247
+ * @returns cookie list
248
+ * * **Examples**:
249
+ * ```typescript
250
+ * const scenario = {
251
+ * url: () => 'https://www.facebook.com/',
252
+ * cookies: () => [
253
+ * {"name":"cm_j","value":"none"},
254
+ * {"name":"datr","value":"yJvIY..."},
255
+ * {"name":"c_user","value":"8917..."},
256
+ * {"name":"xs","value":"95:9WQ..."},
257
+ * // ...
258
+ * ],
259
+ * };
260
+ * ```
261
+ */
154
262
  cookies?: () => Cookies;
263
+ /**
264
+ * String value of the initial url of the page.
265
+ *
266
+ * @returns the string value of the initial url
267
+ * * **Examples**:
268
+ * ```typescript
269
+ * const scenario = {
270
+ * url: () => 'https://www.npmjs.com/',
271
+ * };
272
+ * ```
273
+ * If a test scenario only specifies the `url` callback (without the `action`
274
+ * callback), memlab will try to detect memory leaks from the initial page
275
+ * load. All objects allocated by the initial page load will be candidates
276
+ * for memory leak filtering.
277
+ */
155
278
  url: () => string;
279
+ /**
280
+ * `action` is the callback function that defines the interaction
281
+ * where you want to trigger memory leaks after the initial page load.
282
+ * All JS objects in browser allocated by the browser interactions triggered
283
+ * from the `action` callback will be candidates for memory leak filtering.
284
+ *
285
+ * * **Parameters**:
286
+ * * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
287
+ * object, which provides APIs to interact with the web browser
288
+ *
289
+ * * **Examples**:
290
+ * ```typescript
291
+ * const scenario = {
292
+ * url: () => 'https://www.npmjs.com/',
293
+ * action: async (page) => {
294
+ * await page.click('a[href="/link"]');
295
+ * },
296
+ * back: async (page) => {
297
+ * await page.click('a[href="/back"]');
298
+ * },
299
+ * }
300
+ * ```
301
+ * Note: always clean up external puppeteer references to JS objects
302
+ * in the browser context.
303
+ * ```typescript
304
+ * const scenario = {
305
+ * url: () => 'https://www.npmjs.com/',
306
+ * action: async (page) => {
307
+ * const elements = await page.$x("//button[contains(., 'Text in Button')]");
308
+ * const [button] = elements;
309
+ * if (button) {
310
+ * await button.click();
311
+ * }
312
+ * // dispose external references to JS objects in browser context
313
+ * await promise.all(elements.map(e => e.dispose()));
314
+ * },
315
+ * back: async (page) => ... ,
316
+ * }
317
+ ```
318
+ */
156
319
  action?: InteractionsCallback;
320
+ /**
321
+ * `back` is the callback function that specifies how memlab should
322
+ * back/revert the `action` callback. Think of it as an undo action.
323
+ *
324
+ * * **Examples**:
325
+ * ```typescript
326
+ * const scenario = {
327
+ * url: () => 'https://www.npmjs.com/',
328
+ * action: async (page) => {
329
+ * await page.click('a[href="/link"]');
330
+ * },
331
+ * back: async (page) => {
332
+ * await page.click('a[href="/back"]');
333
+ * },
334
+ * }
335
+ * ```
336
+ * Check out [this page](/docs/how-memlab-works) on why
337
+ * memlab needs to undo/revert the `action` callback.
338
+ */
157
339
  back?: InteractionsCallback;
340
+ /**
341
+ * Specifies how many **extra** `action` and `back` actions performed by memlab.
342
+ * * **Examples**:
343
+ * ```typescript
344
+ * module.exports = {
345
+ * url: () => ... ,
346
+ * action: async (page) => ... ,
347
+ * back: async (page) => ... ,
348
+ * // browser interaction: two additional [ action -> back ]
349
+ * // init-load -> action -> back -> action -> back -> action -> back
350
+ * repeat: () => 2,
351
+ * };
352
+ * ```
353
+ */
158
354
  repeat?: () => number;
355
+ /**
356
+ * Optional callback function that checks if the web page is loaded
357
+ * after for initial page loading and subsequent browser interactions.
358
+ *
359
+ * If this callback is not provided, memlab by default
360
+ * considers a navigation to be finished when there are no network
361
+ * connections for at least 500ms.
362
+ *
363
+ * * **Parameters**:
364
+ * * page: `Page` | the puppeteer [`Page`](https://pptr.dev/api/puppeteer.page)
365
+ * object, which provides APIs to interact with the web browser
366
+ * * **Returns**: a boolean value, if it returns `true`, memlab will consider
367
+ * the navigation completes, if it returns `false`, memlab will keep calling
368
+ * this callback until it returns `true`. This is an async callback, you can
369
+ * also `await` and returns `true` until some async logic is resolved.
370
+ * * **Examples**:
371
+ * ```typescript
372
+ * module.exports = {
373
+ * url: () => ... ,
374
+ * action: async (page) => ... ,
375
+ * back: async (page) => ... ,
376
+ * isPageLoaded: async (page) => {
377
+ * await page.waitForNavigation({
378
+ * // consider navigation to be finished when there are
379
+ * // no more than 2 network connections for at least 500 ms.
380
+ * waitUntil: 'networkidle2',
381
+ * // Maximum navigation time in milliseconds
382
+ * timeout: 5000,
383
+ * });
384
+ * return true;
385
+ * },
386
+ * };
387
+ * ```
388
+ */
159
389
  isPageLoaded?: CheckPageLoadCallback;
390
+ /**
391
+ * Lifecycle function callback that is invoked initially once before
392
+ * the subsequent `leakFilter` function calls. This callback could
393
+ * be used to initialize some data stores or to do some one-off
394
+ * preprocessings.
395
+ *
396
+ * * **Parameters**:
397
+ * * snapshot: `IHeapSnapshot` | the final heap snapshot taken after
398
+ * all browser interactions are done.
399
+ * Check out {@link IHeapSnapshot} for more APIs that queries the heap snapshot.
400
+ * * leakedNodeIds: `Set<number>` | the set of ids of all JS heap objects
401
+ * allocated by the `action` call but not released after the `back` call
402
+ * in browser.
403
+ *
404
+ * * **Examples**:
405
+ * ```typescript
406
+ * module.exports = {
407
+ * url: () => ... ,
408
+ * action: async (page) => ... ,
409
+ * back: async (page) => ... ,
410
+ * beforeLeakFilter: (snapshot, leakedNodeIds) {
411
+ * // initialize some data stores
412
+ * },
413
+ * };
414
+ * ```
415
+ */
160
416
  beforeLeakFilter?: InitLeakFilterCallback;
417
+ /**
418
+ * This callback that defines how you want to filter out the
419
+ * leaked objects. The callback is called for every node (JS heap
420
+ * object in browser) allocated by the `action` callback, but not
421
+ * released after the `back` callback. Those objects could be caches
422
+ * that are retained in memory on purpose, or they are memory leaks.
423
+ *
424
+ * This optional callback allows you to define your own algorithm
425
+ * to cherry pick memory leaks for specific JS program under test.
426
+ *
427
+ * If this optional callback is not defined, memlab will use its
428
+ * built-in leak filter, which considers detached DOM elements
429
+ * and unmounted Fiber nodes (detached from React Fiber tree) as
430
+ * memory leaks.
431
+ *
432
+ * * **Parameters**:
433
+ * * node: `IHeapNode` | one of the heap object allocated but not released.
434
+ * * snapshot: `IHeapSnapshot` | the final heap snapshot taken after
435
+ * all browser interactions are done.
436
+ * Check out {@link IHeapSnapshot} for more APIs that queries the heap snapshot.
437
+ * * leakedNodeIds: `Set<number>` | the set of ids of all JS heap objects
438
+ * allocated by the `action` call but not released after the `back` call
439
+ * in browser.
440
+ *
441
+ * * **Returns**: the boolean value indicating whether the given node in
442
+ * the snapshot should be considered as leaked.
443
+ *
444
+ * * **Examples**:
445
+ * ```typescript
446
+ * module.exports = {
447
+ * url: () => ... ,
448
+ * action: async (page) => ... ,
449
+ * back: async (page) => ... ,
450
+ * leakFilter(node, snapshot, leakedNodeIds) {
451
+ * // any unreleased node (JS heap object) with 1MB+
452
+ * // retained size is considered a memory leak
453
+ * return node.retainedSize > 1000000;
454
+ * },
455
+ * };
456
+ * ```
457
+ */
161
458
  leakFilter?: LeakFilterCallback;
162
459
  }
163
460
  /** @internal */
@@ -245,7 +542,10 @@ export interface IDataBuilder {
245
542
  className: string;
246
543
  state: Record<string, AnyValue>;
247
544
  }
248
- /** @internal */
545
+ /**
546
+ * Callback function to provide if the page is loaded.
547
+ * @param page - puppeteer's [Page](https://pptr.dev/api/puppeteer.page/) object.
548
+ */
249
549
  export declare type CheckPageLoadCallback = (page: Page) => Promise<boolean>;
250
550
  /** @internal */
251
551
  export interface IE2EScenarioVisitPlan {
@@ -307,15 +607,211 @@ export declare type RunMetaInfo = {
307
607
  browserInfo: IBrowserInfo;
308
608
  };
309
609
  export interface IHeapSnapshot {
610
+ /** @internal */
310
611
  snapshot: RawHeapSnapshot;
612
+ /**
613
+ * A pseudo array containing all heap graph nodes (JS objects in heap).
614
+ * A JS heap could contain millions of heap objects, so memlab uses
615
+ * a pseudo array as the collection of all the heap objects. The pseudo
616
+ * array provides API to query and traverse all heap objects.
617
+ *
618
+ * * **Examples**:
619
+ * ```typescript
620
+ * import type {IHeapSnapshot, IHeapNode} from '@memlab/core';
621
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
622
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
623
+ *
624
+ * (async function () {
625
+ * const heapFile = dumpNodeHeapSnapshot();
626
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
627
+ *
628
+ * // get the total number of heap objects
629
+ * heap.nodes.length;
630
+ *
631
+ * heap.nodes.forEach((node: IHeapNode) => {
632
+ * // traverse each heap object
633
+ * });
634
+ * })();
635
+ * ```
636
+ */
311
637
  nodes: IHeapNodes;
638
+ /**
639
+ * A pseudo array containing all heap graph edges (references to heap objects
640
+ * in heap). A JS heap could contain millions of references, so memlab uses
641
+ * a pseudo array as the collection of all the heap edges. The pseudo
642
+ * array provides API to query and traverse all heap references.
643
+ *
644
+ * * **Examples**:
645
+ * ```typescript
646
+ * import type {IHeapSnapshot, IHeapEdge} from '@memlab/core';
647
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
648
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
649
+ *
650
+ * (async function () {
651
+ * const heapFile = dumpNodeHeapSnapshot();
652
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
653
+ *
654
+ * // get the total number of heap references
655
+ * heap.edges.length;
656
+ *
657
+ * heap.edges.forEach((edge: IHeapEdge) => {
658
+ * // traverse each reference in the heap
659
+ * });
660
+ * })();
661
+ * ```
662
+ */
312
663
  edges: IHeapEdges;
664
+ /**
665
+ * If you have the id of a heap node (JS object in heap), use this API
666
+ * to get an {@link IHeapNode} associated with the id.
667
+ * @param id id of the heap node (JS object in heap) you would like to query
668
+ * @returns the API returns `null` if no heap object has the specified id.
669
+ *
670
+ * * **Examples**:
671
+ * ```typescript
672
+ * import type {IHeapSnapshot} from '@memlab/core';
673
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
674
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
675
+ *
676
+ * (async function () {
677
+ * const heapFile = dumpNodeHeapSnapshot();
678
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
679
+ *
680
+ * const node = heap.getNodeById(351);
681
+ * node?.id; // should be 351
682
+ * })();
683
+ * ```
684
+ */
313
685
  getNodeById(id: number): Nullable<IHeapNode>;
314
- clearShortestPathInfo(): void;
686
+ /**
687
+ * Search for the heap and check if there is any JS object instance with
688
+ * a specified constructor name.
689
+ * @param className The contructor name of the object instance
690
+ * @returns `true` if there is at least one such object in the heap
691
+ *
692
+ * * **Examples**: you can write a jest unit test with memory assertions:
693
+ * ```typescript
694
+ * // save as example.test.ts
695
+ * import type {IHeapSnapshot, Nullable} from '@memlab/core';
696
+ * import {config, getNodeInnocentHeap} from '@memlab/core';
697
+ *
698
+ * class TestObject {
699
+ * public arr1 = [1, 2, 3];
700
+ * public arr2 = ['1', '2', '3'];
701
+ * }
702
+ *
703
+ * test('memory test with heap assertion', async () => {
704
+ * config.muteConsole = true; // no console output
705
+ *
706
+ * let obj: Nullable<TestObject> = new TestObject();
707
+ * // get a heap snapshot of the current program state
708
+ * let heap: IHeapSnapshot = await getNodeInnocentHeap();
709
+ *
710
+ * // call some function that may add references to obj
711
+ * rabbitHole(obj)
712
+ *
713
+ * expect(heap.hasObjectWithClassName('TestObject')).toBe(true);
714
+ * obj = null;
715
+ *
716
+ * heap = await getNodeInnocentHeap();
717
+ * // if rabbitHole does not have any side effect that
718
+ * // adds new references to obj, then obj can be GCed
719
+ * expect(heap.hasObjectWithClassName('TestObject')).toBe(false);
720
+ *
721
+ * }, 30000);
722
+ * ```
723
+ */
315
724
  hasObjectWithClassName(className: string): boolean;
725
+ /**
726
+ * Search for the heap and get one of the JS object instances with
727
+ * a specified constructor name (if there is any).
728
+ * @param className The contructor name of the object instance
729
+ * @returns a handle pointing to any one of the object instances, returns
730
+ * `null` if no such object exists in the heap.
731
+ *
732
+ * * **Examples**:
733
+ * ```typescript
734
+ * import type {IHeapSnapshot} from '@memlab/core';
735
+ * import {getNodeInnocentHeap} from '@memlab/core';
736
+ *
737
+ * class TestObject {
738
+ * public arr1 = [1, 2, 3];
739
+ * public arr2 = ['1', '2', '3'];
740
+ * }
741
+ *
742
+ * (async function () {
743
+ * const obj = new TestObject();
744
+ * // get a heap snapshot of the current program state
745
+ * const heap: IHeapSnapshot = await getNodeInnocentHeap();
746
+ *
747
+ * const node = heap.getAnyObjectWithClassName('TestObject');
748
+ * console.log(node?.name); // should be 'TestObject'
749
+ * })();
750
+ * ```
751
+ */
316
752
  getAnyObjectWithClassName(className: string): Nullable<IHeapNode>;
753
+ /**
754
+ * Search for the heap and check if there is any JS object instance with
755
+ * a specified property name.
756
+ * @param nameOrIndex The property name (string) or element index (number)
757
+ * on the object instance
758
+ * @returns returns `true` if there is at least one such object in the heap
759
+ *
760
+ * * **Examples**:
761
+ * ```typescript
762
+ * import type {IHeapSnapshot} from '@memlab/core';
763
+ * import {dumpNodeHeapSnapshot} from '@memlab/core';
764
+ * import {getHeapFromFile} from '@memlab/heap-analysis';
765
+ *
766
+ * (async function () {
767
+ * // eslint-disable-next-line @typescript-eslint/no-unused-vars
768
+ * const object = {'memlab-test-heap-property': 'memlab-test-heap-value'};
769
+ *
770
+ * const heapFile = dumpNodeHeapSnapshot();
771
+ * const heap: IHeapSnapshot = await getHeapFromFile(heapFile);
772
+ *
773
+ * // should be true
774
+ * console.log(heap.hasObjectWithPropertyName('memlab-test-heap-property'));
775
+ * })();
776
+ * ```
777
+ */
317
778
  hasObjectWithPropertyName(nameOrIndex: string | number): boolean;
779
+ /**
780
+ * Search for the heap and check if there is any JS object instance with
781
+ * a marker tagged by {@link tagObject}.
782
+ * @param tag marker name on the object instances tagged by {@link tagObject}
783
+ * @returns returns `true` if there is at least one such object in the heap
784
+ *
785
+ * ```typescript
786
+ * import type {IHeapSnapshot, AnyValue} from '@memlab/core';
787
+ * import {config, getNodeInnocentHeap, tagObject} from '@memlab/core';
788
+ *
789
+ * test('memory test', async () => {
790
+ * config.muteConsole = true;
791
+ * const o1: AnyValue = {};
792
+ * let o2: AnyValue = {};
793
+ *
794
+ * // tag o1 with marker: "memlab-mark-1", does not modify o1 in any way
795
+ * tagObject(o1, 'memlab-mark-1');
796
+ * // tag o2 with marker: "memlab-mark-2", does not modify o2 in any way
797
+ * tagObject(o2, 'memlab-mark-2');
798
+ *
799
+ * o2 = null;
800
+ *
801
+ * const heap: IHeapSnapshot = await getNodeInnocentHeap();
802
+ *
803
+ * // expect object with marker "memlab-mark-1" exists
804
+ * expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);
805
+ *
806
+ * // expect object with marker "memlab-mark-2" can be GCed
807
+ * expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);
808
+ *
809
+ * }, 30000);
810
+ * ```
811
+ */
318
812
  hasObjectWithTag(tag: string): boolean;
813
+ /** @internal */
814
+ clearShortestPathInfo(): void;
319
815
  }
320
816
  export interface IHeapLocation {
321
817
  snapshot: IHeapSnapshot;
package/dist/lib/Types.js CHANGED
@@ -8,4 +8,5 @@
8
8
  * @emails oncall+ws_labs
9
9
  * @format
10
10
  */
11
+ /* eslint-disable @typescript-eslint/no-explicit-any */
11
12
  Object.defineProperty(exports, "__esModule", { value: true });
package/dist/lib/Utils.js CHANGED
@@ -542,6 +542,7 @@ function getSnapshotFromFile(filename, options) {
542
542
  catch (e) {
543
543
  handleSnapshotError(getError(e));
544
544
  }
545
+ Console_1.default.flush();
545
546
  return ret;
546
547
  });
547
548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/core",
3
- "version": "1.0.9",
3
+ "version": "1.1.2",
4
4
  "license": "MIT",
5
5
  "description": "memlab core libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",
@@ -56,9 +56,9 @@
56
56
  "directory": "packages/core"
57
57
  },
58
58
  "scripts": {
59
- "preinstall": "export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true",
60
59
  "build-pkg": "tsc",
61
60
  "test-pkg": "jest .",
61
+ "publish-patch": "npm version patch --force && npm publish",
62
62
  "clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo"
63
63
  },
64
64
  "bugs": {