@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The critical-path score a `schedule` plugin feeds the scheduler, from
|
|
2
|
+
// whatever timings it learned. It imports only types, so the Learn page's
|
|
3
|
+
// scheduler simulator (through private/vx-bench/schedule-policy.ts) bundles
|
|
4
|
+
// it for the browser without the rest of `@vzn/vx`.
|
|
5
|
+
|
|
6
|
+
import type { TaskNode } from './task-graph.js'
|
|
7
|
+
|
|
8
|
+
/** Default duration when neither task history nor a workspace median exists. */
|
|
9
|
+
const DEFAULT_DURATION_MS = 1000
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* For each node, the expected remaining critical-path duration: its own p50
|
|
13
|
+
* plus the maximum over its dependents. A node with no history takes its
|
|
14
|
+
* assumed duration if one was given, else the workspace median; an empty
|
|
15
|
+
* history takes a flat default.
|
|
16
|
+
*/
|
|
17
|
+
export function criticalPath(
|
|
18
|
+
nodes: readonly TaskNode[],
|
|
19
|
+
history: ReadonlyMap<string, { readonly p50DurationMs?: number | undefined }>,
|
|
20
|
+
assume: Readonly<Record<string, number>> = {},
|
|
21
|
+
): ReadonlyMap<string, number> {
|
|
22
|
+
const p50s: number[] = []
|
|
23
|
+
for (const h of history.values()) {
|
|
24
|
+
if (h.p50DurationMs !== undefined) p50s.push(h.p50DurationMs)
|
|
25
|
+
}
|
|
26
|
+
p50s.sort((a, b) => a - b)
|
|
27
|
+
const workspaceMedian =
|
|
28
|
+
p50s.length > 0
|
|
29
|
+
? (p50s[Math.floor(p50s.length / 2)] ?? DEFAULT_DURATION_MS)
|
|
30
|
+
: DEFAULT_DURATION_MS
|
|
31
|
+
|
|
32
|
+
const dependentsOf = new Map<string, string[]>()
|
|
33
|
+
for (const n of nodes) {
|
|
34
|
+
for (const upstreamId of n.deps) {
|
|
35
|
+
const list = dependentsOf.get(upstreamId)
|
|
36
|
+
if (list) list.push(n.id)
|
|
37
|
+
else dependentsOf.set(upstreamId, [n.id])
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// A group runs nothing, so it has no history; the median would charge it
|
|
41
|
+
// a whole task and let a gate outrank the longest chain (PR #3286).
|
|
42
|
+
const ownDuration = (n: TaskNode): number =>
|
|
43
|
+
n.config.exec?.command === undefined
|
|
44
|
+
? 0
|
|
45
|
+
: (history.get(n.id)?.p50DurationMs ?? assume[n.id] ?? workspaceMedian)
|
|
46
|
+
|
|
47
|
+
// Reverse-topological pass: a node's value is final once every dependent's
|
|
48
|
+
// is, so start from the sinks (no dependents) and release each upstream
|
|
49
|
+
// when its last dependent has been scored.
|
|
50
|
+
const memo = new Map<string, number>()
|
|
51
|
+
const nodeById = new Map<string, TaskNode>(nodes.map((n) => [n.id, n]))
|
|
52
|
+
const pending = new Map<string, number>()
|
|
53
|
+
const queue: TaskNode[] = []
|
|
54
|
+
for (const n of nodes) {
|
|
55
|
+
const count = dependentsOf.get(n.id)?.length ?? 0
|
|
56
|
+
pending.set(n.id, count)
|
|
57
|
+
if (count === 0) queue.push(n)
|
|
58
|
+
}
|
|
59
|
+
let head = 0
|
|
60
|
+
while (head < queue.length) {
|
|
61
|
+
const n = queue[head++]!
|
|
62
|
+
let downstream = 0
|
|
63
|
+
for (const dep of dependentsOf.get(n.id) ?? []) {
|
|
64
|
+
const d = memo.get(dep) ?? 0
|
|
65
|
+
if (d > downstream) downstream = d
|
|
66
|
+
}
|
|
67
|
+
memo.set(n.id, ownDuration(n) + downstream)
|
|
68
|
+
for (const up of n.deps) {
|
|
69
|
+
const left = (pending.get(up) ?? 0) - 1
|
|
70
|
+
pending.set(up, left)
|
|
71
|
+
if (left === 0) {
|
|
72
|
+
const upNode = nodeById.get(up)
|
|
73
|
+
if (upNode) queue.push(upNode)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const n of nodes) if (!memo.has(n.id)) memo.set(n.id, ownDuration(n))
|
|
78
|
+
return memo
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TaskConfig } from '../config.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `build` a project gets when neither its config nor a plugin declared
|
|
5
|
+
* one (2026-10-04): a group behind `^build`, keyed on every file of
|
|
6
|
+
* the project. A package consumed as source then carries its files to every
|
|
7
|
+
* dependent behind `^build`, in the key and in `--affected`, and the chain
|
|
8
|
+
* through it to deeper builds holds. A keyed group, as a config may
|
|
9
|
+
* declare one (config-schema.ts).
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_BUILD: TaskConfig = Object.freeze({
|
|
12
|
+
dependsOn: Object.freeze(['^build']),
|
|
13
|
+
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/** The default `build`, which a menu of the tasks a user declared leaves out. */
|
|
17
|
+
export function isDefaultBuild(task: TaskConfig | undefined): boolean {
|
|
18
|
+
return task === DEFAULT_BUILD
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// What a task is, for its history: a task whose command changed under the
|
|
2
|
+
// same name is a new task, and its timings start over (2026-10-23: "task
|
|
3
|
+
// can disappear and appear as a completely new one with the same name").
|
|
4
|
+
// Only what decides what the command costs: the command and the values it
|
|
5
|
+
// is given (`env.define`). Inputs, outputs, cache, sandbox grants, timeout,
|
|
6
|
+
// description and dependencies change no cost, so editing them keeps the
|
|
7
|
+
// history.
|
|
8
|
+
|
|
9
|
+
import { xxh3hex } from '../util/index.js'
|
|
10
|
+
import type { TaskNode } from './task-graph.js'
|
|
11
|
+
|
|
12
|
+
const memo = new WeakMap<TaskNode, string | undefined>()
|
|
13
|
+
|
|
14
|
+
/** The task's definition hash; undefined for one with no command (a group). */
|
|
15
|
+
export function taskDefinition(node: TaskNode): string | undefined {
|
|
16
|
+
if (memo.has(node)) return memo.get(node)
|
|
17
|
+
const exec = node.config.exec
|
|
18
|
+
const def =
|
|
19
|
+
exec?.command === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: xxh3hex(
|
|
22
|
+
`${node.id}\0${exec.command}\0${JSON.stringify(
|
|
23
|
+
Object.entries(exec.env?.define ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
24
|
+
)}`,
|
|
25
|
+
)
|
|
26
|
+
memo.set(node, def)
|
|
27
|
+
return def
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Each task's definition (`taskDefinition`) by id, groups left out: what a history reader matches rows to. */
|
|
31
|
+
export function taskDefinitions(nodes: Iterable<TaskNode>): Map<string, string> {
|
|
32
|
+
const out = new Map<string, string>()
|
|
33
|
+
for (const n of nodes) {
|
|
34
|
+
const def = taskDefinition(n)
|
|
35
|
+
if (def !== undefined) out.set(n.id, def)
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
package/src/graph/folds.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// alone: the hash path, the sandbox's keyed set and `vx lock`'s
|
|
3
3
|
// input-overlap rule read this one selection, so they cannot drift.
|
|
4
4
|
|
|
5
|
-
import { UserError } from '../util/index.js'
|
|
6
5
|
import {
|
|
6
|
+
UserError,
|
|
7
7
|
DependencySpecError,
|
|
8
8
|
compileTaskPattern,
|
|
9
9
|
isTaskPattern,
|
|
10
10
|
parseDependencySpec,
|
|
11
11
|
type DependencySpec,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../util/index.js'
|
|
13
13
|
import type { TaskNode } from './task-graph.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -49,7 +49,6 @@ export function refuseFiltersThroughGroups(nodes: ReadonlyMap<string, TaskNode>)
|
|
|
49
49
|
if (filter === undefined || filter.length === 0) continue
|
|
50
50
|
const deps = keyedDeps(node).flatMap((id) => nodes.get(id) ?? [])
|
|
51
51
|
const groups = deps.filter(isGroupTask)
|
|
52
|
-
if (groups.length === 0) continue
|
|
53
52
|
for (const raw of filter) {
|
|
54
53
|
const spec = parseSpec(raw, node.id)
|
|
55
54
|
if (spec.negated) continue
|
|
@@ -63,16 +62,50 @@ export function refuseFiltersThroughGroups(nodes: ReadonlyMap<string, TaskNode>)
|
|
|
63
62
|
`${node.id}: inputs.tasks: '${raw}' matches ${member.id} only through the group ${group.id}, so it selects nothing; name '${group.taskName}' or depend on ${member.id} directly`,
|
|
64
63
|
)
|
|
65
64
|
}
|
|
65
|
+
if (namesNoDependency(spec, node.config.dependsOn ?? [])) {
|
|
66
|
+
const named = deps.map((d) => d.id).join(', ') || 'none'
|
|
67
|
+
throw new UserError(
|
|
68
|
+
`${node.id}: inputs.tasks: '${raw}' names none of its dependencies (${named}), so it selects nothing and the key would not move with them; fix the name, or use [] to decouple on purpose`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
/**
|
|
76
|
+
* An exact filter that matched no dependency, where no `dependsOn` entry
|
|
77
|
+
* may resolve to nothing for it. The load pairs a `pkg#task` entry, or a
|
|
78
|
+
* `pkg#task` dependency, with any form whose task half matches, since it
|
|
79
|
+
* cannot tell this project's name from another's
|
|
80
|
+
* (`assertFilterNamesDeclaredDeps`), so `['nope#build']` over
|
|
81
|
+
* `dependsOn: ['build']`, `['gen']` over `['b#gen']` and `['^gen']` over
|
|
82
|
+
* this project's own `['a#gen']` each selected nothing and the key stopped
|
|
83
|
+
* moving with the dependency: a stale hit. A `^` entry or a pattern naming
|
|
84
|
+
* the task may match nothing in a preset's project, as a `^build` filter
|
|
85
|
+
* may, so it lets the entry through.
|
|
86
|
+
*/
|
|
87
|
+
function namesNoDependency(spec: DependencySpec, dependsOn: readonly string[]): boolean {
|
|
88
|
+
if (spec.kind === 'wildcardSelf' || spec.kind === 'wildcardDeps' || isTaskPattern(spec.task))
|
|
89
|
+
return false
|
|
90
|
+
if (spec.kind === 'cross' && isTaskPattern(spec.project)) return false
|
|
91
|
+
return !dependsOn.some((raw) => {
|
|
92
|
+
let d: DependencySpec
|
|
93
|
+
try {
|
|
94
|
+
d = parseDependencySpec(raw, 'dependsOn')
|
|
95
|
+
} catch {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
if (d.kind !== 'self' && d.kind !== 'deps' && d.kind !== 'cross') return false
|
|
99
|
+
return (d.kind === 'deps' || isTaskPattern(d.task)) && nameMatcher(d.task)(spec.task)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
function parseSpec(raw: string, selfTaskId: string): DependencySpec {
|
|
71
104
|
try {
|
|
72
|
-
return parseDependencySpec(raw)
|
|
105
|
+
return parseDependencySpec(raw, 'inputs.tasks')
|
|
73
106
|
} catch (err) {
|
|
74
107
|
if (err instanceof DependencySpecError) {
|
|
75
|
-
throw new UserError(`${selfTaskId}:
|
|
108
|
+
throw new UserError(`${selfTaskId}: ${err.message}`)
|
|
76
109
|
}
|
|
77
110
|
throw err
|
|
78
111
|
}
|
package/src/graph/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export {
|
|
|
7
7
|
isTaskPattern,
|
|
8
8
|
parseDependencySpec,
|
|
9
9
|
type DependencySpec,
|
|
10
|
-
} from '
|
|
10
|
+
} from '../util/index.js'
|
|
11
11
|
export {
|
|
12
12
|
type FoldCandidate,
|
|
13
13
|
foldedDeps,
|
|
@@ -16,7 +16,10 @@ export {
|
|
|
16
16
|
refuseFiltersThroughGroups,
|
|
17
17
|
selectFoldedDeps,
|
|
18
18
|
} from './folds.js'
|
|
19
|
+
export { DEFAULT_BUILD, isDefaultBuild } from './default-build.js'
|
|
19
20
|
export { computeReverseDepCount } from './priorities.js'
|
|
21
|
+
export { criticalPath } from './critical-path.js'
|
|
22
|
+
export { taskDefinition, taskDefinitions } from './definition.js'
|
|
20
23
|
export {
|
|
21
24
|
type ContinueMode,
|
|
22
25
|
deadServerBehind,
|
package/src/graph/priorities.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The ready-queue ranking, apart from the scheduler that uses it. Nothing
|
|
2
2
|
// here touches the platform: the Learn page's scheduler simulator
|
|
3
|
-
// (
|
|
3
|
+
// (private/vx-docs, through private/vx-bench/schedule-policy.ts) bundles
|
|
4
4
|
// these two functions for the browser, and an import of the scheduler would
|
|
5
5
|
// bring `util/` and its load-time `Bun` reads with them.
|
|
6
6
|
|
|
@@ -10,7 +10,8 @@ import type { TaskNode } from './task-graph.js'
|
|
|
10
10
|
* `computeReverseDepCount` for a run with a restore tier (item 754). A
|
|
11
11
|
* restore-tier task is a confirmed hit that never waits on its deps, so it
|
|
12
12
|
* blocks only the exec-tier tasks that depend on it: an exec-tier task's
|
|
13
|
-
* count is taken over the exec tier
|
|
13
|
+
* count is taken over the exec tier, an edge through a restore it holds
|
|
14
|
+
* back counted too, and a restore's
|
|
14
15
|
* is the sum over its direct exec-tier dependents of one plus theirs, plus
|
|
15
16
|
* the weight of each restore that depends on it, so a restore that feeds
|
|
16
17
|
* pending work, directly or through other restores, still goes first. A warm run's exec tier
|
|
@@ -37,16 +38,22 @@ export function tieredReverseDepCount(
|
|
|
37
38
|
for (const id of nodes.keys()) zeros.set(id, 0)
|
|
38
39
|
return zeros
|
|
39
40
|
}
|
|
41
|
+
// A restore releases its dependents only once its own deps have settled
|
|
42
|
+
// (scheduler.ts, item 963), so an exec task under a restore blocks what
|
|
43
|
+
// that restore feeds: e1 → r → e2 held e2 until e1 ran, and e1 ranked 0
|
|
44
|
+
// behind every idle task. The exec tier is counted with each restore dep
|
|
45
|
+
// replaced by the exec tasks it waits on, through restores.
|
|
46
|
+
const view = execThroughRestores(nodes, exec, restoreTier)
|
|
40
47
|
// An exec tier with no edge inside it (a warm run's groups) counts 0
|
|
41
48
|
// everywhere: no closure to build.
|
|
42
49
|
let linked = false
|
|
43
|
-
for (const node of
|
|
44
|
-
if (node.deps.some((d) =>
|
|
50
|
+
for (const node of view.values()) {
|
|
51
|
+
if (node.deps.some((d) => view.has(d))) {
|
|
45
52
|
linked = true
|
|
46
53
|
break
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
const counts = linked ? computeReverseDepCount(
|
|
56
|
+
const counts = linked ? computeReverseDepCount(view) : new Map<string, number>()
|
|
50
57
|
if (!linked) for (const id of exec.keys()) counts.set(id, 0)
|
|
51
58
|
let feeds = false
|
|
52
59
|
for (const node of exec.values()) {
|
|
@@ -89,6 +96,52 @@ export function tieredReverseDepCount(
|
|
|
89
96
|
return counts
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The exec tier with each restore dep replaced by the exec tasks that
|
|
101
|
+
* restore waits on (directly or through other restores). A node with no
|
|
102
|
+
* restore dep is itself; the restore tier is walked once, post-order.
|
|
103
|
+
*/
|
|
104
|
+
function execThroughRestores(
|
|
105
|
+
nodes: Map<string, TaskNode>,
|
|
106
|
+
exec: Map<string, TaskNode>,
|
|
107
|
+
restoreTier: ReadonlySet<string>,
|
|
108
|
+
): Map<string, TaskNode> {
|
|
109
|
+
const isRestore = (id: string): boolean => restoreTier.has(id) && nodes.has(id)
|
|
110
|
+
const below = new Map<string, readonly string[]>()
|
|
111
|
+
const belowOf = (root: string): readonly string[] => {
|
|
112
|
+
const stack: Array<[id: string, depsDone: boolean]> = [[root, false]]
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const [id, depsDone] = stack.pop()!
|
|
115
|
+
if (below.has(id)) continue
|
|
116
|
+
const deps = nodes.get(id)!.deps
|
|
117
|
+
if (!depsDone) {
|
|
118
|
+
stack.push([id, true])
|
|
119
|
+
for (const d of deps) if (isRestore(d) && !below.has(d)) stack.push([d, false])
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const out = new Set<string>()
|
|
123
|
+
for (const d of deps) {
|
|
124
|
+
if (exec.has(d)) out.add(d)
|
|
125
|
+
else for (const e of below.get(d) ?? []) out.add(e)
|
|
126
|
+
}
|
|
127
|
+
below.set(id, [...out])
|
|
128
|
+
}
|
|
129
|
+
return below.get(root)!
|
|
130
|
+
}
|
|
131
|
+
let view: Map<string, TaskNode> | undefined
|
|
132
|
+
for (const node of exec.values()) {
|
|
133
|
+
if (!node.deps.some(isRestore)) continue
|
|
134
|
+
const deps = new Set<string>()
|
|
135
|
+
for (const d of node.deps) {
|
|
136
|
+
if (isRestore(d)) for (const e of belowOf(d)) deps.add(e)
|
|
137
|
+
else deps.add(d)
|
|
138
|
+
}
|
|
139
|
+
view ??= new Map(exec)
|
|
140
|
+
view.set(node.id, { ...node, deps: [...deps] })
|
|
141
|
+
}
|
|
142
|
+
return view ?? exec
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
/**
|
|
93
146
|
* Compute, for each task in the graph, how many OTHER tasks are
|
|
94
147
|
* transitively blocked on it. Tasks with the highest count are the
|
|
@@ -98,7 +151,11 @@ export function tieredReverseDepCount(
|
|
|
98
151
|
* dependents only (`calculateReverseDeps` in
|
|
99
152
|
* `packages/nx/src/tasks-runner/utils.ts`, read 2026-09-24).
|
|
100
153
|
*/
|
|
101
|
-
export function computeReverseDepCount(
|
|
154
|
+
export function computeReverseDepCount(
|
|
155
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
156
|
+
/** Count only these dependents (the exec tier); every one when absent. */
|
|
157
|
+
counted?: ReadonlyMap<string, unknown>,
|
|
158
|
+
): Map<string, number> {
|
|
102
159
|
// Exact transitive-dependent COUNTS need closure SETS (diamonds
|
|
103
160
|
// double-count under naive summing). Set-of-strings closures are
|
|
104
161
|
// O(N²) entries and took 8.5s on a 1090-package, 100-layer repo;
|
|
@@ -145,6 +202,10 @@ export function computeReverseDepCount(nodes: ReadonlyMap<string, TaskNode>): Ma
|
|
|
145
202
|
// half the words of indexing by insertion order on a layered graph.
|
|
146
203
|
const pos = new Int32Array(n)
|
|
147
204
|
for (let t = 0; t < tail; t++) pos[topo[t]!] = t
|
|
205
|
+
const mask = new Uint32Array(words)
|
|
206
|
+
if (counted === undefined) mask.fill(0xffffffff)
|
|
207
|
+
else
|
|
208
|
+
for (let t = 0; t < tail; t++) if (counted.has(ids[topo[t]!]!)) mask[t >>> 5]! |= 1 << (t & 31)
|
|
148
209
|
const closure = new Uint32Array(n * words)
|
|
149
210
|
const counts = new Map<string, number>()
|
|
150
211
|
for (let t = tail - 1; t >= 0; t--) {
|
|
@@ -158,7 +219,7 @@ export function computeReverseDepCount(nodes: ReadonlyMap<string, TaskNode>): Ma
|
|
|
158
219
|
}
|
|
159
220
|
let count = 0
|
|
160
221
|
for (let w = t >>> 5; w < words; w++) {
|
|
161
|
-
let v = closure[base + w]!
|
|
222
|
+
let v = closure[base + w]! & mask[w]!
|
|
162
223
|
v = v - ((v >>> 1) & 0x55555555)
|
|
163
224
|
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
|
|
164
225
|
count += (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24
|
|
@@ -240,11 +301,16 @@ export function mergePriorities(
|
|
|
240
301
|
// Baseline reverse-deps counts are bounded by N; scale the override so
|
|
241
302
|
// it sorts above the baseline for any node it covers, and add the
|
|
242
303
|
// baseline as a tie-break for parity within the override set.
|
|
304
|
+
// The lowest weight maps to one SCALE, not to 0: a weight of 0 (a p50
|
|
305
|
+
// that rounds to 0 ms) or below otherwise tied or sank under the
|
|
306
|
+
// unscored nodes it is meant to outrank.
|
|
243
307
|
const SCALE = 1 << 20
|
|
308
|
+
let lo = 0
|
|
309
|
+
for (const w of overrides.values()) if (w < lo) lo = w
|
|
244
310
|
const out = new Map(baseline)
|
|
245
311
|
for (const [id, w] of overrides) {
|
|
246
312
|
const b = baseline.get(id) ?? 0
|
|
247
|
-
out.set(id, w * SCALE + b)
|
|
313
|
+
out.set(id, (w - lo + 1) * SCALE + b)
|
|
248
314
|
}
|
|
249
315
|
return out
|
|
250
316
|
}
|