nx 23.1.0-beta.2 → 23.1.0-beta.4
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/dist/src/command-line/migrate/migrate.d.ts +2 -0
- package/dist/src/command-line/migrate/migrate.js +32 -6
- package/dist/src/daemon/server/project-graph-incremental-recomputation.js +30 -11
- package/dist/src/native/index.d.ts +48 -1
- package/dist/src/native/native-bindings.js +1 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/plugins/js/utils/register.d.ts +31 -10
- package/dist/src/plugins/js/utils/register.js +87 -18
- package/dist/src/tasks-runner/default-tasks-runner.js +6 -3
- package/dist/src/tasks-runner/init-tasks-runner.js +1 -1
- package/dist/src/tasks-runner/life-cycle.d.ts +10 -5
- package/dist/src/tasks-runner/life-cycle.js +4 -4
- package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +156 -0
- package/dist/src/tasks-runner/life-cycles/performance-analysis.js +486 -0
- package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +59 -0
- package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +152 -0
- package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +62 -0
- package/dist/src/tasks-runner/life-cycles/performance-report.js +318 -0
- package/dist/src/tasks-runner/run-command.d.ts +1 -1
- package/dist/src/tasks-runner/run-command.js +12 -2
- package/dist/src/utils/min-release-age/behavior/pnpm.d.ts +6 -5
- package/dist/src/utils/min-release-age/behavior/pnpm.js +87 -339
- package/dist/src/utils/package-manager.d.ts +6 -3
- package/dist/src/utils/package-manager.js +11 -7
- package/dist/src/utils/terminal-link.d.ts +10 -6
- package/dist/src/utils/terminal-link.js +9 -11
- package/package.json +11 -11
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { TaskGraph } from '../../config/task-graph';
|
|
2
|
+
import { NxJsonConfiguration } from '../../config/nx-json';
|
|
3
|
+
import { type Recommendation } from './performance-report';
|
|
4
|
+
import { TaskResult } from '../life-cycle';
|
|
5
|
+
export interface TaskTiming {
|
|
6
|
+
startTime?: number;
|
|
7
|
+
endTime?: number;
|
|
8
|
+
continuous: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A discrete task that ran, with its window and whether it monopolizes the pool (`parallelism: false`). */
|
|
11
|
+
export interface TimedTask {
|
|
12
|
+
id: string;
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
nonParallel: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One [start, end) slice of the run where the counts below hold steady — the gap between
|
|
19
|
+
* two adjacent sweep boundaries (see {@link buildTimespans}). Because nothing changes
|
|
20
|
+
* within the slice, the analysis treats it as a flat rectangle of `end - start` ms.
|
|
21
|
+
*/
|
|
22
|
+
export interface Timespan {
|
|
23
|
+
/** Slice bounds (epoch ms); occ/waiting/nonParallel are constant across [start, end). */
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
/** Slots busy for the whole slice (a `parallelism: false` task counts as the entire pool). */
|
|
27
|
+
occ: number;
|
|
28
|
+
/** Tasks eligible to run but still queued for a free slot. */
|
|
29
|
+
waiting: number;
|
|
30
|
+
/** `parallelism: false` tasks running during the slice (each one holds every slot). */
|
|
31
|
+
nonParallel: number;
|
|
32
|
+
}
|
|
33
|
+
export interface PerformanceSummary {
|
|
34
|
+
runDuration: number;
|
|
35
|
+
criticalPathDuration: number;
|
|
36
|
+
criticalPathTaskCount: number;
|
|
37
|
+
/** Longest critical-path tasks that ran (desc, capped at a few), cache hits excluded; empty when the path was fully cached. */
|
|
38
|
+
criticalPathTop: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
duration: number;
|
|
41
|
+
}>;
|
|
42
|
+
/** runDuration − criticalPathDuration. */
|
|
43
|
+
overhead: number;
|
|
44
|
+
/** Overhead split by lever; these + coordinatorOverhead sum to `overhead`. Their sum is the slot-contention time (derived, never stored, so the halves can't drift). */
|
|
45
|
+
recoverableByParallel: number;
|
|
46
|
+
recoverableByMachines: number;
|
|
47
|
+
coordinatorOverhead: number;
|
|
48
|
+
parallel: number;
|
|
49
|
+
cores: number;
|
|
50
|
+
isCI: boolean;
|
|
51
|
+
/** One structured rec per lever (with link parts); drives the report + payload renderers. */
|
|
52
|
+
structuredRecommendations: Recommendation[];
|
|
53
|
+
/** Coordinator overhead outweighs task work, so the longest tasks aren't the lever (typical cached run). */
|
|
54
|
+
coordinatorDominated: boolean;
|
|
55
|
+
cacheHits: number;
|
|
56
|
+
/** Tasks with any cache outcome (hits + tasks that ran). */
|
|
57
|
+
cacheableCount: number;
|
|
58
|
+
cacheSkipped: boolean;
|
|
59
|
+
remoteCacheEnabled: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Construction-time inputs for {@link PerformanceLifeCycle}. */
|
|
62
|
+
export interface PerformanceLifeCycleOptions {
|
|
63
|
+
/** Whether `--skip-nx-cache` was passed (feeds the cache-skipped check). */
|
|
64
|
+
skipNxCache?: boolean;
|
|
65
|
+
/** Passed in at construction so the lifecycle doesn't re-read nx.json. */
|
|
66
|
+
nxJson?: NxJsonConfiguration;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Pure analysis over one finished run's collected timings — no lifecycle state and no
|
|
70
|
+
* mutation. Built once per run from the {@link PerformanceLifeCycle}'s accumulated
|
|
71
|
+
* maps; everything here is a function of that snapshot plus the environment it reads.
|
|
72
|
+
*/
|
|
73
|
+
export declare class PerformanceAnalysis {
|
|
74
|
+
private readonly timings;
|
|
75
|
+
private readonly statuses;
|
|
76
|
+
private readonly taskGraph;
|
|
77
|
+
private readonly batchSiblings;
|
|
78
|
+
/** Resolved `--parallel` (getThreadPoolSize's `discrete`), set on the lifecycle by the runner; falls back to 1. */
|
|
79
|
+
private readonly parallel;
|
|
80
|
+
private readonly options;
|
|
81
|
+
constructor(timings: Map<string, TaskTiming>, statuses: Map<string, TaskResult['status']>, taskGraph: TaskGraph, batchSiblings: Map<string, string[]>,
|
|
82
|
+
/** Resolved `--parallel` (getThreadPoolSize's `discrete`), set on the lifecycle by the runner; falls back to 1. */
|
|
83
|
+
parallel: number, options: PerformanceLifeCycleOptions);
|
|
84
|
+
/**
|
|
85
|
+
* Whether a remote (Nx Cloud) cache is active, from the nx.json handed in at
|
|
86
|
+
* construction. Assumes no remote cache when it wasn't provided rather than
|
|
87
|
+
* re-reading nx.json from disk — the CLI path always provides it.
|
|
88
|
+
*/
|
|
89
|
+
private remoteCacheEnabled;
|
|
90
|
+
/** Discrete tasks that ran and have a start + end timestamp. */
|
|
91
|
+
private timedTasks;
|
|
92
|
+
/**
|
|
93
|
+
* Longest-duration chain through the dependency DAG — the floor with unlimited
|
|
94
|
+
* slots. `finishEstimate(t) = duration[t] + max(finishEstimate(p) for predecessors
|
|
95
|
+
* p)`. Predecessors are real dependencies *plus* earlier batch siblings (a batch runs
|
|
96
|
+
* sequentially in one process, so that ordering is part of the floor too). On equal
|
|
97
|
+
* finish estimates keep the longer-running predecessor.
|
|
98
|
+
*/
|
|
99
|
+
private criticalPath;
|
|
100
|
+
/**
|
|
101
|
+
* Real predecessors of `id`: dependencies plus any earlier batch sibling that FINISHED
|
|
102
|
+
* before `id` started. A batch runs sequentially in one process, so that ordering is
|
|
103
|
+
* part of the floor; concurrent batch executors aren't, and chaining them would inflate
|
|
104
|
+
* it. (Matches readyTime.)
|
|
105
|
+
*/
|
|
106
|
+
private predecessorsFor;
|
|
107
|
+
/**
|
|
108
|
+
* Longest finish time of any chain reaching `id` — its duration plus the best
|
|
109
|
+
* predecessor's finish estimate — memoized in `ctx`. `ctx.visiting` guards a cycle in
|
|
110
|
+
* a malformed graph (the back-edge contributes nothing).
|
|
111
|
+
*/
|
|
112
|
+
private finishEstimate;
|
|
113
|
+
/**
|
|
114
|
+
* Earliest this task became eligible, independent of slots: the latest of run
|
|
115
|
+
* start, dependency ends, any continuous dependency's *start* (an ordering
|
|
116
|
+
* constraint, not contention), and any earlier batch sibling's end.
|
|
117
|
+
*/
|
|
118
|
+
private readyTime;
|
|
119
|
+
/**
|
|
120
|
+
* Longest critical-path tasks that RAN (desc, capped at 3). Cache hits are excluded
|
|
121
|
+
* (their duration is just restore time); no-status tasks (synthetic test runs) are
|
|
122
|
+
* kept.
|
|
123
|
+
*/
|
|
124
|
+
private computeCriticalPathTop;
|
|
125
|
+
/** Cache outcome: tasks restored (`cacheHits`) and the total with a cache outcome (`cacheableCount` = hits + ran). No-status tasks count for neither. */
|
|
126
|
+
private computeCacheStats;
|
|
127
|
+
/** The structured performance summary, or `null` when no discrete task timings were recorded. */
|
|
128
|
+
summary(): PerformanceSummary | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Union a set of `[start, end]` intervals into an ordered, non-overlapping set, e.g.
|
|
132
|
+
* `[[1, 3], [2, 5]]` → `[[1, 5]]`. Sort by start, then sweep once: each interval
|
|
133
|
+
* either extends the previous merged one (if it overlaps or touches it) or begins a
|
|
134
|
+
* new one.
|
|
135
|
+
*/
|
|
136
|
+
export declare function mergeIntervals(intervals: Array<[number, number]>): Array<[number, number]>;
|
|
137
|
+
/** Total time [a, b] overlaps the given (unsorted, possibly overlapping) intervals. */
|
|
138
|
+
export declare function overlap(a: number, b: number, intervals: Array<[number, number]>): number;
|
|
139
|
+
/**
|
|
140
|
+
* Coordinator hashing wall-clock that ran *before* the first task started — the run
|
|
141
|
+
* window (first task start → last task end) would otherwise miss it. Matters on a
|
|
142
|
+
* cached run where hashing dominates but the tasks restore in milliseconds.
|
|
143
|
+
*
|
|
144
|
+
* `hashWindows` are absolute `[start, end]` spans from nx's `hash*` perf measures.
|
|
145
|
+
*/
|
|
146
|
+
export declare function preDispatchHashTime(firstTaskStart: number, hashWindows: Array<[number, number]>): number;
|
|
147
|
+
/**
|
|
148
|
+
* The occupancy timeline (contiguous {@link Timespan}s) — single source of truth for
|
|
149
|
+
* slot contention. A `parallelism: false` task occupies the whole pool. Integrates
|
|
150
|
+
* over time rather than sampling one instant, so it's robust to when tasks hand off.
|
|
151
|
+
*
|
|
152
|
+
* Builds it via a delta sweep: each task bumps a counter up at its start and down at
|
|
153
|
+
* its end, then we walk the sorted boundaries accumulating the running totals into one
|
|
154
|
+
* timespan per gap.
|
|
155
|
+
*/
|
|
156
|
+
export declare function buildTimespans(timed: TimedTask[], eligible: Map<string, number>, parallel: number): Timespan[];
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PerformanceAnalysis = void 0;
|
|
4
|
+
exports.mergeIntervals = mergeIntervals;
|
|
5
|
+
exports.overlap = overlap;
|
|
6
|
+
exports.preDispatchHashTime = preDispatchHashTime;
|
|
7
|
+
exports.buildTimespans = buildTimespans;
|
|
8
|
+
const tslib_1 = require("tslib");
|
|
9
|
+
const os = tslib_1.__importStar(require("node:os"));
|
|
10
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
11
|
+
const nx_cloud_utils_1 = require("../../utils/nx-cloud-utils");
|
|
12
|
+
const is_ci_1 = require("../../utils/is-ci");
|
|
13
|
+
const performance_report_1 = require("./performance-report");
|
|
14
|
+
const CACHE_HIT_STATUSES = new Set([
|
|
15
|
+
'local-cache',
|
|
16
|
+
'local-cache-kept-existing',
|
|
17
|
+
'remote-cache',
|
|
18
|
+
]);
|
|
19
|
+
/** Gap (ms) still counted as the same pre-dispatch hashing phase; a larger gap means older, unrelated hashing (e.g. a daemon's previous run). */
|
|
20
|
+
const PRE_DISPATCH_HASH_GAP = 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Pure analysis over one finished run's collected timings — no lifecycle state and no
|
|
23
|
+
* mutation. Built once per run from the {@link PerformanceLifeCycle}'s accumulated
|
|
24
|
+
* maps; everything here is a function of that snapshot plus the environment it reads.
|
|
25
|
+
*/
|
|
26
|
+
class PerformanceAnalysis {
|
|
27
|
+
constructor(timings, statuses, taskGraph, batchSiblings,
|
|
28
|
+
/** Resolved `--parallel` (getThreadPoolSize's `discrete`), set on the lifecycle by the runner; falls back to 1. */
|
|
29
|
+
parallel, options) {
|
|
30
|
+
this.timings = timings;
|
|
31
|
+
this.statuses = statuses;
|
|
32
|
+
this.taskGraph = taskGraph;
|
|
33
|
+
this.batchSiblings = batchSiblings;
|
|
34
|
+
this.parallel = parallel;
|
|
35
|
+
this.options = options;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a remote (Nx Cloud) cache is active, from the nx.json handed in at
|
|
39
|
+
* construction. Assumes no remote cache when it wasn't provided rather than
|
|
40
|
+
* re-reading nx.json from disk — the CLI path always provides it.
|
|
41
|
+
*/
|
|
42
|
+
remoteCacheEnabled() {
|
|
43
|
+
return this.options.nxJson ? (0, nx_cloud_utils_1.isNxCloudUsed)(this.options.nxJson) : false;
|
|
44
|
+
}
|
|
45
|
+
// === Analysis (pure functions of the collected timings + task graph) ===
|
|
46
|
+
/** Discrete tasks that ran and have a start + end timestamp. */
|
|
47
|
+
timedTasks() {
|
|
48
|
+
const result = [];
|
|
49
|
+
for (const [id, t] of this.timings) {
|
|
50
|
+
if (!t.continuous && t.startTime != null && t.endTime != null) {
|
|
51
|
+
result.push({
|
|
52
|
+
id,
|
|
53
|
+
start: t.startTime,
|
|
54
|
+
end: t.endTime,
|
|
55
|
+
// A parallelism:false task occupies the whole pool; carried here so the
|
|
56
|
+
// occupancy timeline (buildTimespans) stays a pure function of `timed`.
|
|
57
|
+
nonParallel: this.taskGraph.tasks[id]?.parallelism === false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Longest-duration chain through the dependency DAG — the floor with unlimited
|
|
65
|
+
* slots. `finishEstimate(t) = duration[t] + max(finishEstimate(p) for predecessors
|
|
66
|
+
* p)`. Predecessors are real dependencies *plus* earlier batch siblings (a batch runs
|
|
67
|
+
* sequentially in one process, so that ordering is part of the floor too). On equal
|
|
68
|
+
* finish estimates keep the longer-running predecessor.
|
|
69
|
+
*/
|
|
70
|
+
criticalPath(durations) {
|
|
71
|
+
const ctx = {
|
|
72
|
+
durations,
|
|
73
|
+
memo: new Map(),
|
|
74
|
+
pred: new Map(),
|
|
75
|
+
visiting: new Set(),
|
|
76
|
+
};
|
|
77
|
+
let terminal = null;
|
|
78
|
+
let terminalFinish = -1;
|
|
79
|
+
let terminalDur = -1;
|
|
80
|
+
for (const id of durations.keys()) {
|
|
81
|
+
const f = this.finishEstimate(id, ctx);
|
|
82
|
+
const d = durations.get(id) ?? 0;
|
|
83
|
+
if (finishesLater(f, d, terminalFinish, terminalDur)) {
|
|
84
|
+
terminalFinish = f;
|
|
85
|
+
terminalDur = d;
|
|
86
|
+
terminal = id;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
path: tracePath(terminal, ctx.pred),
|
|
91
|
+
duration: terminal == null ? 0 : terminalFinish,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Real predecessors of `id`: dependencies plus any earlier batch sibling that FINISHED
|
|
96
|
+
* before `id` started. A batch runs sequentially in one process, so that ordering is
|
|
97
|
+
* part of the floor; concurrent batch executors aren't, and chaining them would inflate
|
|
98
|
+
* it. (Matches readyTime.)
|
|
99
|
+
*/
|
|
100
|
+
predecessorsFor(id, durations) {
|
|
101
|
+
const start = this.timings.get(id)?.startTime ?? Infinity;
|
|
102
|
+
const deps = (this.taskGraph.dependencies[id] ?? []).filter((d) => durations.has(d));
|
|
103
|
+
const siblings = (this.batchSiblings.get(id) ?? []).filter((s) => {
|
|
104
|
+
const sEnd = this.timings.get(s)?.endTime;
|
|
105
|
+
return durations.has(s) && sEnd != null && sEnd <= start;
|
|
106
|
+
});
|
|
107
|
+
return [...deps, ...siblings];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Longest finish time of any chain reaching `id` — its duration plus the best
|
|
111
|
+
* predecessor's finish estimate — memoized in `ctx`. `ctx.visiting` guards a cycle in
|
|
112
|
+
* a malformed graph (the back-edge contributes nothing).
|
|
113
|
+
*/
|
|
114
|
+
finishEstimate(id, ctx) {
|
|
115
|
+
const cached = ctx.memo.get(id);
|
|
116
|
+
if (cached != null) {
|
|
117
|
+
return cached;
|
|
118
|
+
}
|
|
119
|
+
if (ctx.visiting.has(id)) {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
ctx.visiting.add(id);
|
|
123
|
+
let best = 0;
|
|
124
|
+
let bestPred = null;
|
|
125
|
+
let bestDur = -1;
|
|
126
|
+
for (const p of this.predecessorsFor(id, ctx.durations)) {
|
|
127
|
+
const f = this.finishEstimate(p, ctx);
|
|
128
|
+
const d = ctx.durations.get(p) ?? 0;
|
|
129
|
+
if (finishesLater(f, d, best, bestDur)) {
|
|
130
|
+
best = f;
|
|
131
|
+
bestPred = p;
|
|
132
|
+
bestDur = d;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ctx.visiting.delete(id);
|
|
136
|
+
const value = best + (ctx.durations.get(id) ?? 0);
|
|
137
|
+
ctx.memo.set(id, value);
|
|
138
|
+
ctx.pred.set(id, bestPred);
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Earliest this task became eligible, independent of slots: the latest of run
|
|
143
|
+
* start, dependency ends, any continuous dependency's *start* (an ordering
|
|
144
|
+
* constraint, not contention), and any earlier batch sibling's end.
|
|
145
|
+
*/
|
|
146
|
+
readyTime(id, runStart) {
|
|
147
|
+
const start = this.timings.get(id)?.startTime;
|
|
148
|
+
let result = runStart;
|
|
149
|
+
for (const dep of this.taskGraph.dependencies[id] ?? []) {
|
|
150
|
+
const end = this.timings.get(dep)?.endTime;
|
|
151
|
+
if (end != null) {
|
|
152
|
+
result = Math.max(result, end);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const cdep of this.taskGraph.continuousDependencies?.[id] ?? []) {
|
|
156
|
+
const cStart = this.timings.get(cdep)?.startTime;
|
|
157
|
+
if (cStart != null) {
|
|
158
|
+
result = Math.max(result, cStart);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const sibling of this.batchSiblings.get(id) ?? []) {
|
|
162
|
+
const end = this.timings.get(sibling)?.endTime;
|
|
163
|
+
if (end != null && start != null && end <= start) {
|
|
164
|
+
result = Math.max(result, end);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Longest critical-path tasks that RAN (desc, capped at 3). Cache hits are excluded
|
|
171
|
+
* (their duration is just restore time); no-status tasks (synthetic test runs) are
|
|
172
|
+
* kept.
|
|
173
|
+
*/
|
|
174
|
+
computeCriticalPathTop(criticalPathTasks, durations) {
|
|
175
|
+
return criticalPathTasks
|
|
176
|
+
.filter((id) => {
|
|
177
|
+
const status = this.statuses.get(id);
|
|
178
|
+
return !status || !CACHE_HIT_STATUSES.has(status);
|
|
179
|
+
})
|
|
180
|
+
.map((id) => ({ id, duration: durations.get(id) ?? 0 }))
|
|
181
|
+
.sort((a, b) => b.duration - a.duration)
|
|
182
|
+
.slice(0, 3);
|
|
183
|
+
}
|
|
184
|
+
/** Cache outcome: tasks restored (`cacheHits`) and the total with a cache outcome (`cacheableCount` = hits + ran). No-status tasks count for neither. */
|
|
185
|
+
computeCacheStats() {
|
|
186
|
+
let cacheHits = 0;
|
|
187
|
+
let cacheRan = 0;
|
|
188
|
+
for (const status of this.statuses.values()) {
|
|
189
|
+
if (CACHE_HIT_STATUSES.has(status)) {
|
|
190
|
+
cacheHits++;
|
|
191
|
+
}
|
|
192
|
+
else if (status === 'success') {
|
|
193
|
+
cacheRan++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { cacheHits, cacheableCount: cacheHits + cacheRan };
|
|
197
|
+
}
|
|
198
|
+
/** The structured performance summary, or `null` when no discrete task timings were recorded. */
|
|
199
|
+
summary() {
|
|
200
|
+
const timed = this.timedTasks();
|
|
201
|
+
if (timed.length === 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const { durations, totalWork, runStart, taskWindow } = computeRunWindow(timed);
|
|
205
|
+
const eligible = new Map();
|
|
206
|
+
for (const { id } of timed) {
|
|
207
|
+
eligible.set(id, this.readyTime(id, runStart));
|
|
208
|
+
}
|
|
209
|
+
const { path: criticalPathTasks, duration: criticalPathDuration } = this.criticalPath(durations);
|
|
210
|
+
const parallel = Math.max(1, this.parallel);
|
|
211
|
+
const cores = detectCoreCount();
|
|
212
|
+
const timespans = buildTimespans(timed, eligible, parallel);
|
|
213
|
+
const hashWindows = collectHashWindows();
|
|
214
|
+
// Pre-dispatch hashing is part of overhead but not the slot split.
|
|
215
|
+
const runDuration = taskWindow + preDispatchHashTime(runStart, hashWindows);
|
|
216
|
+
const overhead = Math.max(0, runDuration - criticalPathDuration);
|
|
217
|
+
const { coordinatorOverhead, recoverableByMachines, recoverableByParallel, } = splitOverhead({
|
|
218
|
+
timespans,
|
|
219
|
+
parallel,
|
|
220
|
+
overhead,
|
|
221
|
+
totalWork,
|
|
222
|
+
cores,
|
|
223
|
+
criticalPathDuration,
|
|
224
|
+
});
|
|
225
|
+
const criticalPathTop = this.computeCriticalPathTop(criticalPathTasks, durations);
|
|
226
|
+
const { cacheHits, cacheableCount } = this.computeCacheStats();
|
|
227
|
+
// `skipNxCache` already folds in NX_SKIP_NX_CACHE / NX_DISABLE_NX_CACHE
|
|
228
|
+
// (normalized in command-line-utils) — don't re-read those env vars here.
|
|
229
|
+
const cacheSkipped = this.options.skipNxCache === true;
|
|
230
|
+
const remoteCacheEnabled = this.remoteCacheEnabled();
|
|
231
|
+
// Coordinator-dominated: hashing/scheduling outweighs task work by >3x the
|
|
232
|
+
// critical path, which keeps cold runs critical-path-bound.
|
|
233
|
+
const coordinatorDominated = coordinatorOverhead >= performance_report_1.MEANINGFUL_OVERHEAD &&
|
|
234
|
+
coordinatorOverhead > 3 * criticalPathDuration;
|
|
235
|
+
// isCiEnv() checks the full set of CI env vars, not just `CI`.
|
|
236
|
+
const isCI = !!(0, is_ci_1.isCI)();
|
|
237
|
+
// TODO: source from the light client's isDistributedExecution() rather than the env var.
|
|
238
|
+
const distributing = !!process.env.NX_CLOUD_DISTRIBUTED_EXECUTION_AGENT_COUNT;
|
|
239
|
+
const structuredRecommendations = (0, performance_report_1.buildRecommendation)({
|
|
240
|
+
recoverableByParallel,
|
|
241
|
+
recoverableByMachines,
|
|
242
|
+
coordinatorDominated,
|
|
243
|
+
runDuration,
|
|
244
|
+
parallel,
|
|
245
|
+
cores,
|
|
246
|
+
// Can only start distributing in CI when not already doing so.
|
|
247
|
+
canDistribute: isCI && !distributing,
|
|
248
|
+
distributing,
|
|
249
|
+
criticalPathTop,
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
runDuration,
|
|
253
|
+
criticalPathDuration,
|
|
254
|
+
criticalPathTaskCount: criticalPathTasks.length,
|
|
255
|
+
criticalPathTop,
|
|
256
|
+
overhead,
|
|
257
|
+
recoverableByParallel,
|
|
258
|
+
recoverableByMachines,
|
|
259
|
+
coordinatorOverhead,
|
|
260
|
+
parallel,
|
|
261
|
+
cores,
|
|
262
|
+
isCI,
|
|
263
|
+
structuredRecommendations,
|
|
264
|
+
coordinatorDominated,
|
|
265
|
+
cacheHits,
|
|
266
|
+
cacheableCount,
|
|
267
|
+
cacheSkipped,
|
|
268
|
+
remoteCacheEnabled,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
exports.PerformanceAnalysis = PerformanceAnalysis;
|
|
273
|
+
// === Analysis helpers (module-level; no instance state) ===
|
|
274
|
+
/**
|
|
275
|
+
* True when a `(finishEstimate, duration)` candidate finishes later than the current
|
|
276
|
+
* best — higher finish wins, ties broken by the longer-running task.
|
|
277
|
+
*/
|
|
278
|
+
function finishesLater(finish, dur, bestFinish, bestDur) {
|
|
279
|
+
return finish > bestFinish || (finish === bestFinish && dur > bestDur);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Follow the recorded best-predecessor links from `end` back to the root, returning
|
|
283
|
+
* the path root → end. `seen` guards against a cycle in a malformed `pred` chain.
|
|
284
|
+
*/
|
|
285
|
+
function tracePath(end, pred) {
|
|
286
|
+
const path = [];
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (let node = end; node != null && !seen.has(node); node = pred.get(node) ?? null) {
|
|
289
|
+
seen.add(node);
|
|
290
|
+
path.unshift(node);
|
|
291
|
+
}
|
|
292
|
+
return path;
|
|
293
|
+
}
|
|
294
|
+
/** Absolute-epoch [start, end] hashing windows from nx's existing `hash*` perf measures; [] if the performance API is unavailable. */
|
|
295
|
+
function collectHashWindows() {
|
|
296
|
+
try {
|
|
297
|
+
const origin = node_perf_hooks_1.performance.timeOrigin;
|
|
298
|
+
return node_perf_hooks_1.performance
|
|
299
|
+
.getEntriesByType('measure')
|
|
300
|
+
.filter((m) => m.name.startsWith('hash'))
|
|
301
|
+
.map((m) => [
|
|
302
|
+
origin + m.startTime,
|
|
303
|
+
origin + m.startTime + m.duration,
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Union a set of `[start, end]` intervals into an ordered, non-overlapping set, e.g.
|
|
312
|
+
* `[[1, 3], [2, 5]]` → `[[1, 5]]`. Sort by start, then sweep once: each interval
|
|
313
|
+
* either extends the previous merged one (if it overlaps or touches it) or begins a
|
|
314
|
+
* new one.
|
|
315
|
+
*/
|
|
316
|
+
function mergeIntervals(intervals) {
|
|
317
|
+
const byStart = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
318
|
+
const merged = [];
|
|
319
|
+
for (const [start, end] of byStart) {
|
|
320
|
+
const previous = merged[merged.length - 1];
|
|
321
|
+
// `previous[1]` is the running end of the last merged interval. This interval
|
|
322
|
+
// overlaps or touches it when its start is at or before that end — so absorb it
|
|
323
|
+
// by extending the end. Otherwise it's disjoint and starts a new interval.
|
|
324
|
+
const overlapsPrevious = previous != null && start <= previous[1];
|
|
325
|
+
if (overlapsPrevious) {
|
|
326
|
+
previous[1] = Math.max(previous[1], end);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
merged.push([start, end]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return merged;
|
|
333
|
+
}
|
|
334
|
+
/** Total time [a, b] overlaps the given (unsorted, possibly overlapping) intervals. */
|
|
335
|
+
function overlap(a, b, intervals) {
|
|
336
|
+
let sum = 0;
|
|
337
|
+
for (const [s, e] of intervals) {
|
|
338
|
+
sum += Math.max(0, Math.min(b, e) - Math.max(a, s));
|
|
339
|
+
}
|
|
340
|
+
return sum;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Coordinator hashing wall-clock that ran *before* the first task started — the run
|
|
344
|
+
* window (first task start → last task end) would otherwise miss it. Matters on a
|
|
345
|
+
* cached run where hashing dominates but the tasks restore in milliseconds.
|
|
346
|
+
*
|
|
347
|
+
* `hashWindows` are absolute `[start, end]` spans from nx's `hash*` perf measures.
|
|
348
|
+
*/
|
|
349
|
+
function preDispatchHashTime(firstTaskStart, hashWindows) {
|
|
350
|
+
// Keep only the part of each window before the first task, drop any that become
|
|
351
|
+
// empty, then union overlaps so shared time isn't counted twice.
|
|
352
|
+
const preDispatchWindows = mergeIntervals(hashWindows
|
|
353
|
+
.map(([start, end]) => [
|
|
354
|
+
start,
|
|
355
|
+
Math.min(end, firstTaskStart),
|
|
356
|
+
])
|
|
357
|
+
.filter(([start, end]) => end > start));
|
|
358
|
+
// Walk newest → oldest, adding each window while it stays contiguous with the one
|
|
359
|
+
// after it. Stop at the first gap wider than PRE_DISPATCH_HASH_GAP — anything older
|
|
360
|
+
// is stale hashing from an earlier run in this process (e.g. the daemon's).
|
|
361
|
+
let total = 0;
|
|
362
|
+
let contiguousFrom = firstTaskStart;
|
|
363
|
+
for (let i = preDispatchWindows.length - 1; i >= 0; i--) {
|
|
364
|
+
const [start, end] = preDispatchWindows[i];
|
|
365
|
+
const gapTooLarge = end < contiguousFrom - PRE_DISPATCH_HASH_GAP;
|
|
366
|
+
if (gapTooLarge) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
total += end - start;
|
|
370
|
+
contiguousFrom = start;
|
|
371
|
+
}
|
|
372
|
+
return total;
|
|
373
|
+
}
|
|
374
|
+
/** Per-task durations, total work, and the run window from the timed tasks. */
|
|
375
|
+
function computeRunWindow(timed) {
|
|
376
|
+
const durations = new Map();
|
|
377
|
+
let totalWork = 0;
|
|
378
|
+
let runStart = Infinity;
|
|
379
|
+
let runEnd = -Infinity;
|
|
380
|
+
for (const { id, start, end } of timed) {
|
|
381
|
+
const duration = Math.max(0, end - start);
|
|
382
|
+
durations.set(id, duration);
|
|
383
|
+
totalWork += duration;
|
|
384
|
+
runStart = Math.min(runStart, start);
|
|
385
|
+
runEnd = Math.max(runEnd, end);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
durations,
|
|
389
|
+
totalWork,
|
|
390
|
+
runStart,
|
|
391
|
+
taskWindow: Math.max(0, runEnd - runStart),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** cgroup-aware core count so a quota-capped CI container gets the right lever (more machines, not a --parallel it can't use). cpus() only as fallback. */
|
|
395
|
+
function detectCoreCount() {
|
|
396
|
+
return typeof os.availableParallelism === 'function'
|
|
397
|
+
? os.availableParallelism()
|
|
398
|
+
: os.cpus().length;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The occupancy timeline (contiguous {@link Timespan}s) — single source of truth for
|
|
402
|
+
* slot contention. A `parallelism: false` task occupies the whole pool. Integrates
|
|
403
|
+
* over time rather than sampling one instant, so it's robust to when tasks hand off.
|
|
404
|
+
*
|
|
405
|
+
* Builds it via a delta sweep: each task bumps a counter up at its start and down at
|
|
406
|
+
* its end, then we walk the sorted boundaries accumulating the running totals into one
|
|
407
|
+
* timespan per gap.
|
|
408
|
+
*/
|
|
409
|
+
function buildTimespans(timed, eligible, parallel) {
|
|
410
|
+
// Per-timestamp +/- deltas the sweep integrates into timespans: busy slots (occ),
|
|
411
|
+
// eligible-but-waiting tasks, and parallelism:false tasks that hold the whole pool.
|
|
412
|
+
const occDelta = new Map();
|
|
413
|
+
const waitDelta = new Map();
|
|
414
|
+
const nonParallelDelta = new Map();
|
|
415
|
+
const addDelta = (deltas, at, delta) => deltas.set(at, (deltas.get(at) ?? 0) + delta);
|
|
416
|
+
for (const { id, start, end, nonParallel: isNonParallel } of timed) {
|
|
417
|
+
// A parallelism:false task holds every slot, so it weighs the whole pool.
|
|
418
|
+
const weight = isNonParallel ? parallel : 1;
|
|
419
|
+
addDelta(occDelta, start, weight);
|
|
420
|
+
addDelta(occDelta, end, -weight);
|
|
421
|
+
if (isNonParallel) {
|
|
422
|
+
addDelta(nonParallelDelta, start, 1);
|
|
423
|
+
addDelta(nonParallelDelta, end, -1);
|
|
424
|
+
}
|
|
425
|
+
const elig = eligible.get(id) ?? start;
|
|
426
|
+
if (elig < start) {
|
|
427
|
+
addDelta(waitDelta, elig, 1);
|
|
428
|
+
addDelta(waitDelta, start, -1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const times = Array.from(new Set([
|
|
432
|
+
...occDelta.keys(),
|
|
433
|
+
...waitDelta.keys(),
|
|
434
|
+
...nonParallelDelta.keys(),
|
|
435
|
+
])).sort((a, b) => a - b);
|
|
436
|
+
const timespans = [];
|
|
437
|
+
let occ = 0;
|
|
438
|
+
let waiting = 0;
|
|
439
|
+
let nonParallel = 0;
|
|
440
|
+
for (let i = 0; i < times.length; i++) {
|
|
441
|
+
occ += occDelta.get(times[i]) ?? 0;
|
|
442
|
+
waiting += waitDelta.get(times[i]) ?? 0;
|
|
443
|
+
nonParallel += nonParallelDelta.get(times[i]) ?? 0;
|
|
444
|
+
const next = times[i + 1];
|
|
445
|
+
if (next != null && next > times[i]) {
|
|
446
|
+
timespans.push({ start: times[i], end: next, occ, waiting, nonParallel });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return timespans;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Split overhead off the occupancy timeline into three buckets that sum to overhead:
|
|
453
|
+
* coordinator time, and slot contention recoverable by more machines vs by a higher
|
|
454
|
+
* --parallel.
|
|
455
|
+
*
|
|
456
|
+
* Slot contention is wall-clock where every slot was busy with a backlog (occ ≥
|
|
457
|
+
* parallel, waiting > 0). Contention while a `parallelism: false` task held the whole
|
|
458
|
+
* pool goes to machines only (a higher local --parallel can't recover it). Of the
|
|
459
|
+
* rest, --parallel helps up to the volume the cores can absorb; the overflow
|
|
460
|
+
* (totalWork/cores − floor) needs more machines.
|
|
461
|
+
*/
|
|
462
|
+
function splitOverhead({ timespans, parallel, overhead, totalWork, cores, criticalPathDuration, }) {
|
|
463
|
+
let slotContendedTime = 0;
|
|
464
|
+
let nonParallelContendedTime = 0;
|
|
465
|
+
for (const s of timespans) {
|
|
466
|
+
if (s.occ >= parallel && s.waiting > 0) {
|
|
467
|
+
const dur = s.end - s.start;
|
|
468
|
+
slotContendedTime += dur;
|
|
469
|
+
if (s.nonParallel > 0) {
|
|
470
|
+
nonParallelContendedTime += dur;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const recoverableBySlots = Math.min(overhead, slotContendedTime);
|
|
475
|
+
const nonParallelRecoverable = Math.min(recoverableBySlots, nonParallelContendedTime);
|
|
476
|
+
const ordinaryBySlots = recoverableBySlots - nonParallelRecoverable;
|
|
477
|
+
const machineBound = parallel < cores
|
|
478
|
+
? Math.max(0, totalWork / cores - criticalPathDuration)
|
|
479
|
+
: Infinity;
|
|
480
|
+
const ordinaryByMachines = Math.min(ordinaryBySlots, machineBound);
|
|
481
|
+
return {
|
|
482
|
+
coordinatorOverhead: overhead - recoverableBySlots,
|
|
483
|
+
recoverableByMachines: nonParallelRecoverable + ordinaryByMachines,
|
|
484
|
+
recoverableByParallel: ordinaryBySlots - ordinaryByMachines,
|
|
485
|
+
};
|
|
486
|
+
}
|