velocious 1.0.684 → 1.0.686

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.
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { randomUUID } from "node:crypto"
4
+ import { readFileSync } from "node:fs"
4
5
  import v8 from "node:v8"
5
6
  import timeout from "awaitery/build/timeout.js"
6
7
  import runJobPayload, { BackgroundJobPerformedFailure } from "./job-runner.js"
@@ -20,6 +21,31 @@ const MEMORY_OBSERVATION_INTERVAL_MS = 10000
20
21
  /** Bound on in-flight job ids carried in a memory observation. */
21
22
  const MEMORY_OBSERVATION_JOB_ID_LIMIT = 32
22
23
 
24
+ /**
25
+ * Reads this process's peak resident set size (the kernel high-water mark,
26
+ * `VmHWM` in `/proc/self/status`). Unlike a point-in-time RSS sample, this is
27
+ * monotonic for the life of the process: once a burst ratchets the working set
28
+ * up, `VmHWM` remembers it even after garbage collection returns the pages.
29
+ * That makes it the honest signal for recycling a pooled child — a child that
30
+ * spiked to several gigabytes during a build burst keeps its ratcheted floor
31
+ * and must be replaced, even though its settled RSS at job-outcome time looks
32
+ * modest. A read failure (no `/proc`) degrades to the current RSS sample so the
33
+ * observation is never blocked on it.
34
+ * @returns {number} Peak RSS in bytes, or the current RSS sample when unreadable.
35
+ */
36
+ function readPeakRssBytes() {
37
+ const fallbackBytes = process.memoryUsage().rss
38
+ try {
39
+ const status = readFileSync("/proc/self/status", "utf8")
40
+ const match = /^VmHWM:\s+(\d+)\s+kB$/m.exec(status)
41
+ if (!match) return fallbackBytes
42
+ const kibibytes = Number(match[1])
43
+ return Number.isFinite(kibibytes) && kibibytes > 0 ? kibibytes * 1024 : fallbackBytes
44
+ } catch {
45
+ return fallbackBytes
46
+ }
47
+ }
48
+
23
49
  setRunnerProcessTitle()
24
50
 
25
51
  /** @type {Promise<void> | undefined} */
@@ -162,7 +188,7 @@ function sendChildAcceptance(type, payload, observedAtMs) {
162
188
  * child runs 25 concurrent jobs) distinguishes a V8 heap-growth leak from
163
189
  * external/array-buffer (native resource) growth, and the in-flight job ids
164
190
  * tie the observation to the work that was running.
165
- * @returns {{activeJobIds: string[], activeJobIdsTruncatedCount: number, childInstanceId: string, childPid: number, childUptimeMs: number, heapStatistics: ReturnType<typeof v8.getHeapStatistics>, jobCount: number, memoryUsage: ReturnType<typeof process.memoryUsage>, observedAtMs: number, rssBytes: number, type: "pooled-child-memory", uptimeMs: number}} - Bounded memory observation for this child.
191
+ * @returns {{activeJobIds: string[], activeJobIdsTruncatedCount: number, childInstanceId: string, childPid: number, childUptimeMs: number, heapStatistics: ReturnType<typeof v8.getHeapStatistics>, jobCount: number, memoryUsage: ReturnType<typeof process.memoryUsage>, observedAtMs: number, peakRssBytes: number, rssBytes: number, type: "pooled-child-memory", uptimeMs: number}} - Bounded memory observation for this child.
166
192
  */
167
193
  function collectMemoryObservation() {
168
194
  const jobIds = [...runningJobIds]
@@ -179,6 +205,7 @@ function collectMemoryObservation() {
179
205
  jobCount: jobIds.length,
180
206
  memoryUsage,
181
207
  observedAtMs: Date.now(),
208
+ peakRssBytes: readPeakRssBytes(),
182
209
  rssBytes: memoryUsage.rss,
183
210
  type: "pooled-child-memory",
184
211
  uptimeMs: Math.floor(process.uptime() * 1000)
@@ -289,6 +316,7 @@ function sendOutcome({jobId, acknowledged, status, error}) {
289
316
  jobId,
290
317
  acknowledged,
291
318
  status,
319
+ peakRssBytes: readPeakRssBytes(),
292
320
  rssBytes: process.memoryUsage().rss,
293
321
  error: error?.message
294
322
  }, () => resolve(undefined))
@@ -90,6 +90,7 @@
90
90
  * @property {number} jobCount - In-flight job count.
91
91
  * @property {ReturnType<typeof import("node:process").memoryUsage>} memoryUsage - Process memory breakdown at the sample.
92
92
  * @property {number} observedAtMs - Epoch ms the child sampled.
93
+ * @property {number} peakRssBytes - Child-reported peak RSS (kernel `VmHWM` high-water mark) in bytes, monotonic for the child's life.
93
94
  * @property {number} rssBytes - Resident set size in bytes at the sample.
94
95
  * @property {"pooled-child-memory"} type - Discriminator.
95
96
  * @property {number} uptimeMs - Process uptime in ms.
@@ -1404,7 +1404,7 @@ export default class BackgroundJobsWorker {
1404
1404
  */
1405
1405
  _handlePooledChildMessage({child, message}) {
1406
1406
  if (!message || typeof message !== "object") return
1407
- const record = /** @type {{type?: ReturnType<typeof JSON.parse>, childInstanceId?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
1407
+ const record = /** @type {{type?: ReturnType<typeof JSON.parse>, childInstanceId?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, peakRssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
1408
1408
  const state = this.pooledChildStates.get(child)
1409
1409
  if (record.type === "ready") {
1410
1410
  if (state) {
@@ -1458,8 +1458,13 @@ export default class BackgroundJobsWorker {
1458
1458
  }
1459
1459
 
1460
1460
  const rssBytes = typeof record.rssBytes === "number" ? record.rssBytes : Number.POSITIVE_INFINITY
1461
+ // The child's peak RSS (VmHWM) is monotonic and outlives the settled
1462
+ // sample: a ratcheted burst working-set must recycle the child even when
1463
+ // settled RSS at outcome time looks modest. A child without peak support
1464
+ // falls back to the settled sample.
1465
+ const peakRssBytes = typeof record.peakRssBytes === "number" ? record.peakRssBytes : rssBytes
1461
1466
  const runnerAgeMs = Date.now() - state.createdAtMs
1462
- if (!state.retiring && (state.jobsRun >= this.pooledRunnerMaxJobs || rssBytes >= this.pooledRunnerMaxRssBytes || runnerAgeMs >= this.pooledRunnerMaxLifetimeMs || this.shouldStop)) {
1467
+ if (!state.retiring && (state.jobsRun >= this.pooledRunnerMaxJobs || peakRssBytes >= this.pooledRunnerMaxRssBytes || runnerAgeMs >= this.pooledRunnerMaxLifetimeMs || this.shouldStop)) {
1463
1468
  this._beginRetirePooledChild(child)
1464
1469
  }
1465
1470
  this._terminateIfDrained(child)
@@ -1524,6 +1529,7 @@ export default class BackgroundJobsWorker {
1524
1529
  childPid: message.childPid,
1525
1530
  childUptimeS: Math.round(message.childUptimeMs / 1000),
1526
1531
  rssMb: Math.round(message.rssBytes / (1024 * 1024)),
1532
+ peakRssMb: Math.round((typeof message.peakRssBytes === "number" ? message.peakRssBytes : message.rssBytes) / (1024 * 1024)),
1527
1533
  heapUsedMb: Math.round(heap.used_heap_size / (1024 * 1024)),
1528
1534
  heapTotalMb: Math.round(heap.total_heap_size / (1024 * 1024)),
1529
1535
  heapLimitMb: Math.round(heap.heap_size_limit / (1024 * 1024)),
@@ -194,7 +194,7 @@
194
194
  * @property {number} [pooledRunnerCount] - Number of warm, reusable child runners owned by each worker. Pooled capacity is separate from inline and forked/spawned capacity. Default: `4`.
195
195
  * @property {number} [pooledRunnerConcurrency] - Number of jobs each pooled child runs concurrently on its own event loop. Total per-worker pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency`. `1` (default) keeps each child serial; raise it for I/O-bound jobs so a bounded set of isolated processes handles high concurrency (like the inline lane) without one process per concurrent job. Default: `1`.
196
196
  * @property {number} [pooledRunnerMaxJobs] - Number of sequential jobs a pooled child runs before it is replaced, bounding process-level resource accumulation. Default: `100`.
197
- * @property {number} [pooledRunnerMaxRssBytes] - RSS bytes after an acknowledged job at which a pooled child is replaced. Default: `536870912` (512 MiB).
197
+ * @property {number} [pooledRunnerMaxRssBytes] - Peak RSS (the kernel `VmHWM` high-water mark reported by the child) at which a pooled child is replaced after an acknowledged job. The child's peak, not its settled RSS at job-outcome time, is the trigger, so a burst working-set that reclaims before the outcome still retires the child. Default: `536870912` (512 MiB).
198
198
  * @property {number} [pooledRunnerMaxLifetimeMs] - Age after an acknowledged job at which a pooled child is replaced. Default: `3600000` (one hour).
199
199
  * @property {Record<string, {maxConcurrent?: number, priority?: number}>} [queues] - Per-queue
200
200
  * concurrency caps and dispatch priorities, Sidekiq-style. A job declares its queue (static