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,59 @@
|
|
|
1
|
+
import type { BatchInfo, PerformanceSummaryPayload } from '../../native';
|
|
2
|
+
import { TaskGraph } from '../../config/task-graph';
|
|
3
|
+
import { LifeCycle, TaskResult } from '../life-cycle';
|
|
4
|
+
import { type PerformanceLifeCycleOptions, type PerformanceSummary } from './performance-analysis';
|
|
5
|
+
/**
|
|
6
|
+
* Measures how much wall-clock a run loses to parallelism contention versus its
|
|
7
|
+
* critical-path floor, and reports it at the end of a run. Added on every run by
|
|
8
|
+
* `constructLifeCycles`, but only emitted where the report is flushed (the CLI
|
|
9
|
+
* `invokeTasksRunner` path) — the programmatic `init-tasks-runner` path collects
|
|
10
|
+
* timings but never displays them.
|
|
11
|
+
*
|
|
12
|
+
* overhead = runDuration − criticalPathDuration, split by CAUSE off the occupancy
|
|
13
|
+
* timeline: slot-queued time (recoverable by parallelism / machines) versus
|
|
14
|
+
* coordinator time (hashing, scheduling, continuous-dep waits).
|
|
15
|
+
*
|
|
16
|
+
* Scope: discrete tasks only. Continuous tasks (no end time) are excluded; a
|
|
17
|
+
* discrete task's wait for a continuous dependency to start is eligibility, not
|
|
18
|
+
* contention.
|
|
19
|
+
*/
|
|
20
|
+
export declare class PerformanceLifeCycle implements LifeCycle {
|
|
21
|
+
private readonly taskGraph;
|
|
22
|
+
private readonly options;
|
|
23
|
+
private readonly timings;
|
|
24
|
+
/** taskId → terminal status (cache hit vs ran), for the cache summary. */
|
|
25
|
+
private readonly statuses;
|
|
26
|
+
/** taskId → other tasks in its batch (batches run sequentially). */
|
|
27
|
+
private readonly batchSiblings;
|
|
28
|
+
/** Resolved `--parallel`, set by the runner via {@link startCommand}'s second arg once the thread pool is sized. */
|
|
29
|
+
private parallel;
|
|
30
|
+
constructor(taskGraph: TaskGraph, options?: PerformanceLifeCycleOptions);
|
|
31
|
+
/**
|
|
32
|
+
* The runner passes the resolved `--parallel` (getThreadPoolSize's `discrete`) as the
|
|
33
|
+
* second arg; the first (thread count) is for the TUI and ignored here.
|
|
34
|
+
*/
|
|
35
|
+
startCommand(_threadCount?: number, parallel?: number): void;
|
|
36
|
+
registerRunningBatch(_batchId: string, batchInfo: BatchInfo): void;
|
|
37
|
+
endTasks(taskResults: TaskResult[]): void;
|
|
38
|
+
private entry;
|
|
39
|
+
/** Analyze the collected timings into a structured summary, or `null` when no discrete task timings were recorded. */
|
|
40
|
+
getSummary(): PerformanceSummary | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Structured report for the TUI's exit-countdown popup, or null when nothing to
|
|
44
|
+
* show. Clears the active lifecycle so the popup owns the report and a later terminal
|
|
45
|
+
* flush can't re-print it. Best-effort: a throw degrades to null.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getPerformanceSummaryPayload(): PerformanceSummaryPayload | null;
|
|
48
|
+
/**
|
|
49
|
+
* The performance report payload for `endCommand`'s TUI exit popup, or undefined when the
|
|
50
|
+
* report should instead be flushed to the terminal — non-TUI runs, or a single task (the
|
|
51
|
+
* complement of run-command's flush gate). Reading it consumes the report so the flush
|
|
52
|
+
* won't reprint it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getPerformanceReport(taskCount: number): PerformanceSummaryPayload | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Print the performance report (if enabled) after the run summary. Called once the
|
|
57
|
+
* terminal is restored, so it appears in every output mode including the TUI.
|
|
58
|
+
*/
|
|
59
|
+
export declare function flushPerformanceReport(): void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PerformanceLifeCycle = void 0;
|
|
4
|
+
exports.getPerformanceSummaryPayload = getPerformanceSummaryPayload;
|
|
5
|
+
exports.getPerformanceReport = getPerformanceReport;
|
|
6
|
+
exports.flushPerformanceReport = flushPerformanceReport;
|
|
7
|
+
const performance_report_1 = require("./performance-report");
|
|
8
|
+
const is_tui_enabled_1 = require("../is-tui-enabled");
|
|
9
|
+
const performance_analysis_1 = require("./performance-analysis");
|
|
10
|
+
/**
|
|
11
|
+
* Measures how much wall-clock a run loses to parallelism contention versus its
|
|
12
|
+
* critical-path floor, and reports it at the end of a run. Added on every run by
|
|
13
|
+
* `constructLifeCycles`, but only emitted where the report is flushed (the CLI
|
|
14
|
+
* `invokeTasksRunner` path) — the programmatic `init-tasks-runner` path collects
|
|
15
|
+
* timings but never displays them.
|
|
16
|
+
*
|
|
17
|
+
* overhead = runDuration − criticalPathDuration, split by CAUSE off the occupancy
|
|
18
|
+
* timeline: slot-queued time (recoverable by parallelism / machines) versus
|
|
19
|
+
* coordinator time (hashing, scheduling, continuous-dep waits).
|
|
20
|
+
*
|
|
21
|
+
* Scope: discrete tasks only. Continuous tasks (no end time) are excluded; a
|
|
22
|
+
* discrete task's wait for a continuous dependency to start is eligibility, not
|
|
23
|
+
* contention.
|
|
24
|
+
*/
|
|
25
|
+
class PerformanceLifeCycle {
|
|
26
|
+
constructor(taskGraph, options = {}) {
|
|
27
|
+
this.taskGraph = taskGraph;
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.timings = new Map();
|
|
30
|
+
/** taskId → terminal status (cache hit vs ran), for the cache summary. */
|
|
31
|
+
this.statuses = new Map();
|
|
32
|
+
/** taskId → other tasks in its batch (batches run sequentially). */
|
|
33
|
+
this.batchSiblings = new Map();
|
|
34
|
+
/** Resolved `--parallel`, set by the runner via {@link startCommand}'s second arg once the thread pool is sized. */
|
|
35
|
+
this.parallel = 1;
|
|
36
|
+
activePerformanceLifeCycle = this;
|
|
37
|
+
}
|
|
38
|
+
// === Lifecycle hooks (called by the orchestrator as the run progresses) ===
|
|
39
|
+
/**
|
|
40
|
+
* The runner passes the resolved `--parallel` (getThreadPoolSize's `discrete`) as the
|
|
41
|
+
* second arg; the first (thread count) is for the TUI and ignored here.
|
|
42
|
+
*/
|
|
43
|
+
startCommand(_threadCount, parallel) {
|
|
44
|
+
if (parallel != null) {
|
|
45
|
+
this.parallel = parallel;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
registerRunningBatch(_batchId, batchInfo) {
|
|
49
|
+
for (const id of batchInfo.taskIds) {
|
|
50
|
+
this.batchSiblings.set(id, batchInfo.taskIds.filter((other) => other !== id));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
endTasks(taskResults) {
|
|
54
|
+
// Called incrementally (per group/batch); accumulate so the last call sees every timing.
|
|
55
|
+
for (const { task, status } of taskResults) {
|
|
56
|
+
const entry = this.entry(task.id);
|
|
57
|
+
// `!= null`, not truthiness: synthetic/relative timelines can legitimately start at 0.
|
|
58
|
+
if (task.startTime != null) {
|
|
59
|
+
entry.startTime = task.startTime;
|
|
60
|
+
}
|
|
61
|
+
if (task.endTime != null) {
|
|
62
|
+
entry.endTime = task.endTime;
|
|
63
|
+
}
|
|
64
|
+
if (status != null) {
|
|
65
|
+
this.statuses.set(task.id, status);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
entry(taskId) {
|
|
70
|
+
let entry = this.timings.get(taskId);
|
|
71
|
+
if (!entry) {
|
|
72
|
+
entry = { continuous: this.taskGraph.tasks[taskId]?.continuous ?? false };
|
|
73
|
+
this.timings.set(taskId, entry);
|
|
74
|
+
}
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
/** Analyze the collected timings into a structured summary, or `null` when no discrete task timings were recorded. */
|
|
78
|
+
getSummary() {
|
|
79
|
+
return new performance_analysis_1.PerformanceAnalysis(this.timings, this.statuses, this.taskGraph, this.batchSiblings, this.parallel, this.options).summary();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.PerformanceLifeCycle = PerformanceLifeCycle;
|
|
83
|
+
/** The most recently constructed performance lifecycle, read after the run. Cleared once consumed. */
|
|
84
|
+
let activePerformanceLifeCycle = null;
|
|
85
|
+
/**
|
|
86
|
+
* Structured report for the TUI's exit-countdown popup, or null when nothing to
|
|
87
|
+
* show. Clears the active lifecycle so the popup owns the report and a later terminal
|
|
88
|
+
* flush can't re-print it. Best-effort: a throw degrades to null.
|
|
89
|
+
*/
|
|
90
|
+
function getPerformanceSummaryPayload() {
|
|
91
|
+
const lifeCycle = activePerformanceLifeCycle;
|
|
92
|
+
if (!lifeCycle) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const summary = lifeCycle.getSummary();
|
|
97
|
+
if (!summary) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
activePerformanceLifeCycle = null;
|
|
101
|
+
return (0, performance_report_1.buildExitSummaryPayload)(summary);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
// Best-effort: the report must never break the run. Surface the cause only under
|
|
105
|
+
// verbose logging so a missing report stays debuggable.
|
|
106
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
107
|
+
console.error(e);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The performance report payload for `endCommand`'s TUI exit popup, or undefined when the
|
|
114
|
+
* report should instead be flushed to the terminal — non-TUI runs, or a single task (the
|
|
115
|
+
* complement of run-command's flush gate). Reading it consumes the report so the flush
|
|
116
|
+
* won't reprint it.
|
|
117
|
+
*/
|
|
118
|
+
function getPerformanceReport(taskCount) {
|
|
119
|
+
if (!(0, is_tui_enabled_1.isTuiEnabled)() || taskCount <= 1) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return getPerformanceSummaryPayload() ?? undefined;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Print the performance report (if enabled) after the run summary. Called once the
|
|
126
|
+
* terminal is restored, so it appears in every output mode including the TUI.
|
|
127
|
+
*/
|
|
128
|
+
function flushPerformanceReport() {
|
|
129
|
+
const lifeCycle = activePerformanceLifeCycle;
|
|
130
|
+
activePerformanceLifeCycle = null;
|
|
131
|
+
if (!lifeCycle) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Cosmetic report; a throw (e.g. EPIPE to a closed pipe) must never mask the
|
|
135
|
+
// real task error or fail an otherwise successful run.
|
|
136
|
+
try {
|
|
137
|
+
const summary = lifeCycle.getSummary();
|
|
138
|
+
if (!summary) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// restore_terminal cooks the terminal back post-TUI, so console.log's plain \n
|
|
142
|
+
// renders fine; it also supplies the single trailing newline formatReport omits.
|
|
143
|
+
console.log((0, performance_report_1.formatReport)(summary));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// Best-effort report; never let it affect the run's exit behavior. Surface the
|
|
147
|
+
// cause only under verbose logging.
|
|
148
|
+
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
149
|
+
console.error(e);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PerformanceSummaryPayload } from '../../native';
|
|
2
|
+
import { formatDuration } from '../../native';
|
|
3
|
+
import type { PerformanceSummary } from './performance-analysis';
|
|
4
|
+
/**
|
|
5
|
+
* A recommendation built from structured parts so the link text comes from the
|
|
6
|
+
* link definition (not a substring scanned out of the assembled report). A part
|
|
7
|
+
* is either literal text or a {@link RecLink}; the renderers below project the
|
|
8
|
+
* same parts to the terminal string, the payload string, and the popup links.
|
|
9
|
+
*/
|
|
10
|
+
type RecPart = string | RecLink;
|
|
11
|
+
export type Recommendation = RecPart[];
|
|
12
|
+
/**
|
|
13
|
+
* A docs link inside a recommendation, in one of two styles:
|
|
14
|
+
*
|
|
15
|
+
* - `url` — the URL itself is the visible text. The clean URL shows (and is the
|
|
16
|
+
* OSC 8 label), the utm-tagged URL is the click target. Without OSC 8 the tagged
|
|
17
|
+
* URL prints verbatim (e.g. the agents recs, the footer).
|
|
18
|
+
* - `phrase` — a sentence is the visible text and the whole of it links to the
|
|
19
|
+
* tagged URL. Without OSC 8 the tagged URL is appended as ` → <url>` (the
|
|
20
|
+
* remote-cache CTA). The payload string for a phrase link is URL-less: the Rust
|
|
21
|
+
* popup re-links the phrase from {@link PerformanceSummaryPayload.links}.
|
|
22
|
+
*/
|
|
23
|
+
interface RecLink {
|
|
24
|
+
/** Visible label: the clean URL (`url`) or the sentence (`phrase`). */
|
|
25
|
+
visible: string;
|
|
26
|
+
/** OSC 8 click target / appended URL: always the utm-tagged URL. */
|
|
27
|
+
href: string;
|
|
28
|
+
style: 'url' | 'phrase';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The recommendation string the napi payload ships and the Rust popup matches
|
|
32
|
+
* against. A `url` link keeps its tagged URL inline; a `phrase` link is URL-less
|
|
33
|
+
* (the popup re-links it from {@link PerformanceSummaryPayload.links}).
|
|
34
|
+
*/
|
|
35
|
+
export declare function recommendationToPayloadString(rec: Recommendation): string;
|
|
36
|
+
/** Below this (ms) overhead is noise, not worth a recommendation. */
|
|
37
|
+
export declare const MEANINGFUL_OVERHEAD = 1000;
|
|
38
|
+
export { formatDuration };
|
|
39
|
+
/** Actionable levers to go faster, one rec per lever. The critical-path case has two (shorten the chain, distribute the rest). Agents advice is omitted unless `canDistribute`. */
|
|
40
|
+
export declare function buildRecommendation(args: {
|
|
41
|
+
recoverableByParallel: number;
|
|
42
|
+
recoverableByMachines: number;
|
|
43
|
+
coordinatorDominated: boolean;
|
|
44
|
+
runDuration: number;
|
|
45
|
+
parallel: number;
|
|
46
|
+
cores: number;
|
|
47
|
+
canDistribute: boolean;
|
|
48
|
+
distributing: boolean;
|
|
49
|
+
criticalPathTop: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
duration: number;
|
|
52
|
+
}>;
|
|
53
|
+
}): Recommendation[];
|
|
54
|
+
/** The full performance report as a terminal string (the TUI popup renders natively from {@link buildExitSummaryPayload} instead). */
|
|
55
|
+
export declare function formatReport(s: PerformanceSummary): string;
|
|
56
|
+
/**
|
|
57
|
+
* Build the payload for the TUI's exit-countdown popup, which renders natively in Rust
|
|
58
|
+
* (the terminal path uses {@link formatReport}). The shape is the generated napi
|
|
59
|
+
* {@link PerformanceSummaryPayload}, imported not re-declared so the producer and the
|
|
60
|
+
* Rust struct can't drift.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildExitSummaryPayload(s: PerformanceSummary): PerformanceSummaryPayload;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatDuration = exports.MEANINGFUL_OVERHEAD = void 0;
|
|
4
|
+
exports.recommendationToPayloadString = recommendationToPayloadString;
|
|
5
|
+
exports.buildRecommendation = buildRecommendation;
|
|
6
|
+
exports.formatReport = formatReport;
|
|
7
|
+
exports.buildExitSummaryPayload = buildExitSummaryPayload;
|
|
8
|
+
const native_1 = require("../../native");
|
|
9
|
+
Object.defineProperty(exports, "formatDuration", { enumerable: true, get: function () { return native_1.formatDuration; } });
|
|
10
|
+
const terminal_link_1 = require("../../utils/terminal-link");
|
|
11
|
+
const NX_AGENTS_URL = 'https://nx.dev/ci/features/distribute-task-execution';
|
|
12
|
+
const NX_REMOTE_CACHE_URL = 'https://nx.dev/ci/features/remote-cache';
|
|
13
|
+
const NX_PERFORMANCE_URL = 'https://nx.dev/docs/concepts/ci-concepts/parallelization-distribution';
|
|
14
|
+
/** utm tag attributing report clicks back to it. */
|
|
15
|
+
const UTM = '?utm=performance-report';
|
|
16
|
+
const NX_PERFORMANCE_LINK = `${NX_PERFORMANCE_URL}${UTM}`;
|
|
17
|
+
const NX_AGENTS_LINK = `${NX_AGENTS_URL}${UTM}`;
|
|
18
|
+
const NX_REMOTE_CACHE_LINK = `${NX_REMOTE_CACHE_URL}${UTM}`;
|
|
19
|
+
const NX_PERFORMANCE_LABEL = `Learn how to improve your run's performance`;
|
|
20
|
+
/**
|
|
21
|
+
* Whole-phrase CTA: the whole sentence is the link. The Rust TUI popup keeps no
|
|
22
|
+
* copy of this string; it gets the phrase + href from the exit payload's `links`.
|
|
23
|
+
*/
|
|
24
|
+
const NX_REMOTE_CACHE_CTA = 'Drastically reduce your run duration by sharing a cache across your team and CI';
|
|
25
|
+
function urlLink(cleanUrl, taggedUrl) {
|
|
26
|
+
return { visible: cleanUrl, href: taggedUrl, style: 'url' };
|
|
27
|
+
}
|
|
28
|
+
function phraseLink(phrase, taggedUrl) {
|
|
29
|
+
return { visible: phrase, href: taggedUrl, style: 'phrase' };
|
|
30
|
+
}
|
|
31
|
+
function isRecLink(part) {
|
|
32
|
+
return typeof part !== 'string';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The recommendation string the napi payload ships and the Rust popup matches
|
|
36
|
+
* against. A `url` link keeps its tagged URL inline; a `phrase` link is URL-less
|
|
37
|
+
* (the popup re-links it from {@link PerformanceSummaryPayload.links}).
|
|
38
|
+
*/
|
|
39
|
+
function recommendationToPayloadString(rec) {
|
|
40
|
+
return rec
|
|
41
|
+
.map((part) => !isRecLink(part) ? part : part.style === 'url' ? part.href : part.visible)
|
|
42
|
+
.join('');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The recommendation as a terminal string. With OSC 8 each link becomes a
|
|
46
|
+
* hyperlink (clean URL or whole phrase as the visible label, tagged URL the
|
|
47
|
+
* target). Without it, a `url` link prints its tagged URL and a `phrase` link
|
|
48
|
+
* gets ` → <tagged url>` appended — `terminalLink` can't do this since it drops
|
|
49
|
+
* the URL entirely when hyperlinks are off.
|
|
50
|
+
*/
|
|
51
|
+
function recommendationToTerminalString(rec, hyperlinks) {
|
|
52
|
+
return rec
|
|
53
|
+
.map((part) => {
|
|
54
|
+
if (!isRecLink(part)) {
|
|
55
|
+
return part;
|
|
56
|
+
}
|
|
57
|
+
if (hyperlinks) {
|
|
58
|
+
return (0, terminal_link_1.terminalLink)(part.visible, part.href);
|
|
59
|
+
}
|
|
60
|
+
return part.style === 'url'
|
|
61
|
+
? part.href
|
|
62
|
+
: `${part.visible} → ${part.href}`;
|
|
63
|
+
})
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
/** The popup links for the phrase-style CTAs in a recommendation list (url links live inline in the strings). */
|
|
67
|
+
function recommendationLinks(recommendations) {
|
|
68
|
+
return recommendations.flatMap((rec) => rec
|
|
69
|
+
.filter(isRecLink)
|
|
70
|
+
.filter((part) => part.style === 'phrase')
|
|
71
|
+
.map((part) => ({ text: part.visible, href: part.href })));
|
|
72
|
+
}
|
|
73
|
+
/** At/below this hit rate, recommend remote cache (if off); above it caching works. */
|
|
74
|
+
const LOW_CACHE_HIT_RATE = 0.1;
|
|
75
|
+
/** Below this (ms) overhead is noise, not worth a recommendation. */
|
|
76
|
+
exports.MEANINGFUL_OVERHEAD = 1000;
|
|
77
|
+
/** Recommend --parallel when recoverable slot time is at least this fraction of the run. */
|
|
78
|
+
const PARALLEL_LEAD_FRACTION = 0.2;
|
|
79
|
+
/** Append "s" unless `count` is 1 (regular plurals only). */
|
|
80
|
+
function pluralize(count, noun) {
|
|
81
|
+
return count === 1 ? noun : `${noun}s`;
|
|
82
|
+
}
|
|
83
|
+
/** Slot-contention time recoverable by parallelism or more machines (the "recoverable" number both the report and TUI payload show). Derived, never stored, so the halves can't drift from their sum. */
|
|
84
|
+
function recoverableTime(s) {
|
|
85
|
+
return s.recoverableByParallel + s.recoverableByMachines;
|
|
86
|
+
}
|
|
87
|
+
/** Render the longest critical-path tasks as aligned columns: task (left), duration (right). */
|
|
88
|
+
function formatTopTaskRows(tasks) {
|
|
89
|
+
// The only caller returns early when empty, so `tasks` is non-empty here.
|
|
90
|
+
const idWidth = Math.max(...tasks.map((t) => t.id.length));
|
|
91
|
+
const durations = tasks.map((t) => (0, native_1.formatDuration)(t.duration));
|
|
92
|
+
const durWidth = Math.max(...durations.map((d) => d.length));
|
|
93
|
+
return tasks.map((t, i) => {
|
|
94
|
+
const id = t.id.padEnd(idWidth);
|
|
95
|
+
const dur = durations[i].padStart(durWidth);
|
|
96
|
+
return ` ${id} ${dur}`;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** An Nx Agents URL link, built from the link definition so the URL text never has to be scanned back out of the assembled report. */
|
|
100
|
+
const agentsLink = urlLink(NX_AGENTS_URL, NX_AGENTS_LINK);
|
|
101
|
+
/** Actionable levers to go faster, one rec per lever. The critical-path case has two (shorten the chain, distribute the rest). Agents advice is omitted unless `canDistribute`. */
|
|
102
|
+
function buildRecommendation(args) {
|
|
103
|
+
const { recoverableByParallel, recoverableByMachines, coordinatorDominated, runDuration, parallel, cores, canDistribute, distributing, criticalPathTop, } = args;
|
|
104
|
+
if (distributing) {
|
|
105
|
+
// Already on agents: more agents (not local --parallel) is the parallelism lever;
|
|
106
|
+
// the cores/machine framing below doesn't apply to a distributed run.
|
|
107
|
+
const recoverable = recoverableByParallel + recoverableByMachines;
|
|
108
|
+
if (runDuration > 0 &&
|
|
109
|
+
recoverable >= PARALLEL_LEAD_FRACTION * runDuration) {
|
|
110
|
+
return [
|
|
111
|
+
[`Add more Nx Agents to recover up to ${(0, native_1.formatDuration)(recoverable)}.`],
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Slot-bound with spare cores: the lever is local --parallel, not agents.
|
|
117
|
+
if (runDuration > 0 &&
|
|
118
|
+
recoverableByParallel >= PARALLEL_LEAD_FRACTION * runDuration &&
|
|
119
|
+
recoverableByParallel >= recoverableByMachines) {
|
|
120
|
+
return [
|
|
121
|
+
[
|
|
122
|
+
`Increase parallelism to recover up to ${(0, native_1.formatDuration)(recoverableByParallel)}.`,
|
|
123
|
+
],
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
// Machine-bound: contention a higher local --parallel can't recover.
|
|
127
|
+
if (recoverableByMachines >= exports.MEANINGFUL_OVERHEAD) {
|
|
128
|
+
if (parallel >= cores) {
|
|
129
|
+
// At the core ceiling and still queuing. Agents for CPU-bound work;
|
|
130
|
+
// otherwise only the I/O-bound --parallel tip applies.
|
|
131
|
+
const base = `You're at this machine's ${cores} ${pluralize(cores, 'core')} and tasks are still queuing for a slot.`;
|
|
132
|
+
return [
|
|
133
|
+
canDistribute
|
|
134
|
+
? [
|
|
135
|
+
`${base} If they're CPU-bound, distribute across machines with Nx Agents → `,
|
|
136
|
+
agentsLink,
|
|
137
|
+
`; if they're I/O-bound, a higher --parallel may help instead.`,
|
|
138
|
+
]
|
|
139
|
+
: [`${base} If they're I/O-bound, a higher --parallel may help.`],
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
// Below the core ceiling but still machine-bound (a parallelism:false task
|
|
143
|
+
// monopolizes the pool, or volume exceeds the cores): only more machines can
|
|
144
|
+
// free those slots, and that lever only exists in CI.
|
|
145
|
+
return canDistribute
|
|
146
|
+
? [
|
|
147
|
+
[
|
|
148
|
+
`Tasks are queuing for a slot that a higher --parallel can't free on one machine. Distribute across machines with Nx Agents → `,
|
|
149
|
+
agentsLink,
|
|
150
|
+
`.`,
|
|
151
|
+
],
|
|
152
|
+
]
|
|
153
|
+
: [];
|
|
154
|
+
}
|
|
155
|
+
// Coordinator-dominated (tasks fast or cached), machine ~maxed: in CI agents are
|
|
156
|
+
// the only lever; locally nothing actionable.
|
|
157
|
+
if (coordinatorDominated) {
|
|
158
|
+
return canDistribute
|
|
159
|
+
? [
|
|
160
|
+
[
|
|
161
|
+
`This run was about as fast as this machine can do it. Distribute the work across multiple machines with Nx Agents to make it faster → `,
|
|
162
|
+
agentsLink,
|
|
163
|
+
`.`,
|
|
164
|
+
],
|
|
165
|
+
]
|
|
166
|
+
: [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Critical-path-bound: shorten the chain's longest tasks and distribute the rest.
|
|
170
|
+
// Nothing ran (fully cached) → no rec.
|
|
171
|
+
if (criticalPathTop.length === 0) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const speedUp = [
|
|
175
|
+
[
|
|
176
|
+
`Speed up or split the longest tasks on the critical path:`,
|
|
177
|
+
...formatTopTaskRows(criticalPathTop),
|
|
178
|
+
].join('\n'),
|
|
179
|
+
];
|
|
180
|
+
const recommendations = [speedUp];
|
|
181
|
+
if (canDistribute) {
|
|
182
|
+
recommendations.push([
|
|
183
|
+
`Distribute tasks across multiple machines with Nx Agents to increase parallelism without overwhelming resource usage → `,
|
|
184
|
+
agentsLink,
|
|
185
|
+
`.`,
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
return recommendations;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Recommendations in display order, cheapest first: "recover up to X" → remote-cache
|
|
192
|
+
* → other levers → "speed up / split" LAST (deepest manual work, the only multi-line
|
|
193
|
+
* rec). Shared by the report and TUI payload.
|
|
194
|
+
*/
|
|
195
|
+
function orderedRecommendations(s) {
|
|
196
|
+
const levers = [...s.structuredRecommendations];
|
|
197
|
+
const cacheAdvice = buildCacheAdvice(s);
|
|
198
|
+
// Classify by the rec's plain text — the structured link parts don't affect order.
|
|
199
|
+
const text = recommendationToPayloadString;
|
|
200
|
+
const isRecoverLever = (r) => text(r).includes('recover up to');
|
|
201
|
+
const isLongestTasks = (r) => text(r).includes('\n');
|
|
202
|
+
return [
|
|
203
|
+
...levers.filter(isRecoverLever),
|
|
204
|
+
...(cacheAdvice ? [cacheAdvice] : []),
|
|
205
|
+
...levers.filter((r) => !isRecoverLever(r) && !isLongestTasks(r)),
|
|
206
|
+
...levers.filter(isLongestTasks),
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
/** Top-of-report cache stat: hit rate or skip marker. Null when there's no cache outcome. */
|
|
210
|
+
function cacheStat(s) {
|
|
211
|
+
if (s.cacheSkipped) {
|
|
212
|
+
return 'Skipped (--skip-nx-cache)';
|
|
213
|
+
}
|
|
214
|
+
if (s.cacheableCount === 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const pct = Math.round((s.cacheHits / s.cacheableCount) * 100);
|
|
218
|
+
return `${s.cacheHits}/${s.cacheableCount} hit (${pct}%)`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Bottom-of-report cache advice, only when there's a lever: a skipped cache (drop
|
|
222
|
+
* the flag) or a barely-used cache with no remote (set up Nx Cloud).
|
|
223
|
+
*/
|
|
224
|
+
function buildCacheAdvice(s) {
|
|
225
|
+
if (s.cacheSkipped) {
|
|
226
|
+
return [
|
|
227
|
+
`Cache: drop --skip-nx-cache to restore unchanged tasks instantly.`,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
if (s.cacheableCount === 0) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const hitRate = s.cacheHits / s.cacheableCount;
|
|
234
|
+
if (!s.remoteCacheEnabled && hitRate <= LOW_CACHE_HIT_RATE) {
|
|
235
|
+
// Whole-phrase link: the sentence is the visible text, the tagged URL the
|
|
236
|
+
// target. The payload string stays URL-less (the popup re-links the phrase).
|
|
237
|
+
return [phraseLink(NX_REMOTE_CACHE_CTA, NX_REMOTE_CACHE_LINK), `.`];
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/** The full performance report as a terminal string (the TUI popup renders natively from {@link buildExitSummaryPayload} instead). */
|
|
242
|
+
function formatReport(s) {
|
|
243
|
+
const fmt = native_1.formatDuration;
|
|
244
|
+
// Shows two of run duration's three parts (critical path + recoverable); the third,
|
|
245
|
+
// coordinator overhead, isn't displayed, so the two don't sum to run duration.
|
|
246
|
+
const recoverable = recoverableTime(s);
|
|
247
|
+
const recoverablePct = s.runDuration > 0 ? Math.round((recoverable / s.runDuration) * 100) : 0;
|
|
248
|
+
// Pad to the widest label ("Recoverable time:" = 17) so values align without a
|
|
249
|
+
// gaping gap. Keep in sync with the Rust popup's stat_line.
|
|
250
|
+
const stat = (label, value) => ` ${`${label}:`.padEnd(17)} ${value}`;
|
|
251
|
+
// No leading blank line: nx's run summary already prints trailing blanks before this.
|
|
252
|
+
const cache = cacheStat(s);
|
|
253
|
+
const lines = [
|
|
254
|
+
stat('Run duration', fmt(s.runDuration)),
|
|
255
|
+
...(cache ? [stat('Cache', cache)] : []),
|
|
256
|
+
stat('Critical path', `${fmt(s.criticalPathDuration)} (${s.criticalPathTaskCount} ${pluralize(s.criticalPathTaskCount, 'task')})`),
|
|
257
|
+
stat('Recoverable time', recoverable > 0
|
|
258
|
+
? `${fmt(recoverable)} (${recoverablePct}% of the run)`
|
|
259
|
+
: fmt(recoverable)),
|
|
260
|
+
];
|
|
261
|
+
const hyperlinks = (0, terminal_link_1.supportsHyperlinks)();
|
|
262
|
+
const recommendations = orderedRecommendations(s);
|
|
263
|
+
const render = (r) => recommendationToTerminalString(r, hyperlinks);
|
|
264
|
+
// A rec may be multi-line (the critical-path one embeds a task list); indent
|
|
265
|
+
// continuation lines under the bullet.
|
|
266
|
+
const renderRec = (r) => {
|
|
267
|
+
const [first, ...rest] = r.split('\n');
|
|
268
|
+
return [` - ${first}`, ...rest.map((l) => ` ${l}`)];
|
|
269
|
+
};
|
|
270
|
+
if (recommendations.length > 0) {
|
|
271
|
+
const onlySingleLine = recommendations.length === 1 &&
|
|
272
|
+
!recommendationToPayloadString(recommendations[0]).includes('\n');
|
|
273
|
+
if (onlySingleLine) {
|
|
274
|
+
lines.push('', ` Recommendation: ${render(recommendations[0])}`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
lines.push('', ' Recommendations:', ...recommendations.flatMap((r) => renderRec(render(r))));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// utm-tagged footer (a url link): with OSC 8 the clean URL shows and hides the utm
|
|
281
|
+
// in the target; without it the tagged URL prints verbatim.
|
|
282
|
+
const footer = [
|
|
283
|
+
` ${NX_PERFORMANCE_LABEL} → `,
|
|
284
|
+
urlLink(NX_PERFORMANCE_URL, NX_PERFORMANCE_LINK),
|
|
285
|
+
];
|
|
286
|
+
lines.push('', render(footer));
|
|
287
|
+
// No trailing newline — the caller's console.log adds the line terminator.
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Build the payload for the TUI's exit-countdown popup, which renders natively in Rust
|
|
292
|
+
* (the terminal path uses {@link formatReport}). The shape is the generated napi
|
|
293
|
+
* {@link PerformanceSummaryPayload}, imported not re-declared so the producer and the
|
|
294
|
+
* Rust struct can't drift.
|
|
295
|
+
*/
|
|
296
|
+
function buildExitSummaryPayload(s) {
|
|
297
|
+
const hasCache = s.cacheableCount > 0;
|
|
298
|
+
const recommendations = orderedRecommendations(s);
|
|
299
|
+
return {
|
|
300
|
+
runDurationMs: s.runDuration,
|
|
301
|
+
criticalPathMs: s.criticalPathDuration,
|
|
302
|
+
criticalPathTaskCount: s.criticalPathTaskCount,
|
|
303
|
+
recoverableMs: recoverableTime(s),
|
|
304
|
+
// One nested field, not a hits/total pair: "one set, the other not" is
|
|
305
|
+
// unrepresentable, so the Rust side drops its defensive match.
|
|
306
|
+
cache: hasCache
|
|
307
|
+
? { hits: s.cacheHits, total: s.cacheableCount }
|
|
308
|
+
: undefined,
|
|
309
|
+
cacheSkipped: s.cacheSkipped,
|
|
310
|
+
// The napi payload ships plain strings; a phrase link (the remote-cache CTA)
|
|
311
|
+
// stays URL-less here and is re-linked from `links` by the popup.
|
|
312
|
+
recommendations: recommendations.map(recommendationToPayloadString),
|
|
313
|
+
footer: { text: NX_PERFORMANCE_LABEL, href: NX_PERFORMANCE_LINK },
|
|
314
|
+
// Phrase links (e.g. the remote-cache CTA) carry their href as data so the
|
|
315
|
+
// popup hyperlinks the phrase in place; surfaced only when shown.
|
|
316
|
+
links: recommendationLinks(recommendations),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -34,7 +34,7 @@ export declare function invokeTasksRunner({ tasks, projectGraph, taskGraph, life
|
|
|
34
34
|
}): Promise<{
|
|
35
35
|
[id: string]: TaskResult;
|
|
36
36
|
}>;
|
|
37
|
-
export declare function constructLifeCycles(lifeCycle: LifeCycle): LifeCycle[];
|
|
37
|
+
export declare function constructLifeCycles(lifeCycle: LifeCycle, taskGraph: TaskGraph, nxJson?: NxJsonConfiguration, skipNxCache?: boolean): LifeCycle[];
|
|
38
38
|
export declare function getRunner(nxArgs: NxArgs, nxJson: NxJsonConfiguration): {
|
|
39
39
|
tasksRunner: TasksRunner;
|
|
40
40
|
runnerOptions: any;
|
|
@@ -37,6 +37,7 @@ const static_run_one_terminal_output_life_cycle_1 = require("./life-cycles/stati
|
|
|
37
37
|
const store_run_information_life_cycle_1 = require("./life-cycles/store-run-information-life-cycle");
|
|
38
38
|
const task_history_life_cycle_1 = require("./life-cycles/task-history-life-cycle");
|
|
39
39
|
const task_profiling_life_cycle_1 = require("./life-cycles/task-profiling-life-cycle");
|
|
40
|
+
const performance_life_cycle_1 = require("./life-cycles/performance-life-cycle");
|
|
40
41
|
const task_results_life_cycle_1 = require("./life-cycles/task-results-life-cycle");
|
|
41
42
|
const task_telemetry_life_cycle_1 = require("./life-cycles/task-telemetry-life-cycle");
|
|
42
43
|
const task_timings_life_cycle_1 = require("./life-cycles/task-timings-life-cycle");
|
|
@@ -139,6 +140,8 @@ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, pr
|
|
|
139
140
|
// Only run the TUI if there are tasks to run
|
|
140
141
|
if (tasks.length > 0) {
|
|
141
142
|
appLifeCycle = new AppLifeCycle(tasks, initiatingTasks.map((t) => t.id), isRunOne ? 0 /* RunMode.RunOne */ : 1 /* RunMode.RunMany */, pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, titleText, workspace_root_1.workspaceRoot, taskGraph);
|
|
143
|
+
// The native endCommand renders the perf report in the exit popup; the runner
|
|
144
|
+
// sources the payload and CompositeLifeCycle forwards it here.
|
|
142
145
|
lifeCycles.unshift(appLifeCycle);
|
|
143
146
|
/**
|
|
144
147
|
* Patch stdout.write and stderr.write methods to pass Nx Cloud client logs to the TUI via the lifecycle
|
|
@@ -365,6 +368,12 @@ async function runCommandForTasks(projectsToRun, currentProjectGraph, { nxJson }
|
|
|
365
368
|
restoreTerminal?.();
|
|
366
369
|
throw e;
|
|
367
370
|
}
|
|
371
|
+
finally {
|
|
372
|
+
// Print the report once, on success or failure (restoreTerminal has run in both
|
|
373
|
+
// paths). No-ops if a multi-task TUI run already delivered it via the popup; its own
|
|
374
|
+
// try/catch means it never masks `e`.
|
|
375
|
+
(0, performance_life_cycle_1.flushPerformanceReport)();
|
|
376
|
+
}
|
|
368
377
|
}
|
|
369
378
|
async function printConfigureAiAgentsDisclaimer() {
|
|
370
379
|
try {
|
|
@@ -643,7 +652,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
|
|
|
643
652
|
await (0, hash_task_1.hashTasksThatDoNotDependOnOutputsOfOtherTasks)(hasher, projectGraph, taskGraph, nxJson, taskDetails);
|
|
644
653
|
const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
|
|
645
654
|
const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
|
|
646
|
-
...constructLifeCycles(lifeCycle),
|
|
655
|
+
...constructLifeCycles(lifeCycle, taskGraph, nxJson, nxArgs.skipNxCache),
|
|
647
656
|
taskResultsLifecycle,
|
|
648
657
|
]);
|
|
649
658
|
let promiseOrObservable = tasksRunner(tasks, {
|
|
@@ -714,7 +723,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
|
|
|
714
723
|
await promiseOrObservable;
|
|
715
724
|
return taskResultsLifecycle.getTaskResults();
|
|
716
725
|
}
|
|
717
|
-
function constructLifeCycles(lifeCycle) {
|
|
726
|
+
function constructLifeCycles(lifeCycle, taskGraph, nxJson, skipNxCache) {
|
|
718
727
|
const lifeCycles = [];
|
|
719
728
|
lifeCycles.push(new store_run_information_life_cycle_1.StoreRunInformationLifeCycle());
|
|
720
729
|
lifeCycles.push(lifeCycle);
|
|
@@ -724,6 +733,7 @@ function constructLifeCycles(lifeCycle) {
|
|
|
724
733
|
if (process.env.NX_PROFILE) {
|
|
725
734
|
lifeCycles.push(new task_profiling_life_cycle_1.TaskProfilingLifeCycle(process.env.NX_PROFILE));
|
|
726
735
|
}
|
|
736
|
+
lifeCycles.push(new performance_life_cycle_1.PerformanceLifeCycle(taskGraph, { skipNxCache, nxJson }));
|
|
727
737
|
lifeCycles.push(new task_telemetry_life_cycle_1.TaskTelemetryLifeCycle());
|
|
728
738
|
const historyLifeCycle = (0, task_history_life_cycle_1.getTasksHistoryLifeCycle)();
|
|
729
739
|
lifeCycles.push(historyLifeCycle);
|