@vzn/vx-dte 0.0.651 → 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/README.md +164 -60
- package/package.json +3 -3
- package/src/dispatch.ts +540 -30
- package/src/forecast.ts +1 -2
- package/src/github.ts +0 -3
- package/src/index.ts +1 -0
- package/src/init.ts +9 -10
- package/src/local.ts +112 -28
- package/src/messages.ts +26 -5
- package/src/orchestrator.ts +678 -233
- package/src/planner.ts +1470 -192
- package/src/plugin.ts +242 -33
- package/src/seal.ts +159 -43
- package/src/server-fit.ts +80 -0
- package/src/tree.ts +215 -7
- package/src/usage.ts +65 -3
- package/src/warm-ups.ts +72 -6
- package/src/wire.ts +119 -49
- package/src/worker.ts +174 -19
package/src/plugin.ts
CHANGED
|
@@ -20,9 +20,15 @@ import type { Need } from './dispatch.js'
|
|
|
20
20
|
import { booked, DEFAULT_STRATEGY, STRATEGIES, type Strategy } from './strategy.js'
|
|
21
21
|
import type { Orchestrator } from './orchestrator.js'
|
|
22
22
|
import type { Proof } from './seal.js'
|
|
23
|
-
import type { Size } from './planner.js'
|
|
23
|
+
import type { Forecast, Size } from './planner.js'
|
|
24
24
|
|
|
25
|
-
/** One
|
|
25
|
+
/** One worker type: the machines of it the adapter can start, by name. */
|
|
26
|
+
export interface WorkerType {
|
|
27
|
+
/** Its machines by name: each a size the adapter may start, named `<type>.<machine>`. */
|
|
28
|
+
readonly machines: Readonly<Record<string, WorkerSize>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One kind of machine the adapter can start. */
|
|
26
32
|
export interface WorkerSize {
|
|
27
33
|
/** How many of it one run may start; absent, as many as the work needs. */
|
|
28
34
|
readonly max?: number
|
|
@@ -32,6 +38,8 @@ export interface WorkerSize {
|
|
|
32
38
|
readonly memory: number | string
|
|
33
39
|
/** Its machines' platform, which a joining worker must report; absent, planned as this machine's and not checked. A task naming `exec.platforms` runs only on a worker of one of them. */
|
|
34
40
|
readonly platform?: Platform
|
|
41
|
+
/** The CI runner label its workers run on, which `vx dte init` writes (`github()`); absent, its platform's GitHub-hosted runner. */
|
|
42
|
+
readonly runsOn?: string
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/** What an adapter is asked to start: one worker of `size`. */
|
|
@@ -103,8 +111,13 @@ export interface DteAdapter {
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
export interface DteOptions {
|
|
106
|
-
/**
|
|
107
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The worker types, in order of preference: a task naming `exec.worker`
|
|
116
|
+
* runs only on that type's machines, any other on the first type with
|
|
117
|
+
* a machine of one of its platforms. Each machine is a size the adapter
|
|
118
|
+
* may start, named `<type>.<machine>` (`ubuntu.small`).
|
|
119
|
+
*/
|
|
120
|
+
readonly workers?: Readonly<Record<string, WorkerType>>
|
|
108
121
|
/** Starts workers; absent, the run uses the workers that join it, which needs `relay`. */
|
|
109
122
|
readonly adapter?: DteAdapter
|
|
110
123
|
/** The relay's URL; absent, the adapter's own. */
|
|
@@ -139,12 +152,14 @@ const KEYS: PluginOptionKinds<DteOptions> = {
|
|
|
139
152
|
headers: 'object',
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
const TYPE_KEYS: PluginOptionKinds<WorkerType> = { machines: 'object' }
|
|
142
156
|
// A typo in a size or in `unmeasured` (`maxx`) was dropped, and the size ran uncapped.
|
|
143
157
|
const SIZE_KEYS: PluginOptionKinds<WorkerSize> = {
|
|
144
158
|
max: 'number',
|
|
145
159
|
cpu: 'number',
|
|
146
160
|
memory: 'any',
|
|
147
161
|
platform: 'string',
|
|
162
|
+
runsOn: 'string',
|
|
148
163
|
}
|
|
149
164
|
const UNMEASURED_KEYS: PluginOptionKinds<NonNullable<DteOptions['unmeasured']>> = {
|
|
150
165
|
cpu: 'number',
|
|
@@ -189,8 +204,19 @@ export function dte(options: DteOptions = {}) {
|
|
|
189
204
|
refuse(`scheduler.${layer}`, `one of ${choices.join(', ')}`, choice)
|
|
190
205
|
;(how as unknown as Record<string, unknown>)[layer] = choice
|
|
191
206
|
}
|
|
192
|
-
const
|
|
193
|
-
|
|
207
|
+
const declared = Object.entries(options.workers ?? {}).flatMap(([type, t]) => {
|
|
208
|
+
refuseUnknownOptions(`dte() option "workers.${type}"`, t, TYPE_KEYS)
|
|
209
|
+
const machines = Object.entries(t?.machines ?? {})
|
|
210
|
+
if (machines.length === 0)
|
|
211
|
+
refuse(
|
|
212
|
+
`workers.${type}.machines`,
|
|
213
|
+
"machines by name, such as small: { cpu: 4, memory: '8GB' }",
|
|
214
|
+
t?.machines,
|
|
215
|
+
)
|
|
216
|
+
return machines.map(([machine, s]) => ({ type, name: `${type}.${machine}`, s }))
|
|
217
|
+
})
|
|
218
|
+
const sized = declared.map(({ type, name, s }) => {
|
|
219
|
+
const at = `workers.${type}.machines.${name.slice(type.length + 1)}`
|
|
194
220
|
refuseUnknownOptions(`dte() option "${at}"`, s, SIZE_KEYS)
|
|
195
221
|
if (!(Number.isInteger(s?.cpu) && s.cpu >= 1)) refuse(`${at}.cpu`, 'a whole number ≥ 1', s?.cpu)
|
|
196
222
|
const memory = bytes(s.memory)
|
|
@@ -198,23 +224,62 @@ export function dte(options: DteOptions = {}) {
|
|
|
198
224
|
refuse(`${at}.memory`, "bytes above 0 or a size such as '8GB'", s.memory)
|
|
199
225
|
if (!(s.max === undefined || (Number.isInteger(s.max) && s.max >= 0)))
|
|
200
226
|
refuse(`${at}.max`, 'a whole number ≥ 0', s.max)
|
|
227
|
+
if (s.runsOn !== undefined && s.runsOn.trim() === '')
|
|
228
|
+
refuse(`${at}.runsOn`, 'a runner label', s.runsOn)
|
|
201
229
|
if (s.platform !== undefined && !PLATFORMS.includes(s.platform))
|
|
202
230
|
refuse(`${at}.platform`, `one of ${PLATFORMS.join(', ')}`, s.platform)
|
|
203
231
|
return {
|
|
232
|
+
type,
|
|
204
233
|
name,
|
|
205
234
|
max: s.max ?? Infinity,
|
|
206
235
|
cpu: s.cpu,
|
|
207
236
|
memory: memory!,
|
|
208
237
|
platform: s.platform ?? hostPlatform(),
|
|
238
|
+
named: s.platform,
|
|
239
|
+
runsOn: s.runsOn,
|
|
209
240
|
}
|
|
210
241
|
})
|
|
242
|
+
const types = [...new Set(sized.map((s) => s.type))]
|
|
243
|
+
const typeOf = new Map(sized.map((s) => [s.name, s.type]))
|
|
244
|
+
// With one type every worker is of it; with more, a worker's type rides
|
|
245
|
+
// its platform as `<type>:<platform>`, which every match compares whole.
|
|
246
|
+
const multi = types.length > 1
|
|
247
|
+
const sizes: Size[] = sized.map((s) => ({
|
|
248
|
+
name: s.name,
|
|
249
|
+
max: s.max,
|
|
250
|
+
cpu: s.cpu,
|
|
251
|
+
memory: s.memory,
|
|
252
|
+
platform: multi ? `${s.type}:${s.platform}` : s.platform,
|
|
253
|
+
}))
|
|
254
|
+
const host = hostPlatform()
|
|
255
|
+
/** The platform a task of `type` naming none runs on: this machine's if the type has it, else its first. */
|
|
256
|
+
const homeOfType = (type: string): string => {
|
|
257
|
+
const of = sized.filter((s) => s.type === type).map((s) => s.platform)
|
|
258
|
+
return of.includes(host) ? host : of[0]!
|
|
259
|
+
}
|
|
260
|
+
/** Where a task runs, as workers' platforms read: its type's place, or as one type's run reads it. */
|
|
261
|
+
const placeOf = (worker: string | undefined, platform: string | undefined): string | undefined =>
|
|
262
|
+
!multi || worker === undefined
|
|
263
|
+
? (platform ?? home)
|
|
264
|
+
: `${worker}:${platform ?? homeOfType(worker)}`
|
|
265
|
+
/** A worker's place: its size's type, else (one that joined on its own) the first type of its platform. */
|
|
266
|
+
const joinedPlace = (
|
|
267
|
+
size: string | undefined,
|
|
268
|
+
platform: string | undefined,
|
|
269
|
+
): string | undefined => {
|
|
270
|
+
if (!multi || platform === undefined) return platform
|
|
271
|
+
const type =
|
|
272
|
+
(size === undefined ? undefined : typeOf.get(size)) ??
|
|
273
|
+
sized.find((s) => s.platform === platform)?.type
|
|
274
|
+
return type === undefined ? platform : `${type}:${platform}`
|
|
275
|
+
}
|
|
211
276
|
// Only a platform the size names refuses a worker reporting another: an
|
|
212
277
|
// unnamed one is planned as this machine's, and takes whatever joins.
|
|
213
278
|
const named = (size: string): { platform?: Platform } => {
|
|
214
|
-
const platform =
|
|
279
|
+
const platform = sized.find((s) => s.name === size)?.named
|
|
215
280
|
return platform === undefined ? {} : { platform }
|
|
216
281
|
}
|
|
217
|
-
const home = homeOf(sizes,
|
|
282
|
+
const home = multi ? undefined : homeOf(sizes, host)
|
|
218
283
|
const unmeasured = unmeasuredOf(options.unmeasured)
|
|
219
284
|
const adapter = options.adapter
|
|
220
285
|
// An adapter's run in its own CI with a relay names itself: the CI file
|
|
@@ -234,7 +299,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
234
299
|
// With none, no worker was ever asked for and the run waited until killed.
|
|
235
300
|
if (adapter !== undefined && !sizes.some((s) => s.max > 0)) {
|
|
236
301
|
throw new UserError(
|
|
237
|
-
"dte(): workers names no
|
|
302
|
+
"dte(): workers names no machine the adapter may start (one with max above 0), such as linux: { machines: { small: { cpu: 2, memory: '8GB' } } }",
|
|
238
303
|
)
|
|
239
304
|
}
|
|
240
305
|
if (adapter === undefined && options.relay === undefined) {
|
|
@@ -246,6 +311,12 @@ export function dte(options: DteOptions = {}) {
|
|
|
246
311
|
`dte() option "relay" must be an http(s) URL, got ${JSON.stringify(shown(options.relay))}`,
|
|
247
312
|
)
|
|
248
313
|
}
|
|
314
|
+
const unrouted = options.relay === undefined ? undefined : unroutable(options.relay)
|
|
315
|
+
if (unrouted !== undefined) {
|
|
316
|
+
throw new UserError(
|
|
317
|
+
`dte() option "relay" ${unrouted}, got ${JSON.stringify(shown(options.relay!))}`,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
249
320
|
// On GitHub Actions with none given, one for the relay's URL.
|
|
250
321
|
const token = async (relay: string): Promise<string | undefined> =>
|
|
251
322
|
(await options.token?.()) ??
|
|
@@ -261,6 +332,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
261
332
|
let peaks: ReadonlyMap<string, TaskResources> = new Map()
|
|
262
333
|
/** Each size's warm-up an earlier run learned (`warm-ups.ts`), and the read of it. */
|
|
263
334
|
let learned: ReadonlyMap<string, number> = new Map()
|
|
335
|
+
/** Each server's base an earlier run learned, then this run's workers' over it (`server-fit.ts`). */
|
|
336
|
+
let bases = new Map<string, number>()
|
|
337
|
+
/** The bases as read, before this run's: kept with this run's over them. */
|
|
338
|
+
let basesRead: ReadonlyMap<string, number> = new Map()
|
|
264
339
|
let warming: Promise<void> = Promise.resolve()
|
|
265
340
|
/** On CI, where the run keeps the warm-ups it learned: `setup`'s cache, root and warn. */
|
|
266
341
|
let keep: { cache: PluginCache; root: string; warn: (m: string) => void } | undefined
|
|
@@ -275,6 +350,11 @@ export function dte(options: DteOptions = {}) {
|
|
|
275
350
|
// Each finished task's cache key, a hit's too: what a worker restores a
|
|
276
351
|
// dependency by.
|
|
277
352
|
const keys = new Map<string, string>()
|
|
353
|
+
// Tasks that run a command and have not ended, by the platform they name
|
|
354
|
+
// ('' for none): a worker the run cannot start again stays while its
|
|
355
|
+
// platform has one (`keepLast`).
|
|
356
|
+
const leftOf = new Map<string, number>()
|
|
357
|
+
const ended = new Set<string>()
|
|
278
358
|
let orchestrator: Orchestrator | undefined
|
|
279
359
|
let closing: Promise<void> | undefined
|
|
280
360
|
|
|
@@ -292,7 +372,18 @@ export function dte(options: DteOptions = {}) {
|
|
|
292
372
|
k.warn(`[vx] dte: warm-ups not kept: ${err instanceof Error ? err.message : String(err)}`),
|
|
293
373
|
)
|
|
294
374
|
}
|
|
375
|
+
if (k !== undefined && o !== undefined) {
|
|
376
|
+
const fitted = new Map([...bases].filter(([id, c]) => basesRead.get(id) !== c))
|
|
377
|
+
const { writeServerBases } = await import('./warm-ups.js')
|
|
378
|
+
await writeServerBases(k.cache, k.root, basesRead, fitted).catch((err: unknown) =>
|
|
379
|
+
k.warn(
|
|
380
|
+
`[vx] dte: server bases not kept: ${err instanceof Error ? err.message : String(err)}`,
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
}
|
|
295
384
|
learned = new Map()
|
|
385
|
+
bases = new Map()
|
|
386
|
+
basesRead = new Map()
|
|
296
387
|
warming = Promise.resolve()
|
|
297
388
|
peaks = new Map()
|
|
298
389
|
tails = undefined
|
|
@@ -337,10 +428,11 @@ export function dte(options: DteOptions = {}) {
|
|
|
337
428
|
workspaceRoot: root,
|
|
338
429
|
dir: git('--show-prefix').replace(/\/$/, ''),
|
|
339
430
|
...(options.relay !== undefined ? { relay: options.relay } : {}),
|
|
340
|
-
sizes:
|
|
431
|
+
sizes: sized.map((s) => ({
|
|
341
432
|
name: s.name,
|
|
342
433
|
...(s.max === Infinity ? {} : { max: s.max }),
|
|
343
|
-
...
|
|
434
|
+
...(s.named === undefined ? {} : { platform: s.named }),
|
|
435
|
+
...(s.runsOn === undefined ? {} : { runsOn: s.runsOn }),
|
|
344
436
|
})),
|
|
345
437
|
manager: managerOf(root),
|
|
346
438
|
tasks: askTasks(),
|
|
@@ -388,6 +480,9 @@ export function dte(options: DteOptions = {}) {
|
|
|
388
480
|
}
|
|
389
481
|
if (!/^https?:\/\//.test(relay))
|
|
390
482
|
throw new UserError(`vx dte worker: VX_DTE_RELAY must be http(s), got ${shown(relay)}`)
|
|
483
|
+
const unroutedEnv = unroutable(relay)
|
|
484
|
+
if (unroutedEnv !== undefined)
|
|
485
|
+
throw new UserError(`vx dte worker: VX_DTE_RELAY ${unroutedEnv}, got ${shown(relay)}`)
|
|
391
486
|
const secret = givenSecret()
|
|
392
487
|
if (secret === undefined && !onGitHub()) {
|
|
393
488
|
throw new UserError(
|
|
@@ -423,6 +518,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
423
518
|
if (ctx.ci) keep = { cache: ctx.cache, root: ctx.workspaceRoot, warn: (m) => ctx.warn(m) }
|
|
424
519
|
ctx.on('onTaskComplete', (node, outcome) => {
|
|
425
520
|
if (outcome.hash !== undefined) keys.set(node.id, outcome.hash)
|
|
521
|
+
if (node.config.exec?.command === undefined || ended.has(node.id)) return
|
|
522
|
+
ended.add(node.id)
|
|
523
|
+
const p = placeOf(node.worker, node.platform) ?? ''
|
|
524
|
+
leftOf.set(p, (leftOf.get(p) ?? 1) - 1)
|
|
426
525
|
})
|
|
427
526
|
},
|
|
428
527
|
|
|
@@ -432,9 +531,32 @@ export function dte(options: DteOptions = {}) {
|
|
|
432
531
|
tails = undefined
|
|
433
532
|
// A run that is not distributed reads nothing.
|
|
434
533
|
if (orchestrator === undefined) return undefined
|
|
534
|
+
// An uncached upstream runs again where its dependant runs, on a
|
|
535
|
+
// worker type or platform it may name otherwise: refused here, not
|
|
536
|
+
// run there (2026-11-08).
|
|
537
|
+
const { apart } = await import('./orchestrator.js')
|
|
538
|
+
const where = (n: TaskNode): string => placeOf(n.worker, n.platform) ?? ''
|
|
539
|
+
const platformOf = (n: TaskNode): string => where(n).slice(where(n).indexOf(':') + 1)
|
|
540
|
+
const bound = apart(
|
|
541
|
+
graph,
|
|
542
|
+
(u, d) =>
|
|
543
|
+
(multi && u.config.exec?.worker !== undefined && u.worker !== d.worker) ||
|
|
544
|
+
(u.config.exec?.platforms !== undefined && platformOf(u) !== platformOf(d)),
|
|
545
|
+
)
|
|
546
|
+
if (bound !== undefined)
|
|
547
|
+
throw new UserError(
|
|
548
|
+
`dte(): ${bound.dependant} runs on ${where(graph.get(bound.dependant)!)} and reads what uncached ${bound.upstream} leaves where it runs, which cannot be there: its exec.worker or exec.platforms say ${where(graph.get(bound.upstream)!)}. Cache ${bound.upstream}, or run both alike`,
|
|
549
|
+
)
|
|
435
550
|
const ids = [...graph.values()]
|
|
436
551
|
.filter((n) => n.config.exec?.command !== undefined)
|
|
437
552
|
.map((n) => n.id)
|
|
553
|
+
leftOf.clear()
|
|
554
|
+
ended.clear()
|
|
555
|
+
for (const id of ids) {
|
|
556
|
+
const n = graph.get(id)!
|
|
557
|
+
const p = placeOf(n.worker, n.platform) ?? ''
|
|
558
|
+
leftOf.set(p, (leftOf.get(p) ?? 0) + 1)
|
|
559
|
+
}
|
|
438
560
|
const read = async (): Promise<void> => {
|
|
439
561
|
if (orchestrator === undefined) return
|
|
440
562
|
try {
|
|
@@ -443,6 +565,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
443
565
|
defs: taskDefinitions(graph.values()),
|
|
444
566
|
}).resourcesFor(ids)
|
|
445
567
|
tails = undefined
|
|
568
|
+
orchestrator?.rerank()
|
|
446
569
|
} catch (err) {
|
|
447
570
|
ctx.warn(
|
|
448
571
|
`[vx] dte: tasks are packed one core each: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -450,6 +573,18 @@ export function dte(options: DteOptions = {}) {
|
|
|
450
573
|
}
|
|
451
574
|
}
|
|
452
575
|
let reading = read()
|
|
576
|
+
const basesIn = import('./warm-ups.js')
|
|
577
|
+
.then((w) => w.readServerBases(ctx.cache, ctx.workspaceRoot))
|
|
578
|
+
.then(
|
|
579
|
+
(m) => {
|
|
580
|
+
basesRead = m
|
|
581
|
+
for (const [id, cores] of m) if (!bases.has(id)) bases.set(id, cores)
|
|
582
|
+
},
|
|
583
|
+
(err: unknown) =>
|
|
584
|
+
ctx.warn(
|
|
585
|
+
`[vx] dte: no server bases learned: ${err instanceof Error ? err.message : String(err)}`,
|
|
586
|
+
),
|
|
587
|
+
)
|
|
453
588
|
warming = import('./warm-ups.js')
|
|
454
589
|
.then((w) => w.readWarmUps(ctx.cache, ctx.workspaceRoot))
|
|
455
590
|
.then(
|
|
@@ -463,7 +598,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
463
598
|
const { likelyMisses } = await import('./forecast.js')
|
|
464
599
|
likely = likelyMisses(graph, changed, ctx.workspaceRoot)
|
|
465
600
|
})
|
|
466
|
-
await Promise.all([reading, guess])
|
|
601
|
+
await Promise.all([reading, guess, basesIn])
|
|
467
602
|
// Needs another plugin fetched (the default branch's timings) land
|
|
468
603
|
// in history after this: the tasks not yet placed pack by them, and
|
|
469
604
|
// the first ask for workers waits for them.
|
|
@@ -488,8 +623,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
488
623
|
const [
|
|
489
624
|
{ Orchestrator },
|
|
490
625
|
{ joinOffLoop },
|
|
491
|
-
{ decide, knownMisses, slowdown, tailsMs, WARM_UP_MS },
|
|
492
|
-
{ snapshot },
|
|
626
|
+
{ decide, keepLast, knownMisses, slowdown, tailsMs, WARM_UP_MS },
|
|
627
|
+
{ snapshot, untrackedWorkspace },
|
|
493
628
|
seal,
|
|
494
629
|
] = await Promise.all([
|
|
495
630
|
import('./orchestrator.js'),
|
|
@@ -518,7 +653,23 @@ export function dte(options: DteOptions = {}) {
|
|
|
518
653
|
proof ??= seal.secretProof(secret!)
|
|
519
654
|
const { commit, tree } = await snapshot(ctx.workspaceRoot)
|
|
520
655
|
if (tree === '') throw new UserError('dte(): a DTE run needs a git commit')
|
|
521
|
-
const
|
|
656
|
+
const untracked = await untrackedWorkspace(ctx.workspaceRoot, tree)
|
|
657
|
+
if (untracked !== undefined)
|
|
658
|
+
throw new UserError(
|
|
659
|
+
`dte(): ${untracked} is untracked, so the commit workers check out lacks it: commit it`,
|
|
660
|
+
)
|
|
661
|
+
const serves = new Map<string, readonly string[]>()
|
|
662
|
+
const need = (id: string): Need => {
|
|
663
|
+
const n = booked(needOf(peaks.get(id), unmeasured), how.book)
|
|
664
|
+
let ids = serves.get(id)
|
|
665
|
+
if (ids === undefined) {
|
|
666
|
+
ids = serversOf(nodes, id)
|
|
667
|
+
// Read again once the graph is: none before it.
|
|
668
|
+
if (nodes !== undefined) serves.set(id, ids)
|
|
669
|
+
}
|
|
670
|
+
if (ids.length === 0) return n
|
|
671
|
+
return { ...n, servers: ids.map((sv) => ({ id: sv, cores: bases.get(sv) ?? 0 })) }
|
|
672
|
+
}
|
|
522
673
|
// The run plans again on it, now when `now`; set once the plan is.
|
|
523
674
|
let onChange = (_now?: boolean): void => {}
|
|
524
675
|
const o = new Orchestrator({
|
|
@@ -534,6 +685,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
534
685
|
tree,
|
|
535
686
|
nodes: () => nodes,
|
|
536
687
|
need,
|
|
688
|
+
bases,
|
|
537
689
|
artifacts: ctx.artifacts,
|
|
538
690
|
cacheFacts: () => ctx.cacheFacts(),
|
|
539
691
|
run,
|
|
@@ -543,6 +695,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
543
695
|
(tails ??= tailsMs(nodes ?? new Map(), (x) => peaks.get(x)?.p50DurationMs))(id),
|
|
544
696
|
how,
|
|
545
697
|
home,
|
|
698
|
+
joinedPlace,
|
|
546
699
|
lost: (id) => {
|
|
547
700
|
const s = sizeOf.get(id)
|
|
548
701
|
if (s === undefined) return
|
|
@@ -583,6 +736,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
583
736
|
cpu: s.cpu,
|
|
584
737
|
memory: s.memory,
|
|
585
738
|
...named(s.name),
|
|
739
|
+
...(multi && s.platform !== undefined ? { place: s.platform } : {}),
|
|
586
740
|
})
|
|
587
741
|
sizeOf.set(id, s)
|
|
588
742
|
return {
|
|
@@ -625,6 +779,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
625
779
|
// The run's deadline (`Plan.boundMs`), and the work its plans knew.
|
|
626
780
|
let deadline: number | undefined
|
|
627
781
|
const known = new Set<string>()
|
|
782
|
+
// The last plan, which stands while what it saw holds (`decide`).
|
|
783
|
+
const last: { forecast?: Forecast } = {}
|
|
628
784
|
// A plan that needs a worker later plans again then.
|
|
629
785
|
let again: ReturnType<typeof setTimeout> | undefined
|
|
630
786
|
// The fewest workers that end the run's known work within a margin of
|
|
@@ -646,7 +802,14 @@ export function dte(options: DteOptions = {}) {
|
|
|
646
802
|
const m = p50(id)
|
|
647
803
|
return m === undefined ? undefined : m * slow
|
|
648
804
|
}
|
|
649
|
-
const
|
|
805
|
+
const state = o.planState(ms, warmUp)
|
|
806
|
+
const running = state.running
|
|
807
|
+
const workers = keepLast(
|
|
808
|
+
state.workers,
|
|
809
|
+
sizes,
|
|
810
|
+
started,
|
|
811
|
+
(p) => (leftOf.get(p) ?? 0) > 0 || (p === home && (leftOf.get('') ?? 0) > 0),
|
|
812
|
+
)
|
|
650
813
|
const final = adapter === undefined
|
|
651
814
|
const p = decide({
|
|
652
815
|
remaining,
|
|
@@ -665,8 +828,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
665
828
|
limitMs: deadline === undefined ? undefined : deadline - performance.now(),
|
|
666
829
|
known,
|
|
667
830
|
how,
|
|
668
|
-
home,
|
|
831
|
+
home: multi ? (n: TaskNode) => placeOf(n.worker, n.platform) : home,
|
|
669
832
|
queued: o.queued(warmUp),
|
|
833
|
+
last,
|
|
834
|
+
now: performance.now(),
|
|
670
835
|
})
|
|
671
836
|
deadline = performance.now() + p.boundMs
|
|
672
837
|
for (const t of p.tasks) known.add(t.id)
|
|
@@ -787,6 +952,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
787
952
|
}
|
|
788
953
|
if (needsIn === undefined) scheduled = forecast
|
|
789
954
|
else forecast()
|
|
955
|
+
// Core asks the footer and the summary after teardown: the last run's graph.
|
|
956
|
+
const depsOf = (id: string): readonly string[] => (nodes ?? ran)?.get(id)?.deps ?? []
|
|
790
957
|
return {
|
|
791
958
|
name: 'dte',
|
|
792
959
|
remote: true,
|
|
@@ -800,29 +967,42 @@ export function dte(options: DteOptions = {}) {
|
|
|
800
967
|
// A worker answers a task's runtime probes before it runs it.
|
|
801
968
|
runtime: true,
|
|
802
969
|
// Its sizes' platforms; workers that join on their own, this machine's.
|
|
803
|
-
platforms:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
970
|
+
platforms: sized.length === 0 ? [host] : [...new Set(sized.map((s) => s.platform))],
|
|
971
|
+
// Its worker types, in declared order, each with its machines' platforms.
|
|
972
|
+
...(types.length === 0
|
|
973
|
+
? {}
|
|
974
|
+
: {
|
|
975
|
+
workers: types.map(
|
|
976
|
+
(t) =>
|
|
977
|
+
[
|
|
978
|
+
t,
|
|
979
|
+
[...new Set(sized.filter((s) => s.type === t).map((s) => s.platform))],
|
|
980
|
+
] as const,
|
|
813
981
|
),
|
|
982
|
+
}),
|
|
983
|
+
// Every ready task: the orchestrator's queue knows which platform's
|
|
984
|
+
// worker is free, core's order does not. A pool of twice the cores,
|
|
985
|
+
// shared by all platforms, held a 2 s arm task 79 s behind x64 work
|
|
986
|
+
// with its worker idle (main a1950a2, 2026-11-07).
|
|
987
|
+
capacity: Number.MAX_SAFE_INTEGER,
|
|
814
988
|
execute: (req) => {
|
|
815
989
|
// A miss: the run's known work grows.
|
|
816
990
|
missed = true
|
|
817
991
|
executed.add(req.taskId)
|
|
818
992
|
replan()
|
|
819
|
-
return o.execute(req)
|
|
993
|
+
return o.execute(multi ? { ...req, platform: placeOf(req.worker, req.platform)! } : req)
|
|
820
994
|
},
|
|
821
|
-
footer: () =>
|
|
995
|
+
footer: () =>
|
|
996
|
+
o.footer(
|
|
997
|
+
() => o.idealMs(depsOf, sizes),
|
|
998
|
+
() => o.advice(depsOf, sizes),
|
|
999
|
+
),
|
|
822
1000
|
machines: () => o.machines(),
|
|
823
|
-
idealMs: () => o.idealMs(
|
|
1001
|
+
idealMs: () => o.idealMs(depsOf, sizes),
|
|
1002
|
+
advice: () => o.advice(depsOf, sizes),
|
|
824
1003
|
profile: () => o.profile(),
|
|
825
1004
|
running: (id) => o.where(id),
|
|
1005
|
+
unneeded: (id) => o.unneeded(id),
|
|
826
1006
|
demand(remaining, misses) {
|
|
827
1007
|
if (pending === undefined) o.probed(remaining.size, misses.size)
|
|
828
1008
|
o.missed = misses
|
|
@@ -928,6 +1108,28 @@ function refuse(option: string, want: string, got: unknown): never {
|
|
|
928
1108
|
throw new UserError(`dte() option "${option}" must be ${want}, got ${said}`)
|
|
929
1109
|
}
|
|
930
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* The persistent tasks `id` needs where it runs, as core hands them to a
|
|
1113
|
+
* `servers` executor: its dependencies that are servers, through groups,
|
|
1114
|
+
* and the servers they depend on.
|
|
1115
|
+
*/
|
|
1116
|
+
export function serversOf(nodes: ReadonlyMap<string, TaskNode> | undefined, id: string): string[] {
|
|
1117
|
+
const out: string[] = []
|
|
1118
|
+
const seen = new Set<string>()
|
|
1119
|
+
const stack = [...(nodes?.get(id)?.deps ?? [])]
|
|
1120
|
+
while (stack.length > 0) {
|
|
1121
|
+
const d = stack.pop()!
|
|
1122
|
+
if (seen.has(d)) continue
|
|
1123
|
+
seen.add(d)
|
|
1124
|
+
const n = nodes?.get(d)
|
|
1125
|
+
if (n === undefined) continue
|
|
1126
|
+
if (n.config.exec?.persistent !== undefined) out.push(d)
|
|
1127
|
+
else if (n.config.exec !== undefined) continue
|
|
1128
|
+
stack.push(...n.deps)
|
|
1129
|
+
}
|
|
1130
|
+
return out
|
|
1131
|
+
}
|
|
1132
|
+
|
|
931
1133
|
/** What a task asks of a worker, from its history. */
|
|
932
1134
|
export function needOf(r: TaskResources | undefined, ask: Need = FRESH): Need {
|
|
933
1135
|
// Unmeasured (new, or over before the Linux sandbox's first look, which
|
|
@@ -949,9 +1151,6 @@ export function needOf(r: TaskResources | undefined, ask: Need = FRESH): Need {
|
|
|
949
1151
|
return { cores: Math.max(0.25, r.maxCpuParallelism), memory: r.maxPeakRssBytes ?? 0 }
|
|
950
1152
|
}
|
|
951
1153
|
|
|
952
|
-
/** The executor's pool when workers are uncapped: more than any run starts at once. */
|
|
953
|
-
const JOINED_CAPACITY = 1024
|
|
954
|
-
|
|
955
1154
|
const UNITS: Record<string, number> = {
|
|
956
1155
|
B: 1,
|
|
957
1156
|
KB: 1e3,
|
|
@@ -1022,6 +1221,16 @@ export function homeOf(sizes: readonly Size[], host: string): string | undefined
|
|
|
1022
1221
|
return of.has(host) && of.size > 1 ? host : undefined
|
|
1023
1222
|
}
|
|
1024
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* Why a relay url no route can be appended to is refused: past a `?` or
|
|
1226
|
+
* `#` every socket asked the relay's root, and one naming /v1 asked /v1/v1/….
|
|
1227
|
+
*/
|
|
1228
|
+
function unroutable(url: string): string | undefined {
|
|
1229
|
+
if (/[?#]/.test(url)) return 'takes no ?query or #fragment: each route is appended to it'
|
|
1230
|
+
if (/\/v1\/*$/.test(url)) return "is the relay's base, without /v1"
|
|
1231
|
+
return undefined
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1025
1234
|
/** A relay URL as a refusal may print it: a `user:pass@` would reach the CI log. */
|
|
1026
1235
|
function shown(url: string): string {
|
|
1027
1236
|
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
|