@piwitests/reporter 0.44.0 → 0.45.0
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/dist/cli/index.js +15 -1
- package/dist/global-setup-module.js +6 -0
- package/dist/index.d.ts +55 -1
- package/dist/index.js +2481 -323
- package/dist/internal/capture/attachments.d.ts +1 -0
- package/dist/internal/capture/attachments.js +2 -1
- package/dist/internal/capture/capture-fixtures.d.ts +374 -5
- package/dist/internal/capture/capture-fixtures.js +949 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,7 +43,9 @@ __export(index_exports, {
|
|
|
43
43
|
module.exports = __toCommonJS(index_exports);
|
|
44
44
|
|
|
45
45
|
// src/public/reporter.ts
|
|
46
|
-
var
|
|
46
|
+
var fs21 = __toESM(require("fs"));
|
|
47
|
+
var os9 = __toESM(require("os"));
|
|
48
|
+
var path23 = __toESM(require("path"));
|
|
47
49
|
|
|
48
50
|
// src/internal/config/desktop.ts
|
|
49
51
|
var fs = __toESM(require("fs"));
|
|
@@ -82,6 +84,8 @@ var DEFAULTS = {
|
|
|
82
84
|
// Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
|
|
83
85
|
captureCodeReach: false,
|
|
84
86
|
captureServerTraces: true,
|
|
87
|
+
captureResources: true,
|
|
88
|
+
leakCheck: "report",
|
|
85
89
|
sampleAriaOnPass: true,
|
|
86
90
|
uploadManifest: true,
|
|
87
91
|
defaultCapture: true,
|
|
@@ -122,6 +126,8 @@ var PIWI_ENV_KEYS = {
|
|
|
122
126
|
captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
|
|
123
127
|
codeReachRoots: "PIWI_CODE_REACH_ROOTS",
|
|
124
128
|
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
129
|
+
captureResources: "PIWI_CAPTURE_RESOURCES",
|
|
130
|
+
leakCheck: "PIWI_LEAK_CHECK",
|
|
125
131
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
126
132
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
127
133
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
@@ -156,6 +162,7 @@ var PIWI_FLAKE_ENV = {
|
|
|
156
162
|
plan: "PIWI_FLAKE_PLAN",
|
|
157
163
|
results: "PIWI_FLAKE_RESULTS"
|
|
158
164
|
};
|
|
165
|
+
var PIWI_RESOURCES_RESULTS_ENV = "PIWI_RESOURCES_RESULTS";
|
|
159
166
|
function readBool(val) {
|
|
160
167
|
if (val === void 0) return void 0;
|
|
161
168
|
return val === "true";
|
|
@@ -184,6 +191,8 @@ var ENV_FALLBACK_SPECS = [
|
|
|
184
191
|
{ option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
|
|
185
192
|
{ option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
|
|
186
193
|
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
194
|
+
{ option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
|
|
195
|
+
{ option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
|
|
187
196
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
188
197
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
189
198
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
@@ -250,6 +259,8 @@ function applyOptionsToEnv(options) {
|
|
|
250
259
|
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
251
260
|
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
252
261
|
else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
|
|
262
|
+
if (options.captureResources !== void 0) env[PIWI_ENV_KEYS.captureResources] = String(options.captureResources);
|
|
263
|
+
if (options.leakCheck !== void 0) env[PIWI_ENV_KEYS.leakCheck] = options.leakCheck;
|
|
253
264
|
if (options.sampleAriaOnPass === false) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "false";
|
|
254
265
|
else if (options.sampleAriaOnPass === true) env[PIWI_ENV_KEYS.sampleAriaOnPass] = "true";
|
|
255
266
|
if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
|
|
@@ -490,7 +501,7 @@ var HttpClient = class {
|
|
|
490
501
|
}
|
|
491
502
|
/** Unified request core: transport, headers, auth, response accumulation, timeout. */
|
|
492
503
|
request(method, pathname, opts) {
|
|
493
|
-
return new Promise((
|
|
504
|
+
return new Promise((resolve7, reject) => {
|
|
494
505
|
if (this.closedReason !== null) {
|
|
495
506
|
reject(new Error(this.closedReason));
|
|
496
507
|
return;
|
|
@@ -513,7 +524,7 @@ var HttpClient = class {
|
|
|
513
524
|
data += chunk;
|
|
514
525
|
});
|
|
515
526
|
res.on("end", () => {
|
|
516
|
-
|
|
527
|
+
resolve7({ status: res.statusCode ?? 0, text: data, headers: res.headers });
|
|
517
528
|
});
|
|
518
529
|
res.on("error", reject);
|
|
519
530
|
res.on("close", () => {
|
|
@@ -608,6 +619,7 @@ function toWireTestCase(tc) {
|
|
|
608
619
|
locatorSnapshots: rest.locatorSnapshots || null,
|
|
609
620
|
locatorPages: rest.locatorPages || null,
|
|
610
621
|
codeReach: rest.codeReach || null,
|
|
622
|
+
resources: rest.resources ?? null,
|
|
611
623
|
didNotRunReason: rest.didNotRunReason ?? null,
|
|
612
624
|
expectedStatus: rest.expectedStatus ?? null,
|
|
613
625
|
blockedBy: rest.blockedBy ?? null
|
|
@@ -674,6 +686,7 @@ function serializeRun(payload, opts) {
|
|
|
674
686
|
isFullRun: payload.isFullRun ?? true,
|
|
675
687
|
filterDetails: payload.filterDetails ?? null
|
|
676
688
|
};
|
|
689
|
+
if (payload.resourceReport) body.resourceReport = payload.resourceReport;
|
|
677
690
|
if (opts.includeTestCases) {
|
|
678
691
|
body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
|
|
679
692
|
}
|
|
@@ -1056,7 +1069,8 @@ var ATTACHMENT_NAMES = {
|
|
|
1056
1069
|
pageState: "piwi-page-state",
|
|
1057
1070
|
userPick: "piwi-user-pick",
|
|
1058
1071
|
aiUsage: "piwi-ai-usage",
|
|
1059
|
-
aiMeta: "piwi-ai-meta"
|
|
1072
|
+
aiMeta: "piwi-ai-meta",
|
|
1073
|
+
resources: "piwi-resources"
|
|
1060
1074
|
};
|
|
1061
1075
|
var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
|
|
1062
1076
|
var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
|
|
@@ -1277,8 +1291,8 @@ var FileHandler = class {
|
|
|
1277
1291
|
}
|
|
1278
1292
|
if (!lastPath) return null;
|
|
1279
1293
|
const hash = crypto2.createHash("sha256");
|
|
1280
|
-
await new Promise((
|
|
1281
|
-
fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end",
|
|
1294
|
+
await new Promise((resolve7, reject) => {
|
|
1295
|
+
fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve7).on("error", reject);
|
|
1282
1296
|
});
|
|
1283
1297
|
return {
|
|
1284
1298
|
tracePath: lastPath,
|
|
@@ -1709,10 +1723,10 @@ function createLimiter(maxConcurrent) {
|
|
|
1709
1723
|
if (run) run();
|
|
1710
1724
|
};
|
|
1711
1725
|
return function limit(fn) {
|
|
1712
|
-
return new Promise((
|
|
1726
|
+
return new Promise((resolve7, reject) => {
|
|
1713
1727
|
const run = () => {
|
|
1714
1728
|
active++;
|
|
1715
|
-
Promise.resolve().then(fn).then(
|
|
1729
|
+
Promise.resolve().then(fn).then(resolve7, reject).finally(next);
|
|
1716
1730
|
};
|
|
1717
1731
|
if (active < limitValue) run();
|
|
1718
1732
|
else queue.push(run);
|
|
@@ -2151,7 +2165,7 @@ var StreamManager = class {
|
|
|
2151
2165
|
this.logger.debugError(
|
|
2152
2166
|
`${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`
|
|
2153
2167
|
);
|
|
2154
|
-
await new Promise((
|
|
2168
|
+
await new Promise((resolve7) => setTimeout(resolve7, Math.min(1e3 * Math.pow(2, attempt), 1e4)));
|
|
2155
2169
|
}
|
|
2156
2170
|
if (!this.pendingEvents.isEmpty) {
|
|
2157
2171
|
const remaining = this.pendingEvents.takeAll();
|
|
@@ -2208,7 +2222,7 @@ var StreamManager = class {
|
|
|
2208
2222
|
if (flush) await flush;
|
|
2209
2223
|
const delays = [0, 1e3, 3e3];
|
|
2210
2224
|
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
2211
|
-
if (delays[attempt]) await new Promise((
|
|
2225
|
+
if (delays[attempt]) await new Promise((resolve7) => setTimeout(resolve7, delays[attempt]));
|
|
2212
2226
|
try {
|
|
2213
2227
|
await this.limitLiveUpload(
|
|
2214
2228
|
() => this.uploader.uploadCaseFiles(
|
|
@@ -3900,8 +3914,8 @@ function parseRouteKey(routeKey2) {
|
|
|
3900
3914
|
return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
|
|
3901
3915
|
}
|
|
3902
3916
|
function patternToRegex(pattern) {
|
|
3903
|
-
const
|
|
3904
|
-
const escaped =
|
|
3917
|
+
const path31 = pattern.split("?")[0] ?? pattern;
|
|
3918
|
+
const escaped = path31.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
|
|
3905
3919
|
return new RegExp(`^${escaped}$`);
|
|
3906
3920
|
}
|
|
3907
3921
|
function suitePathEqual(a, b) {
|
|
@@ -3924,13 +3938,13 @@ function matchProbeItem(plan, test) {
|
|
|
3924
3938
|
function requestMatchesRoute(method, url, routeKey2) {
|
|
3925
3939
|
const { method: rm, pattern } = parseRouteKey(routeKey2);
|
|
3926
3940
|
if (rm && rm !== method.toUpperCase()) return false;
|
|
3927
|
-
let
|
|
3941
|
+
let path31;
|
|
3928
3942
|
try {
|
|
3929
|
-
|
|
3943
|
+
path31 = new URL(url).pathname;
|
|
3930
3944
|
} catch {
|
|
3931
|
-
|
|
3945
|
+
path31 = url.split("?")[0] ?? url;
|
|
3932
3946
|
}
|
|
3933
|
-
return patternToRegex(pattern).test(
|
|
3947
|
+
return patternToRegex(pattern).test(path31);
|
|
3934
3948
|
}
|
|
3935
3949
|
function createProbeState() {
|
|
3936
3950
|
return { navigated: false, matches: 0 };
|
|
@@ -3952,10 +3966,10 @@ function isProbeMode() {
|
|
|
3952
3966
|
function loadPlan() {
|
|
3953
3967
|
if (planCache !== void 0) return planCache;
|
|
3954
3968
|
planCache = null;
|
|
3955
|
-
const
|
|
3956
|
-
if (
|
|
3969
|
+
const path31 = process.env[PIWI_PROBE_ENV.plan];
|
|
3970
|
+
if (path31) {
|
|
3957
3971
|
try {
|
|
3958
|
-
planCache = JSON.parse(fs14.readFileSync(
|
|
3972
|
+
planCache = JSON.parse(fs14.readFileSync(path31, "utf8"));
|
|
3959
3973
|
} catch {
|
|
3960
3974
|
planCache = null;
|
|
3961
3975
|
}
|
|
@@ -3977,10 +3991,10 @@ function outcomeFromStatus(status, applied) {
|
|
|
3977
3991
|
return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
|
|
3978
3992
|
}
|
|
3979
3993
|
function recordProbeOutcome(line) {
|
|
3980
|
-
const
|
|
3981
|
-
if (!
|
|
3994
|
+
const path31 = process.env[PIWI_PROBE_ENV.results];
|
|
3995
|
+
if (!path31) return;
|
|
3982
3996
|
try {
|
|
3983
|
-
fs14.appendFileSync(
|
|
3997
|
+
fs14.appendFileSync(path31, `${JSON.stringify(line)}
|
|
3984
3998
|
`);
|
|
3985
3999
|
} catch {
|
|
3986
4000
|
}
|
|
@@ -4364,7 +4378,7 @@ function originOf(url) {
|
|
|
4364
4378
|
return null;
|
|
4365
4379
|
}
|
|
4366
4380
|
}
|
|
4367
|
-
var sleep = (ms) => new Promise((
|
|
4381
|
+
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, Math.max(0, ms)));
|
|
4368
4382
|
async function performRouteAction(route, action, startedAt) {
|
|
4369
4383
|
switch (action.kind) {
|
|
4370
4384
|
case "abort":
|
|
@@ -5061,6 +5075,15 @@ function emitGitLabDotenv(output, env, logger) {
|
|
|
5061
5075
|
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
5062
5076
|
}
|
|
5063
5077
|
}
|
|
5078
|
+
function emitResourceSummary(lines, logger, env = process.env) {
|
|
5079
|
+
if (!env.GITHUB_ACTIONS || !env.GITHUB_STEP_SUMMARY || lines.length === 0) return;
|
|
5080
|
+
appendFileLines(
|
|
5081
|
+
env.GITHUB_STEP_SUMMARY,
|
|
5082
|
+
["### Piwi resources", "", "```text", ...lines, "```", ""],
|
|
5083
|
+
logger,
|
|
5084
|
+
"step summary"
|
|
5085
|
+
);
|
|
5086
|
+
}
|
|
5064
5087
|
function appendFileLines(file, lines, logger, label) {
|
|
5065
5088
|
try {
|
|
5066
5089
|
fs17.appendFileSync(file, lines.join("\n") + "\n");
|
|
@@ -5212,7 +5235,8 @@ var RunSubmitter = class {
|
|
|
5212
5235
|
shardIndex: run.shardInfo?.current,
|
|
5213
5236
|
shardTotal: run.shardInfo?.total,
|
|
5214
5237
|
isFullRun: run.isFullRun,
|
|
5215
|
-
filterDetails: run.filterDetails
|
|
5238
|
+
filterDetails: run.filterDetails,
|
|
5239
|
+
resourceReport: run.resourceReport ?? null
|
|
5216
5240
|
};
|
|
5217
5241
|
}
|
|
5218
5242
|
async tryFinishStreaming(run, overallStatus, duration, auth) {
|
|
@@ -5243,6 +5267,7 @@ var RunSubmitter = class {
|
|
|
5243
5267
|
isFullRun: run.isFullRun,
|
|
5244
5268
|
filterDetails: run.filterDetails ?? null
|
|
5245
5269
|
};
|
|
5270
|
+
if (run.resourceReport) finishBody.resourceReport = run.resourceReport;
|
|
5246
5271
|
if (run.shardInfo) {
|
|
5247
5272
|
finishBody.shardIndex = run.shardInfo.current;
|
|
5248
5273
|
finishBody.shardTotal = run.shardInfo.total;
|
|
@@ -5327,202 +5352,2269 @@ var RunSubmitter = class {
|
|
|
5327
5352
|
}
|
|
5328
5353
|
};
|
|
5329
5354
|
|
|
5330
|
-
//
|
|
5331
|
-
var
|
|
5332
|
-
var
|
|
5333
|
-
var
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
screenshot: "screenshot",
|
|
5357
|
-
waitFor: "wait",
|
|
5358
|
-
waitForSelector: "waitForSelector",
|
|
5359
|
-
waitForLoadState: "waitForLoadState",
|
|
5360
|
-
waitForFunction: "waitForFunction",
|
|
5361
|
-
waitForResponse: "waitForResponse",
|
|
5362
|
-
waitForRequest: "waitForRequest",
|
|
5363
|
-
waitForEvent: "waitForEvent",
|
|
5364
|
-
waitForTimeout: "waitForTimeout",
|
|
5365
|
-
innerText: "read text",
|
|
5366
|
-
textContent: "read text",
|
|
5367
|
-
inputValue: "read value",
|
|
5368
|
-
getAttribute: "read attribute",
|
|
5369
|
-
isVisible: "visibility check",
|
|
5370
|
-
isEnabled: "enabled check",
|
|
5371
|
-
isChecked: "checked check",
|
|
5372
|
-
boundingBox: "measure",
|
|
5373
|
-
evaluate: "evaluate",
|
|
5374
|
-
goto: "navigation"
|
|
5375
|
-
};
|
|
5376
|
-
var ACTION_GERUNDS = {
|
|
5377
|
-
click: "clicking",
|
|
5378
|
-
dblclick: "double-clicking",
|
|
5379
|
-
fill: "filling",
|
|
5380
|
-
type: "typing into",
|
|
5381
|
-
press: "pressing a key on",
|
|
5382
|
-
pressSequentially: "typing into",
|
|
5383
|
-
check: "checking",
|
|
5384
|
-
uncheck: "unchecking",
|
|
5385
|
-
hover: "hovering",
|
|
5386
|
-
tap: "tapping",
|
|
5387
|
-
focus: "focusing",
|
|
5388
|
-
clear: "clearing",
|
|
5389
|
-
selectOption: "selecting an option in",
|
|
5390
|
-
setInputFiles: "uploading a file to",
|
|
5391
|
-
dragTo: "dragging",
|
|
5392
|
-
waitFor: "waiting for",
|
|
5393
|
-
waitForSelector: "waiting for",
|
|
5394
|
-
evaluate: "evaluating on"
|
|
5395
|
-
};
|
|
5396
|
-
var STATE_PHRASES = {
|
|
5397
|
-
"not-found": "was not found on the page",
|
|
5398
|
-
hidden: "never became visible",
|
|
5399
|
-
"not-visible": "never became visible",
|
|
5400
|
-
"not-enabled": "never became enabled",
|
|
5401
|
-
"not-editable": "never became editable",
|
|
5402
|
-
"not-stable": "never stopped moving",
|
|
5403
|
-
"outside-viewport": "stayed outside the viewport",
|
|
5404
|
-
"intercepts-pointer": "was covered by another element",
|
|
5405
|
-
detached: "was detached from the DOM",
|
|
5406
|
-
navigating: "was still navigating"
|
|
5407
|
-
};
|
|
5408
|
-
var STATE_MATCHERS = {
|
|
5409
|
-
toBeVisible: { met: "visible", unmet: "never became visible" },
|
|
5410
|
-
toBeHidden: { met: "hidden", unmet: "never became hidden" },
|
|
5411
|
-
toBeEnabled: { met: "enabled", unmet: "never became enabled" },
|
|
5412
|
-
toBeDisabled: { met: "disabled", unmet: "never became disabled" },
|
|
5413
|
-
toBeChecked: { met: "checked", unmet: "never became checked" },
|
|
5414
|
-
toBeEditable: { met: "editable", unmet: "never became editable" },
|
|
5415
|
-
toBeFocused: { met: "focused", unmet: "never received focus" },
|
|
5416
|
-
toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
|
|
5417
|
-
toBeDetached: { met: "detached", unmet: "never left the DOM" },
|
|
5418
|
-
toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
|
|
5419
|
-
toBeEmpty: { met: "empty", unmet: "never became empty" }
|
|
5420
|
-
};
|
|
5421
|
-
var VALUE_MATCHERS = {
|
|
5422
|
-
toHaveText: "text",
|
|
5423
|
-
toContainText: "text containing",
|
|
5424
|
-
toHaveValue: "value",
|
|
5425
|
-
toHaveValues: "values",
|
|
5426
|
-
toHaveAttribute: "attribute",
|
|
5427
|
-
toHaveClass: "class",
|
|
5428
|
-
toContainClass: "class",
|
|
5429
|
-
toHaveId: "id",
|
|
5430
|
-
toHaveCSS: "CSS",
|
|
5431
|
-
toHaveJSProperty: "property",
|
|
5432
|
-
toHaveAccessibleName: "accessible name",
|
|
5433
|
-
toHaveAccessibleDescription: "accessible description",
|
|
5434
|
-
toHaveRole: "role",
|
|
5435
|
-
toHaveURL: "URL",
|
|
5436
|
-
toHaveTitle: "title",
|
|
5437
|
-
toHaveScreenshot: "screenshot",
|
|
5438
|
-
toMatchAriaSnapshot: "ARIA snapshot"
|
|
5439
|
-
};
|
|
5440
|
-
var NETWORK_ERRORS = {
|
|
5441
|
-
ERR_CONNECTION_REFUSED: "Connection refused loading",
|
|
5442
|
-
ERR_CONNECTION_RESET: "Connection reset loading",
|
|
5443
|
-
ERR_CONNECTION_CLOSED: "Connection closed loading",
|
|
5444
|
-
ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
|
|
5445
|
-
ERR_TIMED_OUT: "Connection timed out loading",
|
|
5446
|
-
ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
|
|
5447
|
-
ERR_INTERNET_DISCONNECTED: "No network while loading",
|
|
5448
|
-
ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
|
|
5449
|
-
ERR_ABORTED: "Navigation aborted loading",
|
|
5450
|
-
ERR_EMPTY_RESPONSE: "Empty response loading",
|
|
5451
|
-
ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
|
|
5452
|
-
ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
|
|
5453
|
-
ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
|
|
5454
|
-
ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
|
|
5455
|
-
ERR_CERT_DATE_INVALID: "Expired certificate loading",
|
|
5456
|
-
ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
|
|
5457
|
-
ERR_FAILED: "Request failed loading",
|
|
5458
|
-
ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
|
|
5459
|
-
NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
|
|
5460
|
-
NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
|
|
5461
|
-
NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
|
|
5462
|
-
NS_ERROR_OFFLINE: "No network while loading",
|
|
5463
|
-
NS_ERROR_ABORT: "Navigation aborted loading",
|
|
5464
|
-
NS_BINDING_ABORTED: "Navigation aborted loading"
|
|
5465
|
-
};
|
|
5466
|
-
function formatTimeout(ms) {
|
|
5467
|
-
if (ms < 1e3) return `${ms} ms`;
|
|
5468
|
-
const seconds = ms / 1e3;
|
|
5469
|
-
const rounded = Math.round(seconds * 10) / 10;
|
|
5470
|
-
return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
|
|
5355
|
+
// src/internal/capture/resource-ledger.ts
|
|
5356
|
+
var fs19 = __toESM(require("fs"));
|
|
5357
|
+
var path20 = __toESM(require("path"));
|
|
5358
|
+
var import_node_module2 = require("module");
|
|
5359
|
+
|
|
5360
|
+
// src/internal/capture/quiet-capture.ts
|
|
5361
|
+
var import_node_module = require("module");
|
|
5362
|
+
var path18 = __toESM(require("path"));
|
|
5363
|
+
var BOXED_PREFIX_EXPORTS = [
|
|
5364
|
+
[
|
|
5365
|
+
"playwright-core/lib/coreBundle",
|
|
5366
|
+
(mod) => mod?.utils?.setBoxedStackPrefixes
|
|
5367
|
+
],
|
|
5368
|
+
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
5369
|
+
];
|
|
5370
|
+
var boxing;
|
|
5371
|
+
var resolvedPlaywrightPackageJson;
|
|
5372
|
+
function resolvePlaywrightPackageJson() {
|
|
5373
|
+
if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
|
|
5374
|
+
try {
|
|
5375
|
+
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
5376
|
+
resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
5377
|
+
} catch {
|
|
5378
|
+
resolvedPlaywrightPackageJson = null;
|
|
5379
|
+
}
|
|
5380
|
+
return resolvedPlaywrightPackageJson;
|
|
5471
5381
|
}
|
|
5472
|
-
function
|
|
5382
|
+
function resolveBoxing() {
|
|
5473
5383
|
try {
|
|
5474
|
-
const
|
|
5475
|
-
|
|
5384
|
+
const root = findOwnPackageJson(__dirname)?.root;
|
|
5385
|
+
if (!root) return null;
|
|
5386
|
+
const playwrightPackageJson = resolvePlaywrightPackageJson();
|
|
5387
|
+
if (!playwrightPackageJson) return null;
|
|
5388
|
+
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
5389
|
+
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
5390
|
+
let set;
|
|
5391
|
+
try {
|
|
5392
|
+
set = pick(requireFromPlaywright(moduleId));
|
|
5393
|
+
} catch {
|
|
5394
|
+
continue;
|
|
5395
|
+
}
|
|
5396
|
+
if (typeof set !== "function") continue;
|
|
5397
|
+
return {
|
|
5398
|
+
set,
|
|
5399
|
+
prefixes: [path18.dirname(playwrightPackageJson), path18.join(root, "dist") + path18.sep]
|
|
5400
|
+
};
|
|
5401
|
+
}
|
|
5476
5402
|
} catch {
|
|
5477
|
-
return url;
|
|
5478
5403
|
}
|
|
5404
|
+
return null;
|
|
5479
5405
|
}
|
|
5480
|
-
function
|
|
5481
|
-
const
|
|
5482
|
-
|
|
5406
|
+
function boxCaptureFrames(extraPrefixes = []) {
|
|
5407
|
+
const first = boxing === void 0;
|
|
5408
|
+
if (first) boxing = resolveBoxing();
|
|
5409
|
+
if (!boxing) return;
|
|
5410
|
+
const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
|
|
5411
|
+
if (!first && added.length === 0) return;
|
|
5412
|
+
boxing.prefixes.push(...added);
|
|
5413
|
+
try {
|
|
5414
|
+
boxing.set([...boxing.prefixes]);
|
|
5415
|
+
} catch {
|
|
5416
|
+
}
|
|
5483
5417
|
}
|
|
5484
|
-
function
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
|
|
5418
|
+
function internalCall(owner, run) {
|
|
5419
|
+
const wrap = owner?._wrapApiCall;
|
|
5420
|
+
if (typeof wrap !== "function") return run();
|
|
5421
|
+
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
5489
5422
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5423
|
+
|
|
5424
|
+
// src/internal/capture/worker-metrics.ts
|
|
5425
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
5426
|
+
|
|
5427
|
+
// src/internal/support/system-readers.ts
|
|
5428
|
+
var fs18 = __toESM(require("fs"));
|
|
5429
|
+
var path19 = __toESM(require("path"));
|
|
5430
|
+
var CLK_TCK = 100;
|
|
5431
|
+
function readText(file) {
|
|
5432
|
+
try {
|
|
5433
|
+
return fs18.readFileSync(file, "utf8");
|
|
5434
|
+
} catch {
|
|
5435
|
+
return null;
|
|
5436
|
+
}
|
|
5494
5437
|
}
|
|
5495
|
-
function
|
|
5496
|
-
const
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5438
|
+
function parseProcStat(pid, text) {
|
|
5439
|
+
const open = text.indexOf("(");
|
|
5440
|
+
const close = text.lastIndexOf(")");
|
|
5441
|
+
if (open < 0 || close < open) return null;
|
|
5442
|
+
const fields = text.slice(close + 2).trim().split(/\s+/);
|
|
5443
|
+
const ppid = Number(fields[1]);
|
|
5444
|
+
const utime = Number(fields[11]);
|
|
5445
|
+
const stime = Number(fields[12]);
|
|
5446
|
+
const start = Number(fields[19]);
|
|
5447
|
+
if (![ppid, utime, stime, start].every(Number.isFinite)) return null;
|
|
5448
|
+
return { pid, ppid, comm: text.slice(open + 1, close), cpuMs: (utime + stime) * 1e3 / CLK_TCK, start };
|
|
5449
|
+
}
|
|
5450
|
+
function parseSchedstat(text) {
|
|
5451
|
+
const waitNs = Number(text.trim().split(/\s+/)[1]);
|
|
5452
|
+
return Number.isFinite(waitNs) ? waitNs / 1e6 : null;
|
|
5453
|
+
}
|
|
5454
|
+
function parseKbField(text, key) {
|
|
5455
|
+
const match = new RegExp(`^${key}:\\s+(\\d+)`, "m").exec(text);
|
|
5456
|
+
return match ? Number(match[1]) : null;
|
|
5457
|
+
}
|
|
5458
|
+
function parseCpuTicks(text) {
|
|
5459
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
5460
|
+
if (!line.startsWith("cpu ")) return null;
|
|
5461
|
+
const v = line.trim().split(/\s+/).slice(1, 9).map(Number);
|
|
5462
|
+
if (v.length < 8 || v.some((n) => !Number.isFinite(n))) return null;
|
|
5463
|
+
const [user, nice, system, idle, iowait, irq, softirq, steal] = v;
|
|
5464
|
+
return { busy: user + nice + system + irq + softirq, idle, iowait, steal, total: v.reduce((sum, n) => sum + n, 0) };
|
|
5465
|
+
}
|
|
5466
|
+
function parsePsiTotal(text) {
|
|
5467
|
+
const match = /^some\b.*\btotal=(\d+)/m.exec(text);
|
|
5468
|
+
return match ? Number(match[1]) : null;
|
|
5469
|
+
}
|
|
5470
|
+
function parsePsTime(text) {
|
|
5471
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(text.trim());
|
|
5472
|
+
if (!match) return null;
|
|
5473
|
+
const [, days, hours, minutes, seconds] = match;
|
|
5474
|
+
return ((Number(days ?? 0) * 24 + Number(hours ?? 0)) * 3600 + Number(minutes) * 60 + Number(seconds)) * 1e3;
|
|
5500
5475
|
}
|
|
5501
|
-
function
|
|
5502
|
-
const
|
|
5503
|
-
|
|
5476
|
+
function parsePsOutput(text) {
|
|
5477
|
+
const out = [];
|
|
5478
|
+
for (const line of text.split("\n")) {
|
|
5479
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
5480
|
+
if (!match) continue;
|
|
5481
|
+
const cpuMs = parsePsTime(match[4]);
|
|
5482
|
+
if (cpuMs === null) continue;
|
|
5483
|
+
out.push({
|
|
5484
|
+
pid: Number(match[1]),
|
|
5485
|
+
ppid: Number(match[2]),
|
|
5486
|
+
rssKb: Number(match[3]),
|
|
5487
|
+
cpuMs,
|
|
5488
|
+
command: match[5].trim()
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
return out;
|
|
5504
5492
|
}
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5493
|
+
function roleOf(comm, cmdline) {
|
|
5494
|
+
const command = `${comm} ${cmdline}`;
|
|
5495
|
+
if (/workerProcessEntry/.test(cmdline)) return "worker";
|
|
5496
|
+
if (/loaderProcessEntry/.test(cmdline)) return "runner";
|
|
5497
|
+
if (/(^|[\s/])ffmpeg/.test(command)) return "ffmpeg";
|
|
5498
|
+
const type = /--type=([\w-]+)/.exec(cmdline)?.[1];
|
|
5499
|
+
if (/chrom|headless_shell|msedge|Google Chrome/i.test(command)) {
|
|
5500
|
+
if (!type) return "browser";
|
|
5501
|
+
if (type === "renderer") return "renderer";
|
|
5502
|
+
if (type === "gpu-process") return "gpu";
|
|
5503
|
+
return "utility";
|
|
5504
|
+
}
|
|
5505
|
+
if (/firefox/i.test(command)) return /-contentproc/.test(cmdline) ? "renderer" : "browser";
|
|
5506
|
+
if (/WebKitWebProcess/.test(command)) return "renderer";
|
|
5507
|
+
if (/WebKitGPUProcess/.test(command)) return "gpu";
|
|
5508
|
+
if (/WebKitNetworkProcess/.test(command)) return "utility";
|
|
5509
|
+
if (/MiniBrowser|pw_run\.sh/.test(command)) return "browser";
|
|
5510
|
+
return null;
|
|
5511
|
+
}
|
|
5512
|
+
function descendantsOf(root, table) {
|
|
5513
|
+
const children = /* @__PURE__ */ new Map();
|
|
5514
|
+
for (const proc of table) {
|
|
5515
|
+
const list = children.get(proc.ppid) ?? [];
|
|
5516
|
+
list.push(proc);
|
|
5517
|
+
children.set(proc.ppid, list);
|
|
5508
5518
|
}
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5519
|
+
const out = [];
|
|
5520
|
+
const stack = [root];
|
|
5521
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
5522
|
+
while (stack.length > 0) {
|
|
5523
|
+
for (const child of children.get(stack.pop()) ?? []) {
|
|
5524
|
+
if (seen.has(child.pid)) continue;
|
|
5525
|
+
seen.add(child.pid);
|
|
5526
|
+
out.push(child);
|
|
5527
|
+
stack.push(child.pid);
|
|
5528
|
+
}
|
|
5515
5529
|
}
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5530
|
+
return out;
|
|
5531
|
+
}
|
|
5532
|
+
var ProcFs = class {
|
|
5533
|
+
constructor(root = "/proc") {
|
|
5534
|
+
this.root = root;
|
|
5519
5535
|
}
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
return this;
|
|
5536
|
+
/** Whether this looks like a Linux `/proc` at all. */
|
|
5537
|
+
available() {
|
|
5538
|
+
return fs18.existsSync(path19.join(this.root, "stat"));
|
|
5523
5539
|
}
|
|
5524
|
-
|
|
5525
|
-
|
|
5540
|
+
/** Every process, keyed by pid. */
|
|
5541
|
+
table() {
|
|
5542
|
+
const out = /* @__PURE__ */ new Map();
|
|
5543
|
+
let names;
|
|
5544
|
+
try {
|
|
5545
|
+
names = fs18.readdirSync(this.root);
|
|
5546
|
+
} catch {
|
|
5547
|
+
return out;
|
|
5548
|
+
}
|
|
5549
|
+
for (const name of names) {
|
|
5550
|
+
if (!/^\d+$/.test(name)) continue;
|
|
5551
|
+
const text = readText(path19.join(this.root, name, "stat"));
|
|
5552
|
+
const stat = text ? parseProcStat(Number(name), text) : null;
|
|
5553
|
+
if (stat) out.set(stat.pid, stat);
|
|
5554
|
+
}
|
|
5555
|
+
return out;
|
|
5556
|
+
}
|
|
5557
|
+
stat(pid) {
|
|
5558
|
+
const text = readText(path19.join(this.root, String(pid), "stat"));
|
|
5559
|
+
return text ? parseProcStat(pid, text) : null;
|
|
5560
|
+
}
|
|
5561
|
+
runWaitMs(pid) {
|
|
5562
|
+
const text = readText(path19.join(this.root, String(pid), "schedstat"));
|
|
5563
|
+
return text ? parseSchedstat(text) : null;
|
|
5564
|
+
}
|
|
5565
|
+
cmdline(pid) {
|
|
5566
|
+
return (readText(path19.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
|
|
5567
|
+
}
|
|
5568
|
+
/** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
|
|
5569
|
+
statusKb(pid, key) {
|
|
5570
|
+
const text = readText(path19.join(this.root, String(pid), "status"));
|
|
5571
|
+
return text ? parseKbField(text, key) : null;
|
|
5572
|
+
}
|
|
5573
|
+
/** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
|
|
5574
|
+
async pssKb(pid) {
|
|
5575
|
+
try {
|
|
5576
|
+
return parseKbField(await fs18.promises.readFile(path19.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
|
|
5577
|
+
} catch {
|
|
5578
|
+
return null;
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
/** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
|
|
5582
|
+
resetPeakRss(pid) {
|
|
5583
|
+
try {
|
|
5584
|
+
fs18.writeFileSync(path19.join(this.root, String(pid), "clear_refs"), "5");
|
|
5585
|
+
return true;
|
|
5586
|
+
} catch {
|
|
5587
|
+
return false;
|
|
5588
|
+
}
|
|
5589
|
+
}
|
|
5590
|
+
cpuTicks() {
|
|
5591
|
+
const text = readText(path19.join(this.root, "stat"));
|
|
5592
|
+
return text ? parseCpuTicks(text) : null;
|
|
5593
|
+
}
|
|
5594
|
+
/** Total stall time of a resource's pressure file, in µs. */
|
|
5595
|
+
pressureUs(resource) {
|
|
5596
|
+
const text = readText(path19.join(this.root, "pressure", resource));
|
|
5597
|
+
return text ? parsePsiTotal(text) : null;
|
|
5598
|
+
}
|
|
5599
|
+
memAvailableKb() {
|
|
5600
|
+
const text = readText(path19.join(this.root, "meminfo"));
|
|
5601
|
+
return text ? parseKbField(text, "MemAvailable") : null;
|
|
5602
|
+
}
|
|
5603
|
+
/** Open file descriptors of a process. */
|
|
5604
|
+
fdCount(pid) {
|
|
5605
|
+
try {
|
|
5606
|
+
return fs18.readdirSync(path19.join(this.root, String(pid), "fd")).length;
|
|
5607
|
+
} catch {
|
|
5608
|
+
return null;
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
};
|
|
5612
|
+
var UNLIMITED = 2 ** 60;
|
|
5613
|
+
function numberIn(text, pattern) {
|
|
5614
|
+
if (text === null) return null;
|
|
5615
|
+
const raw = pattern ? pattern.exec(text)?.[1] : text.trim();
|
|
5616
|
+
const value = Number(raw);
|
|
5617
|
+
return raw !== void 0 && raw !== "" && Number.isFinite(value) ? value : null;
|
|
5618
|
+
}
|
|
5619
|
+
var CgroupFs = class {
|
|
5620
|
+
constructor(procRoot = "/proc", cgroupRoot = "/sys/fs/cgroup", pid = "self") {
|
|
5621
|
+
this.procRoot = procRoot;
|
|
5622
|
+
this.cgroupRoot = cgroupRoot;
|
|
5623
|
+
this.pid = pid;
|
|
5624
|
+
}
|
|
5625
|
+
/** The directory of a controller (v1) or of the unified hierarchy (v2), when it exists. */
|
|
5626
|
+
dirs() {
|
|
5627
|
+
const text = readText(path19.join(this.procRoot, String(this.pid), "cgroup"));
|
|
5628
|
+
if (!text) return null;
|
|
5629
|
+
const v1 = /* @__PURE__ */ new Map();
|
|
5630
|
+
let v2 = null;
|
|
5631
|
+
for (const line of text.split("\n")) {
|
|
5632
|
+
const match = /^\d+:([^:]*):(.*)$/.exec(line.trim());
|
|
5633
|
+
if (!match) continue;
|
|
5634
|
+
if (match[1] === "") v2 = match[2];
|
|
5635
|
+
else for (const controller of match[1].split(",")) v1.set(controller, match[2]);
|
|
5636
|
+
}
|
|
5637
|
+
const pick = (mount, sub) => {
|
|
5638
|
+
const nested = path19.join(mount, sub);
|
|
5639
|
+
if (fs18.existsSync(nested)) return nested;
|
|
5640
|
+
return fs18.existsSync(mount) ? mount : null;
|
|
5641
|
+
};
|
|
5642
|
+
if (v1.has("memory") || v1.has("cpu")) {
|
|
5643
|
+
const memoryPath = v1.get("memory");
|
|
5644
|
+
const cpuPath = v1.get("cpu");
|
|
5645
|
+
const cpuMount = ["cpu,cpuacct", "cpu"].map((name) => path19.join(this.cgroupRoot, name)).find((dir) => fs18.existsSync(dir));
|
|
5646
|
+
return {
|
|
5647
|
+
version: 1,
|
|
5648
|
+
memory: memoryPath !== void 0 ? pick(path19.join(this.cgroupRoot, "memory"), memoryPath) : null,
|
|
5649
|
+
cpu: cpuPath !== void 0 && cpuMount ? pick(cpuMount, cpuPath) : null
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
if (v2 !== null && fs18.existsSync(path19.join(this.cgroupRoot, "cgroup.controllers"))) {
|
|
5653
|
+
const dir = pick(this.cgroupRoot, v2);
|
|
5654
|
+
return { version: 2, memory: dir, cpu: dir };
|
|
5655
|
+
}
|
|
5656
|
+
return null;
|
|
5657
|
+
}
|
|
5658
|
+
read() {
|
|
5659
|
+
const dirs = this.dirs();
|
|
5660
|
+
if (!dirs) return null;
|
|
5661
|
+
const file = (dir, name) => dir ? readText(path19.join(dir, name)) : null;
|
|
5662
|
+
if (dirs.version === 2) {
|
|
5663
|
+
const max = file(dirs.memory, "memory.max");
|
|
5664
|
+
const cpuMax = file(dirs.cpu, "cpu.max")?.trim().split(/\s+/);
|
|
5665
|
+
const quota = cpuMax && cpuMax[0] !== "max" ? Number(cpuMax[0]) / Number(cpuMax[1]) : null;
|
|
5666
|
+
const throttledUs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_usec (\d+)/m);
|
|
5667
|
+
return {
|
|
5668
|
+
version: 2,
|
|
5669
|
+
memoryLimitBytes: max === null || max.trim() === "max" ? null : numberIn(max),
|
|
5670
|
+
memoryCurrentBytes: numberIn(file(dirs.memory, "memory.current")),
|
|
5671
|
+
memoryPeakBytes: numberIn(file(dirs.memory, "memory.peak")),
|
|
5672
|
+
oomKills: numberIn(file(dirs.memory, "memory.events"), /^oom_kill (\d+)/m),
|
|
5673
|
+
cpuQuotaCores: quota !== null && Number.isFinite(quota) ? quota : null,
|
|
5674
|
+
throttledMs: throttledUs === null ? null : throttledUs / 1e3
|
|
5675
|
+
};
|
|
5676
|
+
}
|
|
5677
|
+
const limit = numberIn(file(dirs.memory, "memory.limit_in_bytes"));
|
|
5678
|
+
const quotaUs = numberIn(file(dirs.cpu, "cpu.cfs_quota_us"));
|
|
5679
|
+
const periodUs = numberIn(file(dirs.cpu, "cpu.cfs_period_us"));
|
|
5680
|
+
const throttledNs = numberIn(file(dirs.cpu, "cpu.stat"), /^throttled_time (\d+)/m);
|
|
5681
|
+
return {
|
|
5682
|
+
version: 1,
|
|
5683
|
+
memoryLimitBytes: limit !== null && limit < UNLIMITED ? limit : null,
|
|
5684
|
+
memoryCurrentBytes: numberIn(file(dirs.memory, "memory.usage_in_bytes")),
|
|
5685
|
+
memoryPeakBytes: numberIn(file(dirs.memory, "memory.max_usage_in_bytes")),
|
|
5686
|
+
oomKills: numberIn(file(dirs.memory, "memory.oom_control"), /^oom_kill (\d+)/m),
|
|
5687
|
+
cpuQuotaCores: quotaUs !== null && quotaUs > 0 && periodUs ? quotaUs / periodUs : null,
|
|
5688
|
+
throttledMs: throttledNs === null ? null : throttledNs / 1e6
|
|
5689
|
+
};
|
|
5690
|
+
}
|
|
5691
|
+
};
|
|
5692
|
+
|
|
5693
|
+
// src/internal/capture/worker-metrics.ts
|
|
5694
|
+
var MB = 1024 * 1024;
|
|
5695
|
+
function round1(value) {
|
|
5696
|
+
return Math.round(value * 10) / 10;
|
|
5697
|
+
}
|
|
5698
|
+
var WorkerMetrics = class {
|
|
5699
|
+
constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
|
|
5700
|
+
this.proc = proc;
|
|
5701
|
+
this.pid = pid;
|
|
5702
|
+
this.loop = null;
|
|
5703
|
+
this.usage = null;
|
|
5704
|
+
/** Each process's CPU and wait at the start of the current window; null before the first. */
|
|
5705
|
+
this.subtree = null;
|
|
5706
|
+
this.roles = /* @__PURE__ */ new Map();
|
|
5707
|
+
this.linux = platform === "linux" && proc.available();
|
|
5708
|
+
let histogram = null;
|
|
5709
|
+
try {
|
|
5710
|
+
histogram = (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
|
|
5711
|
+
histogram.enable();
|
|
5712
|
+
} catch {
|
|
5713
|
+
histogram = null;
|
|
5714
|
+
}
|
|
5715
|
+
this.histogram = histogram;
|
|
5716
|
+
}
|
|
5717
|
+
/** The worker's processes under it, keyed by pid and start time. */
|
|
5718
|
+
tree() {
|
|
5719
|
+
const table = this.proc.table();
|
|
5720
|
+
return descendantsOf(this.pid, table.values()).map((stat) => {
|
|
5721
|
+
const key = `${stat.pid}:${stat.start}`;
|
|
5722
|
+
let role = this.roles.get(key);
|
|
5723
|
+
if (!role) {
|
|
5724
|
+
role = roleOf(stat.comm, this.proc.cmdline(stat.pid)) ?? "other";
|
|
5725
|
+
this.roles.set(key, role);
|
|
5726
|
+
}
|
|
5727
|
+
return { key, pid: stat.pid, cpuMs: stat.cpuMs, role };
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5730
|
+
/**
|
|
5731
|
+
* The processes under the worker now, and the reset of their peak memory: the
|
|
5732
|
+
* start of the next window. Taken once per test, at its end, so the time
|
|
5733
|
+
* between two tests counts toward the next one.
|
|
5734
|
+
*/
|
|
5735
|
+
snapshot() {
|
|
5736
|
+
this.subtree = /* @__PURE__ */ new Map();
|
|
5737
|
+
for (const proc of this.tree()) {
|
|
5738
|
+
this.subtree.set(proc.key, { cpuMs: proc.cpuMs, waitMs: this.proc.runWaitMs(proc.pid) });
|
|
5739
|
+
this.proc.resetPeakRss(proc.pid);
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
/**
|
|
5743
|
+
* Start a test's window. `processes` says whether the worker has started any
|
|
5744
|
+
* browser to read; a worker that never did skips the process tree.
|
|
5745
|
+
*/
|
|
5746
|
+
start(processes = true) {
|
|
5747
|
+
try {
|
|
5748
|
+
this.histogram?.reset();
|
|
5749
|
+
this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
|
|
5750
|
+
this.usage = process.resourceUsage();
|
|
5751
|
+
if (this.linux && processes && !this.subtree) this.snapshot();
|
|
5752
|
+
} catch {
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
/** Close the window. Null when the test's start was never read. */
|
|
5756
|
+
end(processes = true) {
|
|
5757
|
+
if (!this.usage || !this.loop) return null;
|
|
5758
|
+
try {
|
|
5759
|
+
const usage = process.resourceUsage();
|
|
5760
|
+
const loop = import_node_perf_hooks.performance.eventLoopUtilization(this.loop);
|
|
5761
|
+
const metrics = {
|
|
5762
|
+
worker: {
|
|
5763
|
+
cpuMs: Math.round(
|
|
5764
|
+
(usage.userCPUTime - this.usage.userCPUTime + usage.systemCPUTime - this.usage.systemCPUTime) / 1e3
|
|
5765
|
+
),
|
|
5766
|
+
involuntarySwitches: usage.involuntaryContextSwitches - this.usage.involuntaryContextSwitches,
|
|
5767
|
+
loopUtilization: Math.round(loop.utilization * 1e3) / 1e3,
|
|
5768
|
+
loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
|
|
5769
|
+
loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
|
|
5770
|
+
heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
|
|
5771
|
+
fds: this.linux ? this.proc.fdCount(this.pid) : null
|
|
5772
|
+
}
|
|
5773
|
+
};
|
|
5774
|
+
this.usage = null;
|
|
5775
|
+
this.loop = null;
|
|
5776
|
+
if (this.linux && processes) metrics.roles = this.roleCosts();
|
|
5777
|
+
return metrics;
|
|
5778
|
+
} catch {
|
|
5779
|
+
return null;
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
/** The window's cost by role; what it reads becomes the start of the next window. */
|
|
5783
|
+
roleCosts() {
|
|
5784
|
+
const roles = {};
|
|
5785
|
+
const start = this.subtree ?? /* @__PURE__ */ new Map();
|
|
5786
|
+
const next = /* @__PURE__ */ new Map();
|
|
5787
|
+
for (const proc of this.tree()) {
|
|
5788
|
+
const from = start.get(proc.key) ?? { cpuMs: 0, waitMs: 0 };
|
|
5789
|
+
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
5790
|
+
const peakKb = this.proc.statusKb(proc.pid, "VmHWM");
|
|
5791
|
+
const cost = roles[proc.role] ??= { cpuMs: 0, runWaitMs: null, peakRssMb: null, processes: 0 };
|
|
5792
|
+
cost.cpuMs += Math.max(0, proc.cpuMs - from.cpuMs);
|
|
5793
|
+
if (waitMs !== null && from.waitMs !== null)
|
|
5794
|
+
cost.runWaitMs = round1((cost.runWaitMs ?? 0) + Math.max(0, waitMs - from.waitMs));
|
|
5795
|
+
if (peakKb !== null) cost.peakRssMb = Math.max(cost.peakRssMb ?? 0, round1(peakKb / 1024));
|
|
5796
|
+
cost.processes++;
|
|
5797
|
+
next.set(proc.key, { cpuMs: proc.cpuMs, waitMs });
|
|
5798
|
+
this.proc.resetPeakRss(proc.pid);
|
|
5799
|
+
}
|
|
5800
|
+
this.subtree = next;
|
|
5801
|
+
return roles;
|
|
5802
|
+
}
|
|
5803
|
+
dispose() {
|
|
5804
|
+
this.histogram?.disable();
|
|
5805
|
+
}
|
|
5806
|
+
};
|
|
5807
|
+
var PageReader = class {
|
|
5808
|
+
constructor() {
|
|
5809
|
+
this.sessions = /* @__PURE__ */ new WeakMap();
|
|
5810
|
+
}
|
|
5811
|
+
async read(page) {
|
|
5812
|
+
let session = this.sessions.get(page);
|
|
5813
|
+
if (!session) {
|
|
5814
|
+
session = internalCall(page, async () => {
|
|
5815
|
+
const opened2 = await page.context().newCDPSession(page);
|
|
5816
|
+
await opened2.send("Performance.enable", { timeDomain: "threadTicks" });
|
|
5817
|
+
return opened2;
|
|
5818
|
+
}).catch(() => null);
|
|
5819
|
+
this.sessions.set(page, session);
|
|
5820
|
+
}
|
|
5821
|
+
const opened = await session;
|
|
5822
|
+
if (!opened) return null;
|
|
5823
|
+
try {
|
|
5824
|
+
const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
|
|
5825
|
+
const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
|
|
5826
|
+
return {
|
|
5827
|
+
cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
|
|
5828
|
+
heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
|
|
5829
|
+
nodes: values.get("Nodes") ?? 0,
|
|
5830
|
+
listeners: values.get("JSEventListeners") ?? 0
|
|
5831
|
+
};
|
|
5832
|
+
} catch {
|
|
5833
|
+
return null;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
};
|
|
5837
|
+
|
|
5838
|
+
// src/internal/capture/resource-ledger.ts
|
|
5839
|
+
var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
|
|
5840
|
+
var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
|
|
5841
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
5842
|
+
var PAGE_READ_CAP = 50;
|
|
5843
|
+
var PAGE_READ_TIMEOUT_MS = 1e3;
|
|
5844
|
+
var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
|
|
5845
|
+
var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
|
|
5846
|
+
function relativePath(file) {
|
|
5847
|
+
return path20.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path20.sep).join("/");
|
|
5848
|
+
}
|
|
5849
|
+
function isLibraryFile(file) {
|
|
5850
|
+
return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path20.sep}node_modules${path20.sep}`) || file.includes("/node_modules/") || file === __filename;
|
|
5851
|
+
}
|
|
5852
|
+
function userSite(stack) {
|
|
5853
|
+
for (const line of (stack ?? "").split("\n").slice(1)) {
|
|
5854
|
+
const match = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/.exec(line);
|
|
5855
|
+
if (!match) continue;
|
|
5856
|
+
const file = match[1].replace(/^file:\/\//, "");
|
|
5857
|
+
if (isLibraryFile(file) || !/\.[a-z]+$/i.test(file)) continue;
|
|
5858
|
+
return `${relativePath(file)}:${match[2]}`;
|
|
5859
|
+
}
|
|
5860
|
+
return null;
|
|
5861
|
+
}
|
|
5862
|
+
function zoneSite(zone) {
|
|
5863
|
+
for (const frame of zone?.frames ?? []) {
|
|
5864
|
+
if (!frame?.file || isLibraryFile(frame.file)) continue;
|
|
5865
|
+
return `${relativePath(frame.file)}:${frame.line ?? 0}`;
|
|
5866
|
+
}
|
|
5867
|
+
return null;
|
|
5868
|
+
}
|
|
5869
|
+
function testRef2(info) {
|
|
5870
|
+
if (!info?.testId) return null;
|
|
5871
|
+
const titlePath = Array.isArray(info.titlePath) ? info.titlePath : [];
|
|
5872
|
+
return {
|
|
5873
|
+
id: info.testId,
|
|
5874
|
+
file: info.file ? relativePath(info.file) : "",
|
|
5875
|
+
suite: titlePath.slice(1, -1).map(String)
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
function runnableOf(info) {
|
|
5879
|
+
if (!info) return { phase: "worker", fixture: null };
|
|
5880
|
+
const runnable = info._timeoutManager?._running?.runnable;
|
|
5881
|
+
const type = typeof runnable?.type === "string" ? runnable.type : "test";
|
|
5882
|
+
const phase = HOOK_PHASES.has(type) ? type : type === "teardown" ? "worker" : "test";
|
|
5883
|
+
const raw = runnable?.fixture;
|
|
5884
|
+
const fixture = raw && typeof raw.title === "string" ? {
|
|
5885
|
+
title: raw.title,
|
|
5886
|
+
location: raw.location?.file ? `${relativePath(raw.location.file)}:${raw.location.line ?? 0}` : null,
|
|
5887
|
+
worker: raw.slot !== void 0 && raw.slot !== null
|
|
5888
|
+
} : null;
|
|
5889
|
+
return { phase, fixture };
|
|
5890
|
+
}
|
|
5891
|
+
function fallbackRunnable(window) {
|
|
5892
|
+
return { phase: window ? "test" : "worker", fixture: null };
|
|
5893
|
+
}
|
|
5894
|
+
function playwrightTestInfoSource() {
|
|
5895
|
+
try {
|
|
5896
|
+
const packageJson = resolvePlaywrightPackageJson();
|
|
5897
|
+
if (!packageJson) return () => null;
|
|
5898
|
+
const globals = (0, import_node_module2.createRequire)(packageJson)(path20.join(path20.dirname(packageJson), "lib", "globals.js"));
|
|
5899
|
+
const read = globals.currentTestInfo;
|
|
5900
|
+
if (typeof read !== "function") return () => null;
|
|
5901
|
+
return () => {
|
|
5902
|
+
try {
|
|
5903
|
+
return read() ?? null;
|
|
5904
|
+
} catch {
|
|
5905
|
+
return null;
|
|
5906
|
+
}
|
|
5907
|
+
};
|
|
5908
|
+
} catch {
|
|
5909
|
+
return () => null;
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
|
|
5913
|
+
return value === "fail" || value === "close" ? value : "report";
|
|
5914
|
+
}
|
|
5915
|
+
function countHandles() {
|
|
5916
|
+
const counts = {};
|
|
5917
|
+
const read = process.getActiveResourcesInfo;
|
|
5918
|
+
if (typeof read !== "function") return counts;
|
|
5919
|
+
for (const type of read.call(process)) {
|
|
5920
|
+
if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
|
|
5921
|
+
}
|
|
5922
|
+
return counts;
|
|
5923
|
+
}
|
|
5924
|
+
async function settledHandles(start) {
|
|
5925
|
+
const end = countHandles();
|
|
5926
|
+
if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
|
|
5927
|
+
await new Promise((resolve7) => setTimeout(resolve7, 0));
|
|
5928
|
+
return countHandles();
|
|
5929
|
+
}
|
|
5930
|
+
function settleWithin(promise, ms) {
|
|
5931
|
+
let timer;
|
|
5932
|
+
return Promise.race([
|
|
5933
|
+
promise.catch(() => void 0),
|
|
5934
|
+
new Promise((resolve7) => {
|
|
5935
|
+
timer = setTimeout(resolve7, ms);
|
|
5936
|
+
})
|
|
5937
|
+
]).finally(() => clearTimeout(timer));
|
|
5938
|
+
}
|
|
5939
|
+
var ResourceLedger = class {
|
|
5940
|
+
constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
|
|
5941
|
+
this.playwright = playwright;
|
|
5942
|
+
this.workerIndex = workerIndex;
|
|
5943
|
+
this.leakCheck = leakCheck;
|
|
5944
|
+
this.resultsFile = resultsFile;
|
|
5945
|
+
this.testInfoSource = testInfoSource;
|
|
5946
|
+
this.now = now;
|
|
5947
|
+
this.readers = readers;
|
|
5948
|
+
this.nextId = 1;
|
|
5949
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
5950
|
+
this.byObject = /* @__PURE__ */ new WeakMap();
|
|
5951
|
+
this.pendingCalls = /* @__PURE__ */ new Set();
|
|
5952
|
+
this.lastCall = null;
|
|
5953
|
+
/** The page a wrapped page method is calling into: its first reported API call marks it used. */
|
|
5954
|
+
this.usingPage = null;
|
|
5955
|
+
this.born = [];
|
|
5956
|
+
this.closed = [];
|
|
5957
|
+
this.handlesAtStart = {};
|
|
5958
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
5959
|
+
/** The test between `testStarted` and `testEnded`, for when Playwright's own test info cannot be read. */
|
|
5960
|
+
this.window = null;
|
|
5961
|
+
this.listener = null;
|
|
5962
|
+
/** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
|
|
5963
|
+
this.sawBrowser = false;
|
|
5964
|
+
}
|
|
5965
|
+
/** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
|
|
5966
|
+
install() {
|
|
5967
|
+
const instrumentation = this.playwright?._instrumentation;
|
|
5968
|
+
if (!instrumentation || typeof instrumentation.addListener !== "function") return false;
|
|
5969
|
+
this.listener = {
|
|
5970
|
+
onApiCallBegin: (zone) => {
|
|
5971
|
+
if (this.pendingCalls.size > 1e3) this.pendingCalls.clear();
|
|
5972
|
+
this.pendingCalls.add(zone);
|
|
5973
|
+
this.lastCall = zone;
|
|
5974
|
+
if (this.usingPage) {
|
|
5975
|
+
this.usingPage.used = true;
|
|
5976
|
+
this.usingPage = null;
|
|
5977
|
+
}
|
|
5978
|
+
},
|
|
5979
|
+
onApiCallEnd: (zone) => {
|
|
5980
|
+
this.pendingCalls.delete(zone);
|
|
5981
|
+
},
|
|
5982
|
+
runAfterCreateBrowserContext: async (context) => {
|
|
5983
|
+
this.guard(() => this.onContext(context, userSite(new Error().stack)));
|
|
5984
|
+
},
|
|
5985
|
+
runAfterCreateRequestContext: async (request) => {
|
|
5986
|
+
this.guard(() => this.track(request, "request", null, userSite(new Error().stack)));
|
|
5987
|
+
},
|
|
5988
|
+
runBeforeCloseRequestContext: async (request) => {
|
|
5989
|
+
this.guard(() => this.onClosed(request));
|
|
5990
|
+
}
|
|
5991
|
+
};
|
|
5992
|
+
instrumentation.addListener(this.listener);
|
|
5993
|
+
for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
|
|
5994
|
+
if (type) this.wrapBrowserType(type);
|
|
5995
|
+
}
|
|
5996
|
+
return true;
|
|
5997
|
+
}
|
|
5998
|
+
/** Stop listening; the objects already tracked stay in the ledger. */
|
|
5999
|
+
uninstall() {
|
|
6000
|
+
const remove = this.playwright?._instrumentation?.removeListener;
|
|
6001
|
+
if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
|
|
6002
|
+
this.listener = null;
|
|
6003
|
+
}
|
|
6004
|
+
/** Mark a page used: a locator was built on it. */
|
|
6005
|
+
notePageUse(page) {
|
|
6006
|
+
const entry = this.byObject.get(page);
|
|
6007
|
+
if (entry) entry.used = true;
|
|
6008
|
+
}
|
|
6009
|
+
/** Open the census window of a test. */
|
|
6010
|
+
testStarted(info) {
|
|
6011
|
+
this.window = testRef2(info);
|
|
6012
|
+
this.handlesAtStart = countHandles();
|
|
6013
|
+
this.openAtStart = { contexts: 0, pages: 0 };
|
|
6014
|
+
for (const entry of this.openEntries()) {
|
|
6015
|
+
if (entry.birth.kind === "page") this.openAtStart.pages++;
|
|
6016
|
+
else if (entry.birth.kind === "context") this.openAtStart.contexts++;
|
|
6017
|
+
}
|
|
6018
|
+
this.readers.metrics?.start(this.sawBrowser);
|
|
6019
|
+
}
|
|
6020
|
+
/**
|
|
6021
|
+
* Close the census window of a test: apply `PIWI_LEAK_CHECK=close` to what the
|
|
6022
|
+
* test left open, then take the census. Called after the test-scoped fixtures
|
|
6023
|
+
* tore down, so what the test opened through them is closed by now.
|
|
6024
|
+
*/
|
|
6025
|
+
async testEnded(info) {
|
|
6026
|
+
const test = testRef2(info) ?? this.window;
|
|
6027
|
+
const failed = typeof info.status === "string" && typeof info.expectedStatus === "string" && info.status !== info.expectedStatus;
|
|
6028
|
+
const leaks = test && !failed ? this.leaksOf(test.id) : [];
|
|
6029
|
+
if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
|
|
6030
|
+
const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
|
|
6031
|
+
const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
|
|
6032
|
+
const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
|
|
6033
|
+
const census = this.census(ref, handles, await this.readPages());
|
|
6034
|
+
if (metrics) census.metrics = metrics;
|
|
6035
|
+
if (test) {
|
|
6036
|
+
census.openAtStart = this.openAtStart;
|
|
6037
|
+
if (!failed) census.leftOpen = leaks.length;
|
|
6038
|
+
}
|
|
6039
|
+
this.window = null;
|
|
6040
|
+
return { census, leaks };
|
|
6041
|
+
}
|
|
6042
|
+
/** The open pages' main thread and weight, over CDP, within a deadline. */
|
|
6043
|
+
async readPages() {
|
|
6044
|
+
const out = /* @__PURE__ */ new Map();
|
|
6045
|
+
const reader = this.readers.pages;
|
|
6046
|
+
if (!reader) return out;
|
|
6047
|
+
const pages = this.openEntries().filter((entry) => entry.birth.kind === "page").slice(0, PAGE_READ_CAP);
|
|
6048
|
+
await settleWithin(
|
|
6049
|
+
Promise.all(
|
|
6050
|
+
pages.map(async (entry) => {
|
|
6051
|
+
const main = entry.object ? await reader.read(entry.object) : null;
|
|
6052
|
+
if (main) out.set(entry.birth.id, main);
|
|
6053
|
+
})
|
|
6054
|
+
),
|
|
6055
|
+
PAGE_READ_TIMEOUT_MS
|
|
6056
|
+
);
|
|
6057
|
+
return out;
|
|
6058
|
+
}
|
|
6059
|
+
/** The census a worker takes as it shuts down, appended to the results file when the reporter set one. */
|
|
6060
|
+
workerEnded() {
|
|
6061
|
+
const census = this.census(null);
|
|
6062
|
+
if (this.resultsFile) {
|
|
6063
|
+
try {
|
|
6064
|
+
fs19.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
|
|
6065
|
+
`);
|
|
6066
|
+
} catch {
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
this.readers.metrics?.dispose();
|
|
6070
|
+
this.uninstall();
|
|
6071
|
+
return census;
|
|
6072
|
+
}
|
|
6073
|
+
guard(run) {
|
|
6074
|
+
try {
|
|
6075
|
+
run();
|
|
6076
|
+
} catch {
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
wrapBrowserType(type) {
|
|
6080
|
+
for (const method of ["launch", "connect", "connectOverCDP"]) {
|
|
6081
|
+
const original = type[method];
|
|
6082
|
+
if (typeof original !== "function") continue;
|
|
6083
|
+
const bound = original.bind(type);
|
|
6084
|
+
type[method] = async (...args) => {
|
|
6085
|
+
const site = userSite(new Error().stack);
|
|
6086
|
+
const browser = await bound(...args);
|
|
6087
|
+
this.guard(() => this.onBrowser(browser, site));
|
|
6088
|
+
return browser;
|
|
6089
|
+
};
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
track(object, kind, parent, site) {
|
|
6093
|
+
const existing = this.byObject.get(object);
|
|
6094
|
+
if (existing) return existing;
|
|
6095
|
+
const info = this.testInfoSource();
|
|
6096
|
+
const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
6097
|
+
const birth = {
|
|
6098
|
+
id: this.nextId++,
|
|
6099
|
+
kind,
|
|
6100
|
+
parent: parent ? this.byObject.get(parent)?.birth.id ?? null : null,
|
|
6101
|
+
at: this.now(),
|
|
6102
|
+
phase,
|
|
6103
|
+
fixture,
|
|
6104
|
+
test: testRef2(info) ?? this.window,
|
|
6105
|
+
site
|
|
6106
|
+
};
|
|
6107
|
+
const entry = { birth, object, closed: null, used: false };
|
|
6108
|
+
if (kind === "browser" || kind === "context") this.sawBrowser = true;
|
|
6109
|
+
this.entries.set(birth.id, entry);
|
|
6110
|
+
this.byObject.set(object, entry);
|
|
6111
|
+
this.born.push(birth);
|
|
6112
|
+
return entry;
|
|
6113
|
+
}
|
|
6114
|
+
onBrowser(browser, site) {
|
|
6115
|
+
const known = this.byObject.get(browser);
|
|
6116
|
+
if (known) return known;
|
|
6117
|
+
const entry = this.track(browser, "browser", null, site);
|
|
6118
|
+
browser.on("disconnected", () => this.guard(() => this.onClosed(browser)));
|
|
6119
|
+
return entry;
|
|
6120
|
+
}
|
|
6121
|
+
onContext(context, site) {
|
|
6122
|
+
if (this.byObject.has(context)) return;
|
|
6123
|
+
const browser = context.browser();
|
|
6124
|
+
if (browser) this.onBrowser(browser, site);
|
|
6125
|
+
const entry = this.track(context, "context", browser, site);
|
|
6126
|
+
const zone = this.latestPendingCall();
|
|
6127
|
+
if (zone?.apiName?.endsWith(".newPage")) entry.birth.implicit = true;
|
|
6128
|
+
if (context._forReuse) entry.birth.reused = true;
|
|
6129
|
+
context.on("close", () => this.guard(() => this.onClosed(context)));
|
|
6130
|
+
context.on("page", (page) => this.guard(() => this.onPage(page, context)));
|
|
6131
|
+
for (const page of context.pages()) this.onPage(page, context, { site });
|
|
6132
|
+
}
|
|
6133
|
+
/** `initial` is set for a page that came with its context (a persistent context's), which is no popup. */
|
|
6134
|
+
onPage(page, context, initial) {
|
|
6135
|
+
if (this.byObject.has(page)) return;
|
|
6136
|
+
const zone = this.latestPendingCall();
|
|
6137
|
+
const viaNewPage = !!zone?.apiName?.endsWith(".newPage");
|
|
6138
|
+
const popup = !viaNewPage && !initial;
|
|
6139
|
+
const site = viaNewPage ? zoneSite(zone) : initial ? initial.site : zoneSite(this.lastCall);
|
|
6140
|
+
const entry = this.track(page, "page", context, site);
|
|
6141
|
+
if (popup) {
|
|
6142
|
+
const opener = page._opener;
|
|
6143
|
+
const openerEntry = opener ? this.byObject.get(opener) : void 0;
|
|
6144
|
+
if (openerEntry) entry.birth.opener = openerEntry.birth.id;
|
|
6145
|
+
if (this.lastCall?.apiName) entry.birth.trigger = this.lastCall.apiName;
|
|
6146
|
+
}
|
|
6147
|
+
const markUsed = () => {
|
|
6148
|
+
entry.used = true;
|
|
6149
|
+
};
|
|
6150
|
+
page.on("load", markUsed);
|
|
6151
|
+
page.on("domcontentloaded", markUsed);
|
|
6152
|
+
page.on("framenavigated", (frame) => {
|
|
6153
|
+
try {
|
|
6154
|
+
if (frame === page.mainFrame() && frame.url() !== "about:blank") entry.used = true;
|
|
6155
|
+
} catch {
|
|
6156
|
+
}
|
|
6157
|
+
});
|
|
6158
|
+
page.on("close", () => this.guard(() => this.onClosed(page)));
|
|
6159
|
+
const methods = page;
|
|
6160
|
+
for (const method of USE_METHODS) {
|
|
6161
|
+
const original = methods[method];
|
|
6162
|
+
if (typeof original !== "function") continue;
|
|
6163
|
+
const bound = original.bind(page);
|
|
6164
|
+
methods[method] = (...args) => {
|
|
6165
|
+
const previous = this.usingPage;
|
|
6166
|
+
this.usingPage = entry;
|
|
6167
|
+
try {
|
|
6168
|
+
return bound(...args);
|
|
6169
|
+
} finally {
|
|
6170
|
+
this.usingPage = previous;
|
|
6171
|
+
}
|
|
6172
|
+
};
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
onClosed(object, byPiwi = false) {
|
|
6176
|
+
const entry = this.byObject.get(object);
|
|
6177
|
+
if (!entry || entry.closed) return;
|
|
6178
|
+
const info = this.testInfoSource();
|
|
6179
|
+
const { phase } = info ? runnableOf(info) : fallbackRunnable(this.window);
|
|
6180
|
+
const close = {
|
|
6181
|
+
id: entry.birth.id,
|
|
6182
|
+
at: this.now(),
|
|
6183
|
+
phase,
|
|
6184
|
+
test: testRef2(info) ?? this.window
|
|
6185
|
+
};
|
|
6186
|
+
if (entry.birth.kind === "page") close.used = entry.used;
|
|
6187
|
+
if (byPiwi) close.byPiwi = true;
|
|
6188
|
+
entry.closed = close;
|
|
6189
|
+
entry.object = null;
|
|
6190
|
+
this.closed.push(close);
|
|
6191
|
+
}
|
|
6192
|
+
latestPendingCall() {
|
|
6193
|
+
let latest = null;
|
|
6194
|
+
for (const zone of this.pendingCalls) latest = zone;
|
|
6195
|
+
return latest;
|
|
6196
|
+
}
|
|
6197
|
+
openEntries() {
|
|
6198
|
+
return [...this.entries.values()].filter((entry) => !entry.closed && entry.object);
|
|
6199
|
+
}
|
|
6200
|
+
/**
|
|
6201
|
+
* Objects a test opened itself, by its body or its each-hooks, and left open.
|
|
6202
|
+
* A page in a leaked context, or a context in a leaked browser, goes with it;
|
|
6203
|
+
* the context `browser.newPage()` opened for its page goes with the page.
|
|
6204
|
+
*/
|
|
6205
|
+
leaksOf(testId) {
|
|
6206
|
+
const open = this.openEntries().filter(
|
|
6207
|
+
(entry) => entry.birth.test?.id === testId && entry.birth.fixture === null && (entry.birth.phase === "test" || entry.birth.phase === "beforeEach" || entry.birth.phase === "afterEach") && !entry.birth.reused
|
|
6208
|
+
);
|
|
6209
|
+
const openIds = new Set(open.map((entry) => entry.birth.id));
|
|
6210
|
+
const foldsIntoLeak = (entry) => {
|
|
6211
|
+
let parentId = entry.birth.parent;
|
|
6212
|
+
while (parentId !== null) {
|
|
6213
|
+
if (!this.isImplicit(parentId)) return openIds.has(parentId);
|
|
6214
|
+
parentId = this.entries.get(parentId)?.birth.parent ?? null;
|
|
6215
|
+
}
|
|
6216
|
+
return false;
|
|
6217
|
+
};
|
|
6218
|
+
return open.filter((entry) => !(entry.birth.kind === "context" && this.isImplicit(entry.birth.id))).filter((entry) => !foldsIntoLeak(entry)).map((entry) => ({
|
|
6219
|
+
id: entry.birth.id,
|
|
6220
|
+
kind: entry.birth.kind,
|
|
6221
|
+
site: entry.birth.site,
|
|
6222
|
+
pages: entry.birth.kind === "page" ? 0 : this.pagesUnder(entry.birth.id),
|
|
6223
|
+
implicit: false,
|
|
6224
|
+
...entry.birth.opener !== void 0 ? { trigger: entry.birth.trigger ?? "a click" } : {}
|
|
6225
|
+
})).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
6226
|
+
}
|
|
6227
|
+
/** Open pages under a context, or under any context of a browser. */
|
|
6228
|
+
pagesUnder(id) {
|
|
6229
|
+
let count = 0;
|
|
6230
|
+
for (const entry of this.openEntries()) {
|
|
6231
|
+
if (entry.birth.kind !== "page" || entry.birth.parent === null) continue;
|
|
6232
|
+
const context = this.entries.get(entry.birth.parent);
|
|
6233
|
+
if (entry.birth.parent === id || context?.birth.parent === id) count++;
|
|
6234
|
+
}
|
|
6235
|
+
return count;
|
|
6236
|
+
}
|
|
6237
|
+
isImplicit(id) {
|
|
6238
|
+
const entry = this.entries.get(id);
|
|
6239
|
+
if (!entry || entry.birth.kind !== "context") return false;
|
|
6240
|
+
if (entry.birth.implicit) return true;
|
|
6241
|
+
const owner = entry.object ? entry.object._ownerPage : void 0;
|
|
6242
|
+
if (owner) entry.birth.implicit = true;
|
|
6243
|
+
return !!owner;
|
|
6244
|
+
}
|
|
6245
|
+
openPagesOf(contextId) {
|
|
6246
|
+
let count = 0;
|
|
6247
|
+
for (const entry of this.openEntries())
|
|
6248
|
+
if (entry.birth.kind === "page" && entry.birth.parent === contextId) count++;
|
|
6249
|
+
return count;
|
|
6250
|
+
}
|
|
6251
|
+
async closeLeaks(leaks) {
|
|
6252
|
+
for (const leak of leaks) {
|
|
6253
|
+
const entry = this.entries.get(leak.id);
|
|
6254
|
+
const object = entry?.object;
|
|
6255
|
+
if (!entry || entry.closed || !object) continue;
|
|
6256
|
+
const close = leak.kind === "request" ? () => object.dispose() : () => object.close();
|
|
6257
|
+
await settleWithin(internalCall(object, close), CLOSE_TIMEOUT_MS);
|
|
6258
|
+
const closedMeanwhile = entry.closed;
|
|
6259
|
+
if (closedMeanwhile) closedMeanwhile.byPiwi = true;
|
|
6260
|
+
else this.onClosed(object, true);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
census(test, handles, mains = /* @__PURE__ */ new Map()) {
|
|
6264
|
+
const open = [];
|
|
6265
|
+
for (const entry of this.openEntries()) {
|
|
6266
|
+
const object = entry.object;
|
|
6267
|
+
if (entry.birth.kind === "page") {
|
|
6268
|
+
const main = mains.get(entry.birth.id);
|
|
6269
|
+
open.push({
|
|
6270
|
+
id: entry.birth.id,
|
|
6271
|
+
used: entry.used,
|
|
6272
|
+
url: this.pageUrl(object),
|
|
6273
|
+
listeners: this.listenerCount(object),
|
|
6274
|
+
routes: this.routeCount(object),
|
|
6275
|
+
...main ? { main } : {}
|
|
6276
|
+
});
|
|
6277
|
+
} else if (entry.birth.kind === "context") {
|
|
6278
|
+
this.isImplicit(entry.birth.id);
|
|
6279
|
+
open.push({ id: entry.birth.id, pages: this.openPagesOf(entry.birth.id), routes: this.routeCount(object) });
|
|
6280
|
+
} else {
|
|
6281
|
+
open.push({ id: entry.birth.id });
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
const census = {
|
|
6285
|
+
v: 1,
|
|
6286
|
+
worker: this.workerIndex,
|
|
6287
|
+
pid: process.pid,
|
|
6288
|
+
at: this.now(),
|
|
6289
|
+
test,
|
|
6290
|
+
born: this.born,
|
|
6291
|
+
closed: this.closed,
|
|
6292
|
+
open
|
|
6293
|
+
};
|
|
6294
|
+
if (handles) census.handles = handles;
|
|
6295
|
+
this.born = [];
|
|
6296
|
+
this.closed = [];
|
|
6297
|
+
return census;
|
|
6298
|
+
}
|
|
6299
|
+
pageUrl(page) {
|
|
6300
|
+
try {
|
|
6301
|
+
const url = typeof page.url === "function" ? String(page.url()) : "";
|
|
6302
|
+
return url ? url.split(/[?#]/)[0] : void 0;
|
|
6303
|
+
} catch {
|
|
6304
|
+
return void 0;
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
listenerCount(emitter) {
|
|
6308
|
+
try {
|
|
6309
|
+
if (typeof emitter.eventNames !== "function" || typeof emitter.listenerCount !== "function") return void 0;
|
|
6310
|
+
const names = emitter.eventNames();
|
|
6311
|
+
return names.reduce((sum, name) => sum + emitter.listenerCount(name), 0);
|
|
6312
|
+
} catch {
|
|
6313
|
+
return void 0;
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
routeCount(object) {
|
|
6317
|
+
const routes = object._routes;
|
|
6318
|
+
return Array.isArray(routes) ? routes.length : void 0;
|
|
6319
|
+
}
|
|
6320
|
+
};
|
|
6321
|
+
var activeLedger = null;
|
|
6322
|
+
function startResourceLedger(playwright, workerIndex) {
|
|
6323
|
+
if (process.env[PIWI_ENV_KEYS.captureResources] === "false") return null;
|
|
6324
|
+
if (activeLedger) return activeLedger;
|
|
6325
|
+
const metrics = new WorkerMetrics();
|
|
6326
|
+
const ledger = new ResourceLedger(
|
|
6327
|
+
playwright,
|
|
6328
|
+
workerIndex,
|
|
6329
|
+
readLeakCheck(),
|
|
6330
|
+
process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
|
|
6331
|
+
playwrightTestInfoSource(),
|
|
6332
|
+
Date.now,
|
|
6333
|
+
{ metrics, pages: new PageReader() }
|
|
6334
|
+
);
|
|
6335
|
+
if (!ledger.install()) {
|
|
6336
|
+
metrics.dispose();
|
|
6337
|
+
return null;
|
|
6338
|
+
}
|
|
6339
|
+
activeLedger = ledger;
|
|
6340
|
+
return ledger;
|
|
6341
|
+
}
|
|
6342
|
+
function activeResourceLedger() {
|
|
6343
|
+
return activeLedger;
|
|
6344
|
+
}
|
|
6345
|
+
function stopResourceLedger() {
|
|
6346
|
+
const ledger = activeLedger;
|
|
6347
|
+
activeLedger = null;
|
|
6348
|
+
return ledger ? ledger.workerEnded() : null;
|
|
6349
|
+
}
|
|
6350
|
+
function noteResourceUse(page) {
|
|
6351
|
+
activeLedger?.notePageUse(page);
|
|
6352
|
+
}
|
|
6353
|
+
var ARTICLE = {
|
|
6354
|
+
browser: "a browser",
|
|
6355
|
+
context: "a context",
|
|
6356
|
+
page: "a page",
|
|
6357
|
+
request: "an API request context"
|
|
6358
|
+
};
|
|
6359
|
+
function leakFailureMessage(leaks) {
|
|
6360
|
+
const lines = leaks.map((leak) => {
|
|
6361
|
+
const what = leak.trigger ? `a popup opened by ${leak.trigger}` : ARTICLE[leak.kind];
|
|
6362
|
+
const where = leak.site ? `${leak.trigger ? "" : " opened"} at ${leak.site}` : "";
|
|
6363
|
+
const pages = leak.pages > 0 ? `, with ${leak.pages} page${leak.pages === 1 ? "" : "s"}` : "";
|
|
6364
|
+
return ` - ${what}${where}${pages}`;
|
|
6365
|
+
});
|
|
6366
|
+
return [
|
|
6367
|
+
`Piwi: this test left ${leaks.length === 1 ? "one object" : `${leaks.length} objects`} open:`,
|
|
6368
|
+
...lines,
|
|
6369
|
+
"Close each before the test ends (await using, close() or dispose()), or set PIWI_LEAK_CHECK=report to only list them."
|
|
6370
|
+
].join("\n");
|
|
6371
|
+
}
|
|
6372
|
+
async function recordResourceCensus(testInfo) {
|
|
6373
|
+
const ledger = activeLedger;
|
|
6374
|
+
if (!ledger) return;
|
|
6375
|
+
const { census, leaks } = await ledger.testEnded(testInfo);
|
|
6376
|
+
try {
|
|
6377
|
+
await testInfo.attach(ATTACHMENT_NAMES.resources, {
|
|
6378
|
+
contentType: "application/json",
|
|
6379
|
+
body: Buffer.from(JSON.stringify(census))
|
|
6380
|
+
});
|
|
6381
|
+
} catch {
|
|
6382
|
+
}
|
|
6383
|
+
if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
// src/internal/collect/resource-verdicts.ts
|
|
6387
|
+
var path21 = __toESM(require("path"));
|
|
6388
|
+
var OPEN_STEP_TITLES = {
|
|
6389
|
+
"Create context": "context",
|
|
6390
|
+
"Launch persistent context": "context",
|
|
6391
|
+
"Launch browser": "browser",
|
|
6392
|
+
"Connect over CDP": "browser"
|
|
6393
|
+
};
|
|
6394
|
+
var CLOSE_STEP_TITLES = {
|
|
6395
|
+
"Close context": "context",
|
|
6396
|
+
"Close browser": "browser"
|
|
6397
|
+
};
|
|
6398
|
+
function parseResourceCensus(body) {
|
|
6399
|
+
if (!body) return null;
|
|
6400
|
+
try {
|
|
6401
|
+
const value = JSON.parse(body.toString());
|
|
6402
|
+
if (value?.v !== 1 || !Array.isArray(value.born) || !Array.isArray(value.closed) || !Array.isArray(value.open)) {
|
|
6403
|
+
return null;
|
|
6404
|
+
}
|
|
6405
|
+
if (typeof value.worker !== "number" || typeof value.at !== "number") return null;
|
|
6406
|
+
return value;
|
|
6407
|
+
} catch {
|
|
6408
|
+
return null;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
function parseResourceResults(text) {
|
|
6412
|
+
return text.split("\n").map((line) => line.trim() ? parseResourceCensus(line) : null).filter((census) => census !== null);
|
|
6413
|
+
}
|
|
6414
|
+
function userFixturesOf(steps) {
|
|
6415
|
+
const out = [];
|
|
6416
|
+
const walk = (list) => {
|
|
6417
|
+
for (const step of list) {
|
|
6418
|
+
const file = step.location?.file;
|
|
6419
|
+
if (step.category === "fixture" && file && !file.includes(`${path21.sep}node_modules${path21.sep}`)) {
|
|
6420
|
+
const name = step.title.replace(/^(?:Fixture\s+|fixture:\s*)/, "").replace(/^"(.*)"$/, "$1");
|
|
6421
|
+
if (!name.startsWith("piwi")) out.push(`${name} (${relativeFile(file)}:${step.location.line})`);
|
|
6422
|
+
}
|
|
6423
|
+
walk(step.steps ?? []);
|
|
6424
|
+
}
|
|
6425
|
+
};
|
|
6426
|
+
walk(steps);
|
|
6427
|
+
return [...new Set(out)];
|
|
6428
|
+
}
|
|
6429
|
+
function tallyLifecycleSteps(file, steps) {
|
|
6430
|
+
const tally = { file, opened: [], closed: { context: 0, browser: 0 } };
|
|
6431
|
+
const walk = (list) => {
|
|
6432
|
+
for (const step of list) {
|
|
6433
|
+
if (step.category === "fixture") continue;
|
|
6434
|
+
if (step.category === "pw:api") {
|
|
6435
|
+
const opened = OPEN_STEP_TITLES[step.title];
|
|
6436
|
+
const closed = CLOSE_STEP_TITLES[step.title];
|
|
6437
|
+
if (opened) {
|
|
6438
|
+
tally.opened.push({
|
|
6439
|
+
kind: opened,
|
|
6440
|
+
site: step.location ? `${relativeFile(step.location.file)}:${step.location.line}` : null
|
|
6441
|
+
});
|
|
6442
|
+
}
|
|
6443
|
+
if (closed) tally.closed[closed]++;
|
|
6444
|
+
}
|
|
6445
|
+
walk(step.steps ?? []);
|
|
6446
|
+
}
|
|
6447
|
+
};
|
|
6448
|
+
walk(steps);
|
|
6449
|
+
return tally;
|
|
6450
|
+
}
|
|
6451
|
+
function relativeFile(file) {
|
|
6452
|
+
return path21.relative(process.cwd(), file).split(path21.sep).join("/");
|
|
6453
|
+
}
|
|
6454
|
+
function buildResourceReport(input) {
|
|
6455
|
+
const findings = [];
|
|
6456
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
6457
|
+
for (const census of input.censuses) {
|
|
6458
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
6459
|
+
list.push(census);
|
|
6460
|
+
byWorker.set(census.worker, list);
|
|
6461
|
+
}
|
|
6462
|
+
const leaked = [];
|
|
6463
|
+
const idle = [];
|
|
6464
|
+
const all = [];
|
|
6465
|
+
for (const censuses of byWorker.values()) {
|
|
6466
|
+
censuses.sort((a, b) => a.at - b.at);
|
|
6467
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
6468
|
+
for (const census of censuses) {
|
|
6469
|
+
for (const birth of census.born) {
|
|
6470
|
+
tracked.set(birth.id, {
|
|
6471
|
+
worker: census.worker,
|
|
6472
|
+
birth,
|
|
6473
|
+
bornCensus: census,
|
|
6474
|
+
close: null,
|
|
6475
|
+
lastOpenAt: null,
|
|
6476
|
+
used: void 0,
|
|
6477
|
+
series: [],
|
|
6478
|
+
mainCpu: []
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
for (const close of census.closed) {
|
|
6482
|
+
const entry = tracked.get(close.id);
|
|
6483
|
+
if (!entry) continue;
|
|
6484
|
+
entry.close = close;
|
|
6485
|
+
if (close.used !== void 0) entry.used = close.used;
|
|
6486
|
+
}
|
|
6487
|
+
for (const open of census.open) {
|
|
6488
|
+
const entry = tracked.get(open.id);
|
|
6489
|
+
if (!entry) continue;
|
|
6490
|
+
entry.lastOpenAt = census.at;
|
|
6491
|
+
if (open.used !== void 0) entry.used = open.used;
|
|
6492
|
+
if (census.test) entry.series.push({ pages: open.pages, listeners: open.listeners, routes: open.routes });
|
|
6493
|
+
if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
const testCensuses = censuses.filter((census) => census.test !== null);
|
|
6497
|
+
const workerEnded = censuses.some((census) => census.test === null);
|
|
6498
|
+
for (const entry of tracked.values()) {
|
|
6499
|
+
all.push(entry);
|
|
6500
|
+
if (entry.birth.kind === "page" && entry.used === false && !entry.bornCensus.test?.failed) idle.push(entry);
|
|
6501
|
+
const verdict = leakVerdict(entry, testCensuses, workerEnded);
|
|
6502
|
+
if (verdict) leaked.push({ tracked: entry, ...verdict });
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
findings.push(...groupLeaks(leaked, all));
|
|
6506
|
+
findings.push(...pilingFindings(all));
|
|
6507
|
+
findings.push(...handleFindings(byWorker));
|
|
6508
|
+
findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
|
|
6509
|
+
findings.push(...probableFindings(input.tallies ?? []));
|
|
6510
|
+
const counts = { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 };
|
|
6511
|
+
for (const finding of findings) counts[finding.verdict] += finding.verdict === "idle" ? finding.count : 1;
|
|
6512
|
+
return { findings, counts };
|
|
6513
|
+
}
|
|
6514
|
+
function sameFile(birth, ref) {
|
|
6515
|
+
return !!ref && !!birth.test && ref.file === birth.test.file;
|
|
6516
|
+
}
|
|
6517
|
+
function insideBlock(birth, ref) {
|
|
6518
|
+
if (!ref || !sameFile(birth, ref)) return false;
|
|
6519
|
+
return birth.test.suite.every((title, index) => ref.suite[index] === title);
|
|
6520
|
+
}
|
|
6521
|
+
function leakVerdict(entry, testCensuses, workerEnded) {
|
|
6522
|
+
const { birth, close, bornCensus } = entry;
|
|
6523
|
+
if (birth.reused || birth.phase === "worker" || birth.fixture?.worker || !birth.test) return null;
|
|
6524
|
+
if (birth.kind === "context" && birth.implicit) return null;
|
|
6525
|
+
if (close?.byPiwi) return { scope: "test", heldMs: 0, untilWorkerEnd: false };
|
|
6526
|
+
const closedByShutdown = !!close && (close.phase === "worker" || close.test === null);
|
|
6527
|
+
if (birth.phase === "beforeAll" || birth.phase === "afterAll") {
|
|
6528
|
+
if (close && !closedByShutdown && sameFile(birth, close.test)) return null;
|
|
6529
|
+
const ended = workerEnded || testCensuses.some((census) => census.at >= bornCensus.at && !insideBlock(birth, census.test));
|
|
6530
|
+
if (!ended) return null;
|
|
6531
|
+
const lastInside = [...testCensuses].reverse().find((census) => insideBlock(birth, census.test));
|
|
6532
|
+
const blockEnd = lastInside?.at ?? bornCensus.at;
|
|
6533
|
+
const end2 = close?.at ?? entry.lastOpenAt ?? blockEnd;
|
|
6534
|
+
return { scope: "describe", heldMs: Math.max(0, end2 - blockEnd), untilWorkerEnd: !close || closedByShutdown };
|
|
6535
|
+
}
|
|
6536
|
+
if (bornCensus.test === null || bornCensus.test.id !== birth.test.id) return null;
|
|
6537
|
+
if (bornCensus.test.failed) return null;
|
|
6538
|
+
const openAtItsEnd = bornCensus.open.some((open) => open.id === birth.id);
|
|
6539
|
+
if (!openAtItsEnd) return null;
|
|
6540
|
+
if (close && !closedByShutdown && (close.test?.id === birth.test.id || sameFile(birth, close.test))) return null;
|
|
6541
|
+
const end = close?.at ?? entry.lastOpenAt ?? bornCensus.at;
|
|
6542
|
+
return { scope: "test", heldMs: Math.max(0, end - bornCensus.at), untilWorkerEnd: !close || closedByShutdown };
|
|
6543
|
+
}
|
|
6544
|
+
function whereOf(birth) {
|
|
6545
|
+
if (birth.opener !== void 0) {
|
|
6546
|
+
return `popup${birth.trigger ? ` after ${birth.trigger}` : ""}${birth.site ? ` at ${birth.site}` : ""}`;
|
|
6547
|
+
}
|
|
6548
|
+
if (birth.fixture) {
|
|
6549
|
+
const at = birth.site ?? birth.fixture.location;
|
|
6550
|
+
return `fixture "${birth.fixture.title}"${at ? ` at ${at}` : ""}`;
|
|
6551
|
+
}
|
|
6552
|
+
return birth.site ?? "an unknown line";
|
|
6553
|
+
}
|
|
6554
|
+
function afterTestCpu(entry) {
|
|
6555
|
+
const readings = entry.mainCpu;
|
|
6556
|
+
if (readings.length < 2) return 0;
|
|
6557
|
+
const first = readings.find((reading) => reading.at >= entry.bornCensus.at) ?? readings[0];
|
|
6558
|
+
return Math.max(0, readings[readings.length - 1].cpuMs - first.cpuMs);
|
|
6559
|
+
}
|
|
6560
|
+
function siteOf(birth) {
|
|
6561
|
+
return birth.site ?? birth.fixture?.location ?? null;
|
|
6562
|
+
}
|
|
6563
|
+
var keyOf = (worker, id) => `${worker}:${id}`;
|
|
6564
|
+
function groupLeaks(leaked, all) {
|
|
6565
|
+
const leaks = new Map(leaked.map((leak) => [keyOf(leak.tracked.worker, leak.tracked.birth.id), leak]));
|
|
6566
|
+
const tracked = new Map(all.map((entry) => [keyOf(entry.worker, entry.birth.id), entry]));
|
|
6567
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6568
|
+
const groupKey = (leak) => `${leak.scope}|${leak.tracked.birth.kind}|${whereOf(leak.tracked.birth)}`;
|
|
6569
|
+
for (const leak of leaked) {
|
|
6570
|
+
const { birth } = leak.tracked;
|
|
6571
|
+
if (birth.kind === "context" && birth.implicit) continue;
|
|
6572
|
+
if (foldTarget(leak.tracked, leaks, tracked)) continue;
|
|
6573
|
+
const key = groupKey(leak);
|
|
6574
|
+
const group = groups.get(key) ?? {
|
|
6575
|
+
verdict: "leaked",
|
|
6576
|
+
kind: birth.kind,
|
|
6577
|
+
where: whereOf(birth),
|
|
6578
|
+
site: siteOf(birth),
|
|
6579
|
+
scope: leak.scope,
|
|
6580
|
+
tests: 0,
|
|
6581
|
+
count: 0,
|
|
6582
|
+
heldMs: 0,
|
|
6583
|
+
untilWorkerEnd: false,
|
|
6584
|
+
pages: 0,
|
|
6585
|
+
testIds: /* @__PURE__ */ new Set(),
|
|
6586
|
+
...leak.scope === "describe" && birth.test?.suite.length ? { detail: `beforeAll of "${birth.test.suite.join(" \u203A ")}"` } : {}
|
|
6587
|
+
};
|
|
6588
|
+
group.count++;
|
|
6589
|
+
if (birth.test) group.testIds.add(birth.test.id);
|
|
6590
|
+
group.heldMs = Math.max(group.heldMs ?? 0, leak.heldMs);
|
|
6591
|
+
group.untilWorkerEnd = group.untilWorkerEnd || leak.untilWorkerEnd;
|
|
6592
|
+
if (leak.tracked.close?.byPiwi) group.closedByPiwi = true;
|
|
6593
|
+
if (birth.kind === "page" && leak.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
|
|
6594
|
+
groups.set(key, group);
|
|
6595
|
+
}
|
|
6596
|
+
for (const leak of leaked) {
|
|
6597
|
+
if (leak.tracked.birth.kind !== "page") continue;
|
|
6598
|
+
const root = foldTarget(leak.tracked, leaks, tracked);
|
|
6599
|
+
const group = root ? groups.get(groupKey(root)) : void 0;
|
|
6600
|
+
if (!group) continue;
|
|
6601
|
+
group.pages = (group.pages ?? 0) + 1;
|
|
6602
|
+
if (root.scope === "test") addCpu(group, afterTestCpu(leak.tracked));
|
|
6603
|
+
}
|
|
6604
|
+
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => a.scope === b.scope ? b.count - a.count : a.scope === "test" ? -1 : 1);
|
|
6605
|
+
}
|
|
6606
|
+
function addCpu(group, cpuMs) {
|
|
6607
|
+
if (cpuMs > 0) group.afterTestCpuMs = (group.afterTestCpuMs ?? 0) + cpuMs;
|
|
6608
|
+
}
|
|
6609
|
+
function foldTarget(entry, leaks, tracked) {
|
|
6610
|
+
let parentId = entry.birth.parent;
|
|
6611
|
+
while (parentId !== null) {
|
|
6612
|
+
const key = keyOf(entry.worker, parentId);
|
|
6613
|
+
const parent = tracked.get(key);
|
|
6614
|
+
if (!parent) return null;
|
|
6615
|
+
if (!(parent.birth.kind === "context" && parent.birth.implicit)) return leaks.get(key) ?? null;
|
|
6616
|
+
parentId = parent.birth.parent;
|
|
6617
|
+
}
|
|
6618
|
+
return null;
|
|
6619
|
+
}
|
|
6620
|
+
function groupIdle(idle, fixturesByTest) {
|
|
6621
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6622
|
+
for (const entry of idle) {
|
|
6623
|
+
const { birth } = entry;
|
|
6624
|
+
const fixtures = birth.test ? fixturesByTest.get(birth.test.id) ?? [] : [];
|
|
6625
|
+
const detail = birth.fixture && fixtures.length ? `set up with ${fixtures.join(", ")}` : void 0;
|
|
6626
|
+
const where = whereOf(birth);
|
|
6627
|
+
const key = `${where}|${detail ?? ""}`;
|
|
6628
|
+
const group = groups.get(key) ?? {
|
|
6629
|
+
verdict: "idle",
|
|
6630
|
+
kind: "page",
|
|
6631
|
+
where,
|
|
6632
|
+
site: siteOf(birth),
|
|
6633
|
+
tests: 0,
|
|
6634
|
+
count: 0,
|
|
6635
|
+
testIds: /* @__PURE__ */ new Set(),
|
|
6636
|
+
...detail ? { detail } : {}
|
|
6637
|
+
};
|
|
6638
|
+
group.count++;
|
|
6639
|
+
if (birth.test) group.testIds.add(birth.test.id);
|
|
6640
|
+
groups.set(key, group);
|
|
6641
|
+
}
|
|
6642
|
+
return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
|
|
6643
|
+
}
|
|
6644
|
+
function pilingFindings(all) {
|
|
6645
|
+
const out = [];
|
|
6646
|
+
for (const entry of all) {
|
|
6647
|
+
for (const what of ["pages", "listeners", "routes"]) {
|
|
6648
|
+
const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
|
|
6649
|
+
if (values.length < 3) continue;
|
|
6650
|
+
const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
|
|
6651
|
+
const from = values[0];
|
|
6652
|
+
const to = values[values.length - 1];
|
|
6653
|
+
if (!rising || to - from < 2) continue;
|
|
6654
|
+
out.push({
|
|
6655
|
+
verdict: "piling",
|
|
6656
|
+
kind: entry.birth.kind,
|
|
6657
|
+
where: whereOf(entry.birth),
|
|
6658
|
+
site: siteOf(entry.birth),
|
|
6659
|
+
tests: values.length,
|
|
6660
|
+
count: 1,
|
|
6661
|
+
growth: { what, from, to, tests: values.length }
|
|
6662
|
+
});
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
|
|
6666
|
+
}
|
|
6667
|
+
function handleFindings(byWorker) {
|
|
6668
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6669
|
+
for (const censuses of byWorker.values()) {
|
|
6670
|
+
const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
|
|
6671
|
+
for (let i = 0; i < ordered.length - 1; i++) {
|
|
6672
|
+
const census = ordered[i];
|
|
6673
|
+
const next = ordered[i + 1];
|
|
6674
|
+
for (const [type, end] of Object.entries(census.handles.end)) {
|
|
6675
|
+
const grew = end - (census.handles.start[type] ?? 0);
|
|
6676
|
+
if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
|
|
6677
|
+
const test = census.test;
|
|
6678
|
+
const key = `${test.id}|${type}`;
|
|
6679
|
+
const group = groups.get(key) ?? {
|
|
6680
|
+
verdict: "handle",
|
|
6681
|
+
kind: "handle",
|
|
6682
|
+
where: type,
|
|
6683
|
+
tests: 1,
|
|
6684
|
+
count: 0,
|
|
6685
|
+
detail: `"${test.title ?? ""}" (${test.file})`
|
|
6686
|
+
};
|
|
6687
|
+
group.count += grew;
|
|
6688
|
+
groups.set(key, group);
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
return [...groups.values()];
|
|
6693
|
+
}
|
|
6694
|
+
function probableFindings(tallies) {
|
|
6695
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6696
|
+
for (const tally of tallies) {
|
|
6697
|
+
const merged = byFile.get(tally.file) ?? { file: tally.file, opened: [], closed: { context: 0, browser: 0 } };
|
|
6698
|
+
merged.opened.push(...tally.opened);
|
|
6699
|
+
merged.closed.context += tally.closed.context;
|
|
6700
|
+
merged.closed.browser += tally.closed.browser;
|
|
6701
|
+
byFile.set(tally.file, merged);
|
|
6702
|
+
}
|
|
6703
|
+
const out = [];
|
|
6704
|
+
for (const tally of byFile.values()) {
|
|
6705
|
+
for (const kind of ["browser", "context"]) {
|
|
6706
|
+
const opened = tally.opened.filter((o) => o.kind === kind);
|
|
6707
|
+
if (opened.length <= tally.closed[kind]) continue;
|
|
6708
|
+
const sites = [...new Set(opened.map((o) => o.site ?? tally.file))];
|
|
6709
|
+
out.push({
|
|
6710
|
+
verdict: "probable",
|
|
6711
|
+
kind,
|
|
6712
|
+
where: sites.join(", "),
|
|
6713
|
+
tests: 0,
|
|
6714
|
+
count: opened.length - tally.closed[kind],
|
|
6715
|
+
detail: `${opened.length} opened, ${tally.closed[kind]} closed in ${tally.file}`
|
|
6716
|
+
});
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
return out;
|
|
6720
|
+
}
|
|
6721
|
+
function formatHeld(ms) {
|
|
6722
|
+
if (ms < 1e3) return `${Math.round(ms)} ms`;
|
|
6723
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
|
|
6724
|
+
const minutes = Math.floor(ms / 6e4);
|
|
6725
|
+
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
6726
|
+
return seconds ? `${minutes} min ${seconds} s` : `${minutes} min`;
|
|
6727
|
+
}
|
|
6728
|
+
var KIND_LABEL = {
|
|
6729
|
+
browser: "browser",
|
|
6730
|
+
context: "context",
|
|
6731
|
+
page: "page",
|
|
6732
|
+
request: "API context",
|
|
6733
|
+
handle: "handle"
|
|
6734
|
+
};
|
|
6735
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
6736
|
+
var HANDLE_LABEL = {
|
|
6737
|
+
TCPServerWrap: "server",
|
|
6738
|
+
FSEventWrap: "file watcher",
|
|
6739
|
+
StatWatcher: "file watcher"
|
|
6740
|
+
};
|
|
6741
|
+
function findingLine(finding) {
|
|
6742
|
+
const verdict = finding.verdict.padEnd(8);
|
|
6743
|
+
const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
|
|
6744
|
+
const kind = label.padEnd(11);
|
|
6745
|
+
const parts = finding.verdict === "handle" ? [] : [finding.where];
|
|
6746
|
+
switch (finding.verdict) {
|
|
6747
|
+
case "leaked": {
|
|
6748
|
+
if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
|
|
6749
|
+
if (finding.detail) parts.push(finding.detail);
|
|
6750
|
+
if (finding.pages) parts.push(`with ${plural(finding.pages, "page")}`);
|
|
6751
|
+
if (finding.tests) parts.push(plural(finding.tests, "test"));
|
|
6752
|
+
const past = finding.scope === "describe" ? "its describe block" : "its test";
|
|
6753
|
+
if (finding.closedByPiwi) parts.push("closed by Piwi at the end of its test");
|
|
6754
|
+
else if (finding.untilWorkerEnd)
|
|
6755
|
+
parts.push(`open until the worker shut down (${formatHeld(finding.heldMs ?? 0)} past ${past})`);
|
|
6756
|
+
else parts.push(`open ${formatHeld(finding.heldMs ?? 0)} past ${past}`);
|
|
6757
|
+
if ((finding.afterTestCpuMs ?? 0) >= 500)
|
|
6758
|
+
parts.push(`${formatHeld(finding.afterTestCpuMs)} of page CPU after its test`);
|
|
6759
|
+
break;
|
|
6760
|
+
}
|
|
6761
|
+
case "idle":
|
|
6762
|
+
parts.push(plural(finding.count, "page"));
|
|
6763
|
+
if (finding.tests) parts.push(plural(finding.tests, "test"));
|
|
6764
|
+
if (finding.detail) parts.push(finding.detail);
|
|
6765
|
+
break;
|
|
6766
|
+
case "piling": {
|
|
6767
|
+
const growth = finding.growth;
|
|
6768
|
+
const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
|
|
6769
|
+
parts.push(`${growth.from} \u2192 ${growth.to} ${noun} over ${growth.tests} tests`);
|
|
6770
|
+
break;
|
|
6771
|
+
}
|
|
6772
|
+
case "handle":
|
|
6773
|
+
parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
|
|
6774
|
+
break;
|
|
6775
|
+
case "probable":
|
|
6776
|
+
if (finding.detail) parts.push(finding.detail);
|
|
6777
|
+
break;
|
|
6778
|
+
}
|
|
6779
|
+
return ` ${verdict} ${kind} ${parts.join(" \xB7 ")}`;
|
|
6780
|
+
}
|
|
6781
|
+
function formatResourceSummary(report, leakCheck, maxLines = 10) {
|
|
6782
|
+
if (report.findings.length === 0) return [];
|
|
6783
|
+
const { counts } = report;
|
|
6784
|
+
const head = [
|
|
6785
|
+
counts.leaked ? plural(counts.leaked, "leak") : "",
|
|
6786
|
+
counts.idle ? `${plural(counts.idle, "idle page")}` : "",
|
|
6787
|
+
counts.piling ? `${counts.piling} piling up` : "",
|
|
6788
|
+
counts.handle ? plural(counts.handle, "handle") + " left in workers" : "",
|
|
6789
|
+
counts.probable ? plural(counts.probable, "probable leak") : ""
|
|
6790
|
+
].filter(Boolean);
|
|
6791
|
+
const lines = [`Resources: ${head.join(" \xB7 ")}`];
|
|
6792
|
+
for (const finding of report.findings.slice(0, maxLines)) lines.push(findingLine(finding));
|
|
6793
|
+
const rest = report.findings.length - maxLines;
|
|
6794
|
+
if (rest > 0) lines.push(` \u2026 and ${plural(rest, "more finding")}`);
|
|
6795
|
+
const testLeaks = report.findings.some((f) => f.verdict === "leaked" && f.scope === "test" && !f.closedByPiwi);
|
|
6796
|
+
if (testLeaks && leakCheck === "report") {
|
|
6797
|
+
lines.push(
|
|
6798
|
+
"Set leakCheck: 'fail' (PIWI_LEAK_CHECK=fail) to fail the tests that leave what they open, or 'close' to close it."
|
|
6799
|
+
);
|
|
6800
|
+
}
|
|
6801
|
+
if (counts.probable) {
|
|
6802
|
+
lines.push(
|
|
6803
|
+
"Probable leaks are counted from the steps alone; the capture fixtures (extendPiwiFixtures) make them exact."
|
|
6804
|
+
);
|
|
6805
|
+
}
|
|
6806
|
+
return lines;
|
|
6807
|
+
}
|
|
6808
|
+
|
|
6809
|
+
// src/internal/collect/process-sampler.ts
|
|
6810
|
+
var fs20 = __toESM(require("fs"));
|
|
6811
|
+
var os8 = __toESM(require("os"));
|
|
6812
|
+
var path22 = __toESM(require("path"));
|
|
6813
|
+
var import_node_child_process2 = require("child_process");
|
|
6814
|
+
var WALK_FILE_CAP = 1e4;
|
|
6815
|
+
var SERIES_CAP = 3600;
|
|
6816
|
+
var PLAYWRIGHT_TEMP = /^(playwright_\w+dev_profile-|playwright-artifacts-)/;
|
|
6817
|
+
function round12(value) {
|
|
6818
|
+
return Math.round(value * 10) / 10;
|
|
6819
|
+
}
|
|
6820
|
+
function pct(part, whole) {
|
|
6821
|
+
return whole > 0 ? round12(part / whole * 100) : null;
|
|
6822
|
+
}
|
|
6823
|
+
function nodeCpuTicks() {
|
|
6824
|
+
const cpus2 = os8.cpus();
|
|
6825
|
+
if (cpus2.length === 0) return null;
|
|
6826
|
+
let busy = 0;
|
|
6827
|
+
let idle = 0;
|
|
6828
|
+
for (const cpu of cpus2) {
|
|
6829
|
+
busy += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq;
|
|
6830
|
+
idle += cpu.times.idle;
|
|
6831
|
+
}
|
|
6832
|
+
return { busy, idle, iowait: 0, steal: 0, total: busy + idle };
|
|
6833
|
+
}
|
|
6834
|
+
function defaultPs() {
|
|
6835
|
+
return new Promise((resolve7, reject) => {
|
|
6836
|
+
(0, import_node_child_process2.execFile)(
|
|
6837
|
+
"ps",
|
|
6838
|
+
["-A", "-o", "pid=,ppid=,rss=,time=,command="],
|
|
6839
|
+
{ timeout: 2e3, maxBuffer: 8 * 1024 * 1024 },
|
|
6840
|
+
(error, stdout) => error ? reject(error) : resolve7(stdout)
|
|
6841
|
+
);
|
|
6842
|
+
});
|
|
6843
|
+
}
|
|
6844
|
+
function freeBytes(dir) {
|
|
6845
|
+
let current = path22.resolve(dir);
|
|
6846
|
+
for (; ; ) {
|
|
6847
|
+
try {
|
|
6848
|
+
const stats = fs20.statfsSync(current);
|
|
6849
|
+
return stats.bavail * stats.bsize;
|
|
6850
|
+
} catch {
|
|
6851
|
+
const parent = path22.dirname(current);
|
|
6852
|
+
if (parent === current) return null;
|
|
6853
|
+
current = parent;
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
async function dirBytes(dir, budget) {
|
|
6858
|
+
let bytes = 0;
|
|
6859
|
+
let entries;
|
|
6860
|
+
try {
|
|
6861
|
+
entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
6862
|
+
} catch {
|
|
6863
|
+
return 0;
|
|
6864
|
+
}
|
|
6865
|
+
for (const entry of entries) {
|
|
6866
|
+
if (budget.files <= 0) return bytes;
|
|
6867
|
+
const full = path22.join(dir, entry.name);
|
|
6868
|
+
if (entry.isDirectory()) bytes += await dirBytes(full, budget);
|
|
6869
|
+
else if (entry.isFile()) {
|
|
6870
|
+
budget.files--;
|
|
6871
|
+
try {
|
|
6872
|
+
bytes += (await fs20.promises.lstat(full)).size;
|
|
6873
|
+
} catch {
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
return bytes;
|
|
6878
|
+
}
|
|
6879
|
+
var RunSampler = class {
|
|
6880
|
+
constructor(options = {}) {
|
|
6881
|
+
this.timer = null;
|
|
6882
|
+
this.running = null;
|
|
6883
|
+
this.samples = 0;
|
|
6884
|
+
this.startedAt = 0;
|
|
6885
|
+
this.tracked = /* @__PURE__ */ new Map();
|
|
6886
|
+
this.treeMeasured = false;
|
|
6887
|
+
this.waitMeasured = false;
|
|
6888
|
+
this.cpuStart = null;
|
|
6889
|
+
this.cpuPrevious = null;
|
|
6890
|
+
this.series = [];
|
|
6891
|
+
this.pressureStart = { cpu: null, memory: null };
|
|
6892
|
+
this.containerStart = null;
|
|
6893
|
+
this.memoryKind = null;
|
|
6894
|
+
this.peakBytes = null;
|
|
6895
|
+
this.peakAtMs = null;
|
|
6896
|
+
this.largest = null;
|
|
6897
|
+
this.rssFallbackPids = /* @__PURE__ */ new Set();
|
|
6898
|
+
this.lowestAvailableKb = null;
|
|
6899
|
+
this.peakInUse = null;
|
|
6900
|
+
this.peakInUseIsLowerBound = false;
|
|
6901
|
+
this.lowestFree = null;
|
|
6902
|
+
this.rootPid = options.rootPid ?? process.pid;
|
|
6903
|
+
this.platform = options.platform ?? process.platform;
|
|
6904
|
+
this.proc = new ProcFs(options.procRoot);
|
|
6905
|
+
this.cgroup = new CgroupFs(options.procRoot, options.cgroupRoot);
|
|
6906
|
+
this.outputDirs = [...new Set((options.outputDirs ?? []).map((dir) => path22.resolve(dir)))];
|
|
6907
|
+
this.tmpDir = options.tmpDir ?? os8.tmpdir();
|
|
6908
|
+
this.intervalMs = options.intervalMs ?? 1e3;
|
|
6909
|
+
this.heavyEvery = Math.max(1, options.heavyEvery ?? 5);
|
|
6910
|
+
this.now = options.now ?? Date.now;
|
|
6911
|
+
this.runPs = options.runPs ?? defaultPs;
|
|
6912
|
+
this.linux = this.platform === "linux" && this.proc.available();
|
|
6913
|
+
}
|
|
6914
|
+
/** Take the first sample and sample on a timer that never keeps the process alive. */
|
|
6915
|
+
start() {
|
|
6916
|
+
if (this.timer) return;
|
|
6917
|
+
this.startedAt = this.now();
|
|
6918
|
+
this.cpuStart = this.cpuTicks();
|
|
6919
|
+
this.cpuPrevious = this.cpuStart;
|
|
6920
|
+
if (this.linux) {
|
|
6921
|
+
this.pressureStart = { cpu: this.proc.pressureUs("cpu"), memory: this.proc.pressureUs("memory") };
|
|
6922
|
+
this.containerStart = this.cgroup.read();
|
|
6923
|
+
}
|
|
6924
|
+
this.schedule();
|
|
6925
|
+
this.timer = setInterval(() => this.schedule(), this.intervalMs);
|
|
6926
|
+
this.timer.unref?.();
|
|
6927
|
+
}
|
|
6928
|
+
/** Take the last sample and return the run's profile. Never throws. */
|
|
6929
|
+
async stop() {
|
|
6930
|
+
if (this.timer) clearInterval(this.timer);
|
|
6931
|
+
this.timer = null;
|
|
6932
|
+
try {
|
|
6933
|
+
await this.running;
|
|
6934
|
+
await this.sample(true);
|
|
6935
|
+
} catch {
|
|
6936
|
+
}
|
|
6937
|
+
return this.profile(await this.leftoverBytes());
|
|
6938
|
+
}
|
|
6939
|
+
schedule() {
|
|
6940
|
+
if (this.running) return;
|
|
6941
|
+
const heavy = this.samples % this.heavyEvery === 0;
|
|
6942
|
+
this.running = this.sample(heavy).catch(() => void 0).finally(() => {
|
|
6943
|
+
this.running = null;
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
cpuTicks() {
|
|
6947
|
+
return this.linux ? this.proc.cpuTicks() : nodeCpuTicks();
|
|
6948
|
+
}
|
|
6949
|
+
async sample(heavy) {
|
|
6950
|
+
this.samples++;
|
|
6951
|
+
const ticks = this.cpuTicks();
|
|
6952
|
+
if (ticks && this.cpuPrevious) {
|
|
6953
|
+
const busy = pct(ticks.busy - this.cpuPrevious.busy, ticks.total - this.cpuPrevious.total);
|
|
6954
|
+
if (busy !== null && this.series.length < SERIES_CAP) this.series.push(busy);
|
|
6955
|
+
}
|
|
6956
|
+
if (ticks) this.cpuPrevious = ticks;
|
|
6957
|
+
const availableKb = this.linux ? this.proc.memAvailableKb() : Math.round(os8.freemem() / 1024);
|
|
6958
|
+
if (availableKb !== null) this.lowestAvailableKb = Math.min(this.lowestAvailableKb ?? Infinity, availableKb);
|
|
6959
|
+
if (this.linux) await this.sampleLinuxTree(heavy);
|
|
6960
|
+
else if (this.platform === "darwin" && heavy) await this.samplePsTree();
|
|
6961
|
+
if (heavy) await this.sampleDisk();
|
|
6962
|
+
}
|
|
6963
|
+
track(key, role, cpuMs, waitMs) {
|
|
6964
|
+
const known = this.tracked.get(key);
|
|
6965
|
+
if (known) {
|
|
6966
|
+
known.lastCpuMs = cpuMs;
|
|
6967
|
+
known.lastWaitMs = waitMs;
|
|
6968
|
+
if (role) known.role = role;
|
|
6969
|
+
return;
|
|
6970
|
+
}
|
|
6971
|
+
const first = this.samples === 1;
|
|
6972
|
+
this.tracked.set(key, {
|
|
6973
|
+
role,
|
|
6974
|
+
firstCpuMs: first ? cpuMs : 0,
|
|
6975
|
+
firstWaitMs: first ? waitMs : waitMs === null ? null : 0,
|
|
6976
|
+
lastCpuMs: cpuMs,
|
|
6977
|
+
lastWaitMs: waitMs
|
|
6978
|
+
});
|
|
6979
|
+
}
|
|
6980
|
+
/** The role of each process of the tree, a child of the runner being the web server unless it says otherwise. */
|
|
6981
|
+
resolveRoles(tree, explicit) {
|
|
6982
|
+
const roles = /* @__PURE__ */ new Map([[this.rootPid, "runner"]]);
|
|
6983
|
+
for (const proc of tree) {
|
|
6984
|
+
const parent = roles.get(proc.ppid);
|
|
6985
|
+
const inherited = parent === "runner" || parent === "webServer" ? "webServer" : "other";
|
|
6986
|
+
roles.set(proc.pid, explicit(proc) ?? inherited);
|
|
6987
|
+
}
|
|
6988
|
+
return roles;
|
|
6989
|
+
}
|
|
6990
|
+
async sampleLinuxTree(heavy) {
|
|
6991
|
+
const table = this.proc.table();
|
|
6992
|
+
const root = table.get(this.rootPid);
|
|
6993
|
+
if (!root) return;
|
|
6994
|
+
this.treeMeasured = true;
|
|
6995
|
+
const tree = [root, ...descendantsOf(this.rootPid, table.values())];
|
|
6996
|
+
const roles = this.resolveRoles(tree.slice(1), (proc) => {
|
|
6997
|
+
const key = `${proc.pid}:${proc.start}`;
|
|
6998
|
+
const known = this.tracked.get(key)?.role;
|
|
6999
|
+
return known && !heavy ? known : roleOf(proc.comm, this.proc.cmdline(proc.pid));
|
|
7000
|
+
});
|
|
7001
|
+
for (const proc of tree) {
|
|
7002
|
+
const waitMs = this.proc.runWaitMs(proc.pid);
|
|
7003
|
+
if (waitMs !== null) this.waitMeasured = true;
|
|
7004
|
+
this.track(`${proc.pid}:${proc.start}`, roles.get(proc.pid) ?? "other", proc.cpuMs, waitMs);
|
|
7005
|
+
}
|
|
7006
|
+
if (!heavy) return;
|
|
7007
|
+
let total = 0;
|
|
7008
|
+
let largest = null;
|
|
7009
|
+
const memory = await Promise.all(
|
|
7010
|
+
tree.map(async (proc) => {
|
|
7011
|
+
const pss = await this.proc.pssKb(proc.pid);
|
|
7012
|
+
if (pss !== null) return { proc, kb: pss, fallback: false };
|
|
7013
|
+
return { proc, kb: this.proc.statusKb(proc.pid, "VmRSS"), fallback: true };
|
|
7014
|
+
})
|
|
7015
|
+
);
|
|
7016
|
+
for (const { proc, kb, fallback } of memory) {
|
|
7017
|
+
if (kb === null) continue;
|
|
7018
|
+
if (fallback) this.rssFallbackPids.add(`${proc.pid}:${proc.start}`);
|
|
7019
|
+
total += kb * 1024;
|
|
7020
|
+
if (!largest || kb * 1024 > largest.bytes) largest = { role: roles.get(proc.pid) ?? "other", bytes: kb * 1024 };
|
|
7021
|
+
}
|
|
7022
|
+
this.memoryKind = memory.some((m) => m.kb !== null && !m.fallback) ? "pss" : memory.length ? "rss" : null;
|
|
7023
|
+
this.notePeak(total, largest);
|
|
7024
|
+
}
|
|
7025
|
+
async samplePsTree() {
|
|
7026
|
+
let list;
|
|
7027
|
+
try {
|
|
7028
|
+
list = parsePsOutput(await this.runPs());
|
|
7029
|
+
} catch {
|
|
7030
|
+
return;
|
|
7031
|
+
}
|
|
7032
|
+
const root = list.find((proc) => proc.pid === this.rootPid);
|
|
7033
|
+
if (!root) return;
|
|
7034
|
+
this.treeMeasured = true;
|
|
7035
|
+
const tree = [root, ...descendantsOf(this.rootPid, list)];
|
|
7036
|
+
const roles = this.resolveRoles(
|
|
7037
|
+
tree.slice(1),
|
|
7038
|
+
(proc) => roleOf(path22.basename(proc.command.split(" ")[0] ?? ""), proc.command)
|
|
7039
|
+
);
|
|
7040
|
+
let total = 0;
|
|
7041
|
+
let largest = null;
|
|
7042
|
+
for (const proc of tree) {
|
|
7043
|
+
const role = roles.get(proc.pid) ?? "other";
|
|
7044
|
+
this.track(String(proc.pid), role, proc.cpuMs, null);
|
|
7045
|
+
total += proc.rssKb * 1024;
|
|
7046
|
+
if (!largest || proc.rssKb * 1024 > largest.bytes) largest = { role, bytes: proc.rssKb * 1024 };
|
|
7047
|
+
}
|
|
7048
|
+
this.memoryKind = "rss";
|
|
7049
|
+
this.notePeak(total, largest);
|
|
7050
|
+
}
|
|
7051
|
+
notePeak(total, largest) {
|
|
7052
|
+
if (this.peakBytes === null || total > this.peakBytes) {
|
|
7053
|
+
this.peakBytes = total;
|
|
7054
|
+
this.peakAtMs = this.now() - this.startedAt;
|
|
7055
|
+
}
|
|
7056
|
+
if (largest && (!this.largest || largest.bytes > this.largest.bytes)) this.largest = largest;
|
|
7057
|
+
}
|
|
7058
|
+
async sampleDisk() {
|
|
7059
|
+
for (const dir of [...this.outputDirs, this.tmpDir]) {
|
|
7060
|
+
const free = freeBytes(dir);
|
|
7061
|
+
if (free !== null) this.lowestFree = Math.min(this.lowestFree ?? Infinity, free);
|
|
7062
|
+
}
|
|
7063
|
+
const budget = { files: WALK_FILE_CAP };
|
|
7064
|
+
let bytes = 0;
|
|
7065
|
+
for (const dir of this.outputDirs) bytes += await dirBytes(dir, budget);
|
|
7066
|
+
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs >= this.startedAt - 1e3)) {
|
|
7067
|
+
bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
|
|
7068
|
+
}
|
|
7069
|
+
if (budget.files <= 0) this.peakInUseIsLowerBound = true;
|
|
7070
|
+
this.peakInUse = Math.max(this.peakInUse ?? 0, bytes);
|
|
7071
|
+
}
|
|
7072
|
+
/** Playwright's folders in the temp directory whose modification time passes `keep`. */
|
|
7073
|
+
async playwrightTempEntries(keep) {
|
|
7074
|
+
let names;
|
|
7075
|
+
try {
|
|
7076
|
+
names = await fs20.promises.readdir(this.tmpDir);
|
|
7077
|
+
} catch {
|
|
7078
|
+
return [];
|
|
7079
|
+
}
|
|
7080
|
+
const out = [];
|
|
7081
|
+
for (const name of names) {
|
|
7082
|
+
if (!PLAYWRIGHT_TEMP.test(name)) continue;
|
|
7083
|
+
try {
|
|
7084
|
+
if (keep((await fs20.promises.stat(path22.join(this.tmpDir, name))).mtimeMs)) out.push(name);
|
|
7085
|
+
} catch {
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
return out;
|
|
7089
|
+
}
|
|
7090
|
+
async leftoverBytes() {
|
|
7091
|
+
try {
|
|
7092
|
+
const budget = { files: WALK_FILE_CAP };
|
|
7093
|
+
let bytes = 0;
|
|
7094
|
+
for (const name of await this.playwrightTempEntries((mtimeMs) => mtimeMs < this.startedAt - 1e3)) {
|
|
7095
|
+
bytes += await dirBytes(path22.join(this.tmpDir, name), budget);
|
|
7096
|
+
}
|
|
7097
|
+
return bytes;
|
|
7098
|
+
} catch {
|
|
7099
|
+
return null;
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
profile(leftoverBytes) {
|
|
7103
|
+
const wallMs = Math.max(0, this.now() - this.startedAt);
|
|
7104
|
+
const notMeasured = [];
|
|
7105
|
+
let byRole = null;
|
|
7106
|
+
if (this.treeMeasured) {
|
|
7107
|
+
byRole = {};
|
|
7108
|
+
for (const entry of this.tracked.values()) {
|
|
7109
|
+
const role = entry.role ?? "other";
|
|
7110
|
+
const usage = byRole[role] ??= { cpuMs: 0, runWaitMs: this.waitMeasured ? 0 : null, processes: 0 };
|
|
7111
|
+
usage.cpuMs += Math.max(0, entry.lastCpuMs - entry.firstCpuMs);
|
|
7112
|
+
if (usage.runWaitMs !== null && entry.lastWaitMs !== null && entry.firstWaitMs !== null) {
|
|
7113
|
+
usage.runWaitMs += Math.max(0, entry.lastWaitMs - entry.firstWaitMs);
|
|
7114
|
+
}
|
|
7115
|
+
usage.processes++;
|
|
7116
|
+
}
|
|
7117
|
+
} else {
|
|
7118
|
+
notMeasured.push("CPU and memory of the run\u2019s processes");
|
|
7119
|
+
}
|
|
7120
|
+
if (this.treeMeasured && !this.waitMeasured) notMeasured.push("time waiting for a CPU");
|
|
7121
|
+
const end = this.cpuTicks();
|
|
7122
|
+
const start = this.cpuStart;
|
|
7123
|
+
const total = end && start ? end.total - start.total : 0;
|
|
7124
|
+
const machineCpu = {
|
|
7125
|
+
busyPct: end && start ? pct(end.busy - start.busy, total) : null,
|
|
7126
|
+
iowaitPct: this.linux && end && start ? pct(end.iowait - start.iowait, total) : null,
|
|
7127
|
+
stealPct: this.linux && end && start ? pct(end.steal - start.steal, total) : null
|
|
7128
|
+
};
|
|
7129
|
+
if (!this.linux) notMeasured.push("steal");
|
|
7130
|
+
const pressure = (resource, from) => {
|
|
7131
|
+
if (!this.linux || from === null) return null;
|
|
7132
|
+
const to = this.proc.pressureUs(resource);
|
|
7133
|
+
return to === null || wallMs === 0 ? null : Math.min(100, round12((to - from) / 1e3 / wallMs * 100));
|
|
7134
|
+
};
|
|
7135
|
+
const cpuPressure = pressure("cpu", this.pressureStart.cpu);
|
|
7136
|
+
const memoryPressure = pressure("memory", this.pressureStart.memory);
|
|
7137
|
+
if (cpuPressure === null) notMeasured.push("CPU pressure");
|
|
7138
|
+
const container = this.linux ? this.cgroup.read() : null;
|
|
7139
|
+
const startContainer = this.containerStart;
|
|
7140
|
+
const totalMemory = os8.totalmem();
|
|
7141
|
+
const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
|
|
7142
|
+
const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
|
|
7143
|
+
const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
|
|
7144
|
+
const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
|
|
7145
|
+
return {
|
|
7146
|
+
platform: this.platform,
|
|
7147
|
+
wallMs,
|
|
7148
|
+
machine: {
|
|
7149
|
+
cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
|
|
7150
|
+
memoryBytes: totalMemory,
|
|
7151
|
+
memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
|
|
7152
|
+
cpuQuotaCores: container?.cpuQuotaCores ?? null
|
|
7153
|
+
},
|
|
7154
|
+
cpu: {
|
|
7155
|
+
...machineCpu,
|
|
7156
|
+
pressurePct: cpuPressure,
|
|
7157
|
+
series: this.series,
|
|
7158
|
+
byRole,
|
|
7159
|
+
throttledMs: delta(container?.throttledMs, startContainer?.throttledMs)
|
|
7160
|
+
},
|
|
7161
|
+
memory: {
|
|
7162
|
+
kind: this.memoryKind,
|
|
7163
|
+
peakBytes: this.peakBytes,
|
|
7164
|
+
peakAtMs: this.peakAtMs,
|
|
7165
|
+
largest: this.largest,
|
|
7166
|
+
rssFallbacks: this.memoryKind === "pss" ? this.rssFallbackPids.size : 0,
|
|
7167
|
+
pressurePct: memoryPressure,
|
|
7168
|
+
lowestAvailableBytes: this.lowestAvailableKb === null ? null : this.lowestAvailableKb * 1024,
|
|
7169
|
+
containerPeakBytes: raisedPeak,
|
|
7170
|
+
oomKills: delta(container?.oomKills, startContainer?.oomKills)
|
|
7171
|
+
},
|
|
7172
|
+
disk: {
|
|
7173
|
+
peakInUseBytes: this.peakInUse,
|
|
7174
|
+
peakInUseIsLowerBound: this.peakInUseIsLowerBound,
|
|
7175
|
+
lowestFreeBytes: this.lowestFree,
|
|
7176
|
+
leftoverBytes
|
|
7177
|
+
},
|
|
7178
|
+
notMeasured
|
|
7179
|
+
};
|
|
7180
|
+
}
|
|
7181
|
+
};
|
|
7182
|
+
|
|
7183
|
+
// src/internal/collect/machine-panel.ts
|
|
7184
|
+
function workerHealthOf(censuses) {
|
|
7185
|
+
const measured = censuses.filter((census) => census.test && census.metrics);
|
|
7186
|
+
if (measured.length === 0) return null;
|
|
7187
|
+
let utilization = 0;
|
|
7188
|
+
let switches = 0;
|
|
7189
|
+
let worstP99 = 0;
|
|
7190
|
+
for (const census of measured) {
|
|
7191
|
+
const worker = census.metrics.worker;
|
|
7192
|
+
utilization += worker.loopUtilization;
|
|
7193
|
+
switches += worker.involuntarySwitches;
|
|
7194
|
+
worstP99 = Math.max(worstP99, worker.loopDelayP99Ms);
|
|
7195
|
+
}
|
|
7196
|
+
return {
|
|
7197
|
+
tests: measured.length,
|
|
7198
|
+
loopUtilization: utilization / measured.length,
|
|
7199
|
+
loopDelayP99Ms: worstP99,
|
|
7200
|
+
involuntarySwitchesPerTest: switches / measured.length
|
|
7201
|
+
};
|
|
7202
|
+
}
|
|
7203
|
+
function artifactKind(name, contentType) {
|
|
7204
|
+
if (name === "trace") return "trace";
|
|
7205
|
+
if (name === "video" || contentType.startsWith("video/")) return "video";
|
|
7206
|
+
if (name === "screenshot" || contentType.startsWith("image/")) return "screenshot";
|
|
7207
|
+
return "other";
|
|
7208
|
+
}
|
|
7209
|
+
function emptyArtifacts() {
|
|
7210
|
+
return { trace: 0, video: 0, screenshot: 0, other: 0 };
|
|
7211
|
+
}
|
|
7212
|
+
var ROLE_LABEL = {
|
|
7213
|
+
renderer: "renderers",
|
|
7214
|
+
browser: "browsers",
|
|
7215
|
+
worker: "workers",
|
|
7216
|
+
gpu: "GPU",
|
|
7217
|
+
utility: "browser utilities",
|
|
7218
|
+
ffmpeg: "ffmpeg",
|
|
7219
|
+
webServer: "web server",
|
|
7220
|
+
runner: "runner",
|
|
7221
|
+
other: "other"
|
|
7222
|
+
};
|
|
7223
|
+
var ROLE_SINGULAR = {
|
|
7224
|
+
renderer: "renderer",
|
|
7225
|
+
browser: "browser",
|
|
7226
|
+
worker: "worker",
|
|
7227
|
+
gpu: "GPU",
|
|
7228
|
+
utility: "browser utility",
|
|
7229
|
+
ffmpeg: "ffmpeg",
|
|
7230
|
+
webServer: "web server",
|
|
7231
|
+
runner: "runner",
|
|
7232
|
+
other: "other"
|
|
7233
|
+
};
|
|
7234
|
+
var ARTIFACT_LABEL = {
|
|
7235
|
+
trace: "traces",
|
|
7236
|
+
video: "videos",
|
|
7237
|
+
screenshot: "screenshots",
|
|
7238
|
+
other: "other"
|
|
7239
|
+
};
|
|
7240
|
+
var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
7241
|
+
function formatBytes(bytes) {
|
|
7242
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
7243
|
+
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
7244
|
+
return `${Math.max(0, Math.round(bytes / 1024))} kB`;
|
|
7245
|
+
}
|
|
7246
|
+
function sparkline(series, width = 24) {
|
|
7247
|
+
if (series.length < 3) return "";
|
|
7248
|
+
const buckets = Math.min(width, series.length);
|
|
7249
|
+
let out = "";
|
|
7250
|
+
for (let i = 0; i < buckets; i++) {
|
|
7251
|
+
const slice = series.slice(
|
|
7252
|
+
Math.floor(i * series.length / buckets),
|
|
7253
|
+
Math.floor((i + 1) * series.length / buckets)
|
|
7254
|
+
);
|
|
7255
|
+
const mean = slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length);
|
|
7256
|
+
out += BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor(mean / 100 * BARS.length)))];
|
|
7257
|
+
}
|
|
7258
|
+
return out;
|
|
7259
|
+
}
|
|
7260
|
+
function clock(ms) {
|
|
7261
|
+
const seconds = Math.round(ms / 1e3);
|
|
7262
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
7263
|
+
}
|
|
7264
|
+
function cpuLine(profile) {
|
|
7265
|
+
const parts = [];
|
|
7266
|
+
const spark = sparkline(profile.cpu.series);
|
|
7267
|
+
if (profile.cpu.busyPct !== null) parts.push(`${spark ? `${spark} ` : ""}${Math.round(profile.cpu.busyPct)}% busy`);
|
|
7268
|
+
if (profile.cpu.pressurePct !== null) {
|
|
7269
|
+
parts.push(`a task waited for a CPU ${Math.round(profile.cpu.pressurePct)}% of the time`);
|
|
7270
|
+
}
|
|
7271
|
+
const roles = Object.entries(profile.cpu.byRole ?? {});
|
|
7272
|
+
const total = roles.reduce((sum, [, usage]) => sum + (usage?.cpuMs ?? 0), 0);
|
|
7273
|
+
if (total > 0) {
|
|
7274
|
+
const top = roles.filter(([, usage]) => (usage?.cpuMs ?? 0) >= total * 0.02).sort((a, b) => (b[1]?.cpuMs ?? 0) - (a[1]?.cpuMs ?? 0)).slice(0, 5).map(([role, usage]) => `${ROLE_LABEL[role]} ${formatHeld(usage.cpuMs)}`);
|
|
7275
|
+
parts.push(`${formatHeld(total)} of CPU: ${top.join(" \xB7 ")}`);
|
|
7276
|
+
const waiting = roles.filter(([, usage]) => (usage?.runWaitMs ?? 0) >= 1e3).sort((a, b) => (b[1]?.runWaitMs ?? 0) - (a[1]?.runWaitMs ?? 0))[0];
|
|
7277
|
+
if (waiting) parts.push(`${ROLE_LABEL[waiting[0]]} waited ${formatHeld(waiting[1].runWaitMs)} for a CPU`);
|
|
7278
|
+
}
|
|
7279
|
+
if (profile.cpu.throttledMs)
|
|
7280
|
+
parts.push(`the container's CPU quota held it back ${formatHeld(profile.cpu.throttledMs)}`);
|
|
7281
|
+
return parts.length ? ` CPU ${parts.join(" \xB7 ")}` : null;
|
|
7282
|
+
}
|
|
7283
|
+
function memoryLine(profile) {
|
|
7284
|
+
const { memory, machine } = profile;
|
|
7285
|
+
const parts = [];
|
|
7286
|
+
if (memory.kind && memory.peakBytes !== null) {
|
|
7287
|
+
const fallbacks = memory.rssFallbacks ? `, ${memory.rssFallbacks} by RSS` : "";
|
|
7288
|
+
const at = memory.peakAtMs !== null ? ` at ${clock(memory.peakAtMs)}` : "";
|
|
7289
|
+
parts.push(`peak ${formatBytes(memory.peakBytes)} (${memory.kind.toUpperCase()}${fallbacks})${at}`);
|
|
7290
|
+
const limit = machine.memoryLimitBytes;
|
|
7291
|
+
const whole = limit ?? machine.memoryBytes;
|
|
7292
|
+
if (whole > 0)
|
|
7293
|
+
parts.push(`${Math.round(memory.peakBytes / whole * 100)}% of the ${limit ? "limit" : "machine's memory"}`);
|
|
7294
|
+
if (memory.largest)
|
|
7295
|
+
parts.push(`largest process: ${ROLE_SINGULAR[memory.largest.role]} ${formatBytes(memory.largest.bytes)}`);
|
|
7296
|
+
}
|
|
7297
|
+
if (memory.containerPeakBytes !== null)
|
|
7298
|
+
parts.push(`the container peaked at ${formatBytes(memory.containerPeakBytes)}`);
|
|
7299
|
+
if (memory.oomKills) parts.push(`${memory.oomKills} process${memory.oomKills === 1 ? "" : "es"} killed for memory`);
|
|
7300
|
+
if (memory.pressurePct !== null) {
|
|
7301
|
+
parts.push(
|
|
7302
|
+
memory.pressurePct >= 0.1 ? `a task waited for memory ${memory.pressurePct}% of the time` : "no memory pressure"
|
|
7303
|
+
);
|
|
7304
|
+
}
|
|
7305
|
+
if (memory.lowestAvailableBytes !== null && memory.lowestAvailableBytes < machine.memoryBytes * 0.1) {
|
|
7306
|
+
parts.push(`${formatBytes(memory.lowestAvailableBytes)} available at the low point`);
|
|
7307
|
+
}
|
|
7308
|
+
return parts.length ? ` Memory ${parts.join(" \xB7 ")}` : null;
|
|
7309
|
+
}
|
|
7310
|
+
function diskLine(profile, artifacts) {
|
|
7311
|
+
const parts = [];
|
|
7312
|
+
if (artifacts) {
|
|
7313
|
+
const kinds = Object.entries(artifacts).filter(([, bytes]) => bytes > 0);
|
|
7314
|
+
const total = kinds.reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
7315
|
+
parts.push(
|
|
7316
|
+
total > 0 ? `${formatBytes(total)} of artifacts (${kinds.sort((a, b) => b[1] - a[1]).map(([kind, bytes]) => `${ARTIFACT_LABEL[kind]} ${formatBytes(bytes)}`).join(" \xB7 ")})` : "no artifacts"
|
|
7317
|
+
);
|
|
7318
|
+
}
|
|
7319
|
+
const { disk } = profile;
|
|
7320
|
+
if (disk.peakInUseBytes !== null && disk.peakInUseBytes > 0) {
|
|
7321
|
+
parts.push(
|
|
7322
|
+
`${disk.peakInUseIsLowerBound ? "at least " : ""}${formatBytes(disk.peakInUseBytes)} in use at the peak`
|
|
7323
|
+
);
|
|
7324
|
+
}
|
|
7325
|
+
if (disk.lowestFreeBytes !== null) parts.push(`${formatBytes(disk.lowestFreeBytes)} free at the low point`);
|
|
7326
|
+
if (disk.leftoverBytes !== null && disk.leftoverBytes >= 10 * 1024 ** 2) {
|
|
7327
|
+
parts.push(`${formatBytes(disk.leftoverBytes)} left in the temp folder by earlier runs`);
|
|
7328
|
+
}
|
|
7329
|
+
return parts.length ? ` Disk ${parts.join(" \xB7 ")}` : null;
|
|
7330
|
+
}
|
|
7331
|
+
function workersLine(workers) {
|
|
7332
|
+
if (!workers || workers.tests === 0) return null;
|
|
7333
|
+
return ` Workers event loop busy ${Math.round(workers.loopUtilization * 100)}% \xB7 p99 delay ${Math.round(
|
|
7334
|
+
workers.loopDelayP99Ms
|
|
7335
|
+
)} ms at worst \xB7 ${Math.round(workers.involuntarySwitchesPerTest).toLocaleString("en-US")} involuntary context switches per test`;
|
|
7336
|
+
}
|
|
7337
|
+
function formatMachinePanel(profile, extras) {
|
|
7338
|
+
const { machine, cpu } = profile;
|
|
7339
|
+
const head = [
|
|
7340
|
+
`${machine.cores} cores${machine.cpuQuotaCores ? ` (a quota of ${machine.cpuQuotaCores})` : ""}`,
|
|
7341
|
+
formatBytes(machine.memoryBytes)
|
|
7342
|
+
];
|
|
7343
|
+
if (machine.memoryLimitBytes) head.push(`container limit ${formatBytes(machine.memoryLimitBytes)}`);
|
|
7344
|
+
if (cpu.stealPct !== null && cpu.stealPct >= 1) head.push(`steal ${cpu.stealPct}%`);
|
|
7345
|
+
const lines = [
|
|
7346
|
+
`Machine: ${head.join(" \xB7 ")}`,
|
|
7347
|
+
cpuLine(profile),
|
|
7348
|
+
memoryLine(profile),
|
|
7349
|
+
diskLine(profile, extras.artifacts),
|
|
7350
|
+
workersLine(extras.workers)
|
|
7351
|
+
].filter((line) => line !== null);
|
|
7352
|
+
if (profile.notMeasured.length) lines.push(` Not measured here: ${profile.notMeasured.join(", ")}`);
|
|
7353
|
+
return lines;
|
|
7354
|
+
}
|
|
7355
|
+
|
|
7356
|
+
// src/internal/collect/resource-wire.ts
|
|
7357
|
+
var REPORT_FINDINGS_CAP = 100;
|
|
7358
|
+
var REPORT_SERIES_CAP = 240;
|
|
7359
|
+
var REPORT_WORKER_POINTS_CAP = 200;
|
|
7360
|
+
function downsample(series, cap) {
|
|
7361
|
+
if (series.length <= cap) return series;
|
|
7362
|
+
const out = [];
|
|
7363
|
+
for (let i = 0; i < cap; i++) {
|
|
7364
|
+
const slice = series.slice(Math.floor(i * series.length / cap), Math.floor((i + 1) * series.length / cap));
|
|
7365
|
+
out.push(Math.round(slice.reduce((sum, v) => sum + v, 0) / Math.max(1, slice.length) * 10) / 10);
|
|
7366
|
+
}
|
|
7367
|
+
return out;
|
|
7368
|
+
}
|
|
7369
|
+
function nonZero(bytes) {
|
|
7370
|
+
const out = Object.fromEntries(Object.entries(bytes).filter(([, n]) => (n ?? 0) > 0));
|
|
7371
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
7372
|
+
}
|
|
7373
|
+
function executionResources(census, artifacts) {
|
|
7374
|
+
const metrics = census.metrics;
|
|
7375
|
+
if (!metrics) return null;
|
|
7376
|
+
return {
|
|
7377
|
+
workerCpuMs: metrics.worker.cpuMs,
|
|
7378
|
+
roles: metrics.roles ?? null,
|
|
7379
|
+
loopUtilization: metrics.worker.loopUtilization,
|
|
7380
|
+
loopDelayP99Ms: metrics.worker.loopDelayP99Ms,
|
|
7381
|
+
involuntarySwitches: metrics.worker.involuntarySwitches,
|
|
7382
|
+
heapUsedMb: metrics.worker.heapUsedMb,
|
|
7383
|
+
openAtStart: census.openAtStart ?? null,
|
|
7384
|
+
leftOpen: census.leftOpen ?? null,
|
|
7385
|
+
artifactBytes: nonZero(artifacts)
|
|
7386
|
+
};
|
|
7387
|
+
}
|
|
7388
|
+
function openPagesByWorker(censuses) {
|
|
7389
|
+
const byWorker = /* @__PURE__ */ new Map();
|
|
7390
|
+
for (const census of censuses) {
|
|
7391
|
+
const list = byWorker.get(census.worker) ?? [];
|
|
7392
|
+
list.push(census);
|
|
7393
|
+
byWorker.set(census.worker, list);
|
|
7394
|
+
}
|
|
7395
|
+
const out = [];
|
|
7396
|
+
for (const [worker, list] of [...byWorker.entries()].sort((a, b) => a[0] - b[0])) {
|
|
7397
|
+
list.sort((a, b) => a.at - b.at);
|
|
7398
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
7399
|
+
const points = [];
|
|
7400
|
+
for (const census of list) {
|
|
7401
|
+
for (const birth of census.born) kinds.set(birth.id, birth.kind);
|
|
7402
|
+
if (census.test) points.push(census.open.filter((open) => kinds.get(open.id) === "page").length);
|
|
7403
|
+
}
|
|
7404
|
+
if (points.length > 0) out.push({ worker, openPages: downsample(points, REPORT_WORKER_POINTS_CAP) });
|
|
7405
|
+
}
|
|
7406
|
+
return out;
|
|
7407
|
+
}
|
|
7408
|
+
function resourceReportWire(input) {
|
|
7409
|
+
const { report, profile } = input;
|
|
7410
|
+
return {
|
|
7411
|
+
v: 1,
|
|
7412
|
+
shardIndex: input.shardIndex,
|
|
7413
|
+
findings: (report?.findings ?? []).slice(0, REPORT_FINDINGS_CAP),
|
|
7414
|
+
counts: report?.counts ?? { leaked: 0, idle: 0, piling: 0, handle: 0, probable: 0 },
|
|
7415
|
+
profile: profile ? { ...profile, cpu: { ...profile.cpu, series: downsample(profile.cpu.series, REPORT_SERIES_CAP) } } : null,
|
|
7416
|
+
workers: openPagesByWorker(input.censuses),
|
|
7417
|
+
artifactBytes: nonZero(input.artifacts) ?? {},
|
|
7418
|
+
workerHealth: workerHealthOf(input.censuses)
|
|
7419
|
+
};
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7422
|
+
// ../core/src/describe-failure.ts
|
|
7423
|
+
var HEADLINE_MAX_CHARS = 120;
|
|
7424
|
+
var VALUE_MAX_CHARS = 40;
|
|
7425
|
+
var SHORT_VALUE_MAX_CHARS = 20;
|
|
7426
|
+
var MASK_TOKEN_RE = /<(?:N|VALUE|URL|STR|UUID|HASH|EMAIL)>/g;
|
|
7427
|
+
var ACTION_VERBS = {
|
|
7428
|
+
click: "click",
|
|
7429
|
+
dblclick: "double-click",
|
|
7430
|
+
fill: "fill",
|
|
7431
|
+
type: "type",
|
|
7432
|
+
press: "press",
|
|
7433
|
+
pressSequentially: "type",
|
|
7434
|
+
check: "check",
|
|
7435
|
+
uncheck: "uncheck",
|
|
7436
|
+
hover: "hover",
|
|
7437
|
+
tap: "tap",
|
|
7438
|
+
focus: "focus",
|
|
7439
|
+
blur: "blur",
|
|
7440
|
+
clear: "clear",
|
|
7441
|
+
selectOption: "select",
|
|
7442
|
+
selectText: "select text",
|
|
7443
|
+
setInputFiles: "file upload",
|
|
7444
|
+
setChecked: "check",
|
|
7445
|
+
dragTo: "drag",
|
|
7446
|
+
dragAndDrop: "drag",
|
|
7447
|
+
scrollIntoViewIfNeeded: "scroll",
|
|
7448
|
+
screenshot: "screenshot",
|
|
7449
|
+
waitFor: "wait",
|
|
7450
|
+
waitForSelector: "waitForSelector",
|
|
7451
|
+
waitForLoadState: "waitForLoadState",
|
|
7452
|
+
waitForFunction: "waitForFunction",
|
|
7453
|
+
waitForResponse: "waitForResponse",
|
|
7454
|
+
waitForRequest: "waitForRequest",
|
|
7455
|
+
waitForEvent: "waitForEvent",
|
|
7456
|
+
waitForTimeout: "waitForTimeout",
|
|
7457
|
+
innerText: "read text",
|
|
7458
|
+
textContent: "read text",
|
|
7459
|
+
inputValue: "read value",
|
|
7460
|
+
getAttribute: "read attribute",
|
|
7461
|
+
isVisible: "visibility check",
|
|
7462
|
+
isEnabled: "enabled check",
|
|
7463
|
+
isChecked: "checked check",
|
|
7464
|
+
boundingBox: "measure",
|
|
7465
|
+
evaluate: "evaluate",
|
|
7466
|
+
goto: "navigation"
|
|
7467
|
+
};
|
|
7468
|
+
var ACTION_GERUNDS = {
|
|
7469
|
+
click: "clicking",
|
|
7470
|
+
dblclick: "double-clicking",
|
|
7471
|
+
fill: "filling",
|
|
7472
|
+
type: "typing into",
|
|
7473
|
+
press: "pressing a key on",
|
|
7474
|
+
pressSequentially: "typing into",
|
|
7475
|
+
check: "checking",
|
|
7476
|
+
uncheck: "unchecking",
|
|
7477
|
+
hover: "hovering",
|
|
7478
|
+
tap: "tapping",
|
|
7479
|
+
focus: "focusing",
|
|
7480
|
+
clear: "clearing",
|
|
7481
|
+
selectOption: "selecting an option in",
|
|
7482
|
+
setInputFiles: "uploading a file to",
|
|
7483
|
+
dragTo: "dragging",
|
|
7484
|
+
waitFor: "waiting for",
|
|
7485
|
+
waitForSelector: "waiting for",
|
|
7486
|
+
evaluate: "evaluating on"
|
|
7487
|
+
};
|
|
7488
|
+
var STATE_PHRASES = {
|
|
7489
|
+
"not-found": "was not found on the page",
|
|
7490
|
+
hidden: "never became visible",
|
|
7491
|
+
"not-visible": "never became visible",
|
|
7492
|
+
"not-enabled": "never became enabled",
|
|
7493
|
+
"not-editable": "never became editable",
|
|
7494
|
+
"not-stable": "never stopped moving",
|
|
7495
|
+
"outside-viewport": "stayed outside the viewport",
|
|
7496
|
+
"intercepts-pointer": "was covered by another element",
|
|
7497
|
+
detached: "was detached from the DOM",
|
|
7498
|
+
navigating: "was still navigating"
|
|
7499
|
+
};
|
|
7500
|
+
var STATE_MATCHERS = {
|
|
7501
|
+
toBeVisible: { met: "visible", unmet: "never became visible" },
|
|
7502
|
+
toBeHidden: { met: "hidden", unmet: "never became hidden" },
|
|
7503
|
+
toBeEnabled: { met: "enabled", unmet: "never became enabled" },
|
|
7504
|
+
toBeDisabled: { met: "disabled", unmet: "never became disabled" },
|
|
7505
|
+
toBeChecked: { met: "checked", unmet: "never became checked" },
|
|
7506
|
+
toBeEditable: { met: "editable", unmet: "never became editable" },
|
|
7507
|
+
toBeFocused: { met: "focused", unmet: "never received focus" },
|
|
7508
|
+
toBeAttached: { met: "attached", unmet: "never appeared in the DOM" },
|
|
7509
|
+
toBeDetached: { met: "detached", unmet: "never left the DOM" },
|
|
7510
|
+
toBeInViewport: { met: "in the viewport", unmet: "never entered the viewport" },
|
|
7511
|
+
toBeEmpty: { met: "empty", unmet: "never became empty" }
|
|
7512
|
+
};
|
|
7513
|
+
var VALUE_MATCHERS = {
|
|
7514
|
+
toHaveText: "text",
|
|
7515
|
+
toContainText: "text containing",
|
|
7516
|
+
toHaveValue: "value",
|
|
7517
|
+
toHaveValues: "values",
|
|
7518
|
+
toHaveAttribute: "attribute",
|
|
7519
|
+
toHaveClass: "class",
|
|
7520
|
+
toContainClass: "class",
|
|
7521
|
+
toHaveId: "id",
|
|
7522
|
+
toHaveCSS: "CSS",
|
|
7523
|
+
toHaveJSProperty: "property",
|
|
7524
|
+
toHaveAccessibleName: "accessible name",
|
|
7525
|
+
toHaveAccessibleDescription: "accessible description",
|
|
7526
|
+
toHaveRole: "role",
|
|
7527
|
+
toHaveURL: "URL",
|
|
7528
|
+
toHaveTitle: "title",
|
|
7529
|
+
toHaveScreenshot: "screenshot",
|
|
7530
|
+
toMatchAriaSnapshot: "ARIA snapshot"
|
|
7531
|
+
};
|
|
7532
|
+
var NETWORK_ERRORS = {
|
|
7533
|
+
ERR_CONNECTION_REFUSED: "Connection refused loading",
|
|
7534
|
+
ERR_CONNECTION_RESET: "Connection reset loading",
|
|
7535
|
+
ERR_CONNECTION_CLOSED: "Connection closed loading",
|
|
7536
|
+
ERR_CONNECTION_TIMED_OUT: "Connection timed out loading",
|
|
7537
|
+
ERR_TIMED_OUT: "Connection timed out loading",
|
|
7538
|
+
ERR_NAME_NOT_RESOLVED: "DNS lookup failed for",
|
|
7539
|
+
ERR_INTERNET_DISCONNECTED: "No network while loading",
|
|
7540
|
+
ERR_ADDRESS_UNREACHABLE: "Address unreachable loading",
|
|
7541
|
+
ERR_ABORTED: "Navigation aborted loading",
|
|
7542
|
+
ERR_EMPTY_RESPONSE: "Empty response loading",
|
|
7543
|
+
ERR_TOO_MANY_REDIRECTS: "Too many redirects loading",
|
|
7544
|
+
ERR_SSL_PROTOCOL_ERROR: "TLS error loading",
|
|
7545
|
+
ERR_CERT_AUTHORITY_INVALID: "Untrusted certificate loading",
|
|
7546
|
+
ERR_CERT_COMMON_NAME_INVALID: "Certificate name mismatch loading",
|
|
7547
|
+
ERR_CERT_DATE_INVALID: "Expired certificate loading",
|
|
7548
|
+
ERR_BLOCKED_BY_CLIENT: "Request blocked loading",
|
|
7549
|
+
ERR_FAILED: "Request failed loading",
|
|
7550
|
+
ERR_HTTP_RESPONSE_CODE_FAILURE: "HTTP error loading",
|
|
7551
|
+
NS_ERROR_CONNECTION_REFUSED: "Connection refused loading",
|
|
7552
|
+
NS_ERROR_UNKNOWN_HOST: "DNS lookup failed for",
|
|
7553
|
+
NS_ERROR_NET_TIMEOUT: "Connection timed out loading",
|
|
7554
|
+
NS_ERROR_OFFLINE: "No network while loading",
|
|
7555
|
+
NS_ERROR_ABORT: "Navigation aborted loading",
|
|
7556
|
+
NS_BINDING_ABORTED: "Navigation aborted loading"
|
|
7557
|
+
};
|
|
7558
|
+
function formatTimeout(ms) {
|
|
7559
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
7560
|
+
const seconds = ms / 1e3;
|
|
7561
|
+
const rounded = Math.round(seconds * 10) / 10;
|
|
7562
|
+
return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded} s`;
|
|
7563
|
+
}
|
|
7564
|
+
function routeOf(url) {
|
|
7565
|
+
try {
|
|
7566
|
+
const parsed = new URL(url);
|
|
7567
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
7568
|
+
} catch {
|
|
7569
|
+
return url;
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
function truncateValue(value, max) {
|
|
7573
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
7574
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
7575
|
+
}
|
|
7576
|
+
function countNoun(locator, count) {
|
|
7577
|
+
const role = locator ? /getByRole\(\s*['"]([a-z]+)['"]/.exec(locator)?.[1] : null;
|
|
7578
|
+
const noun = role ?? "element";
|
|
7579
|
+
if (count === 1) return noun;
|
|
7580
|
+
return noun.endsWith("s") ? noun : noun.endsWith("x") || noun.endsWith("ch") ? `${noun}es` : `${noun}s`;
|
|
7581
|
+
}
|
|
7582
|
+
function textOfGetByText(locator) {
|
|
7583
|
+
if (!locator) return null;
|
|
7584
|
+
const m = /^getByText\(\s*(['"`])((?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*)\1\s*(?:,\s*\{[^}]*\})?\s*\)$/.exec(locator);
|
|
7585
|
+
return m ? m[2] : null;
|
|
7586
|
+
}
|
|
7587
|
+
function receivedDisplay(parsed) {
|
|
7588
|
+
const received = parsed.received ?? "";
|
|
7589
|
+
if (parsed.assertion !== "toHaveURL") return received;
|
|
7590
|
+
const m = /^"?(https?:\/\/[^"\s]+)"?$/.exec(received);
|
|
7591
|
+
return m ? `"${routeOf(m[1])}"` : received;
|
|
7592
|
+
}
|
|
7593
|
+
function firstLineFallback(parsed) {
|
|
7594
|
+
const line = (parsed.messageHead.split("\n")[0] ?? "").replace(/^Error:\s*/, "").replace(MASK_TOKEN_RE, "\u2026");
|
|
7595
|
+
return truncateValue(line || "Unknown error", HEADLINE_MAX_CHARS);
|
|
7596
|
+
}
|
|
7597
|
+
var Line = class {
|
|
7598
|
+
constructor() {
|
|
7599
|
+
this.parts = [];
|
|
7600
|
+
}
|
|
7601
|
+
text(text) {
|
|
7602
|
+
if (!text) return this;
|
|
7603
|
+
const last = this.parts[this.parts.length - 1];
|
|
7604
|
+
if (last && last.kind === "text") last.text += text;
|
|
7605
|
+
else this.parts.push({ kind: "text", text });
|
|
7606
|
+
return this;
|
|
7607
|
+
}
|
|
7608
|
+
locator(text) {
|
|
7609
|
+
this.parts.push({ kind: "locator", text });
|
|
7610
|
+
return this;
|
|
7611
|
+
}
|
|
7612
|
+
value(text) {
|
|
7613
|
+
this.parts.push({ kind: "value", text });
|
|
7614
|
+
return this;
|
|
7615
|
+
}
|
|
7616
|
+
toString() {
|
|
7617
|
+
return this.parts.map((p) => p.text).join("");
|
|
5526
7618
|
}
|
|
5527
7619
|
};
|
|
5528
7620
|
function subject(line, locator, opts) {
|
|
@@ -5854,7 +7946,7 @@ function testLocation(test) {
|
|
|
5854
7946
|
return `${testFile(test)}:${test.location.line}:${test.location.column}`;
|
|
5855
7947
|
}
|
|
5856
7948
|
function testFile(test) {
|
|
5857
|
-
return
|
|
7949
|
+
return path23.relative(process.cwd(), test.location.file).split(path23.sep).join("/");
|
|
5858
7950
|
}
|
|
5859
7951
|
var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
5860
7952
|
constructor(rawOptions = {}) {
|
|
@@ -5882,6 +7974,20 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5882
7974
|
this.filterDetails = null;
|
|
5883
7975
|
/** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
|
|
5884
7976
|
this.maxFailures = 0;
|
|
7977
|
+
/** The resource censuses the capture fixtures attached, one per test attempt. */
|
|
7978
|
+
this.resourceCensuses = [];
|
|
7979
|
+
/** Lifecycle steps of the tests that ran without the fixtures. */
|
|
7980
|
+
this.lifecycleTallies = [];
|
|
7981
|
+
/** The user fixtures set up by the tests that left a page unused, by test id. */
|
|
7982
|
+
this.fixturesByTest = /* @__PURE__ */ new Map();
|
|
7983
|
+
/** The file workers append their shutdown census to, set in `onBegin`. */
|
|
7984
|
+
this.resourcesFile = null;
|
|
7985
|
+
/** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
|
|
7986
|
+
this.sampler = null;
|
|
7987
|
+
/** Bytes of the files the tests attached, by kind. */
|
|
7988
|
+
this.artifactBytes = emptyArtifacts();
|
|
7989
|
+
/** The run's resource report for the dashboard, built in `onEnd`. */
|
|
7990
|
+
this.resourceReport = null;
|
|
5885
7991
|
this.streamManager = null;
|
|
5886
7992
|
/** Track suite-level setup steps (beforeAll/afterAll) not tied to any test */
|
|
5887
7993
|
this.setupSteps = [];
|
|
@@ -5941,6 +8047,17 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
5941
8047
|
this.startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5942
8048
|
this.playwrightVersion = config.version;
|
|
5943
8049
|
this.maxFailures = config.maxFailures ?? 0;
|
|
8050
|
+
if (this.options.captureResources !== false) {
|
|
8051
|
+
this.resourcesFile = path23.join(os9.tmpdir(), `piwi-resources-${process.pid}-${Date.now()}.jsonl`);
|
|
8052
|
+
process.env[PIWI_RESOURCES_RESULTS_ENV] = this.resourcesFile;
|
|
8053
|
+
try {
|
|
8054
|
+
this.sampler = new RunSampler({ outputDirs: (config.projects ?? []).map((project) => project.outputDir) });
|
|
8055
|
+
this.sampler.start();
|
|
8056
|
+
} catch (error) {
|
|
8057
|
+
this.sampler = null;
|
|
8058
|
+
this.logger.debug(`Run sampler not started: ${errorMessage(error)}`);
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
5944
8061
|
this.logger.info(
|
|
5945
8062
|
`Starting test run for project: ${this.options.projectName} (Playwright v${this.playwrightVersion})`
|
|
5946
8063
|
);
|
|
@@ -6165,6 +8282,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6165
8282
|
}
|
|
6166
8283
|
}
|
|
6167
8284
|
}
|
|
8285
|
+
if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
|
|
6168
8286
|
const reachAttachment = this.options.captureCodeReach === true && this.options.collectPerformanceMetrics !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.codeReach) : void 0;
|
|
6169
8287
|
if (reachAttachment?.body) {
|
|
6170
8288
|
try {
|
|
@@ -6209,6 +8327,78 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6209
8327
|
if (this.options.liveFileUploads) this.streamManager.scheduleLiveUpload(testCase);
|
|
6210
8328
|
}
|
|
6211
8329
|
}
|
|
8330
|
+
/**
|
|
8331
|
+
* Add up the files a test attached, and keep its resource census; for a test
|
|
8332
|
+
* that ran without the capture fixtures, tally the browsers and contexts its
|
|
8333
|
+
* steps opened and closed.
|
|
8334
|
+
*/
|
|
8335
|
+
collectResources(test, result, testCase) {
|
|
8336
|
+
const artifacts = emptyArtifacts();
|
|
8337
|
+
for (const file of result.attachments ?? []) {
|
|
8338
|
+
if (!file.path || INTERNAL_ATTACHMENT_NAMES.has(file.name)) continue;
|
|
8339
|
+
try {
|
|
8340
|
+
const kind = artifactKind(file.name, file.contentType);
|
|
8341
|
+
const bytes = fs21.statSync(file.path).size;
|
|
8342
|
+
artifacts[kind] += bytes;
|
|
8343
|
+
this.artifactBytes[kind] += bytes;
|
|
8344
|
+
} catch {
|
|
8345
|
+
}
|
|
8346
|
+
}
|
|
8347
|
+
const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
|
|
8348
|
+
const census = parseResourceCensus(attachment?.body);
|
|
8349
|
+
if (census) {
|
|
8350
|
+
this.resourceCensuses.push(census);
|
|
8351
|
+
const resources = executionResources(census, artifacts);
|
|
8352
|
+
if (resources) testCase.resources = resources;
|
|
8353
|
+
const leftIdle = census.closed.some((c) => c.used === false) || census.open.some((o) => o.used === false);
|
|
8354
|
+
if (leftIdle) this.fixturesByTest.set(test.id, userFixturesOf(result.steps ?? []));
|
|
8355
|
+
return;
|
|
8356
|
+
}
|
|
8357
|
+
if (!result.steps?.length) return;
|
|
8358
|
+
const tally = tallyLifecycleSteps(testFile(test), result.steps);
|
|
8359
|
+
if (tally.opened.length > 0 || tally.closed.context > 0 || tally.closed.browser > 0)
|
|
8360
|
+
this.lifecycleTallies.push(tally);
|
|
8361
|
+
}
|
|
8362
|
+
/**
|
|
8363
|
+
* Print what the run left open or opened for nothing, from the censuses and
|
|
8364
|
+
* the workers' shutdown file, then what the run cost the machine.
|
|
8365
|
+
*/
|
|
8366
|
+
async reportResources() {
|
|
8367
|
+
if (this.options.captureResources === false) return;
|
|
8368
|
+
const profile = this.sampler ? await this.sampler.stop() : null;
|
|
8369
|
+
this.sampler = null;
|
|
8370
|
+
const censuses = [...this.resourceCensuses];
|
|
8371
|
+
if (this.resourcesFile) {
|
|
8372
|
+
try {
|
|
8373
|
+
censuses.push(...parseResourceResults(fs21.readFileSync(this.resourcesFile, "utf8")));
|
|
8374
|
+
fs21.rmSync(this.resourcesFile, { force: true });
|
|
8375
|
+
} catch {
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
const lines = [];
|
|
8379
|
+
try {
|
|
8380
|
+
const report = censuses.length > 0 || this.lifecycleTallies.length > 0 ? buildResourceReport({ censuses, fixturesByTest: this.fixturesByTest, tallies: this.lifecycleTallies }) : null;
|
|
8381
|
+
if (report) lines.push(...formatResourceSummary(report, readLeakCheck(this.options.leakCheck)));
|
|
8382
|
+
if (profile) {
|
|
8383
|
+
lines.push(
|
|
8384
|
+
...formatMachinePanel(profile, { artifacts: this.artifactBytes, workers: workerHealthOf(censuses) })
|
|
8385
|
+
);
|
|
8386
|
+
}
|
|
8387
|
+
if (report || profile) {
|
|
8388
|
+
this.resourceReport = resourceReportWire({
|
|
8389
|
+
report,
|
|
8390
|
+
profile,
|
|
8391
|
+
censuses,
|
|
8392
|
+
artifacts: this.artifactBytes,
|
|
8393
|
+
shardIndex: this.shardInfo?.current ?? null
|
|
8394
|
+
});
|
|
8395
|
+
}
|
|
8396
|
+
} catch (error) {
|
|
8397
|
+
this.logger.debug(`Resource summary skipped: ${errorMessage(error)}`);
|
|
8398
|
+
}
|
|
8399
|
+
for (const line of lines) this.logger.info(line);
|
|
8400
|
+
emitResourceSummary(lines, this.logger);
|
|
8401
|
+
}
|
|
6212
8402
|
/**
|
|
6213
8403
|
* Synthesize `didnotrun` cases for tests Playwright planned but never reported
|
|
6214
8404
|
* (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
|
|
@@ -6263,6 +8453,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6263
8453
|
failures: this.failedTests + this.timedOutTests
|
|
6264
8454
|
});
|
|
6265
8455
|
this.materializeUnrunTests(unrunReason);
|
|
8456
|
+
await this.reportResources();
|
|
6266
8457
|
try {
|
|
6267
8458
|
await this.submitter.submit(
|
|
6268
8459
|
{
|
|
@@ -6282,7 +8473,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
|
|
|
6282
8473
|
shardInfo: this.shardInfo,
|
|
6283
8474
|
setupSteps: this.setupSteps,
|
|
6284
8475
|
isFullRun: this.isFullRun,
|
|
6285
|
-
filterDetails: this.filterDetails
|
|
8476
|
+
filterDetails: this.filterDetails,
|
|
8477
|
+
resourceReport: this.resourceReport
|
|
6286
8478
|
},
|
|
6287
8479
|
result
|
|
6288
8480
|
);
|
|
@@ -6366,7 +8558,7 @@ async function resolveSelection(options = {}) {
|
|
|
6366
8558
|
}
|
|
6367
8559
|
|
|
6368
8560
|
// src/internal/capture/capture-fixtures.ts
|
|
6369
|
-
var
|
|
8561
|
+
var path27 = __toESM(require("path"));
|
|
6370
8562
|
var import_node_zlib2 = require("zlib");
|
|
6371
8563
|
|
|
6372
8564
|
// ../picker-dom/src/probe.ts
|
|
@@ -6448,7 +8640,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6448
8640
|
};
|
|
6449
8641
|
const roleMemo = /* @__PURE__ */ new Map();
|
|
6450
8642
|
const textMemo = /* @__PURE__ */ new Map();
|
|
6451
|
-
const
|
|
8643
|
+
const roleOf2 = (n) => {
|
|
6452
8644
|
const cached = roleMemo.get(n);
|
|
6453
8645
|
if (cached !== void 0) return cached;
|
|
6454
8646
|
let role;
|
|
@@ -6471,7 +8663,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6471
8663
|
const al = n.getAttribute("aria-level");
|
|
6472
8664
|
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
6473
8665
|
};
|
|
6474
|
-
const targetRole =
|
|
8666
|
+
const targetRole = roleOf2(el);
|
|
6475
8667
|
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
6476
8668
|
const nameOf = (n) => {
|
|
6477
8669
|
const labelledBy = !!n.getAttribute("aria-labelledby");
|
|
@@ -6531,7 +8723,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6531
8723
|
let visibleRoleNameCount = 0;
|
|
6532
8724
|
for (let i = 0; i < nodes.length; i++) {
|
|
6533
8725
|
const n = nodes[i];
|
|
6534
|
-
if (
|
|
8726
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
6535
8727
|
if (n === el) index = roleCountAll;
|
|
6536
8728
|
roleCountAll++;
|
|
6537
8729
|
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
@@ -6584,7 +8776,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6584
8776
|
};
|
|
6585
8777
|
const docRoleCount = (role) => {
|
|
6586
8778
|
let c = 0;
|
|
6587
|
-
for (let i = 0; i < nodes.length; i++) if (
|
|
8779
|
+
for (let i = 0; i < nodes.length; i++) if (roleOf2(nodes[i]) === role) c++;
|
|
6588
8780
|
return c;
|
|
6589
8781
|
};
|
|
6590
8782
|
const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
@@ -6616,7 +8808,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6616
8808
|
const needle = text.toLowerCase();
|
|
6617
8809
|
let c = 0;
|
|
6618
8810
|
for (let i = 0; i < nodes.length; i++) {
|
|
6619
|
-
if (
|
|
8811
|
+
if (roleOf2(nodes[i]) !== role) continue;
|
|
6620
8812
|
if (normText(nodes[i]).indexOf(needle) !== -1) c++;
|
|
6621
8813
|
}
|
|
6622
8814
|
return c;
|
|
@@ -6641,7 +8833,7 @@ function probeElementAttrs(el, arg) {
|
|
|
6641
8833
|
scopedRoleCount = 0;
|
|
6642
8834
|
for (let i = 0; i < scoped.length; i++) {
|
|
6643
8835
|
const n = scoped[i];
|
|
6644
|
-
if (
|
|
8836
|
+
if (roleOf2(n) !== targetRole) continue;
|
|
6645
8837
|
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
6646
8838
|
scopedRoleCount++;
|
|
6647
8839
|
}
|
|
@@ -6989,7 +9181,7 @@ function showAnchorPicker(arg) {
|
|
|
6989
9181
|
}
|
|
6990
9182
|
const Z = 2147483600;
|
|
6991
9183
|
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
6992
|
-
const
|
|
9184
|
+
const roleOf2 = (n) => {
|
|
6993
9185
|
const explicit = n.getAttribute && n.getAttribute("role");
|
|
6994
9186
|
if (explicit) return explicit;
|
|
6995
9187
|
const tag = (n.tagName || "").toLowerCase();
|
|
@@ -7012,7 +9204,7 @@ function showAnchorPicker(arg) {
|
|
|
7012
9204
|
let matched = 0;
|
|
7013
9205
|
for (let i = 0; i < nodes.length; i++) {
|
|
7014
9206
|
const n = nodes[i];
|
|
7015
|
-
if (
|
|
9207
|
+
if (roleOf2(n) !== leafRole) continue;
|
|
7016
9208
|
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
7017
9209
|
matched++;
|
|
7018
9210
|
}
|
|
@@ -7045,7 +9237,7 @@ function showAnchorPicker(arg) {
|
|
|
7045
9237
|
const testId = node.getAttribute("data-testid");
|
|
7046
9238
|
const id = node.getAttribute("id");
|
|
7047
9239
|
const ariaLabel = node.getAttribute("aria-label");
|
|
7048
|
-
const role =
|
|
9240
|
+
const role = roleOf2(node);
|
|
7049
9241
|
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
7050
9242
|
if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
|
|
7051
9243
|
if (id) {
|
|
@@ -7058,7 +9250,7 @@ function showAnchorPicker(arg) {
|
|
|
7058
9250
|
let roleCount = 0;
|
|
7059
9251
|
let labeledCount = 0;
|
|
7060
9252
|
for (const n of roleNodes) {
|
|
7061
|
-
if (
|
|
9253
|
+
if (roleOf2(n) !== role) continue;
|
|
7062
9254
|
roleCount++;
|
|
7063
9255
|
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
7064
9256
|
}
|
|
@@ -7107,7 +9299,7 @@ function showAnchorPicker(arg) {
|
|
|
7107
9299
|
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
7108
9300
|
if (nodes.length > 2e3) return [];
|
|
7109
9301
|
return nodes.filter(
|
|
7110
|
-
(n) =>
|
|
9302
|
+
(n) => roleOf2(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
7111
9303
|
);
|
|
7112
9304
|
} catch {
|
|
7113
9305
|
return [];
|
|
@@ -7760,7 +9952,7 @@ function mergeCandidates(base, extra) {
|
|
|
7760
9952
|
}
|
|
7761
9953
|
|
|
7762
9954
|
// src/internal/capture/locator-healing.ts
|
|
7763
|
-
var
|
|
9955
|
+
var path24 = __toESM(require("path"));
|
|
7764
9956
|
|
|
7765
9957
|
// ../core/src/locator-fingerprint.ts
|
|
7766
9958
|
var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
|
|
@@ -8130,10 +10322,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
8130
10322
|
}
|
|
8131
10323
|
let rel = file;
|
|
8132
10324
|
try {
|
|
8133
|
-
rel =
|
|
10325
|
+
rel = path24.relative(process.cwd(), file);
|
|
8134
10326
|
} catch {
|
|
8135
10327
|
}
|
|
8136
|
-
rel = rel.split(
|
|
10328
|
+
rel = rel.split(path24.sep).join("/");
|
|
8137
10329
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
8138
10330
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
8139
10331
|
}
|
|
@@ -8147,7 +10339,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8147
10339
|
const doc = g.document;
|
|
8148
10340
|
if (!doc || !g.location) return null;
|
|
8149
10341
|
const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
8150
|
-
const
|
|
10342
|
+
const roleOf2 = (el) => {
|
|
8151
10343
|
const explicit = el.getAttribute("role");
|
|
8152
10344
|
if (explicit) return explicit.toLowerCase();
|
|
8153
10345
|
const tag = (el.tagName || "").toLowerCase();
|
|
@@ -8201,7 +10393,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8201
10393
|
return text;
|
|
8202
10394
|
};
|
|
8203
10395
|
const nameOf = (el) => {
|
|
8204
|
-
const role =
|
|
10396
|
+
const role = roleOf2(el);
|
|
8205
10397
|
const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
|
|
8206
10398
|
if (!holdsValue(el, role)) chain.push(el.textContent);
|
|
8207
10399
|
chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
|
|
@@ -8219,7 +10411,7 @@ function collectPageInventoryInPage(maxEntries) {
|
|
|
8219
10411
|
const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
|
|
8220
10412
|
for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
|
|
8221
10413
|
if (budget <= 0) break;
|
|
8222
|
-
const role =
|
|
10414
|
+
const role = roleOf2(el);
|
|
8223
10415
|
const name = nameOf(el);
|
|
8224
10416
|
if (!name) continue;
|
|
8225
10417
|
const key = `${role}\0${name}`;
|
|
@@ -8344,7 +10536,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
8344
10536
|
}
|
|
8345
10537
|
|
|
8346
10538
|
// src/internal/capture/pick-on-failure.ts
|
|
8347
|
-
var
|
|
10539
|
+
var path25 = __toESM(require("path"));
|
|
8348
10540
|
var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
|
|
8349
10541
|
function parseLeafLocatorExpression(rawExpr) {
|
|
8350
10542
|
const leaf = parseLeafLocatorCall(rawExpr);
|
|
@@ -8361,7 +10553,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
|
|
|
8361
10553
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
8362
10554
|
if (!parsed) continue;
|
|
8363
10555
|
const loc = err.location;
|
|
8364
|
-
const location = loc ? `${
|
|
10556
|
+
const location = loc ? `${path25.relative(process.cwd(), loc.file).split(path25.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
8365
10557
|
return { method: parsed.method, args: parsed.args, location };
|
|
8366
10558
|
}
|
|
8367
10559
|
return null;
|
|
@@ -8509,64 +10701,11 @@ function applyPickToSnapshots(snapshots, pick) {
|
|
|
8509
10701
|
return true;
|
|
8510
10702
|
}
|
|
8511
10703
|
|
|
8512
|
-
// src/internal/capture/quiet-capture.ts
|
|
8513
|
-
var import_node_module = require("module");
|
|
8514
|
-
var path21 = __toESM(require("path"));
|
|
8515
|
-
var BOXED_PREFIX_EXPORTS = [
|
|
8516
|
-
[
|
|
8517
|
-
"playwright-core/lib/coreBundle",
|
|
8518
|
-
(mod) => mod?.utils?.setBoxedStackPrefixes
|
|
8519
|
-
],
|
|
8520
|
-
["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
|
|
8521
|
-
];
|
|
8522
|
-
var boxing;
|
|
8523
|
-
function resolveBoxing() {
|
|
8524
|
-
try {
|
|
8525
|
-
const root = findOwnPackageJson(__dirname)?.root;
|
|
8526
|
-
if (!root) return null;
|
|
8527
|
-
const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
|
|
8528
|
-
const playwrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
|
|
8529
|
-
const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
|
|
8530
|
-
for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
|
|
8531
|
-
let set;
|
|
8532
|
-
try {
|
|
8533
|
-
set = pick(requireFromPlaywright(moduleId));
|
|
8534
|
-
} catch {
|
|
8535
|
-
continue;
|
|
8536
|
-
}
|
|
8537
|
-
if (typeof set !== "function") continue;
|
|
8538
|
-
return {
|
|
8539
|
-
set,
|
|
8540
|
-
prefixes: [path21.dirname(playwrightPackageJson), path21.join(root, "dist") + path21.sep]
|
|
8541
|
-
};
|
|
8542
|
-
}
|
|
8543
|
-
} catch {
|
|
8544
|
-
}
|
|
8545
|
-
return null;
|
|
8546
|
-
}
|
|
8547
|
-
function boxCaptureFrames(extraPrefixes = []) {
|
|
8548
|
-
const first = boxing === void 0;
|
|
8549
|
-
if (first) boxing = resolveBoxing();
|
|
8550
|
-
if (!boxing) return;
|
|
8551
|
-
const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
|
|
8552
|
-
if (!first && added.length === 0) return;
|
|
8553
|
-
boxing.prefixes.push(...added);
|
|
8554
|
-
try {
|
|
8555
|
-
boxing.set([...boxing.prefixes]);
|
|
8556
|
-
} catch {
|
|
8557
|
-
}
|
|
8558
|
-
}
|
|
8559
|
-
function internalCall(owner, run) {
|
|
8560
|
-
const wrap = owner?._wrapApiCall;
|
|
8561
|
-
if (typeof wrap !== "function") return run();
|
|
8562
|
-
return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
|
|
8563
|
-
}
|
|
8564
|
-
|
|
8565
10704
|
// src/internal/capture/code-reach.ts
|
|
8566
10705
|
var crypto3 = __toESM(require("crypto"));
|
|
8567
|
-
var
|
|
8568
|
-
var
|
|
8569
|
-
var
|
|
10706
|
+
var fs22 = __toESM(require("fs"));
|
|
10707
|
+
var path26 = __toESM(require("path"));
|
|
10708
|
+
var import_node_child_process3 = require("child_process");
|
|
8570
10709
|
|
|
8571
10710
|
// ../core/src/source-map.ts
|
|
8572
10711
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -8821,24 +10960,24 @@ function repositoryRoot(from) {
|
|
|
8821
10960
|
if (cachedRepoRoot?.from === from) return cachedRepoRoot.root;
|
|
8822
10961
|
let root = from;
|
|
8823
10962
|
try {
|
|
8824
|
-
root = (0,
|
|
10963
|
+
root = (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
8825
10964
|
cwd: from,
|
|
8826
10965
|
encoding: "utf-8",
|
|
8827
10966
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8828
10967
|
}).trim();
|
|
8829
10968
|
} catch {
|
|
8830
10969
|
}
|
|
8831
|
-
cachedRepoRoot = { from, root:
|
|
10970
|
+
cachedRepoRoot = { from, root: path26.resolve(root) };
|
|
8832
10971
|
return cachedRepoRoot.root;
|
|
8833
10972
|
}
|
|
8834
10973
|
function codeReachRoots(configDir, configured) {
|
|
8835
10974
|
const repoRoot = repositoryRoot(configDir);
|
|
8836
|
-
const roots = configured?.length ? configured.map((r) =>
|
|
10975
|
+
const roots = configured?.length ? configured.map((r) => path26.resolve(configDir, r)) : [configDir];
|
|
8837
10976
|
return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
|
|
8838
10977
|
}
|
|
8839
10978
|
function isFile(file) {
|
|
8840
10979
|
try {
|
|
8841
|
-
return
|
|
10980
|
+
return fs22.statSync(file).isFile();
|
|
8842
10981
|
} catch {
|
|
8843
10982
|
return false;
|
|
8844
10983
|
}
|
|
@@ -8846,10 +10985,10 @@ function isFile(file) {
|
|
|
8846
10985
|
function resolveSourcePath(source, roots) {
|
|
8847
10986
|
const normalized = normalizeSourcePath(source);
|
|
8848
10987
|
if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
|
|
8849
|
-
if (
|
|
8850
|
-
const
|
|
10988
|
+
if (path26.isAbsolute(normalized) && isFile(normalized)) return path26.resolve(normalized);
|
|
10989
|
+
const relative13 = normalized.replace(/^\/+/, "");
|
|
8851
10990
|
for (const root of roots) {
|
|
8852
|
-
const candidate =
|
|
10991
|
+
const candidate = path26.resolve(root, relative13);
|
|
8853
10992
|
if (isFile(candidate)) return candidate;
|
|
8854
10993
|
}
|
|
8855
10994
|
return null;
|
|
@@ -8881,7 +11020,7 @@ function fileForUrl(url, roots, bases) {
|
|
|
8881
11020
|
return file;
|
|
8882
11021
|
}
|
|
8883
11022
|
function relativeToRepo(file, repoRoot) {
|
|
8884
|
-
return
|
|
11023
|
+
return path26.relative(repoRoot, file).split(path26.sep).join("/");
|
|
8885
11024
|
}
|
|
8886
11025
|
function scriptHash(entry) {
|
|
8887
11026
|
return crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
|
|
@@ -9071,7 +11210,7 @@ function configuredCodeReachRoots() {
|
|
|
9071
11210
|
if (Array.isArray(parsed)) return parsed.filter((r) => typeof r === "string" && r !== "");
|
|
9072
11211
|
} catch {
|
|
9073
11212
|
}
|
|
9074
|
-
return raw.split(
|
|
11213
|
+
return raw.split(path27.delimiter).filter(Boolean);
|
|
9075
11214
|
}
|
|
9076
11215
|
async function stopSinkCodeReach(sink, closing) {
|
|
9077
11216
|
const session = sink.codeReach;
|
|
@@ -9086,7 +11225,7 @@ async function stopSinkCodeReach(sink, closing) {
|
|
|
9086
11225
|
if (!entries) return;
|
|
9087
11226
|
const info = sink.testInfo;
|
|
9088
11227
|
const configFile = info?.config.configFile;
|
|
9089
|
-
const configDir = configFile ?
|
|
11228
|
+
const configDir = configFile ? path27.dirname(configFile) : info?.config.rootDir ?? process.cwd();
|
|
9090
11229
|
session.files = await resolveCodeReach(
|
|
9091
11230
|
entries,
|
|
9092
11231
|
codeReachRoots(configDir, configuredCodeReachRoots()),
|
|
@@ -9102,8 +11241,8 @@ async function drainPendingProbes(capMs) {
|
|
|
9102
11241
|
let cap;
|
|
9103
11242
|
await Promise.race([
|
|
9104
11243
|
Promise.allSettled(PENDING_PROBES),
|
|
9105
|
-
new Promise((
|
|
9106
|
-
cap = setTimeout(
|
|
11244
|
+
new Promise((resolve7) => {
|
|
11245
|
+
cap = setTimeout(resolve7, capMs);
|
|
9107
11246
|
})
|
|
9108
11247
|
]);
|
|
9109
11248
|
clearTimeout(cap);
|
|
@@ -9143,10 +11282,10 @@ async function readWebVitals(page) {
|
|
|
9143
11282
|
const key = entry.name.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
9144
11283
|
paint[key] = Math.round(entry.startTime);
|
|
9145
11284
|
}
|
|
9146
|
-
const readBuffered = (type, extra) => new Promise((
|
|
11285
|
+
const readBuffered = (type, extra) => new Promise((resolve7) => {
|
|
9147
11286
|
try {
|
|
9148
11287
|
const PO = globalThis.PerformanceObserver;
|
|
9149
|
-
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return
|
|
11288
|
+
if (!PO || !(PO.supportedEntryTypes || []).includes(type)) return resolve7(null);
|
|
9150
11289
|
const out = [];
|
|
9151
11290
|
const po = new PO((list) => out.push(...list.getEntries()));
|
|
9152
11291
|
po.observe({ type, buffered: true, ...extra });
|
|
@@ -9156,10 +11295,10 @@ async function readWebVitals(page) {
|
|
|
9156
11295
|
po.disconnect();
|
|
9157
11296
|
} catch {
|
|
9158
11297
|
}
|
|
9159
|
-
|
|
11298
|
+
resolve7(out);
|
|
9160
11299
|
}, 0);
|
|
9161
11300
|
} catch {
|
|
9162
|
-
|
|
11301
|
+
resolve7(null);
|
|
9163
11302
|
}
|
|
9164
11303
|
});
|
|
9165
11304
|
const [lcpRaw, shiftRaw, eventRaw, firstInputRaw] = await Promise.all([
|
|
@@ -9272,12 +11411,12 @@ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
|
|
|
9272
11411
|
var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
|
|
9273
11412
|
var MAX_PAGE_INVENTORY_WINDOWS = 64;
|
|
9274
11413
|
function withTimeout(promise, ms) {
|
|
9275
|
-
return new Promise((
|
|
11414
|
+
return new Promise((resolve7, reject) => {
|
|
9276
11415
|
const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
|
|
9277
11416
|
promise.then(
|
|
9278
11417
|
(value) => {
|
|
9279
11418
|
clearTimeout(timer);
|
|
9280
|
-
|
|
11419
|
+
resolve7(value);
|
|
9281
11420
|
},
|
|
9282
11421
|
(error) => {
|
|
9283
11422
|
clearTimeout(timer);
|
|
@@ -9650,6 +11789,7 @@ function instrumentPage(page) {
|
|
|
9650
11789
|
const original = factories[method].bind(page);
|
|
9651
11790
|
factories[method] = (...args) => {
|
|
9652
11791
|
if (currentSink) currentSink.lastActivePage = page;
|
|
11792
|
+
noteResourceUse(page);
|
|
9653
11793
|
return wrapLocator(page, original(...args), method, args);
|
|
9654
11794
|
};
|
|
9655
11795
|
}
|
|
@@ -9836,8 +11976,8 @@ async function flushSink(sink, testInfo) {
|
|
|
9836
11976
|
let drainDeadline;
|
|
9837
11977
|
await Promise.race([
|
|
9838
11978
|
Promise.allSettled(sink.capturePromises),
|
|
9839
|
-
new Promise((
|
|
9840
|
-
drainDeadline = setTimeout(
|
|
11979
|
+
new Promise((resolve7) => {
|
|
11980
|
+
drainDeadline = setTimeout(resolve7, 2e3);
|
|
9841
11981
|
})
|
|
9842
11982
|
]);
|
|
9843
11983
|
clearTimeout(drainDeadline);
|
|
@@ -9979,6 +12119,22 @@ async function flushSink(sink, testInfo) {
|
|
|
9979
12119
|
}
|
|
9980
12120
|
}
|
|
9981
12121
|
var piwiFixtures = {
|
|
12122
|
+
// Auto, worker-scoped: the worker's resource ledger. Set up before any hook
|
|
12123
|
+
// (worker fixtures come first), so a `beforeAll` context is in it too, and
|
|
12124
|
+
// torn down after the browser closed, so its census sees what only the
|
|
12125
|
+
// worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
|
|
12126
|
+
piwiResources: [
|
|
12127
|
+
async ({ playwright }, use, workerInfo) => {
|
|
12128
|
+
boxCaptureFrames();
|
|
12129
|
+
startResourceLedger(playwright, workerInfo.workerIndex);
|
|
12130
|
+
try {
|
|
12131
|
+
await use();
|
|
12132
|
+
} finally {
|
|
12133
|
+
stopResourceLedger();
|
|
12134
|
+
}
|
|
12135
|
+
},
|
|
12136
|
+
{ scope: "worker", auto: true }
|
|
12137
|
+
],
|
|
9982
12138
|
// Worker-scoped: patch the shared browser so every page/context created from
|
|
9983
12139
|
// it — including by user fixtures that take `browser` directly — is captured.
|
|
9984
12140
|
browser: [
|
|
@@ -10026,6 +12182,7 @@ var piwiFixtures = {
|
|
|
10026
12182
|
file: testInfo.file,
|
|
10027
12183
|
titlePath: testInfo.titlePath
|
|
10028
12184
|
});
|
|
12185
|
+
activeResourceLedger()?.testStarted(testInfo);
|
|
10029
12186
|
currentSink = sink;
|
|
10030
12187
|
try {
|
|
10031
12188
|
await use();
|
|
@@ -10052,6 +12209,7 @@ var piwiFixtures = {
|
|
|
10052
12209
|
}
|
|
10053
12210
|
if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
|
|
10054
12211
|
await flushSink(sink, testInfo);
|
|
12212
|
+
await recordResourceCensus(testInfo);
|
|
10055
12213
|
}
|
|
10056
12214
|
},
|
|
10057
12215
|
{ auto: true }
|
|
@@ -10083,13 +12241,13 @@ function extendPiwiFixtures(test) {
|
|
|
10083
12241
|
}
|
|
10084
12242
|
|
|
10085
12243
|
// src/internal/ai/ai-fixtures.ts
|
|
10086
|
-
var
|
|
10087
|
-
var
|
|
12244
|
+
var fs24 = __toESM(require("fs"));
|
|
12245
|
+
var path30 = __toESM(require("path"));
|
|
10088
12246
|
var import_test = require("@playwright/test");
|
|
10089
12247
|
|
|
10090
12248
|
// src/internal/ai/artifact.ts
|
|
10091
|
-
var
|
|
10092
|
-
var
|
|
12249
|
+
var fs23 = __toESM(require("fs"));
|
|
12250
|
+
var path28 = __toESM(require("path"));
|
|
10093
12251
|
var ARTIFACT_VERSION = 1;
|
|
10094
12252
|
var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
|
|
10095
12253
|
var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
|
|
@@ -10182,7 +12340,7 @@ function parseEntry(text) {
|
|
|
10182
12340
|
function readEntry(file) {
|
|
10183
12341
|
let text;
|
|
10184
12342
|
try {
|
|
10185
|
-
text =
|
|
12343
|
+
text = fs23.readFileSync(file, "utf8");
|
|
10186
12344
|
} catch (error) {
|
|
10187
12345
|
if (error.code === "ENOENT") return null;
|
|
10188
12346
|
throw error;
|
|
@@ -10192,28 +12350,28 @@ function readEntry(file) {
|
|
|
10192
12350
|
function writeEntry(file, entry) {
|
|
10193
12351
|
const canonical = serializeEntry(entry);
|
|
10194
12352
|
return withEntryLock(file, () => {
|
|
10195
|
-
if (
|
|
10196
|
-
|
|
12353
|
+
if (fs23.existsSync(file) && fs23.readFileSync(file, "utf8") === canonical) return { written: false };
|
|
12354
|
+
fs23.mkdirSync(path28.dirname(file), { recursive: true });
|
|
10197
12355
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10198
|
-
|
|
10199
|
-
|
|
12356
|
+
fs23.writeFileSync(tmp, canonical);
|
|
12357
|
+
fs23.renameSync(tmp, file);
|
|
10200
12358
|
return { written: true };
|
|
10201
12359
|
});
|
|
10202
12360
|
}
|
|
10203
12361
|
function withEntryLock(file, fn) {
|
|
10204
12362
|
const lock = `${file}.lock`;
|
|
10205
|
-
|
|
12363
|
+
fs23.mkdirSync(path28.dirname(file), { recursive: true });
|
|
10206
12364
|
const deadline = Date.now() + 5e3;
|
|
10207
12365
|
for (; ; ) {
|
|
10208
12366
|
try {
|
|
10209
|
-
const fd =
|
|
10210
|
-
|
|
12367
|
+
const fd = fs23.openSync(lock, "wx");
|
|
12368
|
+
fs23.closeSync(fd);
|
|
10211
12369
|
break;
|
|
10212
12370
|
} catch (error) {
|
|
10213
12371
|
if (error.code !== "EEXIST") throw error;
|
|
10214
12372
|
if (Date.now() > deadline) {
|
|
10215
12373
|
try {
|
|
10216
|
-
|
|
12374
|
+
fs23.unlinkSync(lock);
|
|
10217
12375
|
} catch {
|
|
10218
12376
|
}
|
|
10219
12377
|
}
|
|
@@ -10223,7 +12381,7 @@ function withEntryLock(file, fn) {
|
|
|
10223
12381
|
return fn();
|
|
10224
12382
|
} finally {
|
|
10225
12383
|
try {
|
|
10226
|
-
|
|
12384
|
+
fs23.unlinkSync(lock);
|
|
10227
12385
|
} catch {
|
|
10228
12386
|
}
|
|
10229
12387
|
}
|
|
@@ -10231,7 +12389,7 @@ function withEntryLock(file, fn) {
|
|
|
10231
12389
|
|
|
10232
12390
|
// src/internal/ai/keys.ts
|
|
10233
12391
|
var crypto4 = __toESM(require("crypto"));
|
|
10234
|
-
var
|
|
12392
|
+
var path29 = __toESM(require("path"));
|
|
10235
12393
|
var DEFAULT_AI_DIR = "__piwi__";
|
|
10236
12394
|
function normalizeTemplate(template) {
|
|
10237
12395
|
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -10245,13 +12403,13 @@ function slug(text, maxLength = 40) {
|
|
|
10245
12403
|
return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
|
|
10246
12404
|
}
|
|
10247
12405
|
function entryDir(specFile, dir = DEFAULT_AI_DIR) {
|
|
10248
|
-
return
|
|
12406
|
+
return path29.join(path29.dirname(specFile), dir, path29.basename(specFile));
|
|
10249
12407
|
}
|
|
10250
12408
|
function entryPath(params) {
|
|
10251
12409
|
const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
|
|
10252
12410
|
const hash = hashTemplate(basis, params.ordinal ?? 0);
|
|
10253
12411
|
const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
|
|
10254
|
-
return
|
|
12412
|
+
return path29.join(entryDir(params.specFile, params.dir), name);
|
|
10255
12413
|
}
|
|
10256
12414
|
function parseLocation2(location) {
|
|
10257
12415
|
const match = /:(\d+):(\d+)$/.exec(location);
|
|
@@ -10765,7 +12923,7 @@ function testIdentity(testInfo) {
|
|
|
10765
12923
|
return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
|
|
10766
12924
|
}
|
|
10767
12925
|
function relativeToCwd(file) {
|
|
10768
|
-
return
|
|
12926
|
+
return path30.relative(process.cwd(), file).split(path30.sep).join("/");
|
|
10769
12927
|
}
|
|
10770
12928
|
function recordIntents(intents, entry) {
|
|
10771
12929
|
const add = (locator, kind) => {
|
|
@@ -10792,7 +12950,7 @@ function createAiApi(page, testInfo, config, used, intents) {
|
|
|
10792
12950
|
const readSource = () => {
|
|
10793
12951
|
if (source === null) {
|
|
10794
12952
|
try {
|
|
10795
|
-
source =
|
|
12953
|
+
source = fs24.readFileSync(testInfo.file, "utf8");
|
|
10796
12954
|
} catch {
|
|
10797
12955
|
source = "";
|
|
10798
12956
|
}
|