next-leak 0.1.2 → 0.2.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/README.md +49 -13
- package/THIRD-PARTY-NOTICES.md +2678 -0
- package/dist/bootstrap.js +1 -1
- package/dist/{chunk-PF7KYD5N.js → chunk-6XYFBOL2.js} +12 -3
- package/dist/{chunk-2BQZCZ4Z.js → chunk-E5ZKAANQ.js} +90 -3
- package/dist/{chunk-WHC6S57X.js → chunk-HAKKAIHN.js} +148 -156
- package/dist/{chunk-5ZYZW2BL.js → chunk-MYYPTZJW.js} +3 -1
- package/dist/{chunk-C3KH5Z2W.js → chunk-SKAPGI62.js} +2 -1
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli.js +4 -3
- package/dist/confidence.d.ts +9 -2
- package/dist/{html-report-I4PNSLKY.js → html-report-2SOTNXDG.js} +3 -3
- package/dist/index.js +7 -6
- package/dist/{issue-report-VQEUXP2E.js → issue-report-X5JYAVN5.js} +2 -2
- package/dist/launcher.d.ts +9 -0
- package/dist/ritual.d.ts +21 -2
- package/dist/runner.d.ts +33 -2
- package/dist/trend.d.ts +36 -1
- package/package.json +16 -4
package/dist/bootstrap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-6XYFBOL2.js";
|
|
3
3
|
|
|
4
4
|
// src/bootstrap.ts
|
|
5
5
|
import { mkdir, writeFile } from "fs/promises";
|
|
@@ -11,11 +11,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
11
11
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
12
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
13
|
});
|
|
14
|
-
var __esm = (fn, res) => function __init() {
|
|
15
|
-
|
|
14
|
+
var __esm = (fn, res, err) => function __init() {
|
|
15
|
+
if (err) throw err[0];
|
|
16
|
+
try {
|
|
17
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
throw err = [e], e;
|
|
20
|
+
}
|
|
16
21
|
};
|
|
17
22
|
var __commonJS = (cb, mod) => function __require2() {
|
|
18
|
-
|
|
23
|
+
try {
|
|
24
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
throw mod = 0, e;
|
|
27
|
+
}
|
|
19
28
|
};
|
|
20
29
|
var __export = (target, all) => {
|
|
21
30
|
for (var name in all)
|
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
|
|
3
|
+
// src/trend.ts
|
|
4
|
+
var MIN_GROWTH_NOISE_FLOOR = 256 * 1024;
|
|
5
|
+
var MIN_GROWTH_PER_1000_REQUESTS = 51.2 * 1024;
|
|
6
|
+
function minGrowthFor(requestsPerCycle) {
|
|
7
|
+
return Math.max(MIN_GROWTH_NOISE_FLOOR, MIN_GROWTH_PER_1000_REQUESTS * requestsPerCycle / 1e3);
|
|
8
|
+
}
|
|
9
|
+
var STEPWISE_MIN_GROWING_CYCLES = 2;
|
|
10
|
+
var STEPWISE_MAX_DRAWDOWN_RATIO = 0.1;
|
|
11
|
+
function maxDrawdown(samples) {
|
|
12
|
+
let peak = samples[1] ?? 0;
|
|
13
|
+
let worst = 0;
|
|
14
|
+
for (let i = 1; i < samples.length; i += 1) {
|
|
15
|
+
const value = samples[i] ?? 0;
|
|
16
|
+
peak = Math.max(peak, value);
|
|
17
|
+
worst = Math.max(worst, peak - value);
|
|
18
|
+
}
|
|
19
|
+
return worst;
|
|
20
|
+
}
|
|
21
|
+
function isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth) {
|
|
22
|
+
if (growingCycles < STEPWISE_MIN_GROWING_CYCLES || mean < minGrowth) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const netGrowth = deltas.reduce((sum, delta) => sum + delta, 0);
|
|
26
|
+
if (netGrowth <= 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return maxDrawdown(samples) <= netGrowth * STEPWISE_MAX_DRAWDOWN_RATIO;
|
|
30
|
+
}
|
|
31
|
+
function classifyTrend(samples, options = {}) {
|
|
32
|
+
const minGrowth = options.minGrowthPerCycle ?? MIN_GROWTH_NOISE_FLOOR;
|
|
33
|
+
if (samples.length < 4) {
|
|
34
|
+
return { verdict: "inconclusive", growthPerCycle: 0, deltas: [] };
|
|
35
|
+
}
|
|
36
|
+
const deltas = [];
|
|
37
|
+
for (let i = 2; i < samples.length; i += 1) {
|
|
38
|
+
const current = samples[i];
|
|
39
|
+
const previous = samples[i - 1];
|
|
40
|
+
if (current === void 0 || previous === void 0) {
|
|
41
|
+
return { verdict: "inconclusive", growthPerCycle: 0, deltas: [] };
|
|
42
|
+
}
|
|
43
|
+
deltas.push(current - previous);
|
|
44
|
+
}
|
|
45
|
+
const mean = deltas.reduce((sum, d) => sum + d, 0) / deltas.length;
|
|
46
|
+
const allGrow = deltas.every((d) => d >= minGrowth);
|
|
47
|
+
const anyFlatOrDown = deltas.some((d) => d <= 0);
|
|
48
|
+
const growingCycles = deltas.filter((d) => d >= minGrowth).length;
|
|
49
|
+
if (allGrow) {
|
|
50
|
+
return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
|
|
51
|
+
}
|
|
52
|
+
if (isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth)) {
|
|
53
|
+
return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
|
|
54
|
+
}
|
|
55
|
+
if (anyFlatOrDown || mean < minGrowth) {
|
|
56
|
+
return { verdict: "stable", growthPerCycle: mean, deltas, source: "heap" };
|
|
57
|
+
}
|
|
58
|
+
return { verdict: "inconclusive", growthPerCycle: mean, deltas, source: "heap" };
|
|
59
|
+
}
|
|
60
|
+
function classifyMemoryTrend(heapSamples, externalSamples, options = {}) {
|
|
61
|
+
const heap = classifyTrend(heapSamples, options);
|
|
62
|
+
const external = classifyTrend(externalSamples, options);
|
|
63
|
+
const severity = { leak: 0, inconclusive: 1, stable: 2 };
|
|
64
|
+
if (severity[external.verdict] < severity[heap.verdict]) {
|
|
65
|
+
return { ...external, source: "external" };
|
|
66
|
+
}
|
|
67
|
+
return heap;
|
|
68
|
+
}
|
|
69
|
+
|
|
3
70
|
// src/confidence.ts
|
|
4
71
|
function effectiveVerdict(report) {
|
|
5
72
|
return report.confidence.supersededVerdict ?? report.trend.verdict;
|
|
@@ -11,7 +78,6 @@ var VERDICT_WEAKENING = /* @__PURE__ */ new Set([
|
|
|
11
78
|
function warrantsIssueDraft(report) {
|
|
12
79
|
return effectiveVerdict(report) === "leak" && !report.confidence.warnings.some((warning) => VERDICT_WEAKENING.has(warning.code));
|
|
13
80
|
}
|
|
14
|
-
var DEFAULT_MIN_GROWTH = 256 * 1024;
|
|
15
81
|
var LOAD_COMPLETION_FLOOR = 0.99;
|
|
16
82
|
var ABANDON_EFFECTIVE_FLOOR = 0.9;
|
|
17
83
|
var MID_STREAM_FLOOR = 0.1;
|
|
@@ -99,6 +165,23 @@ function noiseFloorWarnings(trend, minGrowth) {
|
|
|
99
165
|
detail: `growth of ${mb(trend.growthPerCycle)}/cycle barely clears the ${mb(minGrowth)} threshold \u2014 raise --load-requests so the signal outgrows the noise`
|
|
100
166
|
}];
|
|
101
167
|
}
|
|
168
|
+
var HEAP_CEILING_RATIO = 0.7;
|
|
169
|
+
function heapCeilingWarnings(input) {
|
|
170
|
+
const samples = input.memorySamples;
|
|
171
|
+
const capMb = input.maxOldSpaceMb;
|
|
172
|
+
if (samples === void 0 || capMb === void 0 || samples.length === 0) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
const peak = Math.max(...samples.map((sample) => sample.heapUsed));
|
|
176
|
+
const capBytes = capMb * 1024 * 1024;
|
|
177
|
+
if (peak < capBytes * HEAP_CEILING_RATIO) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
return [{
|
|
181
|
+
code: "near-heap-ceiling",
|
|
182
|
+
detail: `the heap peaked at ${mb(peak)} against a ${capMb} MB cap (${pct(peak, capBytes)}) \u2014 the curve may have been clipped by the ceiling rather than by the app; re-run with a larger --max-old-space`
|
|
183
|
+
}];
|
|
184
|
+
}
|
|
102
185
|
function isVerdictInvalid(input) {
|
|
103
186
|
if (input.trend.verdict !== "leak") {
|
|
104
187
|
return false;
|
|
@@ -108,12 +191,13 @@ function isVerdictInvalid(input) {
|
|
|
108
191
|
return neverSettled || abandonedNothing;
|
|
109
192
|
}
|
|
110
193
|
function assessConfidence(input) {
|
|
111
|
-
const minGrowth = input.minGrowthPerCycle ??
|
|
194
|
+
const minGrowth = input.minGrowthPerCycle ?? MIN_GROWTH_NOISE_FLOOR;
|
|
112
195
|
const warnings = [
|
|
113
196
|
...settleWarnings(input.settleOutcomes),
|
|
114
197
|
...loadWarnings(input.loadOutcomes, input.abandonAfterMs),
|
|
115
198
|
...growthShapeWarnings(input.trend),
|
|
116
|
-
...noiseFloorWarnings(input.trend, minGrowth)
|
|
199
|
+
...noiseFloorWarnings(input.trend, minGrowth),
|
|
200
|
+
...heapCeilingWarnings(input)
|
|
117
201
|
];
|
|
118
202
|
return {
|
|
119
203
|
level: warnings.length === 0 ? "high" : "low",
|
|
@@ -123,6 +207,9 @@ function assessConfidence(input) {
|
|
|
123
207
|
}
|
|
124
208
|
|
|
125
209
|
export {
|
|
210
|
+
minGrowthFor,
|
|
211
|
+
classifyTrend,
|
|
212
|
+
classifyMemoryTrend,
|
|
126
213
|
effectiveVerdict,
|
|
127
214
|
warrantsIssueDraft,
|
|
128
215
|
assessConfidence
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
assessConfidence,
|
|
4
|
+
classifyMemoryTrend,
|
|
5
|
+
classifyTrend,
|
|
4
6
|
effectiveVerdict,
|
|
7
|
+
minGrowthFor,
|
|
5
8
|
warrantsIssueDraft
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-E5ZKAANQ.js";
|
|
7
10
|
import {
|
|
8
11
|
__commonJS,
|
|
9
12
|
__esm,
|
|
@@ -11,7 +14,7 @@ import {
|
|
|
11
14
|
__require,
|
|
12
15
|
__toCommonJS,
|
|
13
16
|
__toESM
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6XYFBOL2.js";
|
|
15
18
|
|
|
16
19
|
// node_modules/.pnpm/fs-extra@4.0.3/node_modules/fs-extra/lib/util/assign.js
|
|
17
20
|
var require_assign = __commonJS({
|
|
@@ -91969,14 +91972,20 @@ var FLAGS = [
|
|
|
91969
91972
|
argName: "<list>",
|
|
91970
91973
|
help: "Only measure these routes \u2014 comma-separated templates or prefixes (e.g. /api,/dashboard)"
|
|
91971
91974
|
},
|
|
91972
|
-
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default
|
|
91975
|
+
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default 4, minimum 3)" },
|
|
91973
91976
|
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
91974
91977
|
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
91975
91978
|
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
91979
|
+
{
|
|
91980
|
+
flag: "--max-old-space",
|
|
91981
|
+
value: "int",
|
|
91982
|
+
argName: "<mb>",
|
|
91983
|
+
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
91984
|
+
},
|
|
91976
91985
|
{
|
|
91977
91986
|
flag: "--quick",
|
|
91978
91987
|
value: "none",
|
|
91979
|
-
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 the
|
|
91988
|
+
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
91980
91989
|
},
|
|
91981
91990
|
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
91982
91991
|
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
@@ -92015,8 +92024,10 @@ var LIMITS = {
|
|
|
92015
92024
|
"--cycles": 100,
|
|
92016
92025
|
"--requests": 1e6,
|
|
92017
92026
|
"--connections": 1e4,
|
|
92018
|
-
"--idle": 3600
|
|
92027
|
+
"--idle": 3600,
|
|
92028
|
+
"--max-old-space": 65536
|
|
92019
92029
|
};
|
|
92030
|
+
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
92020
92031
|
function findSpec(argument) {
|
|
92021
92032
|
return FLAGS.find((spec) => spec.flag === argument || spec.alias === argument);
|
|
92022
92033
|
}
|
|
@@ -92044,10 +92055,16 @@ function applyNumericFlag(flag, value, options) {
|
|
|
92044
92055
|
if (flag === "--cycles" && parsed < 3) {
|
|
92045
92056
|
return flagError("the trend verdict needs at least 3 cycles (--cycles 3 or more)");
|
|
92046
92057
|
}
|
|
92058
|
+
if (flag === "--max-old-space" && parsed < MIN_MAX_OLD_SPACE_MB) {
|
|
92059
|
+
return flagError(
|
|
92060
|
+
`option "--max-old-space" needs at least ${MIN_MAX_OLD_SPACE_MB} MB (got ${parsed}) \u2014 below that the measured app cannot start`
|
|
92061
|
+
);
|
|
92062
|
+
}
|
|
92047
92063
|
if (flag === "--cycles") options.cycles = parsed;
|
|
92048
92064
|
if (flag === "--requests") options.requests = parsed;
|
|
92049
92065
|
if (flag === "--connections") options.connections = parsed;
|
|
92050
92066
|
if (flag === "--idle") options.idleSeconds = parsed;
|
|
92067
|
+
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
92051
92068
|
return FLAG_OK;
|
|
92052
92069
|
}
|
|
92053
92070
|
function applyFlag(spec, value, options) {
|
|
@@ -92058,6 +92075,7 @@ function applyFlag(spec, value, options) {
|
|
|
92058
92075
|
case "--requests":
|
|
92059
92076
|
case "--connections":
|
|
92060
92077
|
case "--idle":
|
|
92078
|
+
case "--max-old-space":
|
|
92061
92079
|
return applyNumericFlag(spec.flag, value, options);
|
|
92062
92080
|
case "--quick":
|
|
92063
92081
|
options.quick = true;
|
|
@@ -92113,6 +92131,7 @@ function parseCliArgs(argv) {
|
|
|
92113
92131
|
requests: null,
|
|
92114
92132
|
connections: null,
|
|
92115
92133
|
idleSeconds: null,
|
|
92134
|
+
maxOldSpaceMb: null,
|
|
92116
92135
|
quick: false,
|
|
92117
92136
|
diffAll: false,
|
|
92118
92137
|
output: null
|
|
@@ -92162,6 +92181,7 @@ import path from "path";
|
|
|
92162
92181
|
import { pathToFileURL } from "url";
|
|
92163
92182
|
import { z } from "zod";
|
|
92164
92183
|
var controlFileSchema = z.object({ port: z.number(), pid: z.number() });
|
|
92184
|
+
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92165
92185
|
var LaunchError = class extends Error {
|
|
92166
92186
|
constructor(message) {
|
|
92167
92187
|
super(message);
|
|
@@ -92209,7 +92229,7 @@ async function launchInstrumented(options) {
|
|
|
92209
92229
|
process.execPath,
|
|
92210
92230
|
[
|
|
92211
92231
|
"--expose-gc",
|
|
92212
|
-
`--max-old-space-size=${options.maxOldSpaceMb ??
|
|
92232
|
+
`--max-old-space-size=${options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB}`,
|
|
92213
92233
|
"--import",
|
|
92214
92234
|
pathToFileURL(options.bootstrapPath).href,
|
|
92215
92235
|
options.serverPath
|
|
@@ -92304,69 +92324,6 @@ async function launchInstrumented(options) {
|
|
|
92304
92324
|
}
|
|
92305
92325
|
}
|
|
92306
92326
|
|
|
92307
|
-
// src/trend.ts
|
|
92308
|
-
var DEFAULT_MIN_GROWTH = 256 * 1024;
|
|
92309
|
-
var STEPWISE_MIN_GROWING_CYCLES = 2;
|
|
92310
|
-
var STEPWISE_MAX_DRAWDOWN_RATIO = 0.1;
|
|
92311
|
-
function maxDrawdown(samples) {
|
|
92312
|
-
let peak = samples[1] ?? 0;
|
|
92313
|
-
let worst = 0;
|
|
92314
|
-
for (let i = 1; i < samples.length; i += 1) {
|
|
92315
|
-
const value = samples[i] ?? 0;
|
|
92316
|
-
peak = Math.max(peak, value);
|
|
92317
|
-
worst = Math.max(worst, peak - value);
|
|
92318
|
-
}
|
|
92319
|
-
return worst;
|
|
92320
|
-
}
|
|
92321
|
-
function isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth) {
|
|
92322
|
-
if (growingCycles < STEPWISE_MIN_GROWING_CYCLES || mean < minGrowth) {
|
|
92323
|
-
return false;
|
|
92324
|
-
}
|
|
92325
|
-
const netGrowth = deltas.reduce((sum, delta) => sum + delta, 0);
|
|
92326
|
-
if (netGrowth <= 0) {
|
|
92327
|
-
return false;
|
|
92328
|
-
}
|
|
92329
|
-
return maxDrawdown(samples) <= netGrowth * STEPWISE_MAX_DRAWDOWN_RATIO;
|
|
92330
|
-
}
|
|
92331
|
-
function classifyTrend(samples, options = {}) {
|
|
92332
|
-
const minGrowth = options.minGrowthPerCycle ?? DEFAULT_MIN_GROWTH;
|
|
92333
|
-
if (samples.length < 4) {
|
|
92334
|
-
return { verdict: "inconclusive", growthPerCycle: 0, deltas: [] };
|
|
92335
|
-
}
|
|
92336
|
-
const deltas = [];
|
|
92337
|
-
for (let i = 2; i < samples.length; i += 1) {
|
|
92338
|
-
const current = samples[i];
|
|
92339
|
-
const previous = samples[i - 1];
|
|
92340
|
-
if (current === void 0 || previous === void 0) {
|
|
92341
|
-
return { verdict: "inconclusive", growthPerCycle: 0, deltas: [] };
|
|
92342
|
-
}
|
|
92343
|
-
deltas.push(current - previous);
|
|
92344
|
-
}
|
|
92345
|
-
const mean = deltas.reduce((sum, d) => sum + d, 0) / deltas.length;
|
|
92346
|
-
const allGrow = deltas.every((d) => d >= minGrowth);
|
|
92347
|
-
const anyFlatOrDown = deltas.some((d) => d <= 0);
|
|
92348
|
-
const growingCycles = deltas.filter((d) => d >= minGrowth).length;
|
|
92349
|
-
if (allGrow) {
|
|
92350
|
-
return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
|
|
92351
|
-
}
|
|
92352
|
-
if (isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth)) {
|
|
92353
|
-
return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
|
|
92354
|
-
}
|
|
92355
|
-
if (anyFlatOrDown || mean < minGrowth) {
|
|
92356
|
-
return { verdict: "stable", growthPerCycle: mean, deltas, source: "heap" };
|
|
92357
|
-
}
|
|
92358
|
-
return { verdict: "inconclusive", growthPerCycle: mean, deltas, source: "heap" };
|
|
92359
|
-
}
|
|
92360
|
-
function classifyMemoryTrend(heapSamples, externalSamples, options = {}) {
|
|
92361
|
-
const heap = classifyTrend(heapSamples, options);
|
|
92362
|
-
const external = classifyTrend(externalSamples, options);
|
|
92363
|
-
const severity = { leak: 0, inconclusive: 1, stable: 2 };
|
|
92364
|
-
if (severity[external.verdict] < severity[heap.verdict]) {
|
|
92365
|
-
return { ...external, source: "external" };
|
|
92366
|
-
}
|
|
92367
|
-
return heap;
|
|
92368
|
-
}
|
|
92369
|
-
|
|
92370
92327
|
// src/report.ts
|
|
92371
92328
|
var MB = 1024 * 1024;
|
|
92372
92329
|
var formatMb = (bytes) => `${(bytes / MB).toFixed(1)} MB`;
|
|
@@ -92475,11 +92432,14 @@ function formatReport(report) {
|
|
|
92475
92432
|
for (const route of report.routes) {
|
|
92476
92433
|
lines.push(...routeLines(route));
|
|
92477
92434
|
}
|
|
92478
|
-
|
|
92479
|
-
lines.push(
|
|
92480
|
-
|
|
92481
|
-
|
|
92482
|
-
|
|
92435
|
+
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
92436
|
+
lines.push(
|
|
92437
|
+
"",
|
|
92438
|
+
`judged over ${cycles} cycles \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
92439
|
+
`snapshots and run.json: ${report.workDir}`,
|
|
92440
|
+
`report: ${report.bundle.htmlReport}`,
|
|
92441
|
+
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
92442
|
+
);
|
|
92483
92443
|
const inconclusive = report.routes.filter(
|
|
92484
92444
|
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
92485
92445
|
);
|
|
@@ -92582,7 +92542,7 @@ function classifySource(rawSource) {
|
|
|
92582
92542
|
}
|
|
92583
92543
|
const nodeModulesSplit = source.split("/node_modules/");
|
|
92584
92544
|
if (nodeModulesSplit.length > 1) {
|
|
92585
|
-
const tail = nodeModulesSplit
|
|
92545
|
+
const tail = nodeModulesSplit.at(-1) ?? "";
|
|
92586
92546
|
const segments = tail.split("/");
|
|
92587
92547
|
const packageName = (segments[0]?.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0]) ?? null;
|
|
92588
92548
|
if (packageName === "next" || packageName === "react" || packageName === "react-dom") {
|
|
@@ -92890,6 +92850,16 @@ var routesManifestSchema = z3.looseObject({
|
|
|
92890
92850
|
});
|
|
92891
92851
|
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
92892
92852
|
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
92853
|
+
var STATIC_ASSET_PATHS = /* @__PURE__ */ new Set(["/favicon.ico"]);
|
|
92854
|
+
function unaddressableReason(routePath) {
|
|
92855
|
+
if (routePath.split("/").some((segment) => INTERCEPTING_SEGMENT.test(segment))) {
|
|
92856
|
+
return "intercepting route \u2014 only reachable via client navigation";
|
|
92857
|
+
}
|
|
92858
|
+
if (STATIC_ASSET_PATHS.has(routePath)) {
|
|
92859
|
+
return "static asset served by a generated handler \u2014 no user code to leak";
|
|
92860
|
+
}
|
|
92861
|
+
return void 0;
|
|
92862
|
+
}
|
|
92893
92863
|
function toRequestPath(manifestKey) {
|
|
92894
92864
|
let kind;
|
|
92895
92865
|
let withoutSuffix;
|
|
@@ -92927,14 +92897,12 @@ function discoverRoutes(appPaths) {
|
|
|
92927
92897
|
if (parsed === null || INTERNAL_PATHS.has(parsed.path) || routes.has(parsed.path)) {
|
|
92928
92898
|
continue;
|
|
92929
92899
|
}
|
|
92930
|
-
const
|
|
92900
|
+
const reason = unaddressableReason(parsed.path);
|
|
92931
92901
|
routes.set(parsed.path, {
|
|
92932
92902
|
path: parsed.path,
|
|
92933
92903
|
kind: parsed.kind,
|
|
92934
92904
|
dynamic: parsed.path.includes("["),
|
|
92935
|
-
...
|
|
92936
|
-
unaddressableReason: "intercepting route \u2014 only reachable via client navigation"
|
|
92937
|
-
}
|
|
92905
|
+
...reason !== void 0 && { unaddressableReason: reason }
|
|
92938
92906
|
});
|
|
92939
92907
|
}
|
|
92940
92908
|
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -93283,55 +93251,55 @@ Host: ${target.host}\r
|
|
|
93283
93251
|
errors: 0
|
|
93284
93252
|
};
|
|
93285
93253
|
let remaining = options.amount;
|
|
93254
|
+
const sendOne = () => new Promise((resolve) => {
|
|
93255
|
+
const socket = net.connect({ host: target.hostname, port });
|
|
93256
|
+
let settled = false;
|
|
93257
|
+
let responseStarted = false;
|
|
93258
|
+
let timer;
|
|
93259
|
+
const finish = () => {
|
|
93260
|
+
if (settled) {
|
|
93261
|
+
return;
|
|
93262
|
+
}
|
|
93263
|
+
settled = true;
|
|
93264
|
+
clearTimeout(timer);
|
|
93265
|
+
socket.destroy();
|
|
93266
|
+
resolve();
|
|
93267
|
+
};
|
|
93268
|
+
const giveUp = () => {
|
|
93269
|
+
if (!settled) {
|
|
93270
|
+
result.abandoned += 1;
|
|
93271
|
+
if (responseStarted) {
|
|
93272
|
+
result.abandonedMidStream += 1;
|
|
93273
|
+
}
|
|
93274
|
+
}
|
|
93275
|
+
finish();
|
|
93276
|
+
};
|
|
93277
|
+
socket.once("connect", () => {
|
|
93278
|
+
result.sent += 1;
|
|
93279
|
+
socket.write(request2);
|
|
93280
|
+
timer = setTimeout(giveUp, options.abandonAfterMs);
|
|
93281
|
+
timer.unref();
|
|
93282
|
+
});
|
|
93283
|
+
socket.on("data", () => {
|
|
93284
|
+
responseStarted = true;
|
|
93285
|
+
});
|
|
93286
|
+
socket.once("end", () => {
|
|
93287
|
+
if (!settled) {
|
|
93288
|
+
result.completed += 1;
|
|
93289
|
+
}
|
|
93290
|
+
finish();
|
|
93291
|
+
});
|
|
93292
|
+
socket.once("error", () => {
|
|
93293
|
+
if (!settled) {
|
|
93294
|
+
result.errors += 1;
|
|
93295
|
+
}
|
|
93296
|
+
finish();
|
|
93297
|
+
});
|
|
93298
|
+
});
|
|
93286
93299
|
const worker = async () => {
|
|
93287
93300
|
while (remaining > 0) {
|
|
93288
93301
|
remaining -= 1;
|
|
93289
|
-
await
|
|
93290
|
-
const socket = net.connect({ host: target.hostname, port });
|
|
93291
|
-
let settled = false;
|
|
93292
|
-
let responseStarted = false;
|
|
93293
|
-
let timer;
|
|
93294
|
-
const finish = () => {
|
|
93295
|
-
if (settled) {
|
|
93296
|
-
return;
|
|
93297
|
-
}
|
|
93298
|
-
settled = true;
|
|
93299
|
-
clearTimeout(timer);
|
|
93300
|
-
socket.destroy();
|
|
93301
|
-
resolve();
|
|
93302
|
-
};
|
|
93303
|
-
socket.once("connect", () => {
|
|
93304
|
-
result.sent += 1;
|
|
93305
|
-
socket.write(request2);
|
|
93306
|
-
timer = setTimeout(() => {
|
|
93307
|
-
if (!settled) {
|
|
93308
|
-
result.abandoned += 1;
|
|
93309
|
-
if (responseStarted) {
|
|
93310
|
-
result.abandonedMidStream += 1;
|
|
93311
|
-
}
|
|
93312
|
-
}
|
|
93313
|
-
finish();
|
|
93314
|
-
}, options.abandonAfterMs);
|
|
93315
|
-
timer.unref();
|
|
93316
|
-
});
|
|
93317
|
-
socket.on("data", () => {
|
|
93318
|
-
responseStarted = true;
|
|
93319
|
-
});
|
|
93320
|
-
socket.once("end", () => {
|
|
93321
|
-
if (!settled) {
|
|
93322
|
-
result.completed += 1;
|
|
93323
|
-
}
|
|
93324
|
-
clearTimeout(timer);
|
|
93325
|
-
finish();
|
|
93326
|
-
});
|
|
93327
|
-
socket.once("error", () => {
|
|
93328
|
-
if (!settled) {
|
|
93329
|
-
result.errors += 1;
|
|
93330
|
-
}
|
|
93331
|
-
clearTimeout(timer);
|
|
93332
|
-
finish();
|
|
93333
|
-
});
|
|
93334
|
-
});
|
|
93302
|
+
await sendOne();
|
|
93335
93303
|
}
|
|
93336
93304
|
};
|
|
93337
93305
|
await Promise.all(
|
|
@@ -93373,7 +93341,7 @@ var RITUAL_DEFAULTS = {
|
|
|
93373
93341
|
warmupRequests: 200,
|
|
93374
93342
|
loadRequests: 5e3,
|
|
93375
93343
|
connections: 100,
|
|
93376
|
-
cycles:
|
|
93344
|
+
cycles: 4,
|
|
93377
93345
|
idleMs: 3e4
|
|
93378
93346
|
};
|
|
93379
93347
|
async function runRitual(options, deps = defaultDeps) {
|
|
@@ -93386,24 +93354,20 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93386
93354
|
throw new Error("the trend verdict needs at least 3 cycles");
|
|
93387
93355
|
}
|
|
93388
93356
|
await mkdir(options.workDir, { recursive: true });
|
|
93357
|
+
const launchOptions = {
|
|
93358
|
+
serverPath: options.serverPath,
|
|
93359
|
+
workDir: options.workDir,
|
|
93360
|
+
bootstrapPath: options.bootstrapPath,
|
|
93361
|
+
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb }
|
|
93362
|
+
};
|
|
93389
93363
|
let app;
|
|
93390
93364
|
try {
|
|
93391
|
-
app = await deps.launch({
|
|
93392
|
-
serverPath: options.serverPath,
|
|
93393
|
-
workDir: options.workDir,
|
|
93394
|
-
appPort: options.appPort,
|
|
93395
|
-
bootstrapPath: options.bootstrapPath
|
|
93396
|
-
});
|
|
93365
|
+
app = await deps.launch({ ...launchOptions, appPort: options.appPort });
|
|
93397
93366
|
} catch (cause) {
|
|
93398
93367
|
if (!String(cause).includes("EADDRINUSE")) {
|
|
93399
93368
|
throw cause;
|
|
93400
93369
|
}
|
|
93401
|
-
app = await deps.launch({
|
|
93402
|
-
serverPath: options.serverPath,
|
|
93403
|
-
workDir: options.workDir,
|
|
93404
|
-
appPort: options.appPort + 1,
|
|
93405
|
-
bootstrapPath: options.bootstrapPath
|
|
93406
|
-
});
|
|
93370
|
+
app = await deps.launch({ ...launchOptions, appPort: options.appPort + 1 });
|
|
93407
93371
|
}
|
|
93408
93372
|
const timings = [];
|
|
93409
93373
|
const loadOutcomes = [];
|
|
@@ -93488,6 +93452,7 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93488
93452
|
}
|
|
93489
93453
|
const samples = memorySamples.map((sample) => sample.heapUsed);
|
|
93490
93454
|
const externalSamples = memorySamples.map((sample) => sample.external);
|
|
93455
|
+
const minGrowthPerCycle = minGrowthFor(loadRequests);
|
|
93491
93456
|
return {
|
|
93492
93457
|
route: options.route,
|
|
93493
93458
|
timings,
|
|
@@ -93497,8 +93462,9 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93497
93462
|
memorySamples,
|
|
93498
93463
|
baselineSnapshot: baseline.file,
|
|
93499
93464
|
afterSnapshot,
|
|
93500
|
-
trend: classifyMemoryTrend(samples, externalSamples),
|
|
93501
|
-
requestsPerCycle: loadRequests
|
|
93465
|
+
trend: classifyMemoryTrend(samples, externalSamples, { minGrowthPerCycle }),
|
|
93466
|
+
requestsPerCycle: loadRequests,
|
|
93467
|
+
minGrowthPerCycle
|
|
93502
93468
|
};
|
|
93503
93469
|
} finally {
|
|
93504
93470
|
await app.close();
|
|
@@ -93682,9 +93648,20 @@ import { createServer } from "net";
|
|
|
93682
93648
|
import path6 from "path";
|
|
93683
93649
|
var ESTIMATED_RPS = 250;
|
|
93684
93650
|
var PER_ROUTE_OVERHEAD_SECONDS = 10;
|
|
93685
|
-
|
|
93686
|
-
|
|
93687
|
-
|
|
93651
|
+
var MIN_SETTLE_SECONDS = 4;
|
|
93652
|
+
function estimateRun(routeCount, parameters) {
|
|
93653
|
+
const settleSeconds = Math.min(MIN_SETTLE_SECONDS, parameters.idleMs / 1e3);
|
|
93654
|
+
const slowPerRoute = parameters.warmupRequests / ESTIMATED_RPS + parameters.cycles * (parameters.loadRequests / ESTIMATED_RPS + parameters.idleMs / 1e3) + PER_ROUTE_OVERHEAD_SECONDS;
|
|
93655
|
+
const fastPerRoute = PER_ROUTE_OVERHEAD_SECONDS + parameters.cycles * settleSeconds;
|
|
93656
|
+
return {
|
|
93657
|
+
fastSeconds: Math.round(routeCount * fastPerRoute),
|
|
93658
|
+
slowSeconds: Math.round(routeCount * slowPerRoute)
|
|
93659
|
+
};
|
|
93660
|
+
}
|
|
93661
|
+
function formatEstimate(estimate) {
|
|
93662
|
+
const fast = formatDuration(estimate.fastSeconds);
|
|
93663
|
+
const slow = formatDuration(estimate.slowSeconds);
|
|
93664
|
+
return fast === slow ? `\u2248 ${slow}` : `\u2248 ${fast}\u2013${slow}`;
|
|
93688
93665
|
}
|
|
93689
93666
|
function formatDuration(seconds) {
|
|
93690
93667
|
if (seconds < 90) {
|
|
@@ -93732,7 +93709,7 @@ function rssTrend(memorySamples) {
|
|
|
93732
93709
|
return deltas.reduce((sum, delta) => sum + delta, 0) / deltas.length;
|
|
93733
93710
|
}
|
|
93734
93711
|
function routeSlug(route) {
|
|
93735
|
-
const sanitized = route.replaceAll(/[^a-zA-Z0-9-]+/g, "_").replace(/^_
|
|
93712
|
+
const sanitized = route.replaceAll(/[^a-zA-Z0-9-]+/g, "_").replace(/^_/, "").replace(/_$/, "");
|
|
93736
93713
|
if (route === "/") {
|
|
93737
93714
|
return "root";
|
|
93738
93715
|
}
|
|
@@ -93742,9 +93719,16 @@ function routeSlug(route) {
|
|
|
93742
93719
|
const digest = createHash3("sha1").update(route).digest("hex").slice(0, 6);
|
|
93743
93720
|
return sanitized === "" ? `route-${digest}` : `${sanitized}-${digest}`;
|
|
93744
93721
|
}
|
|
93722
|
+
function trimTrailingSlashes(value) {
|
|
93723
|
+
let end = value.length;
|
|
93724
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
93725
|
+
end -= 1;
|
|
93726
|
+
}
|
|
93727
|
+
return value.slice(0, end);
|
|
93728
|
+
}
|
|
93745
93729
|
function filterRoutes(routes, selectors, progress) {
|
|
93746
93730
|
const matches = (routePath, selector) => {
|
|
93747
|
-
const normalized = selector
|
|
93731
|
+
const normalized = trimTrailingSlashes(selector);
|
|
93748
93732
|
if (normalized === "") {
|
|
93749
93733
|
return routePath === "/";
|
|
93750
93734
|
}
|
|
@@ -93779,6 +93763,7 @@ async function measureRoute(context, route, requestPath, index) {
|
|
|
93779
93763
|
...options.connections !== void 0 && { connections: options.connections },
|
|
93780
93764
|
...options.cycles !== void 0 && { cycles: options.cycles },
|
|
93781
93765
|
...options.idleMs !== void 0 && { idleMs: options.idleMs },
|
|
93766
|
+
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
93782
93767
|
...routeConfig.headers !== void 0 && { headers: routeConfig.headers },
|
|
93783
93768
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
93784
93769
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
@@ -93788,6 +93773,11 @@ async function measureRoute(context, route, requestPath, index) {
|
|
|
93788
93773
|
trend: result.trend,
|
|
93789
93774
|
loadOutcomes: result.loadOutcomes,
|
|
93790
93775
|
settleOutcomes: result.settleOutcomes,
|
|
93776
|
+
// The audit has to grade against the gate the verdict actually used, or
|
|
93777
|
+
// the noise-floor warning describes a threshold nobody applied.
|
|
93778
|
+
minGrowthPerCycle: result.minGrowthPerCycle,
|
|
93779
|
+
memorySamples: result.memorySamples,
|
|
93780
|
+
maxOldSpaceMb: options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB,
|
|
93791
93781
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
93792
93782
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
93793
93783
|
}
|
|
@@ -93822,8 +93812,8 @@ async function measureRoute(context, route, requestPath, index) {
|
|
|
93822
93812
|
};
|
|
93823
93813
|
}
|
|
93824
93814
|
async function writeEvidenceBundle(report, workDir) {
|
|
93825
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
93826
|
-
const { renderIssueMarkdown } = await import("./issue-report-
|
|
93815
|
+
const { renderHtmlReport } = await import("./html-report-2SOTNXDG.js");
|
|
93816
|
+
const { renderIssueMarkdown } = await import("./issue-report-X5JYAVN5.js");
|
|
93827
93817
|
for (const route of report.routes) {
|
|
93828
93818
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
93829
93819
|
const file = path6.join(workDir, `ISSUE-${routeSlug(route.route)}.md`);
|
|
@@ -93865,22 +93855,25 @@ async function planRun(options, target, deps, progress) {
|
|
|
93865
93855
|
progress(
|
|
93866
93856
|
`module registry: ${registry.size} modules` + (nextVersion === null ? "" : ` \xB7 next ${nextVersion}`)
|
|
93867
93857
|
);
|
|
93858
|
+
const loadRequests = options.loadRequests ?? RITUAL_DEFAULTS.loadRequests;
|
|
93868
93859
|
const parameters = {
|
|
93869
93860
|
warmupRequests: options.warmupRequests ?? RITUAL_DEFAULTS.warmupRequests,
|
|
93870
|
-
loadRequests
|
|
93861
|
+
loadRequests,
|
|
93871
93862
|
connections: options.connections ?? RITUAL_DEFAULTS.connections,
|
|
93872
93863
|
cycles: options.cycles ?? RITUAL_DEFAULTS.cycles,
|
|
93873
|
-
idleMs: options.idleMs ?? RITUAL_DEFAULTS.idleMs
|
|
93864
|
+
idleMs: options.idleMs ?? RITUAL_DEFAULTS.idleMs,
|
|
93865
|
+
maxOldSpaceMb: options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB,
|
|
93866
|
+
minGrowthPerCycle: minGrowthFor(loadRequests)
|
|
93874
93867
|
};
|
|
93875
|
-
const
|
|
93876
|
-
|
|
93877
|
-
|
|
93878
|
-
);
|
|
93868
|
+
const measurable = routes.filter(
|
|
93869
|
+
(route) => skipReason(route, resolveRoutePath(route.path, routeConfig)) === null
|
|
93870
|
+
).length;
|
|
93871
|
+
const estimate = estimateRun(measurable, parameters);
|
|
93879
93872
|
progress(
|
|
93880
|
-
`${routes.length} routes discovered \xB7
|
|
93873
|
+
`${routes.length} routes discovered` + (measurable === routes.length ? "" : ` \xB7 ${measurable} measurable`) + ` \xB7 estimated ${formatEstimate(estimate)}` + // Long default runs are where first-time users give up; point at the two
|
|
93881
93874
|
// ways out. Suppressed once load parameters were tuned by hand (or by
|
|
93882
93875
|
// --quick, which arrives here as explicit loadRequests/idleMs).
|
|
93883
|
-
(
|
|
93876
|
+
(estimate.slowSeconds > 15 * 60 && options.loadRequests === void 0 && options.idleMs === void 0 ? " \u2014 use --quick for the fast validated preset, or narrow with --routes" : "")
|
|
93884
93877
|
);
|
|
93885
93878
|
return { routes, routeConfig, registry, nextVersion, parameters };
|
|
93886
93879
|
}
|
|
@@ -93945,7 +93938,6 @@ export {
|
|
|
93945
93938
|
LaunchError,
|
|
93946
93939
|
killActiveChildren,
|
|
93947
93940
|
launchInstrumented,
|
|
93948
|
-
classifyTrend,
|
|
93949
93941
|
formatReport,
|
|
93950
93942
|
RouteConfigError,
|
|
93951
93943
|
ROUTE_CONFIG_FILE,
|