@vzn/vx-dte 0.0.652 → 0.0.653

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/src/usage.ts CHANGED
@@ -17,9 +17,19 @@ export interface UsageStats {
17
17
  readonly cpuAvg: number
18
18
  readonly memoryPeak: number
19
19
  readonly memoryAvg: number
20
- /** Core-time its tasks used over the core-time it had. */
20
+ /**
21
+ * Its machine's busy core-time over the core-time it had, from its first
22
+ * task's send to its last one's end: whether the run left cores idle. Its
23
+ * tasks' own CPU when its machine said none.
24
+ */
21
25
  readonly utilization: number
26
+ /** Its tasks' core-time over the same, and its worker process's (undefined when it said none). */
27
+ readonly tasksUtilization: number
28
+ readonly workerUtilization?: number | undefined
22
29
  readonly cpuMs: number
30
+ /** From its first task's send to its last one's end, and its busy core-time then. */
31
+ readonly workMs: number
32
+ readonly workCoreMs: number
23
33
  /** Cores the run held for its tasks, on average over its time online: what packing gave it. */
24
34
  readonly bookedAvg: number
25
35
  /**
@@ -30,6 +40,12 @@ export interface UsageStats {
30
40
  readonly machineAvg?: number | undefined
31
41
  }
32
42
 
43
+ interface Point {
44
+ readonly at: number
45
+ readonly busy: number
46
+ readonly self?: number
47
+ }
48
+
33
49
  export class Usage {
34
50
  done = 0
35
51
  failed = 0
@@ -48,8 +64,23 @@ export class Usage {
48
64
  private bookedAt = 0
49
65
  /** Its machine's busy cores on average, as its last frame said. */
50
66
  machineAvg: number | undefined
67
+ /** Its machine's busy core-time since it joined, as its last frame said, and when. */
68
+ private heard: Point
69
+ private first: Point | undefined
70
+ private last: Point | undefined
71
+ private firstAt: number | undefined
72
+ private lastAt: number | undefined
51
73
 
52
- constructor(readonly from: number = performance.now()) {}
74
+ constructor(readonly from: number = performance.now()) {
75
+ this.heard = { at: from, busy: 0, self: 0 }
76
+ }
77
+
78
+ /** Its machine said `avg` cores busy on average since it started, its worker process `self`. */
79
+ machine(avg: number, self: number | undefined, at: number = performance.now()): void {
80
+ this.machineAvg = avg
81
+ const ms = Math.max(0, at - this.from)
82
+ this.heard = { at, busy: avg * ms, ...(self === undefined ? {} : { self: self * ms }) }
83
+ }
53
84
 
54
85
  /**
55
86
  * A task it finished, as it ends: on it `from` its send (else its
@@ -73,6 +104,8 @@ export class Usage {
73
104
  this.aborted++
74
105
  else this.failed++
75
106
  const end = performance.now()
107
+ this.lastAt = end
108
+ this.last = this.heard
76
109
  const ms = Math.max(1, from === undefined ? r.durationMs : end - from)
77
110
  this.spans.push({
78
111
  start: end - ms,
@@ -82,6 +115,13 @@ export class Usage {
82
115
  })
83
116
  }
84
117
 
118
+ /** The run sent it a task at `at`: its first send opens the window utilization reads. */
119
+ sent(at: number = performance.now()): void {
120
+ if (this.firstAt !== undefined) return
121
+ this.firstAt = at
122
+ this.first = this.heard
123
+ }
124
+
85
125
  /** The run now holds `cores` more of it for its tasks (fewer when negative). */
86
126
  book(cores: number, at: number = performance.now()): void {
87
127
  this.bookedMs += this.booked * (at - this.bookedAt)
@@ -119,6 +159,24 @@ export class Usage {
119
159
  cpuMs += s.cores * (s.end - s.start)
120
160
  memoryMs += s.memory * (s.end - s.start)
121
161
  }
162
+ const workMs =
163
+ this.firstAt !== undefined && this.lastAt !== undefined && this.lastAt > this.firstAt
164
+ ? this.lastAt - this.firstAt
165
+ : onlineMs
166
+ const f = this.first
167
+ const l = this.last
168
+ // Between the frames nearest its first send and its last end: an avg
169
+ // since it started says nothing of a window inside that.
170
+ const framed = f !== undefined && l !== undefined && l.at > f.at
171
+ const tasksUtilization = Math.min(1, cpuMs / (cores * workMs))
172
+ const utilization =
173
+ framed && this.machineAvg !== undefined
174
+ ? Math.min(1, (l.busy - f.busy) / (cores * (l.at - f.at)))
175
+ : tasksUtilization
176
+ const workerUtilization =
177
+ framed && l.self !== undefined && f.self !== undefined
178
+ ? Math.min(1, (l.self - f.self) / (cores * (l.at - f.at)))
179
+ : undefined
122
180
  return {
123
181
  onlineMs,
124
182
  busyMs,
@@ -126,8 +184,12 @@ export class Usage {
126
184
  cpuAvg: cpuMs / onlineMs,
127
185
  memoryPeak,
128
186
  memoryAvg: memoryMs / onlineMs,
129
- utilization: cpuMs / (cores * onlineMs),
187
+ utilization,
188
+ tasksUtilization,
189
+ ...(workerUtilization === undefined ? {} : { workerUtilization }),
130
190
  cpuMs,
191
+ workMs,
192
+ workCoreMs: utilization * cores * workMs,
131
193
  bookedAvg: (this.bookedMs + this.booked * Math.max(0, end - this.bookedAt)) / onlineMs,
132
194
  machineAvg: this.machineAvg,
133
195
  }
package/src/warm-ups.ts CHANGED
@@ -56,14 +56,24 @@ async function commits(root: string): Promise<string[]> {
56
56
  }
57
57
 
58
58
  /** The newest warm-ups an ancestor's run wrote, the base branch's on a scoped run. */
59
- export async function readWarmUps(cache: PluginCache, root: string): Promise<Map<string, number>> {
60
- const keys = (await commits(root)).map(keyOf)
59
+ export function readWarmUps(cache: PluginCache, root: string): Promise<Map<string, number>> {
60
+ return newest(cache, root, keyOf, parseWarmUps)
61
+ }
62
+
63
+ /** The newest blob under an ancestor's `key` that `parse` reads; an empty map for none. */
64
+ async function newest(
65
+ cache: PluginCache,
66
+ root: string,
67
+ key: (sha: string) => string,
68
+ parse: (bytes: Uint8Array) => Map<string, number> | null,
69
+ ): Promise<Map<string, number>> {
70
+ const keys = (await commits(root)).map(key)
61
71
  if (keys.length === 0) return new Map()
62
72
  const held = await cache.hasMany(keys, false)
63
- for (const key of keys) {
64
- if (held !== null && !held.has(key)) continue
65
- const bytes = await cache.get(key, false)
66
- const got = bytes === null ? null : parseWarmUps(bytes)
73
+ for (const k of keys) {
74
+ if (held !== null && !held.has(k)) continue
75
+ const bytes = await cache.get(k, false)
76
+ const got = bytes === null ? null : parse(bytes)
67
77
  if (got !== null) return got
68
78
  }
69
79
  return new Map()
@@ -83,3 +93,59 @@ export async function writeWarmUps(
83
93
  for (const [name, ms] of [...learned, ...run]) sizes[name] = Math.round(ms)
84
94
  await cache.put(keyOf(head), new TextEncoder().encode(JSON.stringify({ version: 1, sizes })))
85
95
  }
96
+
97
+ // Each server's base, the cores it uses with no dependant's work
98
+ // (`server-fit.ts`): booked once on each worker that starts it. Kept as
99
+ // the warm-ups are, under each CI run's commit; untrusted, it only books.
100
+
101
+ const MAX_SERVERS = 1024
102
+ /** More cores than any machine a run books. */
103
+ const MAX_CORES = 4096
104
+
105
+ const basesKeyOf = (sha: string): string => `server-bases\0${sha}`
106
+
107
+ /** `{ version: 1, servers: { id: cores } }`'s bases, each finite in [0, MAX_CORES]; null for anything else. */
108
+ export function parseServerBases(bytes: Uint8Array): Map<string, number> | null {
109
+ if (bytes.byteLength > MAX_BYTES) return null
110
+ let parsed: unknown
111
+ try {
112
+ parsed = JSON.parse(new TextDecoder().decode(bytes))
113
+ // quiet: not JSON reads as no blob
114
+ } catch {
115
+ return null
116
+ }
117
+ if (typeof parsed !== 'object' || parsed === null) return null
118
+ const { version, servers } = parsed as { version?: unknown; servers?: unknown }
119
+ if (version !== 1 || typeof servers !== 'object' || servers === null || Array.isArray(servers))
120
+ return null
121
+ const entries = Object.entries(servers)
122
+ if (entries.length > MAX_SERVERS) return null
123
+ const out = new Map<string, number>()
124
+ for (const [id, cores] of entries)
125
+ if (typeof cores === 'number' && Number.isFinite(cores) && cores >= 0 && cores <= MAX_CORES)
126
+ out.set(id, cores)
127
+ return out
128
+ }
129
+
130
+ /** The newest server bases an ancestor's run wrote. */
131
+ export function readServerBases(cache: PluginCache, root: string): Promise<Map<string, number>> {
132
+ return newest(cache, root, basesKeyOf, parseServerBases)
133
+ }
134
+
135
+ /** `learned` with this run's own over it, under HEAD. */
136
+ export async function writeServerBases(
137
+ cache: PluginCache,
138
+ root: string,
139
+ learned: ReadonlyMap<string, number>,
140
+ run: ReadonlyMap<string, number>,
141
+ ): Promise<void> {
142
+ if (run.size === 0) return
143
+ const [head] = await commits(root)
144
+ if (head === undefined) return
145
+ const servers: Record<string, number> = {}
146
+ for (const [id, cores] of [...learned, ...run]) servers[id] = Math.round(cores * 100) / 100
147
+ await cache.put(
148
+ basesKeyOf(head),
149
+ new TextEncoder().encode(JSON.stringify({ version: 1, servers })),
150
+ )
151
+ }
package/src/wire.ts CHANGED
@@ -107,48 +107,70 @@ const answerMs = (heartbeatMs: number): number =>
107
107
  Math.min(Math.max(heartbeatMs, MIN_WAIT_MS), REPLY_MS)
108
108
 
109
109
  /**
110
- * A send as it travels: numbered per receiver, so a resend is dropped as
111
- * seen, and carrying the sender's ack of what it took from that receiver,
112
- * so most acks cost no message of their own.
110
+ * A send as it travels: numbered (`n`) per receiver, so a resend is
111
+ * dropped as seen, its body `b`, and carrying the sender's ack (`a`) of
112
+ * what it took from that receiver, so most acks cost no message of their
113
+ * own. One letter each: they travel in every frame, twice.
113
114
  */
114
115
  interface Numbered {
115
- readonly seq: number
116
- readonly body: unknown
117
- readonly acked?: number
116
+ readonly n: number
117
+ readonly b: unknown
118
+ readonly a?: number
118
119
  }
119
- /** The receiver took every send to it numbered up to `acked`. */
120
+ /** The receiver took every send to it numbered up to `a`. */
120
121
  interface Acked {
121
- readonly acked: number
122
+ readonly a: number
122
123
  }
123
124
  const isNumbered = (b: unknown): b is Numbered =>
124
125
  typeof b === 'object' &&
125
126
  b !== null &&
126
- typeof (b as Numbered).seq === 'number' &&
127
- 'body' in b &&
128
- Object.keys(b).length === ('acked' in b ? 3 : 2)
127
+ typeof (b as Numbered).n === 'number' &&
128
+ 'b' in b &&
129
+ Object.keys(b).length === ('a' in b ? 3 : 2)
129
130
  /**
130
131
  * What a side with nothing else to say a heartbeat sends: a numbered send
131
132
  * with no body, carrying its acks, delivered to no one.
132
133
  */
133
134
  interface Beat {
134
- readonly seq: number
135
- readonly acked?: number
135
+ readonly n: number
136
+ readonly a?: number
136
137
  }
137
138
  const isBeat = (b: unknown): b is Beat =>
138
139
  typeof b === 'object' &&
139
140
  b !== null &&
140
- typeof (b as Beat).seq === 'number' &&
141
- !('body' in b) &&
142
- Object.keys(b).length === ('acked' in b ? 2 : 1)
141
+ typeof (b as Beat).n === 'number' &&
142
+ !('b' in b) &&
143
+ Object.keys(b).length === ('a' in b ? 2 : 1)
143
144
  const isAcked = (b: unknown): b is Acked =>
144
145
  typeof b === 'object' &&
145
146
  b !== null &&
146
- typeof (b as Acked).acked === 'number' &&
147
+ typeof (b as Acked).a === 'number' &&
147
148
  Object.keys(b).length === 1
148
149
 
150
+ /** The last of `paced`'s tries failed: `cause` is that try's error. */
151
+ class Exhausted extends Error {
152
+ constructor(
153
+ override readonly cause: unknown,
154
+ readonly tries: number,
155
+ readonly ms: number,
156
+ ) {
157
+ super(cause instanceof Error ? cause.message : String(cause))
158
+ }
159
+ }
160
+
161
+ /**
162
+ * The wait before try `tries + 2`: doubling from an eighth of `REPLY_MS`,
163
+ * jittered down to half, so `RETRIES` waits span 4.4 to 8.75 s of it. A
164
+ * fault that fails at once (a refused or unresolved host) spent four tries
165
+ * in about 2 s under a 250 ms start (PR #5082's CI, 2026-11-07).
166
+ */
167
+ const pause = (tries: number): number =>
168
+ ((REPLY_MS / 2 ** RETRIES) * 2 ** tries * (1 + Math.random())) / 2
169
+
149
170
  /** Runs `f` again after a fault, at most `RETRIES` times and within `REPLY_MS`. */
150
171
  async function paced<T>(f: () => Promise<T>): Promise<T> {
151
- const by = performance.now() + REPLY_MS
172
+ const at = performance.now()
173
+ const by = at + REPLY_MS
152
174
  for (let tries = 0; ; tries++) {
153
175
  try {
154
176
  return await f()
@@ -156,9 +178,10 @@ async function paced<T>(f: () => Promise<T>): Promise<T> {
156
178
  // A request the relay held past its deadline already took the whole wait.
157
179
  const fault =
158
180
  err instanceof RelayError ? err.status >= 500 : (err as Error).name !== 'TimeoutError'
159
- const wait = Math.random() * FIRST_REDIAL_MS * 2 ** tries
160
- if (!fault || tries >= RETRIES) throw err
161
- if (performance.now() + wait >= by) throw err
181
+ if (!fault) throw err
182
+ const wait = pause(tries)
183
+ if (tries >= RETRIES || performance.now() + wait >= by)
184
+ throw new Exhausted(err, tries + 1, performance.now() - at)
162
185
  await Bun.sleep(wait)
163
186
  }
164
187
  }
@@ -285,7 +308,7 @@ export async function join(
285
308
  owing.delete(peer)
286
309
  said.set(peer, performance.now())
287
310
  // quiet: a lost ack only costs a resend, which is dropped as seen
288
- if (upTo !== undefined) put(peer, { acked: upTo } satisfies Acked).catch(() => undefined)
311
+ if (upTo !== undefined) put(peer, { a: upTo } satisfies Acked).catch(() => undefined)
289
312
  }
290
313
  const flushAcks = (): void => {
291
314
  acking = undefined
@@ -318,9 +341,9 @@ export async function join(
318
341
  const said = new Map<string, number>()
319
342
  let onQuiet: ((peer: string) => void) | undefined
320
343
  /** The ack owed `to`, if any, for a send to it to carry; and `to` was just said to. */
321
- const owed = (to: string): { acked?: number } => {
344
+ const owed = (to: string): { a?: number } => {
322
345
  said.set(to, performance.now())
323
- return owing.delete(to) ? { acked: taken.get(to)!.upTo } : {}
346
+ return owing.delete(to) ? { a: taken.get(to)!.upTo } : {}
324
347
  }
325
348
  /**
326
349
  * When each peer was last heard from: the orchestrator's workers from
@@ -363,12 +386,12 @@ export async function join(
363
386
  }
364
387
  const from = heldIds.get(m.from) ?? m.from
365
388
  if (heard.has(from)) heard.set(from, performance.now())
366
- if (isAcked(body)) settle(from, body.acked)
389
+ if (isAcked(body)) settle(from, body.a)
367
390
  else if (isBeat(body)) {
368
- if (body.acked !== undefined) settle(from, body.acked)
391
+ if (body.a !== undefined) settle(from, body.a)
369
392
  const t = taken.get(from) ?? { upTo: 0, ahead: new Map() }
370
393
  taken.set(from, t)
371
- if (body.seq > t.upTo) t.ahead.set(body.seq, BEAT)
394
+ if (body.n > t.upTo) t.ahead.set(body.n, BEAT)
372
395
  while (t.ahead.has(t.upTo + 1)) {
373
396
  const next = t.ahead.get(++t.upTo)
374
397
  t.ahead.delete(t.upTo)
@@ -379,13 +402,13 @@ export async function join(
379
402
  if (role === 'orchestrator') {
380
403
  owing.delete(from)
381
404
  // quiet: a lost ack only costs a resend, which is dropped as seen
382
- put(from, { acked: t.upTo } satisfies Acked).catch(() => undefined)
405
+ put(from, { a: t.upTo } satisfies Acked).catch(() => undefined)
383
406
  } else owe(from)
384
407
  } else if (isNumbered(body)) {
385
- if (body.acked !== undefined) settle(from, body.acked)
408
+ if (body.a !== undefined) settle(from, body.a)
386
409
  let t = taken.get(from)
387
410
  if (t === undefined) taken.set(from, (t = { upTo: 0, ahead: new Map() }))
388
- if (body.seq > t.upTo) t.ahead.set(body.seq, body.body)
411
+ if (body.n > t.upTo) t.ahead.set(body.n, body.b)
389
412
  while (t.ahead.has(t.upTo + 1)) {
390
413
  const next = t.ahead.get(++t.upTo)
391
414
  t.ahead.delete(t.upTo)
@@ -462,7 +485,7 @@ export async function join(
462
485
  const seq = ++o.next
463
486
  o.unacked.set(seq, { body: BEAT, at: now })
464
487
  // quiet: unacknowledged, it goes again
465
- put(peer, { seq, ...owed(peer) } satisfies Beat).catch(() => undefined)
488
+ put(peer, { n: seq, ...owed(peer) } satisfies Beat).catch(() => undefined)
466
489
  }
467
490
  }
468
491
  /** Sends again what waited `ms` unacknowledged, to `only` or to all; all of it, at 0. */
@@ -474,7 +497,7 @@ export async function join(
474
497
  if (now - u.at < ms) continue
475
498
  u.at = now
476
499
  // quiet: unacknowledged, it goes again next heartbeat
477
- const again = u.body === BEAT ? { seq } : { seq, body: u.body }
500
+ const again = u.body === BEAT ? { n: seq } : { n: seq, b: u.body }
478
501
  put(to, { ...again, ...owed(to) }).catch(() => undefined)
479
502
  }
480
503
  }
@@ -494,16 +517,19 @@ export async function join(
494
517
  return { tokenless }
495
518
  }
496
519
  const s = await connect(base, query, h)
497
- // A fault: dialed again within `REPLY_MS`.
498
520
  if (s === undefined) throw new Error('no socket')
499
521
  return { s }
500
522
  })
501
523
  } catch (err) {
502
524
  if (err instanceof RelayError) throw err
503
- throw new RelayError(
504
- 504,
505
- `cannot connect to the relay ${new URL(base).origin}: no answer in ${REPLY_MS / 1000} s`,
506
- )
525
+ const cause = err instanceof Exhausted ? err.cause : err
526
+ if (cause instanceof RelayError) throw cause
527
+ // Said as it was: a wait only when one was waited out.
528
+ const why =
529
+ err instanceof Exhausted && (cause as Error).name !== 'TimeoutError'
530
+ ? `${err.message}, ${err.tries} dials in ${(err.ms / 1000).toFixed(1)} s`
531
+ : `no answer in ${REPLY_MS / 1000} s`
532
+ throw new RelayError(504, `cannot connect to the relay ${new URL(base).origin}: ${why}`)
507
533
  }
508
534
  // A token it cannot get is said as itself, not as a relay that never
509
535
  // answered: the relay was not asked.
@@ -701,7 +727,7 @@ export async function join(
701
727
  }
702
728
  try {
703
729
  if (s?.open !== true) throw ended!
704
- await s.send(relayIds.get(to) ?? to, { seq, body, ...owed(to) } satisfies Numbered)
730
+ await s.send(relayIds.get(to) ?? to, { n: seq, b: body, ...owed(to) } satisfies Numbered)
705
731
  } catch (err) {
706
732
  // Closed under it: it goes again on the next socket.
707
733
  if (!(err instanceof RelayError)) return
@@ -867,8 +893,14 @@ async function connect(
867
893
  // Its handlers are set before it opens: a relay that refuses closes at
868
894
  // once, and Bun may report that close, or its frame, with the open.
869
895
  let opening: (ok: boolean) => void = () => {}
896
+ /** Why the dial failed, as the platform said it (`ECONNREFUSED`, `ENOTFOUND`). */
897
+ let failure = ''
898
+ let waited = false
870
899
  const opened = new Promise<boolean>((resolve) => {
871
- const timer = setTimeout(() => resolve(false), wait)
900
+ const timer = setTimeout(() => {
901
+ waited = true
902
+ resolve(false)
903
+ }, wait)
872
904
  opening = (ok) => {
873
905
  clearTimeout(timer)
874
906
  resolve(ok)
@@ -991,7 +1023,9 @@ async function connect(
991
1023
  shut()
992
1024
  opening(false)
993
1025
  }
994
- ws.onerror = () => {
1026
+ ws.onerror = (e) => {
1027
+ const err = (e as ErrorEvent).error as { code?: unknown } | undefined
1028
+ failure ||= typeof err?.code === 'string' ? err.code : ((e as ErrorEvent).message ?? '')
995
1029
  closed()
996
1030
  opening(false)
997
1031
  }
@@ -1079,13 +1113,21 @@ async function connect(
1079
1113
  if (heartbeatMs !== undefined) return undefined
1080
1114
  }
1081
1115
  if (heartbeatMs === undefined) {
1082
- const timer = setTimeout(() => said(undefined), wait)
1116
+ const timer = setTimeout(() => {
1117
+ waited = true
1118
+ said(undefined)
1119
+ }, wait)
1083
1120
  const j = await joined
1084
1121
  clearTimeout(timer)
1085
1122
  if (j === undefined) {
1086
1123
  await me.close()
1087
1124
  const status = refusal(code)
1088
- if (status === undefined) return undefined
1125
+ if (status === undefined) {
1126
+ // A fault, dialed again: its words say why, or that it was waited out.
1127
+ const err = new Error(printable(failure || reason || 'socket closed'))
1128
+ if (waited) err.name = 'TimeoutError'
1129
+ throw err
1130
+ }
1089
1131
  // A GitLab job with no `id_tokens` entry was told only `401 no bearer
1090
1132
  // token`, not what gives it one (2026-10-26).
1091
1133
  const hint =