@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/README.md +135 -62
- package/package.json +3 -3
- package/src/dispatch.ts +39 -4
- 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 +14 -3
- package/src/orchestrator.ts +265 -33
- package/src/planner.ts +183 -55
- package/src/plugin.ts +209 -30
- 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 +84 -42
- package/src/worker.ts +148 -15
package/src/plugin.ts
CHANGED
|
@@ -22,7 +22,13 @@ import type { Orchestrator } from './orchestrator.js'
|
|
|
22
22
|
import type { Proof } from './seal.js'
|
|
23
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) {
|
|
@@ -267,6 +332,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
267
332
|
let peaks: ReadonlyMap<string, TaskResources> = new Map()
|
|
268
333
|
/** Each size's warm-up an earlier run learned (`warm-ups.ts`), and the read of it. */
|
|
269
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()
|
|
270
339
|
let warming: Promise<void> = Promise.resolve()
|
|
271
340
|
/** On CI, where the run keeps the warm-ups it learned: `setup`'s cache, root and warn. */
|
|
272
341
|
let keep: { cache: PluginCache; root: string; warn: (m: string) => void } | undefined
|
|
@@ -281,6 +350,11 @@ export function dte(options: DteOptions = {}) {
|
|
|
281
350
|
// Each finished task's cache key, a hit's too: what a worker restores a
|
|
282
351
|
// dependency by.
|
|
283
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>()
|
|
284
358
|
let orchestrator: Orchestrator | undefined
|
|
285
359
|
let closing: Promise<void> | undefined
|
|
286
360
|
|
|
@@ -298,7 +372,18 @@ export function dte(options: DteOptions = {}) {
|
|
|
298
372
|
k.warn(`[vx] dte: warm-ups not kept: ${err instanceof Error ? err.message : String(err)}`),
|
|
299
373
|
)
|
|
300
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
|
+
}
|
|
301
384
|
learned = new Map()
|
|
385
|
+
bases = new Map()
|
|
386
|
+
basesRead = new Map()
|
|
302
387
|
warming = Promise.resolve()
|
|
303
388
|
peaks = new Map()
|
|
304
389
|
tails = undefined
|
|
@@ -343,10 +428,11 @@ export function dte(options: DteOptions = {}) {
|
|
|
343
428
|
workspaceRoot: root,
|
|
344
429
|
dir: git('--show-prefix').replace(/\/$/, ''),
|
|
345
430
|
...(options.relay !== undefined ? { relay: options.relay } : {}),
|
|
346
|
-
sizes:
|
|
431
|
+
sizes: sized.map((s) => ({
|
|
347
432
|
name: s.name,
|
|
348
433
|
...(s.max === Infinity ? {} : { max: s.max }),
|
|
349
|
-
...
|
|
434
|
+
...(s.named === undefined ? {} : { platform: s.named }),
|
|
435
|
+
...(s.runsOn === undefined ? {} : { runsOn: s.runsOn }),
|
|
350
436
|
})),
|
|
351
437
|
manager: managerOf(root),
|
|
352
438
|
tasks: askTasks(),
|
|
@@ -432,6 +518,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
432
518
|
if (ctx.ci) keep = { cache: ctx.cache, root: ctx.workspaceRoot, warn: (m) => ctx.warn(m) }
|
|
433
519
|
ctx.on('onTaskComplete', (node, outcome) => {
|
|
434
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)
|
|
435
525
|
})
|
|
436
526
|
},
|
|
437
527
|
|
|
@@ -441,9 +531,32 @@ export function dte(options: DteOptions = {}) {
|
|
|
441
531
|
tails = undefined
|
|
442
532
|
// A run that is not distributed reads nothing.
|
|
443
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
|
+
)
|
|
444
550
|
const ids = [...graph.values()]
|
|
445
551
|
.filter((n) => n.config.exec?.command !== undefined)
|
|
446
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
|
+
}
|
|
447
560
|
const read = async (): Promise<void> => {
|
|
448
561
|
if (orchestrator === undefined) return
|
|
449
562
|
try {
|
|
@@ -452,6 +565,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
452
565
|
defs: taskDefinitions(graph.values()),
|
|
453
566
|
}).resourcesFor(ids)
|
|
454
567
|
tails = undefined
|
|
568
|
+
orchestrator?.rerank()
|
|
455
569
|
} catch (err) {
|
|
456
570
|
ctx.warn(
|
|
457
571
|
`[vx] dte: tasks are packed one core each: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -459,6 +573,18 @@ export function dte(options: DteOptions = {}) {
|
|
|
459
573
|
}
|
|
460
574
|
}
|
|
461
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
|
+
)
|
|
462
588
|
warming = import('./warm-ups.js')
|
|
463
589
|
.then((w) => w.readWarmUps(ctx.cache, ctx.workspaceRoot))
|
|
464
590
|
.then(
|
|
@@ -472,7 +598,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
472
598
|
const { likelyMisses } = await import('./forecast.js')
|
|
473
599
|
likely = likelyMisses(graph, changed, ctx.workspaceRoot)
|
|
474
600
|
})
|
|
475
|
-
await Promise.all([reading, guess])
|
|
601
|
+
await Promise.all([reading, guess, basesIn])
|
|
476
602
|
// Needs another plugin fetched (the default branch's timings) land
|
|
477
603
|
// in history after this: the tasks not yet placed pack by them, and
|
|
478
604
|
// the first ask for workers waits for them.
|
|
@@ -497,8 +623,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
497
623
|
const [
|
|
498
624
|
{ Orchestrator },
|
|
499
625
|
{ joinOffLoop },
|
|
500
|
-
{ decide, knownMisses, slowdown, tailsMs, WARM_UP_MS },
|
|
501
|
-
{ snapshot },
|
|
626
|
+
{ decide, keepLast, knownMisses, slowdown, tailsMs, WARM_UP_MS },
|
|
627
|
+
{ snapshot, untrackedWorkspace },
|
|
502
628
|
seal,
|
|
503
629
|
] = await Promise.all([
|
|
504
630
|
import('./orchestrator.js'),
|
|
@@ -527,7 +653,23 @@ export function dte(options: DteOptions = {}) {
|
|
|
527
653
|
proof ??= seal.secretProof(secret!)
|
|
528
654
|
const { commit, tree } = await snapshot(ctx.workspaceRoot)
|
|
529
655
|
if (tree === '') throw new UserError('dte(): a DTE run needs a git commit')
|
|
530
|
-
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
|
+
}
|
|
531
673
|
// The run plans again on it, now when `now`; set once the plan is.
|
|
532
674
|
let onChange = (_now?: boolean): void => {}
|
|
533
675
|
const o = new Orchestrator({
|
|
@@ -543,6 +685,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
543
685
|
tree,
|
|
544
686
|
nodes: () => nodes,
|
|
545
687
|
need,
|
|
688
|
+
bases,
|
|
546
689
|
artifacts: ctx.artifacts,
|
|
547
690
|
cacheFacts: () => ctx.cacheFacts(),
|
|
548
691
|
run,
|
|
@@ -552,6 +695,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
552
695
|
(tails ??= tailsMs(nodes ?? new Map(), (x) => peaks.get(x)?.p50DurationMs))(id),
|
|
553
696
|
how,
|
|
554
697
|
home,
|
|
698
|
+
joinedPlace,
|
|
555
699
|
lost: (id) => {
|
|
556
700
|
const s = sizeOf.get(id)
|
|
557
701
|
if (s === undefined) return
|
|
@@ -592,6 +736,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
592
736
|
cpu: s.cpu,
|
|
593
737
|
memory: s.memory,
|
|
594
738
|
...named(s.name),
|
|
739
|
+
...(multi && s.platform !== undefined ? { place: s.platform } : {}),
|
|
595
740
|
})
|
|
596
741
|
sizeOf.set(id, s)
|
|
597
742
|
return {
|
|
@@ -657,7 +802,14 @@ export function dte(options: DteOptions = {}) {
|
|
|
657
802
|
const m = p50(id)
|
|
658
803
|
return m === undefined ? undefined : m * slow
|
|
659
804
|
}
|
|
660
|
-
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
|
+
)
|
|
661
813
|
const final = adapter === undefined
|
|
662
814
|
const p = decide({
|
|
663
815
|
remaining,
|
|
@@ -676,7 +828,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
676
828
|
limitMs: deadline === undefined ? undefined : deadline - performance.now(),
|
|
677
829
|
known,
|
|
678
830
|
how,
|
|
679
|
-
home,
|
|
831
|
+
home: multi ? (n: TaskNode) => placeOf(n.worker, n.platform) : home,
|
|
680
832
|
queued: o.queued(warmUp),
|
|
681
833
|
last,
|
|
682
834
|
now: performance.now(),
|
|
@@ -815,23 +967,30 @@ export function dte(options: DteOptions = {}) {
|
|
|
815
967
|
// A worker answers a task's runtime probes before it runs it.
|
|
816
968
|
runtime: true,
|
|
817
969
|
// Its sizes' platforms; workers that join on their own, this machine's.
|
|
818
|
-
platforms:
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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,
|
|
828
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,
|
|
829
988
|
execute: (req) => {
|
|
830
989
|
// A miss: the run's known work grows.
|
|
831
990
|
missed = true
|
|
832
991
|
executed.add(req.taskId)
|
|
833
992
|
replan()
|
|
834
|
-
return o.execute(req)
|
|
993
|
+
return o.execute(multi ? { ...req, platform: placeOf(req.worker, req.platform)! } : req)
|
|
835
994
|
},
|
|
836
995
|
footer: () =>
|
|
837
996
|
o.footer(
|
|
@@ -843,6 +1002,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
843
1002
|
advice: () => o.advice(depsOf, sizes),
|
|
844
1003
|
profile: () => o.profile(),
|
|
845
1004
|
running: (id) => o.where(id),
|
|
1005
|
+
unneeded: (id) => o.unneeded(id),
|
|
846
1006
|
demand(remaining, misses) {
|
|
847
1007
|
if (pending === undefined) o.probed(remaining.size, misses.size)
|
|
848
1008
|
o.missed = misses
|
|
@@ -948,6 +1108,28 @@ function refuse(option: string, want: string, got: unknown): never {
|
|
|
948
1108
|
throw new UserError(`dte() option "${option}" must be ${want}, got ${said}`)
|
|
949
1109
|
}
|
|
950
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
|
+
|
|
951
1133
|
/** What a task asks of a worker, from its history. */
|
|
952
1134
|
export function needOf(r: TaskResources | undefined, ask: Need = FRESH): Need {
|
|
953
1135
|
// Unmeasured (new, or over before the Linux sandbox's first look, which
|
|
@@ -969,9 +1151,6 @@ export function needOf(r: TaskResources | undefined, ask: Need = FRESH): Need {
|
|
|
969
1151
|
return { cores: Math.max(0.25, r.maxCpuParallelism), memory: r.maxPeakRssBytes ?? 0 }
|
|
970
1152
|
}
|
|
971
1153
|
|
|
972
|
-
/** The executor's pool when workers are uncapped: more than any run starts at once. */
|
|
973
|
-
const JOINED_CAPACITY = 1024
|
|
974
|
-
|
|
975
1154
|
const UNITS: Record<string, number> = {
|
|
976
1155
|
B: 1,
|
|
977
1156
|
KB: 1e3,
|