Skip to content

Commit cb89aa1

Browse files
committed
chore: lazy-load modules, add module profiling and json schema validator
1 parent d665c3c commit cb89aa1

18 files changed

Lines changed: 406 additions & 60 deletions

File tree

packages/playwright-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"default": "./index.js"
2323
},
2424
"./package.json": "./package.json",
25+
"./lib/bootstrap": "./lib/bootstrap.js",
2526
"./lib/outofprocess": "./lib/outofprocess.js",
2627
"./lib/cli/program": "./lib/cli/program.js",
2728
"./lib/tools/cli-client/program": "./lib/tools/cli-client/program.js",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
if (process.env.PW_INSTRUMENT_MODULES) {
18+
const Module = require('module');
19+
const originalLoad = Module._load;
20+
21+
type TreeNode = { name: string, selfMs: number, totalMs: number, childrenMs: number, children: TreeNode[] };
22+
const root: TreeNode = { name: '<root>', selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
23+
let current = root;
24+
const stack: TreeNode[] = [];
25+
26+
Module._load = function(request: any, _parent: any, _isMain: any) {
27+
const node: TreeNode = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
28+
current.children.push(node);
29+
stack.push(current);
30+
current = node;
31+
const start = performance.now();
32+
let result;
33+
try {
34+
result = originalLoad.apply(this, arguments);
35+
} catch (e) {
36+
// Module load failed (e.g. optional dep not found) — unwind stack.
37+
current = stack.pop()!;
38+
current.children.pop();
39+
throw e;
40+
}
41+
const duration = performance.now() - start;
42+
node.totalMs = duration;
43+
node.selfMs = Math.max(0, duration - node.childrenMs);
44+
current = stack.pop()!;
45+
current.childrenMs += duration;
46+
return result;
47+
};
48+
49+
process.on('exit', () => {
50+
function printTree(node: TreeNode, prefix: string, isLast: boolean, lines: string[], depth: number) {
51+
if (node.totalMs < 1 && depth > 0)
52+
return;
53+
const connector = depth === 0 ? '' : isLast ? '└── ' : '├── ';
54+
const time = `${node.totalMs.toFixed(1).padStart(8)}ms`;
55+
const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : '';
56+
lines.push(`${time} ${prefix}${connector}${node.name}${self}`);
57+
const childPrefix = prefix + (depth === 0 ? '' : isLast ? ' ' : '│ ');
58+
const sorted = node.children.slice().sort((a, b) => b.totalMs - a.totalMs);
59+
for (let i = 0; i < sorted.length; i++)
60+
printTree(sorted[i], childPrefix, i === sorted.length - 1, lines, depth + 1);
61+
}
62+
63+
let totalModules = 0;
64+
function count(n: TreeNode) { totalModules++; n.children.forEach(count); }
65+
root.children.forEach(count);
66+
67+
const lines: string[] = [];
68+
const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs);
69+
for (let i = 0; i < sorted.length; i++)
70+
printTree(sorted[i], '', i === sorted.length - 1, lines, 0);
71+
72+
const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0);
73+
process.stderr.write(`\n--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total ---\n` + lines.join('\n') + '\n');
74+
75+
// Flat list: aggregate selfMs across all tree nodes by name.
76+
const flat = new Map<string, { selfMs: number, totalMs: number, count: number }>();
77+
function gather(n: TreeNode) {
78+
const existing = flat.get(n.name);
79+
if (existing) {
80+
existing.selfMs += n.selfMs;
81+
existing.totalMs += n.totalMs;
82+
existing.count++;
83+
} else {
84+
flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 });
85+
}
86+
n.children.forEach(gather);
87+
}
88+
root.children.forEach(gather);
89+
const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50);
90+
const flatLines = top50.map(([mod, { selfMs, totalMs, count }]) =>
91+
`${selfMs.toFixed(1).padStart(8)}ms self ${totalMs.toFixed(1).padStart(8)}ms total (x${String(count).padStart(3)}) ${mod}`
92+
);
93+
process.stderr.write(`\n--- Top 50 modules by self time ---\n` + flatLines.join('\n') + '\n');
94+
});
95+
}

packages/playwright-core/src/cli/program.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
/* eslint-disable no-console */
1818

19+
import '../bootstrap';
1920
import { gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
2021
import { addTraceCommands } from '../tools/trace/traceCli';
2122
import { program } from '../utilsBundle';

packages/playwright-core/src/server/har/harRecorder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { Artifact } from '../artifact';
2121
import { HarTracer } from './harTracer';
2222
import { createGuid } from '../utils/crypto';
2323
import { ManualPromise } from '../../utils/isomorphic/manualPromise';
24-
import { yazl } from '../../zipBundle';
2524

2625
import type { BrowserContext } from '../browserContext';
2726
import type { HarTracerDelegate } from './harTracer';
@@ -52,6 +51,7 @@ export class HarRecorder implements HarTracerDelegate {
5251
waitForContentOnStop: true,
5352
urlFilter: urlFilterRe ?? options.urlGlob,
5453
});
54+
const { yazl } = require('../../zipBundle');
5555
this._zipFile = content === 'attach' || expectsZip ? new yazl.ZipFile() : null;
5656
this._tracer.start({ omitScripts: false });
5757
}

packages/playwright-core/src/server/localUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { calculateSha1 } from './utils/crypto';
2222
import { HarBackend } from './harBackend';
2323
import { ManualPromise } from '../utils/isomorphic/manualPromise';
2424
import { ZipFile } from './utils/zipFile';
25-
import { yauzl, yazl } from '../zipBundle';
2625
import { serializeClientSideCallMetadata } from '../utils/isomorphic/trace/traceUtils';
2726
import { assert } from '../utils/isomorphic/assert';
2827
import { removeFolders } from './utils/fileUtils';
@@ -43,6 +42,7 @@ export type StackSession = {
4342

4443
export async function zip(progress: Progress, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsZipParams): Promise<void> {
4544
const promise = new ManualPromise<void>();
45+
const { yauzl, yazl } = await import('../zipBundle');
4646
const zipFile = new yazl.ZipFile();
4747
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
4848

packages/playwright-core/src/server/utils/fileUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import path from 'path';
2121
import { calculateSha1 } from './crypto';
2222

2323
import { ManualPromise } from '../../utils/isomorphic/manualPromise';
24-
import { yazl } from '../../zipBundle';
2524

2625
import type { EventEmitter } from 'events';
2726

@@ -201,6 +200,7 @@ export class SerializedFS {
201200
return;
202201
}
203202
case 'zip': {
203+
const { yazl } = await import('playwright-core/lib/zipBundle');
204204
const zipFile = new yazl.ZipFile();
205205
const result = new ManualPromise<void>();
206206
(zipFile as any as EventEmitter).on('error', error => result.reject(error));

packages/playwright-core/src/server/utils/zipFile.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { yauzl } from '../../zipBundle';
18-
1917
import type { Entry, UnzipFile } from '../../zipBundle';
2018

2119
export class ZipFile {
@@ -30,6 +28,7 @@ export class ZipFile {
3028
}
3129

3230
private async _open() {
31+
const { yauzl } = await import('../../zipBundle');
3332
await new Promise<UnzipFile>((fulfill, reject) => {
3433
yauzl.open(this._fileName, { autoClose: false }, (e, z) => {
3534
if (e) {

packages/playwright-core/src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './utils/isomorphic/assert';
1919
export * from './utils/isomorphic/colors';
2020
export * from './utils/isomorphic/headers';
2121
export * from './utils/isomorphic/imageUtils';
22+
export * from './utils/isomorphic/jsonSchema';
2223
export * from './utils/isomorphic/locatorGenerators';
2324
export * from './utils/isomorphic/manualPromise';
2425
export * from './utils/isomorphic/mimeType';
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export type JsonSchema = {
18+
type?: string;
19+
properties?: Record<string, JsonSchema>;
20+
required?: string[];
21+
items?: JsonSchema;
22+
oneOf?: JsonSchema[];
23+
pattern?: string;
24+
patternError?: string;
25+
};
26+
27+
const regexCache = new Map<string, RegExp>();
28+
29+
export function validate(value: unknown, schema: JsonSchema, path: string): string[] {
30+
const errors: string[] = [];
31+
32+
if (schema.oneOf) {
33+
let bestErrors: string[] | undefined;
34+
for (const variant of schema.oneOf) {
35+
const variantErrors = validate(value, variant, path);
36+
if (variantErrors.length === 0)
37+
return [];
38+
// Prefer the variant with fewest errors (closest match).
39+
if (!bestErrors || variantErrors.length < bestErrors.length)
40+
bestErrors = variantErrors;
41+
}
42+
return bestErrors!;
43+
}
44+
45+
if (schema.type === 'string') {
46+
if (typeof value !== 'string') {
47+
errors.push(`${path}: expected string, got ${typeof value}`);
48+
return errors;
49+
}
50+
if (schema.pattern && !cachedRegex(schema.pattern).test(value))
51+
errors.push(schema.patternError || `${path}: must match pattern "${schema.pattern}"`);
52+
return errors;
53+
}
54+
55+
if (schema.type === 'array') {
56+
if (!Array.isArray(value)) {
57+
errors.push(`${path}: expected array, got ${typeof value}`);
58+
return errors;
59+
}
60+
if (schema.items) {
61+
for (let i = 0; i < value.length; i++)
62+
errors.push(...validate(value[i], schema.items, `${path}[${i}]`));
63+
}
64+
return errors;
65+
}
66+
67+
if (schema.type === 'object') {
68+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
69+
errors.push(`${path}: expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
70+
return errors;
71+
}
72+
const obj = value as Record<string, unknown>;
73+
for (const key of schema.required || []) {
74+
if (obj[key] === undefined)
75+
errors.push(`${path}.${key}: required`);
76+
}
77+
for (const [key, propSchema] of Object.entries(schema.properties || {})) {
78+
if (obj[key] !== undefined)
79+
errors.push(...validate(obj[key], propSchema, `${path}.${key}`));
80+
}
81+
return errors;
82+
}
83+
84+
return errors;
85+
}
86+
87+
function cachedRegex(pattern: string): RegExp {
88+
let regex = regexCache.get(pattern);
89+
if (!regex) {
90+
regex = new RegExp(pattern);
91+
regexCache.set(pattern, regex);
92+
}
93+
return regex;
94+
}

packages/playwright/src/common/process.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import 'playwright-core/lib/bootstrap';
1718
import { ManualPromise, setTimeOrigin, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
1819

1920
import { serializeError } from '../util';

0 commit comments

Comments
 (0)