next-leak 0.4.1 → 0.4.3

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.
@@ -68,6 +68,7 @@ function classifyMemoryTrend(heapSamples, externalSamples, options = {}) {
68
68
  }
69
69
 
70
70
  // src/confidence.ts
71
+ var resolveCycles = (cycles) => Math.max(cycles * 2, 6);
71
72
  function effectiveVerdict(report) {
72
73
  return report.confidence.supersededVerdict ?? report.trend.verdict;
73
74
  }
@@ -235,6 +236,7 @@ export {
235
236
  minGrowthFor,
236
237
  classifyTrend,
237
238
  classifyMemoryTrend,
239
+ resolveCycles,
238
240
  effectiveVerdict,
239
241
  warrantsIssueDraft,
240
242
  assessConfidence
@@ -5,8 +5,9 @@ import {
5
5
  classifyTrend,
6
6
  effectiveVerdict,
7
7
  minGrowthFor,
8
+ resolveCycles,
8
9
  warrantsIssueDraft
9
- } from "./chunk-FFCY6POI.js";
10
+ } from "./chunk-2FZXZLZW.js";
10
11
  import {
11
12
  assessPeakPressure,
12
13
  describePeakPressure
@@ -3656,10 +3657,10 @@ var require_ansi_styles = __commonJS({
3656
3657
  };
3657
3658
  var ansi2ansi = (n) => n;
3658
3659
  var rgb2rgb = (r, g, b) => [r, g, b];
3659
- var setLazyProperty = (object, property, get) => {
3660
+ var setLazyProperty = (object, property, get2) => {
3660
3661
  Object.defineProperty(object, property, {
3661
3662
  get: () => {
3662
- const value = get();
3663
+ const value = get2();
3663
3664
  Object.defineProperty(object, property, {
3664
3665
  value,
3665
3666
  enumerable: true,
@@ -36042,18 +36043,18 @@ var require_gen_mapping_umd = __commonJS({
36042
36043
  function cast(set) {
36043
36044
  return set;
36044
36045
  }
36045
- function get(setarr, key) {
36046
+ function get2(setarr, key) {
36046
36047
  return cast(setarr)._indexes[key];
36047
36048
  }
36048
36049
  function put(setarr, key) {
36049
- const index = get(setarr, key);
36050
+ const index = get2(setarr, key);
36050
36051
  if (index !== void 0) return index;
36051
36052
  const { array, _indexes: indexes } = cast(setarr);
36052
36053
  const length = array.push(key);
36053
36054
  return indexes[key] = length - 1;
36054
36055
  }
36055
36056
  function remove(setarr, key) {
36056
- const index = get(setarr, key);
36057
+ const index = get2(setarr, key);
36057
36058
  if (index === void 0) return;
36058
36059
  const { array, _indexes: indexes } = cast(setarr);
36059
36060
  for (let i = index + 1; i < array.length; i++) {
@@ -60211,7 +60212,7 @@ var require_family = __commonJS({
60211
60212
  });
60212
60213
  exports2._getKey = _getKey;
60213
60214
  exports2._getPattern = _getPattern;
60214
- exports2.get = get;
60215
+ exports2.get = get2;
60215
60216
  exports2.getAllNextSiblings = getAllNextSiblings;
60216
60217
  exports2.getAllPrevSiblings = getAllPrevSiblings;
60217
60218
  exports2.getAssignmentIdentifiers = getAssignmentIdentifiers;
@@ -60442,7 +60443,7 @@ var require_family = __commonJS({
60442
60443
  }
60443
60444
  return siblings;
60444
60445
  }
60445
- function get(key, context = true) {
60446
+ function get2(key, context = true) {
60446
60447
  if (context === true) context = this.context;
60447
60448
  const parts = key.split(".");
60448
60449
  if (parts.length === 1) {
@@ -65956,7 +65957,7 @@ var require_dist5 = __commonJS({
65956
65957
  var pathVar = getPathVar(env, platform);
65957
65958
  var separator = getSeparator(platform);
65958
65959
  var originalPath = env[pathVar];
65959
- return { push, unshift, get, restore };
65960
+ return { push, unshift, get: get2, restore };
65960
65961
  function push() {
65961
65962
  for (var _len = arguments.length, paths = Array(_len), _key = 0; _key < _len; _key++) {
65962
65963
  paths[_key] = arguments[_key];
@@ -65969,12 +65970,12 @@ var require_dist5 = __commonJS({
65969
65970
  }
65970
65971
  return change(false, paths);
65971
65972
  }
65972
- function get() {
65973
+ function get2() {
65973
65974
  return env[pathVar];
65974
65975
  }
65975
65976
  function restore() {
65976
65977
  env[pathVar] = originalPath;
65977
- return get();
65978
+ return get2();
65978
65979
  }
65979
65980
  function change(append, paths) {
65980
65981
  if (!append) {
@@ -65985,7 +65986,7 @@ var require_dist5 = __commonJS({
65985
65986
  addExistingPath(pathArray, env[pathVar], append);
65986
65987
  env[pathVar] = pathArray.join(separator);
65987
65988
  });
65988
- return get();
65989
+ return get2();
65989
65990
  }
65990
65991
  }
65991
65992
  function getPathArray(pathToAdd) {
@@ -86349,7 +86350,7 @@ var require_form_data = __commonJS({
86349
86350
  var CombinedStream = require_combined_stream();
86350
86351
  var util = __require("util");
86351
86352
  var path7 = __require("path");
86352
- var http = __require("http");
86353
+ var http2 = __require("http");
86353
86354
  var https = __require("https");
86354
86355
  var parseUrl = __require("url").parse;
86355
86356
  var fs = __require("fs");
@@ -86624,7 +86625,7 @@ var require_form_data = __commonJS({
86624
86625
  if (options.protocol === "https:") {
86625
86626
  request2 = https.request(options);
86626
86627
  } else {
86627
- request2 = http.request(options);
86628
+ request2 = http2.request(options);
86628
86629
  }
86629
86630
  this.getLength(function(err, length) {
86630
86631
  if (err && err !== "Unknown stream") {
@@ -92487,7 +92488,7 @@ function formatReport(report) {
92487
92488
  );
92488
92489
  if (inconclusive.length > 0) {
92489
92490
  const routeList = inconclusive.map((route) => route.route).join(",");
92490
- const moreCycles = Math.max(report.parameters.cycles * 2, 6);
92491
+ const moreCycles = resolveCycles(report.parameters.cycles);
92491
92492
  lines.push(
92492
92493
  "",
92493
92494
  "hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
@@ -93238,6 +93239,7 @@ async function runLoadPhase(options) {
93238
93239
  import { mkdir } from "fs/promises";
93239
93240
 
93240
93241
  // src/control-client.ts
93242
+ import http from "http";
93241
93243
  import { z as z5 } from "zod";
93242
93244
  var sampleSchema = z5.object({
93243
93245
  gcExposed: z5.boolean(),
@@ -93253,12 +93255,70 @@ var ControlError = class extends Error {
93253
93255
  this.name = "ControlError";
93254
93256
  }
93255
93257
  };
93256
- async function request(port, pathname) {
93257
- const response = await fetch(`http://127.0.0.1:${port}${pathname}`);
93258
- if (!response.ok) {
93258
+ var DEADLINES_MS = {
93259
+ "/mem": 3e4,
93260
+ "/gc": 18e4,
93261
+ "/snapshot": 18e5
93262
+ };
93263
+ var DEFAULT_DEADLINE_MS = 3e4;
93264
+ function deadlineFor(pathname) {
93265
+ const operation = pathname.split("?")[0] ?? pathname;
93266
+ return DEADLINES_MS[operation] ?? DEFAULT_DEADLINE_MS;
93267
+ }
93268
+ function get(port, pathname, deadlineMs) {
93269
+ return new Promise((resolve, reject) => {
93270
+ let settled = false;
93271
+ const settle = (outcome) => {
93272
+ if (settled) {
93273
+ return;
93274
+ }
93275
+ settled = true;
93276
+ clearTimeout(timer);
93277
+ if ("ok" in outcome) {
93278
+ resolve(outcome.ok);
93279
+ } else {
93280
+ reject(outcome.error);
93281
+ }
93282
+ };
93283
+ const request2 = http.get({ host: "127.0.0.1", port, path: pathname }, (response) => {
93284
+ const chunks = [];
93285
+ response.on("data", (chunk) => chunks.push(chunk));
93286
+ response.once(
93287
+ "end",
93288
+ () => settle({
93289
+ ok: { status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") }
93290
+ })
93291
+ );
93292
+ });
93293
+ const timer = setTimeout(() => {
93294
+ settle({
93295
+ error: new ControlError(
93296
+ `control channel ${pathname} on port ${port} did not answer within ${Math.round(deadlineMs / 1e3)}s \u2014 the measured process is wedged, or this snapshot is larger than anything this tool has been validated on`
93297
+ )
93298
+ });
93299
+ request2.destroy();
93300
+ }, deadlineMs);
93301
+ timer.unref();
93302
+ request2.once(
93303
+ "error",
93304
+ (cause) => settle({
93305
+ error: new ControlError(
93306
+ `control channel ${pathname} on port ${port} did not answer (${cause.message}) \u2014 the measured process is gone or its event loop is blocked`
93307
+ )
93308
+ })
93309
+ );
93310
+ });
93311
+ }
93312
+ async function request(port, pathname, deadlineMs = deadlineFor(pathname)) {
93313
+ const response = await get(port, pathname, deadlineMs);
93314
+ if (response.status !== 200) {
93259
93315
  throw new ControlError(`control channel ${pathname} responded ${response.status}`);
93260
93316
  }
93261
- return response.json();
93317
+ try {
93318
+ return JSON.parse(response.body);
93319
+ } catch {
93320
+ throw new ControlError(`control channel ${pathname} answered with malformed JSON`);
93321
+ }
93262
93322
  }
93263
93323
  async function requestGc(port) {
93264
93324
  const sample = sampleSchema.parse(await request(port, "/gc"));
@@ -93436,6 +93496,7 @@ async function waitUntilSettled(controlPort, maxIdleMs, deps) {
93436
93496
  var defaultDeps = {
93437
93497
  launch: launchInstrumented,
93438
93498
  load: runLoadPhase,
93499
+ abandon: runAbandonPhase,
93439
93500
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
93440
93501
  readMemory: requestMemory
93441
93502
  };
@@ -93494,7 +93555,7 @@ async function runRitual(options, deps = defaultDeps) {
93494
93555
  });
93495
93556
  return;
93496
93557
  }
93497
- const outcome = await runAbandonPhase({
93558
+ const outcome = await deps.abandon({
93498
93559
  url: `http://127.0.0.1:${app.appPort}${options.route}`,
93499
93560
  amount,
93500
93561
  connections,
@@ -93936,7 +93997,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
93936
93997
  };
93937
93998
  }
93938
93999
  async function writeEvidenceBundle(report, workDir) {
93939
- const { renderHtmlReport } = await import("./html-report-76DP5ADD.js");
94000
+ const { renderHtmlReport } = await import("./html-report-X2LUOZRH.js");
93940
94001
  const { renderIssueMarkdown } = await import("./issue-report-HKA26WNV.js");
93941
94002
  for (const route of report.routes) {
93942
94003
  if (route.status === "measured" && warrantsIssueDraft(route)) {
@@ -93947,7 +94008,6 @@ async function writeEvidenceBundle(report, workDir) {
93947
94008
  }
93948
94009
  await writeFile(report.bundle.htmlReport, renderHtmlReport(report));
93949
94010
  }
93950
- var resolveCycles = (cycles) => Math.max(cycles * 2, 6);
93951
94011
  async function routeReportFor(context, route, index, total) {
93952
94012
  const { routeConfig, progress } = context;
93953
94013
  const label = `${route.path} (${index + 1}/${total})`;
@@ -93964,12 +94024,22 @@ async function routeReportFor(context, route, index, total) {
93964
94024
  if (first.status !== "measured" || context.options.resolveInconclusive === false || effectiveVerdict(first) !== "inconclusive") {
93965
94025
  return first;
93966
94026
  }
94027
+ if (context.options.signal?.aborted === true) {
94028
+ return first;
94029
+ }
93967
94030
  const cycles = resolveCycles(context.options.cycles ?? RITUAL_DEFAULTS.cycles);
93968
94031
  progress(`re-measuring ${route.path} with ${cycles} cycles: the first pass could not call it`);
93969
- return await measureRoute(context, route, requestPath, index, {
93970
- cycles,
93971
- dirSuffix: "-resolve"
93972
- });
94032
+ try {
94033
+ return await measureRoute(context, route, requestPath, index, {
94034
+ cycles,
94035
+ dirSuffix: "-resolve"
94036
+ });
94037
+ } catch (cause) {
94038
+ progress(
94039
+ `re-measurement of ${route.path} failed (${cause instanceof Error ? cause.message : String(cause)}) \u2014 keeping the first pass`
94040
+ );
94041
+ return first;
94042
+ }
93973
94043
  } catch (cause) {
93974
94044
  const failure = cause instanceof Error ? cause.message : String(cause);
93975
94045
  progress(`failed ${label}: ${failure}`);
@@ -1,7 +1,7 @@
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
  effectiveVerdict
4
- } from "./chunk-FFCY6POI.js";
4
+ } from "./chunk-2FZXZLZW.js";
5
5
  import {
6
6
  assessPeakPressure,
7
7
  describePeakPressure
package/dist/cli.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  killActiveChildren,
10
10
  parseCliArgs,
11
11
  runMeasurement
12
- } from "./chunk-OJIXF7RP.js";
13
- import "./chunk-FFCY6POI.js";
12
+ } from "./chunk-EQ2DV2YO.js";
13
+ import "./chunk-2FZXZLZW.js";
14
14
  import "./chunk-XHPUAMJG.js";
15
15
  import "./chunk-6XYFBOL2.js";
16
16
 
@@ -45,6 +45,12 @@ export type ConfidenceInput = {
45
45
  /** Old-space cap the measured process ran under (MB). */
46
46
  maxOldSpaceMb?: number;
47
47
  };
48
+ /**
49
+ * Cycles a re-measurement uses when a verdict came back `inconclusive` — the
50
+ * same figure the report's manual re-run hint prints. One definition, imported
51
+ * by both, so the tool can never recommend one number and use another.
52
+ */
53
+ export declare const resolveCycles: (cycles: number) => number;
48
54
  /**
49
55
  * The verdict a route's evidence actually supports.
50
56
  *
@@ -1,4 +1,5 @@
1
1
  import type { HeapSample } from "./control-server.js";
2
+ declare function request(port: number, pathname: string, deadlineMs?: number): Promise<unknown>;
2
3
  /** Forces GC in the measured process and returns a settled memory sample. */
3
4
  export declare function requestGc(port: number): Promise<HeapSample>;
4
5
  /**
@@ -11,3 +12,6 @@ export declare function requestSnapshot(port: number, name: string): Promise<{
11
12
  file: string;
12
13
  sample: HeapSample;
13
14
  }>;
15
+ /** Test seam: the production deadlines would make a hung-server test take minutes. */
16
+ export declare const requestWithDeadline: typeof request;
17
+ export {};
@@ -1,8 +1,8 @@
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
  renderHtmlReport
4
- } from "./chunk-BTWQYJOW.js";
5
- import "./chunk-FFCY6POI.js";
4
+ } from "./chunk-NNDGXIGQ.js";
5
+ import "./chunk-2FZXZLZW.js";
6
6
  import "./chunk-XHPUAMJG.js";
7
7
  import "./chunk-6XYFBOL2.js";
8
8
  export {
package/dist/index.js CHANGED
@@ -39,13 +39,13 @@ import {
39
39
  sourceIndexAt,
40
40
  summarizeBaseline,
41
41
  validateTarget
42
- } from "./chunk-OJIXF7RP.js";
42
+ } from "./chunk-EQ2DV2YO.js";
43
43
  import {
44
44
  renderHtmlReport
45
- } from "./chunk-BTWQYJOW.js";
45
+ } from "./chunk-NNDGXIGQ.js";
46
46
  import {
47
47
  classifyTrend
48
- } from "./chunk-FFCY6POI.js";
48
+ } from "./chunk-2FZXZLZW.js";
49
49
  import {
50
50
  renderIssueMarkdown
51
51
  } from "./chunk-WXAFXVWS.js";
package/dist/ritual.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { HeapSample } from "./control-server.js";
2
2
  import { launchInstrumented } from "./launcher.js";
3
+ import { runAbandonPhase } from "./abandon-load.js";
3
4
  import { runLoadPhase } from "./load.js";
4
5
  import { type TrendResult } from "./trend.js";
5
6
  export type RitualOptions = {
@@ -103,6 +104,8 @@ export type RitualResult = {
103
104
  export type RitualDeps = {
104
105
  launch: typeof launchInstrumented;
105
106
  load: typeof runLoadPhase;
107
+ /** Early-disconnect load; a seam like `load`, for the same reasons. */
108
+ abandon: typeof runAbandonPhase;
106
109
  sleep: (ms: number) => Promise<void>;
107
110
  /** GC-free read, polled while the app is under load. */
108
111
  readMemory: (port: number) => Promise<HeapSample>;
package/dist/runner.d.ts CHANGED
@@ -153,11 +153,6 @@ export type RunnerDeps = {
153
153
  };
154
154
  export declare function freePort(): Promise<number>;
155
155
  export declare function routeSlug(route: string): string;
156
- /**
157
- * Cycles a second pass uses — the same figure the report recommends for a
158
- * manual re-run, so the tool follows its own advice.
159
- */
160
- export declare const resolveCycles: (cycles: number) => number;
161
156
  /**
162
157
  * Full measurement run: validate the target, discover routes, run the ritual
163
158
  * per route in a fresh process, diff snapshots for non-stable verdicts, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-leak",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
5
5
  "keywords": [
6
6
  "nextjs",