@memlab/e2e 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,21 +7,24 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { AnyFunction, OperationArgs, MemLabConfig } from '@memlab/core';
11
- import type { CDPSession, Page } from 'puppeteer';
10
+ import type { Browser, CDPSession, Page, Target } from 'puppeteer';
11
+ import type { AnyFunction, Nullable, OperationArgs, MemLabConfig } from '@memlab/core';
12
12
  import { TestPlanner } from './lib/operations/TestPlanner';
13
13
  declare type PageInteractOptions = {
14
14
  config?: MemLabConfig;
15
15
  testPlanner?: TestPlanner;
16
16
  };
17
17
  export default class E2EInteractionManager {
18
- private cdpsession;
18
+ private mainThreadCdpsession;
19
19
  private page;
20
+ private browser;
20
21
  private pageHistoryLength;
21
22
  private evalFuncAfterInitLoad;
22
23
  private networkManager;
23
- constructor(page: Page);
24
- getCDPSession(): Promise<CDPSession>;
24
+ constructor(page: Page, browser: Browser);
25
+ getChosenCDPSession(): Promise<CDPSession>;
26
+ getMainThreadCDPSession(): Promise<CDPSession>;
27
+ selectCDPSession(predicate: (t: Target) => boolean): Promise<Nullable<CDPSession>>;
25
28
  clearCDPSession(): void;
26
29
  setEvalFuncAfterInitLoad(func: AnyFunction | null): void;
27
30
  protected initialLoad(page: Page, url: string, opArgs?: OperationArgs): Promise<void>;
@@ -35,7 +38,7 @@ export default class E2EInteractionManager {
35
38
  private writeSnapshotFileFromCDPSession;
36
39
  private saveHeapSnapshotToFile;
37
40
  private fullGC;
38
- private forceGC;
41
+ private forceMainThreadGC;
39
42
  private collectMetrics;
40
43
  }
41
44
  export {};
@@ -30,23 +30,56 @@ const TestPlanner_1 = __importDefault(require("./lib/operations/TestPlanner"));
30
30
  const NetworkManager_1 = __importDefault(require("./NetworkManager"));
31
31
  const { logMetaData, setPermissions, logTabProgress, maybeWaitForConsoleInput, applyAsyncWithRetry, compareURL, waitExtraForTab, checkURL, injectPageReloadChecker, checkPageReload, dispatchOperation, clearConsole, getNavigationHistoryLength, checkLastSnapshotChunk, getURLParameter, } = E2EUtils_1.default;
32
32
  class E2EInteractionManager {
33
- constructor(page) {
33
+ constructor(page, browser) {
34
34
  this.pageHistoryLength = [];
35
35
  this.evalFuncAfterInitLoad = null;
36
36
  this.page = page;
37
+ this.browser = browser;
37
38
  this.networkManager = new NetworkManager_1.default(page);
38
39
  }
39
- getCDPSession() {
40
+ getChosenCDPSession() {
40
41
  return __awaiter(this, void 0, void 0, function* () {
41
- if (!this.cdpsession) {
42
- this.cdpsession = yield this.page.target().createCDPSession();
43
- this.networkManager.setCDPSession(this.cdpsession);
42
+ if (core_1.config.isAnalyzingMainThread) {
43
+ return this.getMainThreadCDPSession();
44
+ }
45
+ // get web worker thread target
46
+ const cdpSession = yield this.selectCDPSession(target => {
47
+ var _a, _b;
48
+ const t = target;
49
+ const isWorker = ((_a = t._targetInfo) === null || _a === void 0 ? void 0 : _a.type) === 'worker';
50
+ let isTitleMatch = true;
51
+ if (core_1.config.targetWorkerTitle != null) {
52
+ isTitleMatch = ((_b = t._targetInfo) === null || _b === void 0 ? void 0 : _b.title) === core_1.config.targetWorkerTitle;
53
+ }
54
+ return isWorker && isTitleMatch;
55
+ });
56
+ if (cdpSession == null) {
57
+ throw core_1.utils.haltOrThrow('web worker or main thread heap under analysis not found');
58
+ }
59
+ return cdpSession;
60
+ });
61
+ }
62
+ getMainThreadCDPSession() {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ if (!this.mainThreadCdpsession) {
65
+ this.mainThreadCdpsession = yield this.page.target().createCDPSession();
66
+ this.networkManager.setCDPSession(this.mainThreadCdpsession);
67
+ }
68
+ return this.mainThreadCdpsession;
69
+ });
70
+ }
71
+ selectCDPSession(predicate) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ const targets = yield this.browser.targets();
74
+ const target = targets.find(predicate);
75
+ if (!target) {
76
+ return null;
44
77
  }
45
- return this.cdpsession;
78
+ return yield target.createCDPSession();
46
79
  });
47
80
  }
48
81
  clearCDPSession() {
49
- this.cdpsession = null;
82
+ this.mainThreadCdpsession = null;
50
83
  }
51
84
  setEvalFuncAfterInitLoad(func) {
52
85
  this.evalFuncAfterInitLoad = func;
@@ -73,7 +106,8 @@ class E2EInteractionManager {
73
106
  }
74
107
  beforeInteractions() {
75
108
  return __awaiter(this, void 0, void 0, function* () {
76
- const session = yield this.getCDPSession();
109
+ // tracking main thread for network interception
110
+ const session = yield this.getMainThreadCDPSession();
77
111
  if (core_1.config.interceptScript) {
78
112
  this.networkManager.setCDPSession(session);
79
113
  yield this.networkManager.interceptScript();
@@ -270,7 +304,7 @@ class E2EInteractionManager {
270
304
  if (core_1.config.verbose) {
271
305
  core_1.info.lowLevel('Start tracking JS heap');
272
306
  }
273
- const session = yield this.getCDPSession();
307
+ const session = yield this.getMainThreadCDPSession();
274
308
  yield session.send('HeapProfiler.enable');
275
309
  });
276
310
  }
@@ -305,7 +339,7 @@ class E2EInteractionManager {
305
339
  return __awaiter(this, void 0, void 0, function* () {
306
340
  core_1.info.beginSection('heap snapshot');
307
341
  const start = Date.now();
308
- const session = yield this.getCDPSession();
342
+ const session = yield this.getChosenCDPSession();
309
343
  yield this.writeSnapshotFileFromCDPSession(file, session);
310
344
  const spanMs = Date.now() - start;
311
345
  if (core_1.config.verbose) {
@@ -328,12 +362,12 @@ class E2EInteractionManager {
328
362
  core_1.info.overwrite('running a full GC...');
329
363
  }
330
364
  // force GC 6 times to release feedback_cells
331
- yield this.forceGC(6);
365
+ yield this.forceMainThreadGC(6);
332
366
  });
333
367
  }
334
- forceGC(repeat = 1) {
368
+ forceMainThreadGC(repeat = 1) {
335
369
  return __awaiter(this, void 0, void 0, function* () {
336
- const client = yield this.getCDPSession();
370
+ const client = yield this.getMainThreadCDPSession();
337
371
  for (let i = 0; i < repeat; i++) {
338
372
  yield client.send('HeapProfiler.collectGarbage');
339
373
  // wait for a while and let GC do the job
@@ -351,7 +385,7 @@ class E2EInteractionManager {
351
385
  if (!core_1.config.runningMode.shouldGetMetrics(tabInfo)) {
352
386
  return;
353
387
  }
354
- yield this.forceGC();
388
+ yield this.forceMainThreadGC();
355
389
  // collect heap size
356
390
  const builtInMetrics = yield this.page.metrics();
357
391
  const size = core_1.utils.getReadableBytes(builtInMetrics.JSHeapUsedSize);
@@ -7,7 +7,7 @@
7
7
  * @format
8
8
  * @oncall web_perf_infra
9
9
  */
10
- import type { AnyOptions, AnyValue, E2EOperation, E2EStepInfo, IE2EScenarioVisitPlan, IScenario, Nullable, OperationArgs } from '@memlab/core';
10
+ import { AnyOptions, AnyValue, E2EOperation, E2EStepInfo, IE2EScenarioVisitPlan, IScenario, Nullable, OperationArgs } from '@memlab/core';
11
11
  import type { CDPSession, Page } from 'puppeteer';
12
12
  declare type ExceptionHandler = (ex: Error) => void;
13
13
  declare function checkLastSnapshotChunk(chunk: string): void;
@@ -21,15 +21,16 @@ 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
- const fs_1 = __importDefault(require("fs"));
25
24
  const core_1 = require("@memlab/core");
25
+ const fs_1 = __importDefault(require("fs"));
26
+ const core_2 = require("@memlab/core");
26
27
  const BaseOperation_1 = __importDefault(require("./operations/BaseOperation"));
27
28
  const InteractionUtils_1 = __importDefault(require("./operations/InteractionUtils"));
28
- const core_2 = require("@memlab/core");
29
+ const core_3 = require("@memlab/core");
29
30
  function checkLastSnapshotChunk(chunk) {
30
31
  const regex = /\}\s*$/;
31
32
  if (!regex.test(chunk)) {
32
- core_1.utils.throwError(new Error('resolved `HeapProfiler.takeHeapSnapshot` before writing the last chunk'));
33
+ core_2.utils.throwError(new Error('resolved `HeapProfiler.takeHeapSnapshot` before writing the last chunk'));
33
34
  }
34
35
  }
35
36
  // get URL parameter for a specific step
@@ -46,17 +47,17 @@ function getURLParameter(tab, visitPlan) {
46
47
  return ret;
47
48
  }
48
49
  function compareURL(page, url) {
49
- if (!core_1.config.verbose) {
50
+ if (!core_2.config.verbose) {
50
51
  return;
51
52
  }
52
53
  const actual = unescape(page.url());
53
54
  url = unescape(url);
54
- if (!core_1.utils.isURLEqual(url, actual)) {
55
- core_1.info.warning('URL changed:');
56
- core_1.info.lowLevel(` Expected: ${url}`);
57
- core_1.info.lowLevel('----');
58
- core_1.info.lowLevel(` Actual: ${actual}`);
59
- core_1.info.lowLevel('');
55
+ if (!core_2.utils.isURLEqual(url, actual)) {
56
+ core_2.info.warning('URL changed:');
57
+ core_2.info.lowLevel(` Expected: ${url}`);
58
+ core_2.info.lowLevel('----');
59
+ core_2.info.lowLevel(` Actual: ${actual}`);
60
+ core_2.info.lowLevel('');
60
61
  }
61
62
  }
62
63
  function logTabProgress(i, visitPlan) {
@@ -65,32 +66,32 @@ function logTabProgress(i, visitPlan) {
65
66
  const progress = `[${i + 1}/${len}]`;
66
67
  const tabType = tab.type ? `(${tab.type})` : '';
67
68
  const msg = `${progress} visiting ${tab.name} ${tabType}`;
68
- if (core_1.config.verbose) {
69
- core_1.info.topLevel(msg);
69
+ if (core_2.config.verbose) {
70
+ core_2.info.topLevel(msg);
70
71
  }
71
- core_1.info.topLevel(core_1.serializer.summarizeTabsOrder(visitPlan.tabsOrder, {
72
+ core_2.info.topLevel(core_2.serializer.summarizeTabsOrder(visitPlan.tabsOrder, {
72
73
  color: true,
73
74
  progress: i,
74
75
  }));
75
- core_1.browserInfo.addMarker(`[memlab]: ${msg}`);
76
+ core_2.browserInfo.addMarker(`[memlab]: ${msg}`);
76
77
  }
77
78
  function serializeVisitPlan(visitPlan) {
78
- fs_1.default.writeFileSync(core_1.config.snapshotSequenceFile, JSON.stringify(visitPlan.tabsOrder, null, 2), 'UTF-8');
79
+ fs_1.default.writeFileSync(core_2.config.snapshotSequenceFile, JSON.stringify(visitPlan.tabsOrder, null, 2), 'UTF-8');
79
80
  }
80
81
  function logMetaData(visitPlan, opt = {}) {
81
82
  // save the visiting info to disk
82
83
  serializeVisitPlan(visitPlan);
83
84
  // save the run meta info to disk
84
85
  const runMeta = {
85
- app: core_1.config.targetApp,
86
+ app: core_2.config.targetApp,
86
87
  type: visitPlan.type,
87
- interaction: core_1.config.targetTab,
88
- browserInfo: core_1.browserInfo,
88
+ interaction: core_2.config.targetTab,
89
+ browserInfo: core_2.browserInfo,
89
90
  };
90
- fs_1.default.writeFileSync(core_1.config.runMetaFile, JSON.stringify(runMeta), 'UTF-8');
91
+ core_1.runInfoUtils.runMetaInfoManager.saveRunMetaInfo(runMeta);
91
92
  // additional post processing of collected data
92
93
  if (opt.final) {
93
- core_1.config.runningMode.postProcessData(visitPlan);
94
+ core_2.config.runningMode.postProcessData(visitPlan);
94
95
  }
95
96
  }
96
97
  function setPermissions(page, origin) {
@@ -100,7 +101,7 @@ function setPermissions(page, origin) {
100
101
  }
101
102
  const browser = page.browser();
102
103
  const context = browser.defaultBrowserContext();
103
- yield context.overridePermissions(origin, core_1.config.grantedPermissions);
104
+ yield context.overridePermissions(origin, core_2.config.grantedPermissions);
104
105
  });
105
106
  }
106
107
  // check if the URL is correct
@@ -120,7 +121,7 @@ function checkPageReload(page) {
120
121
  // @ts-expect-error TODO: add Window shim type
121
122
  const flag = yield page.evaluate(() => window.__memlab_check_reload);
122
123
  if (flag !== 1) {
123
- core_1.utils.haltOrThrow('The page is reloaded. MemLab cannot analyze heap across page reloads. ' +
124
+ core_2.utils.haltOrThrow('The page is reloaded. MemLab cannot analyze heap across page reloads. ' +
124
125
  'Please remove window.reload() calls, page.goto() calls, ' +
125
126
  'or any reload logic.');
126
127
  }
@@ -128,8 +129,8 @@ function checkPageReload(page) {
128
129
  }
129
130
  function maybeWaitForConsoleInput(stepId) {
130
131
  return __awaiter(this, void 0, void 0, function* () {
131
- if (core_1.config.isManualDebug) {
132
- yield core_1.info.waitForConsole(`Press Enter (or Return) to continue step-${stepId}:`);
132
+ if (core_2.config.isManualDebug) {
133
+ yield core_2.info.waitForConsole(`Press Enter (or Return) to continue step-${stepId}:`);
133
134
  }
134
135
  });
135
136
  }
@@ -144,7 +145,7 @@ exceptionHandler = (_ex) => {
144
145
  ret = yield f.apply(self, args);
145
146
  }
146
147
  catch (ex) {
147
- exceptionHandler(core_1.utils.getError(ex));
148
+ exceptionHandler(core_2.utils.getError(ex));
148
149
  }
149
150
  return ret;
150
151
  });
@@ -154,19 +155,19 @@ function applyAsyncWithRetry(f, self, args, options = {}) {
154
155
  options.retry = options.retry || 0;
155
156
  const retry = options.retry;
156
157
  const exceptionHandler = (ex) => __awaiter(this, void 0, void 0, function* () {
157
- if (retry <= 0 || core_1.config.verbose) {
158
+ if (retry <= 0 || core_2.config.verbose) {
158
159
  // if the browser UI is enabled, MemLab should wait for a while
159
160
  // so we can have a chance to manually inspect the page
160
- if (core_1.config.openDevtoolsConsole) {
161
- yield InteractionUtils_1.default.waitFor(core_1.config.delayBeforeExitUponException);
161
+ if (core_2.config.openDevtoolsConsole) {
162
+ yield InteractionUtils_1.default.waitFor(core_2.config.delayBeforeExitUponException);
162
163
  }
163
- core_1.utils.haltOrThrow(core_1.utils.getError(ex), {
164
+ core_2.utils.haltOrThrow(core_2.utils.getError(ex), {
164
165
  printCallback: () => {
165
- core_1.info.warning('interaction fail');
166
+ core_2.info.warning('interaction fail');
166
167
  },
167
168
  });
168
169
  }
169
- core_1.info.warning(`interaction fail, making ${retry} more attempt(s)...`);
170
+ core_2.info.warning(`interaction fail, making ${retry} more attempt(s)...`);
170
171
  if (!!options.delayBeforeRetry && options.delayBeforeRetry > 0) {
171
172
  yield InteractionUtils_1.default.waitFor(options.delayBeforeRetry);
172
173
  }
@@ -180,7 +181,7 @@ function applyAsyncWithRetry(f, self, args, options = {}) {
180
181
  }
181
182
  function clearConsole(page) {
182
183
  return __awaiter(this, void 0, void 0, function* () {
183
- if (core_1.config.clearConsole) {
184
+ if (core_2.config.clearConsole) {
184
185
  yield page.evaluate(() => {
185
186
  try {
186
187
  console.clear();
@@ -195,7 +196,7 @@ function clearConsole(page) {
195
196
  function dispatchOperation(page, operation, opArgs) {
196
197
  return __awaiter(this, void 0, void 0, function* () {
197
198
  if (!(operation instanceof BaseOperation_1.default)) {
198
- throw core_1.utils.haltOrThrow(`unknown operation: ${operation}`);
199
+ throw core_2.utils.haltOrThrow(`unknown operation: ${operation}`);
199
200
  }
200
201
  yield operation.do(page, opArgs);
201
202
  });
@@ -203,22 +204,22 @@ function dispatchOperation(page, operation, opArgs) {
203
204
  function waitExtraForTab(tabInfo) {
204
205
  return __awaiter(this, void 0, void 0, function* () {
205
206
  let delay = 0;
206
- const mode = core_1.config.runningMode;
207
+ const mode = core_2.config.runningMode;
207
208
  if (tabInfo.type === 'target') {
208
209
  if (!mode.shouldExtraWaitForTarget(tabInfo)) {
209
210
  return;
210
211
  }
211
- delay += core_1.config.extraWaitingForTarget;
212
+ delay += core_2.config.extraWaitingForTarget;
212
213
  }
213
214
  else if (tabInfo.type === 'final') {
214
215
  if (!mode.shouldExtraWaitForFinal(tabInfo)) {
215
216
  return;
216
217
  }
217
- delay += core_1.config.extraWaitingForFinal;
218
+ delay += core_2.config.extraWaitingForFinal;
218
219
  }
219
220
  // wait for extra time
220
221
  if (delay > 0) {
221
- core_1.info.overwrite(`wait extra ${delay / 1000}s for ${tabInfo.type} page...`);
222
+ core_2.info.overwrite(`wait extra ${delay / 1000}s for ${tabInfo.type} page...`);
222
223
  yield InteractionUtils_1.default.waitFor(delay);
223
224
  }
224
225
  });
@@ -243,7 +244,7 @@ function startTrackingHeapAllocation(page, file) {
243
244
  return __awaiter(this, void 0, void 0, function* () {
244
245
  const heap = '';
245
246
  fs_1.default.writeFileSync(file, heap, 'UTF-8');
246
- core_1.info.lowLevel('tracking heap allocation...');
247
+ core_2.info.lowLevel('tracking heap allocation...');
247
248
  const cdpSession = yield page.target().createCDPSession();
248
249
  cdpSession.on('HeapProfiler.addHeapSnapshotChunk', data => {
249
250
  fs_1.default.appendFileSync(file, data.chunk, 'UTF-8');
@@ -264,7 +265,7 @@ function getScenarioAppName(scenario = null) {
264
265
  function getScenarioDefaultAppName() {
265
266
  return 'default-app-for-scenario';
266
267
  }
267
- exports.default = (0, core_2.setInternalValue)({
268
+ exports.default = (0, core_3.setInternalValue)({
268
269
  applyAsyncWithGuard,
269
270
  applyAsyncWithRetry,
270
271
  checkLastSnapshotChunk,
@@ -284,4 +285,4 @@ exports.default = (0, core_2.setInternalValue)({
284
285
  setPermissions,
285
286
  startTrackingHeapAllocation,
286
287
  waitExtraForTab,
287
- }, __filename, core_2.constant.internalDir);
288
+ }, __filename, core_3.constant.internalDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memlab/e2e",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "license": "MIT",
5
5
  "description": "memlab browser E2E interaction libraries",
6
6
  "author": "Liang Gong <lgong@fb.com>",
@@ -1,18 +1,41 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <link rel="icon" href="/favicon.ico" />
6
- <meta name="viewport" content="width=device-width,initial-scale=1" />
7
- <meta name="theme-color" content="#000000" />
8
- <meta
9
- name="description"
10
- content="Web page for e2e tests"
11
- />
12
- <title>React App</title>
13
- <script defer="defer" src="min.js"></script>
14
- </head>
15
- <body>
16
- <div id="root"></div>
17
- </body>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon" href="/favicon.ico" />
7
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
8
+ <meta name="theme-color" content="#000000" />
9
+ <meta name="description" content="Web page for e2e tests" />
10
+ <title>React App</title>
11
+ <script>
12
+ // create a web worker
13
+ var blobURL = URL.createObjectURL(
14
+ new Blob(
15
+ [
16
+ '(',
17
+ function () {
18
+ function WorkerTestObject() {
19
+ this.idx = Math.random();
20
+ }
21
+ setInterval(() => {
22
+ self.holder = self.holder || [];
23
+ self.holder.push(new WorkerTestObject());
24
+ }, 5);
25
+ }.toString(),
26
+ ')()',
27
+ ],
28
+ { type: 'application/javascript' },
29
+ ),
30
+ );
31
+ var worker = new Worker(blobURL, {name: 'test-worker'});
32
+ URL.revokeObjectURL(blobURL);
33
+ </script>
34
+ <script defer="defer" src="min.js"></script>
35
+ </head>
36
+
37
+ <body>
38
+ <div id="root"></div>
39
+ </body>
40
+
18
41
  </html>