@vercel/devlow-bench 0.1.3 → 0.2.4

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/README.md CHANGED
@@ -86,12 +86,12 @@ describe(
86
86
  import { measureTime, reportMeasurement } from "devlow-bench";
87
87
 
88
88
  // Measure a time
89
- measureTime("name of the timing", {
89
+ await measureTime("name of the timing", {
90
90
  /* optional options */
91
91
  });
92
92
 
93
93
  // Report some other measurement
94
- reportMeasurement("name of the measurement", value, unit, {
94
+ await reportMeasurement("name of the measurement", value, unit, {
95
95
  /* optional options */
96
96
  });
97
97
  ```
@@ -118,6 +118,10 @@ await session.softNavigationByClick("metric name", ".selector-to-click");
118
118
  await session.close();
119
119
  ```
120
120
 
121
+ Run with `BROWSER_OUTPUT=1` to show the output of the browser.
122
+
123
+ Run with `HEADLESS=false` to show the actual browser window.
124
+
121
125
  ## Shell operations
122
126
 
123
127
  The `devlow-bench` package provides a few helper functions to run operations in the shell.
@@ -151,6 +155,8 @@ shell.output
151
155
  await shell.kill();
152
156
  ```
153
157
 
158
+ Run with `SHELL_OUTPUT=1` to show the output of the shell commands.
159
+
154
160
  ## File operations
155
161
 
156
162
  The `devlow-bench` package provides a few helper functions to run operations on the file system.
package/dist/browser.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Page } from "playwright-chromium";
2
2
  interface BrowserSession {
3
3
  close(): Promise<void>;
4
- hardNavigation(url: string, metricName?: string): Promise<Page>;
4
+ hardNavigation(metricName: string, url: string): Promise<Page>;
5
5
  softNavigationByClick(metricName: string, selector: string): Promise<void>;
6
6
  reload(metricName: string): Promise<void>;
7
7
  }
package/dist/browser.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { chromium } from "playwright-chromium";
2
2
  import { measureTime, reportMeasurement } from "./index.js";
3
+ const browserOutput = !!process.env.BROWSER_OUTPUT;
3
4
  async function withRequestMetrics(metricName, page, fn) {
4
5
  const activePromises = [];
5
6
  const sizeByExtension = new Map();
@@ -25,50 +26,118 @@ async function withRequestMetrics(metricName, page, fn) {
25
26
  }
26
27
  })());
27
28
  };
29
+ let errorCount = 0;
30
+ let warningCount = 0;
31
+ let logCount = 0;
32
+ const consoleHandler = (message) => {
33
+ const type = message.type();
34
+ if (type === "error") {
35
+ errorCount++;
36
+ }
37
+ else if (type === "warning") {
38
+ warningCount++;
39
+ }
40
+ else {
41
+ logCount++;
42
+ }
43
+ if (browserOutput) {
44
+ activePromises.push((async () => {
45
+ const args = [];
46
+ try {
47
+ const text = message.text();
48
+ for (const arg of message.args()) {
49
+ args.push(await arg.jsonValue());
50
+ }
51
+ console.log(`[${type}] ${text}`, ...args);
52
+ }
53
+ catch {
54
+ // Ignore
55
+ }
56
+ })());
57
+ }
58
+ };
59
+ let uncaughtCount = 0;
60
+ const exceptionHandler = (error) => {
61
+ uncaughtCount++;
62
+ if (browserOutput) {
63
+ console.error(`[UNCAUGHT]`, error);
64
+ }
65
+ };
28
66
  try {
29
67
  page.on("response", responseHandler);
68
+ page.on("console", consoleHandler);
69
+ page.on("pageerror", exceptionHandler);
30
70
  await fn();
31
71
  await Promise.all(activePromises);
32
72
  let totalDownload = 0;
33
73
  for (const [extension, size] of sizeByExtension.entries()) {
34
- reportMeasurement(`${metricName}/responseSizes/${extension}`, size, "bytes");
74
+ await reportMeasurement(`${metricName}/responseSizes/${extension}`, size, "bytes");
35
75
  totalDownload += size;
36
76
  }
37
- reportMeasurement(`${metricName}/responseSizes`, totalDownload, "bytes");
77
+ await reportMeasurement(`${metricName}/responseSizes`, totalDownload, "bytes");
38
78
  let totalRequests = 0;
39
79
  for (const [extension, count] of requestsByExtension.entries()) {
40
- reportMeasurement(`${metricName}/requests/${extension}`, count, "requests");
80
+ await reportMeasurement(`${metricName}/requests/${extension}`, count, "requests");
41
81
  totalRequests += count;
42
82
  }
43
- reportMeasurement(`${metricName}/requests`, totalRequests, "requests");
83
+ await reportMeasurement(`${metricName}/requests`, totalRequests, "requests");
84
+ await reportMeasurement(`${metricName}/console/logs`, logCount, "messages");
85
+ await reportMeasurement(`${metricName}/console/warnings`, warningCount, "messages");
86
+ await reportMeasurement(`${metricName}/console/errors`, errorCount, "messages");
87
+ await reportMeasurement(`${metricName}/console/uncaught`, uncaughtCount, "messages");
88
+ await reportMeasurement(`${metricName}/console`, logCount + warningCount + errorCount + uncaughtCount, "messages");
44
89
  }
45
90
  finally {
46
91
  page.off("response", responseHandler);
47
92
  }
48
93
  }
49
- function networkIdle(page) {
50
- return new Promise((resolve) => {
94
+ function networkIdle(page, delay = 300, rejectTimeout = 180000) {
95
+ return new Promise((resolve, reject) => {
51
96
  const cleanup = () => {
52
97
  page.off("request", requestHandler);
53
98
  page.off("requestfailed", requestFinishedHandler);
54
99
  page.off("requestfinished", requestFinishedHandler);
100
+ clearTimeout(fullTimeout);
101
+ if (timeout) {
102
+ clearTimeout(timeout);
103
+ }
55
104
  };
56
105
  let activeRequests = 0;
57
106
  let timeout = null;
58
- const requestHandler = (request) => {
107
+ const requests = new Set();
108
+ const fullTimeout = setTimeout(() => {
109
+ cleanup();
110
+ reject(new Error(`Timeout while waiting for network idle. These requests are still pending: ${Array.from(requests).join(", ")}}`));
111
+ }, rejectTimeout);
112
+ const requestFilter = async (request) => {
113
+ return (await request.headers().accept) !== "text/event-stream";
114
+ };
115
+ const requestHandler = async (request) => {
116
+ requests.add(request.url());
59
117
  activeRequests++;
60
118
  if (timeout) {
61
119
  clearTimeout(timeout);
62
120
  timeout = null;
63
121
  }
122
+ // Avoid tracking some requests, but we only know this after awaiting
123
+ // so we need to do this weird stunt to ensure that
124
+ if (!(await requestFilter(request))) {
125
+ await requestFinishedInternal(request);
126
+ }
64
127
  };
65
- const requestFinishedHandler = (request) => {
128
+ const requestFinishedHandler = async (request) => {
129
+ if (await requestFilter(request)) {
130
+ requestFinishedInternal(request);
131
+ }
132
+ };
133
+ const requestFinishedInternal = async (request) => {
134
+ requests.delete(request.url());
66
135
  activeRequests--;
67
136
  if (activeRequests === 0) {
68
137
  timeout = setTimeout(() => {
69
138
  cleanup();
70
139
  resolve();
71
- }, 300);
140
+ }, delay);
72
141
  }
73
142
  };
74
143
  page.on("request", requestHandler);
@@ -93,6 +162,7 @@ class BrowserSessionImpl {
93
162
  const page = (this.page = this.page ?? (await this.context.newPage()));
94
163
  await withRequestMetrics(metricName, page, async () => {
95
164
  measureTime(`${metricName}/start`);
165
+ const idle = networkIdle(page, 3000);
96
166
  await page.goto(url, {
97
167
  waitUntil: "commit",
98
168
  });
@@ -107,8 +177,9 @@ class BrowserSessionImpl {
107
177
  measureTime(`${metricName}/load`, {
108
178
  relativeTo: `${metricName}/start`,
109
179
  });
110
- await page.waitForLoadState("networkidle");
180
+ await idle;
111
181
  measureTime(`${metricName}`, {
182
+ offset: 3000,
112
183
  relativeTo: `${metricName}/start`,
113
184
  });
114
185
  });
@@ -122,7 +193,7 @@ class BrowserSessionImpl {
122
193
  await withRequestMetrics(metricName, page, async () => {
123
194
  measureTime(`${metricName}/start`);
124
195
  const firstResponse = new Promise((resolve) => page.once("response", () => resolve()));
125
- const idle = networkIdle(page);
196
+ const idle = networkIdle(page, 3000);
126
197
  await page.click(selector);
127
198
  await firstResponse;
128
199
  measureTime(`${metricName}/firstResponse`, {
@@ -130,6 +201,7 @@ class BrowserSessionImpl {
130
201
  });
131
202
  await idle;
132
203
  measureTime(`${metricName}`, {
204
+ offset: 3000,
133
205
  relativeTo: `${metricName}/start`,
134
206
  });
135
207
  });
@@ -141,6 +213,7 @@ class BrowserSessionImpl {
141
213
  }
142
214
  await withRequestMetrics(metricName, page, async () => {
143
215
  measureTime(`${metricName}/start`);
216
+ const idle = networkIdle(page, 3000);
144
217
  await page.reload({
145
218
  waitUntil: "commit",
146
219
  });
@@ -155,8 +228,9 @@ class BrowserSessionImpl {
155
228
  measureTime(`${metricName}/load`, {
156
229
  relativeTo: `${metricName}/start`,
157
230
  });
158
- await page.waitForLoadState("networkidle");
231
+ await idle;
159
232
  measureTime(`${metricName}`, {
233
+ offset: 3000,
160
234
  relativeTo: `${metricName}/start`,
161
235
  });
162
236
  });
@@ -10,9 +10,10 @@ export declare function measureTime(name: string, options?: {
10
10
  relativeTo?: string | typeof PREVIOUS;
11
11
  scenario?: string;
12
12
  props?: Record<string, string | number | null>;
13
- }): void;
13
+ offset?: number;
14
+ }): Promise<void>;
14
15
  export declare function reportMeasurement(name: string, value: number, unit: string, options?: {
15
16
  relativeTo?: string | typeof PREVIOUS;
16
17
  scenario?: string;
17
18
  props?: Record<string, string | number | null>;
18
- }): void;
19
+ }): Promise<void>;
package/dist/describe.js CHANGED
@@ -56,14 +56,32 @@ export function withCurrent(current, fn) {
56
56
  });
57
57
  }
58
58
  export const PREVIOUS = Symbol("previous measurement with that unit");
59
- export function measureTime(name, options = {}) {
60
- const end = Date.now();
61
- reportMeasurement(name, end, "ms", { relativeTo: PREVIOUS, ...options });
59
+ export async function measureTime(name, options = {}) {
60
+ const end = Date.now() - (options.offset || 0);
61
+ await reportMeasurement(name, end, "ms", {
62
+ relativeTo: PREVIOUS,
63
+ ...options,
64
+ });
62
65
  }
63
- export function reportMeasurement(name, value, unit, options = {}) {
66
+ export async function reportMeasurement(name, value, unit, options = {}) {
64
67
  if (!currentScenario) {
65
68
  throw new Error("reportMeasurement() must be called inside of describe()");
66
69
  }
70
+ if (typeof name !== "string") {
71
+ throw new Error("reportMeasurement() must be called with a name that is a string");
72
+ }
73
+ if (typeof value !== "number") {
74
+ throw new Error("reportMeasurement() must be called with a value that is a number");
75
+ }
76
+ if (isNaN(value)) {
77
+ throw new Error("reportMeasurement() must be called with a value that is not NaN");
78
+ }
79
+ if (!isFinite(value)) {
80
+ throw new Error("reportMeasurement() must be called with a value that is finite");
81
+ }
82
+ if (typeof unit !== "string") {
83
+ throw new Error("reportMeasurement() must be called with a unit that is a string");
84
+ }
67
85
  let { relativeTo, scenario, props } = options;
68
86
  if (relativeTo === PREVIOUS) {
69
87
  relativeTo = "previous";
@@ -83,12 +101,12 @@ export function reportMeasurement(name, value, unit, options = {}) {
83
101
  if (!prev) {
84
102
  throw new Error(`No measurement named ${relativeTo} found`);
85
103
  }
86
- if (prev.unit !== "ms") {
104
+ if (prev.unit !== unit) {
87
105
  throw new Error(`Measurement ${relativeTo} is not a "${unit}" measurement`);
88
106
  }
89
107
  reportedValue -= prev.value;
90
108
  }
91
- currentScenario.iface.measurement(scenario ?? currentScenario.scenario.scenario.name, props
109
+ await currentScenario.iface.measurement(scenario ?? currentScenario.scenario.scenario.name, props
92
110
  ? {
93
111
  ...currentScenario.scenario.props,
94
112
  ...props,
package/dist/shell.js CHANGED
@@ -91,7 +91,7 @@ class CommandImpl {
91
91
  .filter((x) => x)
92
92
  .map((x) => x.memory)
93
93
  .reduce((a, b) => a + b, 0);
94
- reportMeasurement(metricName, memUsage, "bytes", options);
94
+ await reportMeasurement(metricName, memUsage, "bytes", options);
95
95
  }
96
96
  catch (e) {
97
97
  // ignore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/devlow-bench",
3
- "version": "0.1.3",
3
+ "version": "0.2.4",
4
4
  "description": "Benchmarking tool for the developer workflow",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,8 @@
25
25
  "@types/inquirer": "^9.0.3",
26
26
  "@types/minimist": "^1.2.2",
27
27
  "@types/node": "^20.3.0",
28
- "@types/split2": "^4.2.0"
28
+ "@types/split2": "^4.2.0",
29
+ "@turbo/eslint-config": "0.0.0"
29
30
  },
30
31
  "dependencies": {
31
32
  "@datadog/datadog-api-client": "^1.13.0",
@@ -38,6 +39,7 @@
38
39
  "tree-kill": "^1.2.2"
39
40
  },
40
41
  "scripts": {
42
+ "lint": "eslint src/",
41
43
  "prerelease": "pnpm run build:ts",
42
44
  "build:ts": "tsc"
43
45
  }