@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/planner.ts
CHANGED
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
} from './dispatch.js'
|
|
19
19
|
import { DEFAULT_STRATEGY, type Strategy } from './strategy.js'
|
|
20
20
|
|
|
21
|
+
/** Where a task naming no platform runs, or (several worker types) where each task runs. */
|
|
22
|
+
type Home = string | ((node: TaskNode) => string | undefined)
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* A task's duration when history has none: a test suite's, so a run with
|
|
23
26
|
* no history starts workers for its work rather than one for all of it.
|
|
@@ -53,6 +56,8 @@ export interface PlanTask {
|
|
|
53
56
|
/** Its number in the run's graph and its deps' (`planTasks`): a plan's shape reads them, not ids. */
|
|
54
57
|
readonly n?: number
|
|
55
58
|
readonly depsN?: Int32Array
|
|
59
|
+
/** Its servers' bases (`Need.servers`): booked once on each worker. */
|
|
60
|
+
readonly servers?: Need['servers']
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/** The run's tasks numbered once, and each one's deps by number. */
|
|
@@ -97,6 +102,8 @@ export interface PlanWorker {
|
|
|
97
102
|
readonly platform?: string | undefined
|
|
98
103
|
/** The size it was started as (`Size.name`): what its warm-up is learned by. */
|
|
99
104
|
readonly size?: string | undefined
|
|
105
|
+
/** The servers it runs, each with the base booked there. */
|
|
106
|
+
readonly serving?: ReadonlyMap<string, number> | undefined
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/** A worker's warm-up, one for all, or by its size (`Size.name`; none for one that joined on its own). */
|
|
@@ -131,14 +138,21 @@ interface Ahead {
|
|
|
131
138
|
/** When the window it played moves on, from now: plan again then. */
|
|
132
139
|
readonly untilInMs: number | undefined
|
|
133
140
|
readonly freeInMs: ReadonlyMap<string, number>
|
|
141
|
+
/** The tasks it gives each worker. */
|
|
142
|
+
readonly given: ReadonlyMap<string, readonly string[]>
|
|
134
143
|
readonly later: readonly {
|
|
135
144
|
readonly size: Size
|
|
136
145
|
readonly inMs: number
|
|
137
146
|
readonly freeInMs: number
|
|
138
147
|
readonly joinInMs: number
|
|
148
|
+
readonly given: readonly string[]
|
|
149
|
+
}[]
|
|
150
|
+
/** For each started now, when its work ends, when it joins and what it runs. */
|
|
151
|
+
readonly started: readonly {
|
|
152
|
+
readonly freeInMs: number
|
|
153
|
+
readonly joinInMs: number
|
|
154
|
+
readonly given: readonly string[]
|
|
139
155
|
}[]
|
|
140
|
-
/** For each started now, when its work ends and when it joins. */
|
|
141
|
-
readonly started: readonly { readonly freeInMs: number; readonly joinInMs: number }[]
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
/** History the run's finished tasks must have had before `slowdown` says anything. */
|
|
@@ -202,7 +216,7 @@ export function planTasks(
|
|
|
202
216
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
203
217
|
need: (id: string) => Need,
|
|
204
218
|
durationMs: (id: string) => number | undefined,
|
|
205
|
-
home?:
|
|
219
|
+
home?: Home,
|
|
206
220
|
): PlanTask[] {
|
|
207
221
|
// The graph is the run's, fixed: each task is numbered and its deps
|
|
208
222
|
// walked once a run, not at each plan (2026-11-05: a 5000-task sim
|
|
@@ -247,9 +261,10 @@ export function planTasks(
|
|
|
247
261
|
above: n.above,
|
|
248
262
|
ms: durationMs(id),
|
|
249
263
|
deps,
|
|
250
|
-
platform: e.node.platform ?? home,
|
|
264
|
+
platform: typeof home === 'function' ? home(e.node) : (e.node.platform ?? home),
|
|
251
265
|
n: e.n,
|
|
252
266
|
depsN: e.depsN,
|
|
267
|
+
...(n.servers === undefined ? {} : { servers: n.servers }),
|
|
253
268
|
})
|
|
254
269
|
}
|
|
255
270
|
return out
|
|
@@ -272,7 +287,7 @@ export function missedWork(
|
|
|
272
287
|
executed: ReadonlySet<string>,
|
|
273
288
|
need: (id: string) => Need,
|
|
274
289
|
durationMs: (id: string) => number | undefined,
|
|
275
|
-
home?:
|
|
290
|
+
home?: Home,
|
|
276
291
|
): PlanTask[] {
|
|
277
292
|
const changed = new Map<string, boolean>()
|
|
278
293
|
// Whether `id`'s key changed: a cached task that missed, or one after it.
|
|
@@ -294,6 +309,43 @@ export function missedWork(
|
|
|
294
309
|
return planTasks([...remaining].filter(missed), nodes, need, durationMs, home)
|
|
295
310
|
}
|
|
296
311
|
|
|
312
|
+
/**
|
|
313
|
+
* `workers` as a plan may let them go: of a platform whose sizes can start
|
|
314
|
+
* no other (`max` spent; one let go still counts), the last idle one stays
|
|
315
|
+
* while a task of that platform has not ended (`left`). A hit core's probe
|
|
316
|
+
* found can still miss when its restore fails, and only a worker of its
|
|
317
|
+
* platform runs it: main let its one arm worker go, check.binary.linux-arm64
|
|
318
|
+
* came back as a miss, and the run waited out its timeout (2026-11-06).
|
|
319
|
+
*/
|
|
320
|
+
export function keepLast(
|
|
321
|
+
workers: readonly PlanWorker[],
|
|
322
|
+
sizes: readonly Size[],
|
|
323
|
+
started: ReadonlyMap<string, number>,
|
|
324
|
+
left: (platform: string) => boolean,
|
|
325
|
+
): readonly PlanWorker[] {
|
|
326
|
+
const of = new Map<string, PlanWorker[]>()
|
|
327
|
+
for (const w of workers) {
|
|
328
|
+
if (w.platform === undefined) continue
|
|
329
|
+
const all = of.get(w.platform)
|
|
330
|
+
if (all === undefined) of.set(w.platform, [w])
|
|
331
|
+
else all.push(w)
|
|
332
|
+
}
|
|
333
|
+
let kept: Set<PlanWorker> | undefined
|
|
334
|
+
for (const [platform, all] of of) {
|
|
335
|
+
// A busy one is not let go; one starting, cancelled, frees its start.
|
|
336
|
+
if (all.some((w) => !w.idle && w.starting !== true)) continue
|
|
337
|
+
const idle = all.find((w) => w.idle && w.starting !== true)
|
|
338
|
+
if (idle === undefined) continue
|
|
339
|
+
const own = sizes.filter((s) => s.platform === platform)
|
|
340
|
+
if (own.length === 0 || own.some((s) => (started.get(s.name) ?? 0) < s.max)) continue
|
|
341
|
+
if (!left(platform)) continue
|
|
342
|
+
;(kept ??= new Set()).add(idle)
|
|
343
|
+
}
|
|
344
|
+
return kept === undefined
|
|
345
|
+
? workers
|
|
346
|
+
: workers.map((w) => (kept!.has(w) ? { ...w, idle: false } : w))
|
|
347
|
+
}
|
|
348
|
+
|
|
297
349
|
/**
|
|
298
350
|
* The tasks a plan counts as missed: those handed over to run, the
|
|
299
351
|
* misses core's probe found, and of the commit's forecast (`likely`) only
|
|
@@ -319,6 +371,8 @@ interface Slot extends Bin {
|
|
|
319
371
|
free?: number
|
|
320
372
|
/** When its last task given in the play ends. */
|
|
321
373
|
last?: number
|
|
374
|
+
/** The tasks given it in the play. */
|
|
375
|
+
given?: string[]
|
|
322
376
|
}
|
|
323
377
|
|
|
324
378
|
/**
|
|
@@ -350,6 +404,7 @@ function play(
|
|
|
350
404
|
first: Map<string, number>
|
|
351
405
|
ends: Map<string | undefined, number>
|
|
352
406
|
free: () => Map<string, number>
|
|
407
|
+
given: () => Map<string, readonly string[]>
|
|
353
408
|
} {
|
|
354
409
|
// A dependency running now ends when it does; one ended or a hit, now.
|
|
355
410
|
const runningIds = new Set<string>()
|
|
@@ -366,8 +421,9 @@ function play(
|
|
|
366
421
|
memory: w.memory,
|
|
367
422
|
platform: w.platform,
|
|
368
423
|
at: w.at,
|
|
369
|
-
used: { cores: 0, memory: 0 },
|
|
424
|
+
used: { cores: [...(w.serving?.values() ?? [])].reduce((a, b) => a + b, 0), memory: 0 },
|
|
370
425
|
running: 0,
|
|
426
|
+
serving: new Set(w.serving?.keys()),
|
|
371
427
|
}))
|
|
372
428
|
// Ends to come: [at, slot, cores, memory, task or undefined].
|
|
373
429
|
const ends = new Ends()
|
|
@@ -479,6 +535,7 @@ function play(
|
|
|
479
535
|
readyCount--
|
|
480
536
|
count(t, -1)
|
|
481
537
|
best.first ??= now
|
|
538
|
+
;(best.given ??= []).push(id)
|
|
482
539
|
best.free = Math.max(best.free ?? 0, now + (t.ms ?? UNKNOWN_MS))
|
|
483
540
|
const end = now + (t.ms ?? UNKNOWN_MS)
|
|
484
541
|
ends.push(
|
|
@@ -497,8 +554,10 @@ function play(
|
|
|
497
554
|
new Map(slots.flatMap((s) => (s.first === undefined ? [] : [[s.id, s.first] as const])))
|
|
498
555
|
const free = (): Map<string, number> =>
|
|
499
556
|
new Map(slots.flatMap((s) => (s.free === undefined ? [] : [[s.id, s.free] as const])))
|
|
557
|
+
const given = (): Map<string, readonly string[]> =>
|
|
558
|
+
new Map(slots.flatMap((s) => (s.given === undefined ? [] : [[s.id, s.given] as const])))
|
|
500
559
|
if (left === 0 && readyCount === 0)
|
|
501
|
-
return { end: last, first: first(), ends: byPlatform(), free }
|
|
560
|
+
return { end: last, first: first(), ends: byPlatform(), free, given }
|
|
502
561
|
// The next moment anything changes: a task ends or a worker joins.
|
|
503
562
|
let next = ends.next()
|
|
504
563
|
for (const s of slots) if (s.at > now) next = Math.min(next, s.at)
|
|
@@ -512,18 +571,24 @@ function play(
|
|
|
512
571
|
cores.set(undefined, (cores.get(undefined) ?? 0) + s.cores)
|
|
513
572
|
if (s.platform !== undefined) cores.set(s.platform, (cores.get(s.platform) ?? 0) + s.cores)
|
|
514
573
|
}
|
|
515
|
-
|
|
516
|
-
|
|
574
|
+
// A running task's rest on its own platform too: counted on all
|
|
575
|
+
// alone, a Mac fewer read the Linux end later (2026-11-07).
|
|
576
|
+
for (const e of ends.all()) {
|
|
577
|
+
const w = e.cores * (e.at - now)
|
|
578
|
+
work.set(undefined, (work.get(undefined) ?? 0) + w)
|
|
579
|
+
const p = e.s.platform
|
|
580
|
+
if (p !== undefined) work.set(p, (work.get(p) ?? 0) + w)
|
|
581
|
+
}
|
|
517
582
|
let end = last
|
|
518
583
|
for (const [p, w] of work) end = Math.max(end, now + w / Math.max(cores.get(p) ?? 0, 1e-9))
|
|
519
584
|
for (const t of ready.live()) end = Math.max(end, now + tailOf(t.id))
|
|
520
585
|
for (const e of ends.all())
|
|
521
586
|
for (const d of (e.place >= 0 ? afterAt(e.place) : after.get(e.id!)) ?? [])
|
|
522
587
|
end = Math.max(end, e.at + tailAt(d))
|
|
523
|
-
return { end, cut: true, first: first(), ends: byPlatform(), free }
|
|
588
|
+
return { end, cut: true, first: first(), ends: byPlatform(), free, given }
|
|
524
589
|
}
|
|
525
590
|
if (next === Infinity)
|
|
526
|
-
return { end: left === 0 ? last : Infinity, first: first(), ends: byPlatform(), free }
|
|
591
|
+
return { end: left === 0 ? last : Infinity, first: first(), ends: byPlatform(), free, given }
|
|
527
592
|
now = next
|
|
528
593
|
while (ends.next() <= now) {
|
|
529
594
|
const { s, cores, memory, place, id } = ends.pop()
|
|
@@ -782,6 +847,55 @@ function chainMs(tasks: readonly PlanTask[], workers: readonly PlanWorker[]): nu
|
|
|
782
847
|
const twins = (a: PlanWorker, b: PlanWorker): boolean =>
|
|
783
848
|
a.cores === b.cores && a.memory === b.memory && a.platform === b.platform && a.at === b.at
|
|
784
849
|
|
|
850
|
+
type Played = ReturnType<typeof play>
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* `kept` less each of `added` (its last ones, in the order the run would
|
|
854
|
+
* start them) the run ends as soon without, tried last first. Sizes come in
|
|
855
|
+
* turns, so a prefix holds ones it ends as soon without: a second Mac the
|
|
856
|
+
* plan needed came after a second Linux worker it did not, and main e118ccf
|
|
857
|
+
* cancelled that one unjoined (2026-10-24). Dropped when the end moves no
|
|
858
|
+
* later and its platform's moves no further than the margin, or still ends
|
|
859
|
+
* two warm-ups before the run: room for runners the CI queues. To the run's
|
|
860
|
+
* end alone, a 110 s guess of an unmeasured chain cut every other platform
|
|
861
|
+
* to it; to its platform's alone, 4 Macs started for work 2 ended 40 s
|
|
862
|
+
* before the Linux work (main a1950a2, 2026-11-07: "we optimize for total
|
|
863
|
+
* wall time"). The live plan and the footer's advice both trim by it.
|
|
864
|
+
*/
|
|
865
|
+
function dropSpare(
|
|
866
|
+
added: readonly PlanWorker[],
|
|
867
|
+
kept: readonly PlanWorker[],
|
|
868
|
+
before: Played,
|
|
869
|
+
run: (ws: readonly PlanWorker[]) => Played,
|
|
870
|
+
): { kept: PlanWorker[]; before: Played } {
|
|
871
|
+
let out = [...kept]
|
|
872
|
+
for (let i = added.length - 1, dropped = false; i >= 0; i--) {
|
|
873
|
+
const w = added[i]!
|
|
874
|
+
// Its twin after it stayed, and nothing moved since: without this
|
|
875
|
+
// one the play is that one's again, so it stays too. Each was a
|
|
876
|
+
// play of the whole run (2026-11-05).
|
|
877
|
+
if (i < added.length - 1 && !dropped && twins(w, added[i + 1]!)) continue
|
|
878
|
+
const without = out.filter((k) => k !== w)
|
|
879
|
+
// Given no task in a whole play, the play without it is the same.
|
|
880
|
+
if (!before.first.has(w.id) && before.cut !== true) {
|
|
881
|
+
out = without
|
|
882
|
+
dropped = true
|
|
883
|
+
continue
|
|
884
|
+
}
|
|
885
|
+
const after = run(without)
|
|
886
|
+
const ends = after.ends.get(w.platform) ?? 0
|
|
887
|
+
dropped =
|
|
888
|
+
after.end <= before.end &&
|
|
889
|
+
(ends <= (before.ends.get(w.platform) ?? 0) + MARGIN_MS ||
|
|
890
|
+
ends <= before.end - Math.max(MARGIN_MS, 2 * w.at))
|
|
891
|
+
if (dropped) {
|
|
892
|
+
out = without
|
|
893
|
+
before = after
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { kept: out, before }
|
|
897
|
+
}
|
|
898
|
+
|
|
785
899
|
/** A worker of `s` that joins `at`. */
|
|
786
900
|
function fresh(
|
|
787
901
|
s: { cpu: number; memory: number; platform?: string | undefined; size?: string | undefined },
|
|
@@ -875,36 +989,9 @@ export function plan(
|
|
|
875
989
|
}
|
|
876
990
|
const n = lo
|
|
877
991
|
let kept = [...workers, ...added.slice(0, n)]
|
|
878
|
-
//
|
|
879
|
-
// without: a second Mac the plan needed came after a second Linux worker
|
|
880
|
-
// it did not, and main e118ccf cancelled that one unjoined (2026-10-24).
|
|
881
|
-
// Dropped only when neither the end nor its platform's moves past the
|
|
882
|
-
// margin: to the run's end alone, a 110 s guess of an unmeasured chain
|
|
883
|
-
// cut every other platform to it.
|
|
992
|
+
// The ones the run ends as soon without go (`dropSpare`).
|
|
884
993
|
let before = best < Infinity && n > 0 ? playOf(n) : undefined
|
|
885
|
-
if (before !== undefined)
|
|
886
|
-
for (let i = n - 1, dropped = false; i >= 0; i--) {
|
|
887
|
-
// Its twin after it stayed, and nothing moved since: without this
|
|
888
|
-
// one the play is that one's again, so it stays too. Each was a
|
|
889
|
-
// play of the whole run (2026-11-05).
|
|
890
|
-
if (i < n - 1 && !dropped && twins(added[i]!, added[i + 1]!)) continue
|
|
891
|
-
const platform = added[i]!.platform
|
|
892
|
-
const without = kept.filter((k) => k !== added[i])
|
|
893
|
-
// Given no task in a whole play, the play without it is the same.
|
|
894
|
-
if (unused(before, added[i]!.id)) {
|
|
895
|
-
kept = without
|
|
896
|
-
dropped = true
|
|
897
|
-
continue
|
|
898
|
-
}
|
|
899
|
-
const after = run(without)
|
|
900
|
-
dropped =
|
|
901
|
-
after.end <= before.end &&
|
|
902
|
-
(after.ends.get(platform) ?? 0) <= (before.ends.get(platform) ?? 0) + MARGIN_MS
|
|
903
|
-
if (dropped) {
|
|
904
|
-
kept = without
|
|
905
|
-
before = after
|
|
906
|
-
}
|
|
907
|
-
}
|
|
994
|
+
if (before !== undefined) ({ kept, before } = dropSpare(added.slice(0, n), kept, before, run))
|
|
908
995
|
const release: string[] = []
|
|
909
996
|
// Played with the rest: left out, the next idle one and the plan's end
|
|
910
997
|
// counted on a worker the run lets go.
|
|
@@ -977,11 +1064,12 @@ export function plan(
|
|
|
977
1064
|
}
|
|
978
1065
|
}
|
|
979
1066
|
}
|
|
980
|
-
const { end, first, free, cut } = run([...kept, ...standIns])
|
|
1067
|
+
const { end, first, free, cut, given } = run([...kept, ...standIns])
|
|
981
1068
|
const start: Size[] = []
|
|
982
1069
|
const frees = free()
|
|
983
|
-
const
|
|
984
|
-
const
|
|
1070
|
+
const gave = given()
|
|
1071
|
+
const started: Ahead['started'][number][] = []
|
|
1072
|
+
const later: Ahead['later'][number][] = []
|
|
985
1073
|
let againInMs: number | undefined
|
|
986
1074
|
for (let i = 0; i < n; i++) {
|
|
987
1075
|
const id = added[i]!.id
|
|
@@ -993,11 +1081,17 @@ export function plan(
|
|
|
993
1081
|
// same plan 100,000 times at one instant (tests/sim.test.ts).
|
|
994
1082
|
if (at < warmUp + MARGIN_MS + 1) {
|
|
995
1083
|
start.push(candidates[i]!)
|
|
996
|
-
started.push({ freeInMs: frees.get(id)!, joinInMs: warmUp })
|
|
1084
|
+
started.push({ freeInMs: frees.get(id)!, joinInMs: warmUp, given: gave.get(id)! })
|
|
997
1085
|
} else {
|
|
998
1086
|
const inMs = at - warmUp - MARGIN_MS
|
|
999
1087
|
againInMs = Math.min(againInMs ?? Infinity, inMs)
|
|
1000
|
-
later.push({
|
|
1088
|
+
later.push({
|
|
1089
|
+
size: candidates[i]!,
|
|
1090
|
+
inMs,
|
|
1091
|
+
freeInMs: frees.get(id)!,
|
|
1092
|
+
joinInMs: warmUp,
|
|
1093
|
+
given: gave.get(id)!,
|
|
1094
|
+
})
|
|
1001
1095
|
}
|
|
1002
1096
|
}
|
|
1003
1097
|
later.sort((a, b) => a.inMs - b.inMs)
|
|
@@ -1011,6 +1105,7 @@ export function plan(
|
|
|
1011
1105
|
againInMs: untilInMs === undefined ? againInMs : Math.min(againInMs ?? Infinity, untilInMs),
|
|
1012
1106
|
untilInMs,
|
|
1013
1107
|
freeInMs: frees,
|
|
1108
|
+
given: gave,
|
|
1014
1109
|
later,
|
|
1015
1110
|
started,
|
|
1016
1111
|
}
|
|
@@ -1090,8 +1185,8 @@ export function decide(input: {
|
|
|
1090
1185
|
readonly known?: ReadonlySet<string>
|
|
1091
1186
|
/** How it orders, places and scales (`need` books already). */
|
|
1092
1187
|
readonly how?: Strategy
|
|
1093
|
-
/** The platform a task naming none runs on (`homeOf`). */
|
|
1094
|
-
readonly home?:
|
|
1188
|
+
/** The platform a task naming none runs on (`homeOf`), or where each task runs (several worker types). */
|
|
1189
|
+
readonly home?: Home | undefined
|
|
1095
1190
|
/**
|
|
1096
1191
|
* Sizes with a worker asked and not joined past its warm-up: the CI
|
|
1097
1192
|
* queued it, and one asked now queues behind it. None of them starts.
|
|
@@ -1152,7 +1247,7 @@ export function decide(input: {
|
|
|
1152
1247
|
return { ...backlog(tasks, input.workers, sizes, input.warmUpMs, how), tasks }
|
|
1153
1248
|
const step = had === undefined ? undefined : follow(had, tasks, input, sizes, now)
|
|
1154
1249
|
if (step !== undefined && step !== 'full') return shown(input.last!, had!, step, now, tasks)
|
|
1155
|
-
const { freeInMs, later, started, untilInMs, ...p } = plan(
|
|
1250
|
+
const { freeInMs, given, later, started, untilInMs, ...p } = plan(
|
|
1156
1251
|
tasks,
|
|
1157
1252
|
input.workers,
|
|
1158
1253
|
sizes,
|
|
@@ -1163,7 +1258,13 @@ export function decide(input: {
|
|
|
1163
1258
|
how,
|
|
1164
1259
|
)
|
|
1165
1260
|
if (input.last !== undefined)
|
|
1166
|
-
input.last.forecast = forecastOf(
|
|
1261
|
+
input.last.forecast = forecastOf(
|
|
1262
|
+
p,
|
|
1263
|
+
{ freeInMs, given, later, started, untilInMs },
|
|
1264
|
+
tasks,
|
|
1265
|
+
input,
|
|
1266
|
+
now,
|
|
1267
|
+
)
|
|
1167
1268
|
return { ...p, tasks }
|
|
1168
1269
|
}
|
|
1169
1270
|
|
|
@@ -1186,16 +1287,23 @@ export interface Forecast {
|
|
|
1186
1287
|
readonly count: number
|
|
1187
1288
|
/** When each worker's last planned task ends, on the run's clock. */
|
|
1188
1289
|
readonly free: ReadonlyMap<string, number>
|
|
1290
|
+
/** The tasks planned on each worker. */
|
|
1291
|
+
readonly given: ReadonlyMap<string, readonly string[]>
|
|
1189
1292
|
/** When each worker still starting joins, on the run's clock. */
|
|
1190
1293
|
readonly join: ReadonlyMap<string, number>
|
|
1191
1294
|
/** Those of each worker started and not seen yet, in the order started. */
|
|
1192
|
-
readonly fresh: readonly {
|
|
1295
|
+
readonly fresh: readonly {
|
|
1296
|
+
readonly free: number
|
|
1297
|
+
readonly join: number
|
|
1298
|
+
readonly given: readonly string[]
|
|
1299
|
+
}[]
|
|
1193
1300
|
/** Starts the plan wants later, each when due, on the run's clock. */
|
|
1194
1301
|
readonly later: readonly {
|
|
1195
1302
|
readonly size: Size
|
|
1196
1303
|
readonly at: number
|
|
1197
1304
|
readonly free: number
|
|
1198
1305
|
readonly join: number
|
|
1306
|
+
readonly given: readonly string[]
|
|
1199
1307
|
}[]
|
|
1200
1308
|
/** Work it saw that has since been seen running: work that left otherwise moves the plan. */
|
|
1201
1309
|
readonly seen: Set<string>
|
|
@@ -1280,12 +1388,18 @@ function forecastOf(
|
|
|
1280
1388
|
join: new Map(
|
|
1281
1389
|
input.workers.flatMap((w) => (w.starting === true ? [[w.id, now + w.at] as const] : [])),
|
|
1282
1390
|
),
|
|
1283
|
-
|
|
1391
|
+
given: ahead.given,
|
|
1392
|
+
fresh: ahead.started.map((a) => ({
|
|
1393
|
+
free: now + a.freeInMs,
|
|
1394
|
+
join: now + a.joinInMs,
|
|
1395
|
+
given: a.given,
|
|
1396
|
+
})),
|
|
1284
1397
|
later: ahead.later.map((l) => ({
|
|
1285
1398
|
size: l.size,
|
|
1286
1399
|
at: now + l.inMs,
|
|
1287
1400
|
free: now + l.freeInMs,
|
|
1288
1401
|
join: now + l.inMs + l.joinInMs,
|
|
1402
|
+
given: l.given,
|
|
1289
1403
|
})),
|
|
1290
1404
|
seen: new Set(),
|
|
1291
1405
|
ends: new Map(
|
|
@@ -1429,6 +1543,7 @@ function follow(
|
|
|
1429
1543
|
}
|
|
1430
1544
|
const workers = new Set(f.workers)
|
|
1431
1545
|
const free = new Map(f.free)
|
|
1546
|
+
const given = new Map(f.given)
|
|
1432
1547
|
const join = new Map(f.join)
|
|
1433
1548
|
const fresh = [...f.fresh]
|
|
1434
1549
|
const release: string[] = []
|
|
@@ -1439,6 +1554,7 @@ function follow(
|
|
|
1439
1554
|
if (next === undefined) return undefined
|
|
1440
1555
|
workers.add(w.id)
|
|
1441
1556
|
free.set(w.id, next.free)
|
|
1557
|
+
given.set(w.id, next.given)
|
|
1442
1558
|
join.set(w.id, next.join)
|
|
1443
1559
|
}
|
|
1444
1560
|
// Joined sooner or later than played, past the margin.
|
|
@@ -1459,6 +1575,11 @@ function follow(
|
|
|
1459
1575
|
// and one let go still counts against its size's max. A Mac let go
|
|
1460
1576
|
// so left a Mac task no worker, and the run never ended (#4989).
|
|
1461
1577
|
if (f.until < Infinity) return undefined
|
|
1578
|
+
// A task planned on it not started: its last one ended and the next
|
|
1579
|
+
// not handed to it yet. Let go, that one had no worker and the run
|
|
1580
|
+
// never ended (a two-task chain on a max-1 size, its rest within the
|
|
1581
|
+
// slack). Played again, the plan sees what waits for it.
|
|
1582
|
+
if (given.get(w.id)?.some((id) => !f.seen.has(id)) === true) return undefined
|
|
1462
1583
|
release.push(w.id)
|
|
1463
1584
|
workers.delete(w.id)
|
|
1464
1585
|
}
|
|
@@ -1466,7 +1587,7 @@ function follow(
|
|
|
1466
1587
|
const start: Size[] = []
|
|
1467
1588
|
const later = [...f.later]
|
|
1468
1589
|
while (later.length > 0 && now >= later[0]!.at) {
|
|
1469
|
-
const { size, free: ends, join: joins } = later.shift()!
|
|
1590
|
+
const { size, free: ends, join: joins, given: runs } = later.shift()!
|
|
1470
1591
|
// Its size taken up since: the plan no longer fits the run. Read
|
|
1471
1592
|
// from what the run started, not from every task: each start read
|
|
1472
1593
|
// them all (2026-11-05).
|
|
@@ -1481,13 +1602,14 @@ function follow(
|
|
|
1481
1602
|
return undefined
|
|
1482
1603
|
} else if (!sizes.some((s) => s.name === size.name)) return undefined
|
|
1483
1604
|
start.push(size)
|
|
1484
|
-
fresh.push({ free: ends, join: joins })
|
|
1605
|
+
fresh.push({ free: ends, join: joins, given: runs })
|
|
1485
1606
|
}
|
|
1486
1607
|
const next: Forecast = {
|
|
1487
1608
|
...f,
|
|
1488
1609
|
workers,
|
|
1489
1610
|
count: f.count + start.length - release.length,
|
|
1490
1611
|
free,
|
|
1612
|
+
given,
|
|
1491
1613
|
join,
|
|
1492
1614
|
fresh,
|
|
1493
1615
|
later,
|
|
@@ -2052,8 +2174,14 @@ export function advise(
|
|
|
2052
2174
|
if (simulate(tasks, all.slice(0, mid), how) <= bound) hi = mid
|
|
2053
2175
|
else lo = mid + 1
|
|
2054
2176
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2177
|
+
// Then each the run ends as soon without, as the live plan trims.
|
|
2178
|
+
const { kept, before } = dropSpare(
|
|
2179
|
+
all.slice(0, lo),
|
|
2180
|
+
all.slice(0, lo),
|
|
2181
|
+
play(tasks, all.slice(0, lo), how),
|
|
2182
|
+
(ws) => play(tasks, ws, how),
|
|
2183
|
+
)
|
|
2184
|
+
return { fleets: { fleet: fleetOf(kept, of), endMs: before.end }, kept }
|
|
2057
2185
|
}
|
|
2058
2186
|
const declared = sizes.length === 0 ? undefined : halved(sizes)?.fleets
|
|
2059
2187
|
const lifted = sizes.map((s) => ({ ...s, max: Math.max(s.max, capOf(s.cpu, s.platform)) }))
|