nx 23.1.0-beta.3 → 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.
Files changed (26) hide show
  1. package/dist/src/command-line/migrate/migrate.d.ts +1 -0
  2. package/dist/src/command-line/migrate/migrate.js +25 -4
  3. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +30 -11
  4. package/dist/src/native/index.d.ts +48 -1
  5. package/dist/src/native/native-bindings.js +1 -0
  6. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  7. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  8. package/dist/src/tasks-runner/default-tasks-runner.js +6 -3
  9. package/dist/src/tasks-runner/init-tasks-runner.js +1 -1
  10. package/dist/src/tasks-runner/life-cycle.d.ts +10 -5
  11. package/dist/src/tasks-runner/life-cycle.js +4 -4
  12. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +156 -0
  13. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +486 -0
  14. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +59 -0
  15. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +152 -0
  16. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +62 -0
  17. package/dist/src/tasks-runner/life-cycles/performance-report.js +318 -0
  18. package/dist/src/tasks-runner/run-command.d.ts +1 -1
  19. package/dist/src/tasks-runner/run-command.js +12 -2
  20. package/dist/src/utils/min-release-age/behavior/pnpm.d.ts +6 -5
  21. package/dist/src/utils/min-release-age/behavior/pnpm.js +87 -339
  22. package/dist/src/utils/package-manager.d.ts +6 -3
  23. package/dist/src/utils/package-manager.js +11 -7
  24. package/dist/src/utils/terminal-link.d.ts +10 -6
  25. package/dist/src/utils/terminal-link.js +9 -11
  26. package/package.json +11 -11
@@ -85,6 +85,7 @@ export declare class Migrator {
85
85
  }>;
86
86
  private createMigrateJson;
87
87
  private buildPackageJsonUpdates;
88
+ private resolveVersionForCascade;
88
89
  private populatePackageJsonUpdatesAndGetPackagesToCheck;
89
90
  private getPackageJsonUpdatesFromMigrationConfig;
90
91
  /**
@@ -192,16 +192,37 @@ class Migrator {
192
192
  !this.areIncompatiblePackagesPresent(packageUpdate.incompatibleWith) &&
193
193
  (!this.interactive ||
194
194
  (await this.runPackageJsonUpdatesConfirmationPrompt(packageUpdate, packageUpdateKey, packageToCheck.package)))) {
195
- Object.entries(packageUpdate.packages).forEach(([name, update]) => {
195
+ const updateEntries = Object.entries(packageUpdate.packages);
196
+ // Validate all up front so invalid metadata fails fast, before any
197
+ // resolution does I/O.
198
+ for (const [name, update] of updateEntries) {
196
199
  this.validatePackageUpdateVersion(packageToCheck.package, name, update);
197
- filteredUpdates[name] = update;
198
- this.packageUpdates[name] = update;
199
- });
200
+ }
201
+ // Resolve serially: resolution can prompt (pnpm strict cooldown) and
202
+ // append to minimumReleaseAgeExclude, so a serial loop avoids
203
+ // overlapping prompts and keeps packageUpdates ordering stable.
204
+ for (const [name, update] of updateEntries) {
205
+ const resolvedUpdate = {
206
+ ...update,
207
+ version: await this.resolveVersionForCascade(name, update.version),
208
+ };
209
+ filteredUpdates[name] = resolvedUpdate;
210
+ this.packageUpdates[name] = resolvedUpdate;
211
+ }
200
212
  }
201
213
  }
202
214
  await Promise.all(Object.entries(filteredUpdates).map(([name, update]) => this.buildPackageJsonUpdates(name, update)));
203
215
  }
204
216
  }
217
+ async resolveVersionForCascade(packageName, version) {
218
+ // Already a fully-qualified semver (incl. prereleases) - nothing to resolve.
219
+ if ((0, semver_1.valid)(version)) {
220
+ return version;
221
+ }
222
+ // Otherwise resolve the spec (range/tag) through the min-release-age policy,
223
+ // which also honors any configured minimumReleaseAgeExclude entries.
224
+ return (0, resolve_package_version_1.resolvePackageVersionRespectingMinReleaseAge)(packageName, version);
225
+ }
205
226
  async populatePackageJsonUpdatesAndGetPackagesToCheck(targetPackage, target) {
206
227
  let targetVersion = target.version;
207
228
  if (this.to[targetPackage]) {
@@ -178,9 +178,26 @@ async function getCachedSerializedProjectGraphPromise(socket) {
178
178
  }
179
179
  function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFiles) {
180
180
  ++fileChangeCounter;
181
- for (let f of [...createdFiles, ...updatedFiles]) {
181
+ // Hash the changed files up front and drop no-op rewrites before they can
182
+ // trigger an expensive recompute. Restoring a cached task output, a
183
+ // `git checkout` back to the same content, or a formatter that changes
184
+ // nothing all rewrite a file (new inode) the watcher reports as changed
185
+ // even though the bytes are identical. updateFilesInContext updates the
186
+ // workspace context and returns only the files whose content actually
187
+ // changed. Hashing here — once per watcher batch — rather than inside the
188
+ // recompute keeps it off the stale-retry path, which would otherwise see
189
+ // "no change" after the first pass already updated the context hashes.
190
+ perf_hooks_1.performance.mark('hash-watched-changes-start');
191
+ const changedFileHashes = createdFiles.length > 0 ||
192
+ updatedFiles.length > 0 ||
193
+ deletedFiles.length > 0
194
+ ? ((0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, [...createdFiles, ...updatedFiles], deletedFiles) ?? {})
195
+ : {};
196
+ perf_hooks_1.performance.mark('hash-watched-changes-end');
197
+ perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
198
+ for (const [f, hash] of Object.entries(changedFileHashes)) {
182
199
  collectedDeletedFiles.delete(f);
183
- collectedUpdatedFiles.set(f, fileChangeCounter);
200
+ collectedUpdatedFiles.set(f, { version: fileChangeCounter, hash });
184
201
  }
185
202
  for (let f of deletedFiles) {
186
203
  collectedUpdatedFiles.delete(f);
@@ -188,9 +205,7 @@ function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFi
188
205
  }
189
206
  // The native watcher already coalesces a burst of events into one batch,
190
207
  // so socket + listener notifications dispatch immediately.
191
- if (createdFiles.length > 0 ||
192
- updatedFiles.length > 0 ||
193
- deletedFiles.length > 0) {
208
+ if (Object.keys(changedFileHashes).length > 0 || deletedFiles.length > 0) {
194
209
  (0, file_change_events_1.notifyFileChangeListeners)({ createdFiles, updatedFiles, deletedFiles });
195
210
  (0, file_watcher_sockets_1.notifyFileWatcherSockets)(createdFiles, updatedFiles, deletedFiles);
196
211
  // Bump generation synchronously so any in-flight compute fails its
@@ -284,14 +299,18 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
284
299
  return cachedSerializedProjectGraphPromise;
285
300
  };
286
301
  try {
287
- perf_hooks_1.performance.mark('hash-watched-changes-start');
288
302
  const updatedFilesSnapshot = new Map(collectedUpdatedFiles);
289
303
  const deletedFilesSnapshot = new Map(collectedDeletedFiles);
290
304
  const updatedFiles = [...updatedFilesSnapshot.keys()];
291
305
  const deletedFiles = [...deletedFilesSnapshot.keys()];
292
- let updatedFileHashes = (0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, updatedFiles, deletedFiles);
293
- perf_hooks_1.performance.mark('hash-watched-changes-end');
294
- perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
306
+ // Hashes were already computed (and the workspace context updated) in
307
+ // scheduleProjectGraphRecomputation, which also dropped no-op rewrites.
308
+ // Reuse them so the context isn't re-hashed on every (possibly stale)
309
+ // recompute attempt.
310
+ const updatedFileHashes = {};
311
+ for (const [f, { hash }] of updatedFilesSnapshot) {
312
+ updatedFileHashes[f] = hash;
313
+ }
295
314
  logger_1.serverLogger.requestLog(`Updated workspace context based on watched changes, recomputing project graph...`);
296
315
  logger_1.serverLogger.requestLog(updatedFiles);
297
316
  logger_1.serverLogger.requestLog(deletedFiles);
@@ -333,8 +352,8 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
333
352
  // from the daemon's view (project graph misses recently added files).
334
353
  // Match version-stamps so a file modified mid-flight (higher version)
335
354
  // stays in the queue for reprocessing.
336
- for (const [f, version] of updatedFilesSnapshot) {
337
- if (collectedUpdatedFiles.get(f) === version) {
355
+ for (const [f, { version }] of updatedFilesSnapshot) {
356
+ if (collectedUpdatedFiles.get(f)?.version === version) {
338
357
  collectedUpdatedFiles.delete(f);
339
358
  }
340
359
  }
@@ -34,7 +34,7 @@ export declare class AppLifeCycle {
34
34
  startTasks(tasks: Array<Task>, metadata: object): void
35
35
  printTaskTerminalOutput(task: Task, status: string, output: string): void
36
36
  endTasks(taskResults: Array<TaskResult>, metadata: object): void
37
- endCommand(): void
37
+ endCommand(summary?: PerformanceSummaryPayload | undefined | null): void
38
38
  __init(doneCallback: (() => unknown)): void
39
39
  registerRunningTask(taskId: string, parserAndWriter: ExternalObject<[ParserArc, WriterArc]>): void
40
40
  registerRunningTaskWithEmptyParser(taskId: string): void
@@ -275,6 +275,15 @@ export interface CachedResult {
275
275
  size?: number
276
276
  }
277
277
 
278
+ /**
279
+ * Cache hits vs total; present only when there was a cache outcome. A bypassed
280
+ * cache is signalled separately by `cache_skipped`.
281
+ */
282
+ export interface CacheStat {
283
+ hits: number
284
+ total: number
285
+ }
286
+
278
287
  export declare function canInstallNxConsole(): Promise<boolean>
279
288
 
280
289
  export declare function canInstallNxConsoleForEditor(editor: SupportedEditor): Promise<boolean>
@@ -375,6 +384,13 @@ export declare function findImports(projectFileMap: Record<string, Array<string>
375
384
  */
376
385
  export declare function flushTelemetry(): void
377
386
 
387
+ /**
388
+ * The single duration formatter — used by the task list, terminal report, and TUI
389
+ * popup. Exposed to JS as `formatDuration` so all three share one implementation.
390
+ * 0 (or sub-millisecond) → "<1ms", then "470ms", "13.4s", "1m 30s".
391
+ */
392
+ export declare function formatDuration(ms: number): string
393
+
378
394
  export declare function getBinaryTarget(): string
379
395
 
380
396
  export declare function getDefaultMaxCacheSize(cachePath: string): number
@@ -524,6 +540,15 @@ export declare function killProcessTree(rootPid: number, signal?: string | numbe
524
540
  */
525
541
  export declare function killProcessTreeGraceful(rootPid: number, signal?: string | number | undefined | null, gracePeriodMs?: number | undefined | null): Promise<void>
526
542
 
543
+ /**
544
+ * A docs link rendered as an OSC 8 hyperlink. Both fields come from TS so the
545
+ * popup never hardcodes a URL.
546
+ */
547
+ export interface Link {
548
+ text: string
549
+ href: string
550
+ }
551
+
527
552
  export declare function logDebug(message: string): void
528
553
 
529
554
  /** Combined metadata for groups and processes */
@@ -564,6 +589,28 @@ export interface NxWorkspaceFilesExternals {
564
589
 
565
590
  export declare function parseTaskStatus(stringStatus: string): TaskStatus
566
591
 
592
+ /**
593
+ * Structured run report shown in the exit-countdown popup. The TUI builds the
594
+ * visual from these numbers rather than receiving a pre-formatted string.
595
+ */
596
+ export interface PerformanceSummaryPayload {
597
+ runDurationMs: number
598
+ criticalPathMs: number
599
+ criticalPathTaskCount: number
600
+ recoverableMs: number
601
+ cache?: CacheStat
602
+ cacheSkipped: boolean
603
+ /** Already in display order; a multi-line entry embeds a task list. */
604
+ recommendations: Array<string>
605
+ /** The docs footer link, rendered as a bullet and hyperlinked. */
606
+ footer: Link
607
+ /**
608
+ * Phrases already in `recommendations` to hyperlink in place (e.g. the
609
+ * remote-cache CTA); empty when none apply.
610
+ */
611
+ links: Array<Link>
612
+ }
613
+
567
614
  /** Process metadata (static, doesn't change during process lifetime) */
568
615
  export interface ProcessMetadata {
569
616
  ppid: number
@@ -605,6 +605,7 @@ module.exports.EventType = nativeBinding.EventType
605
605
  module.exports.expandOutputs = nativeBinding.expandOutputs
606
606
  module.exports.findImports = nativeBinding.findImports
607
607
  module.exports.flushTelemetry = nativeBinding.flushTelemetry
608
+ module.exports.formatDuration = nativeBinding.formatDuration
608
609
  module.exports.getBinaryTarget = nativeBinding.getBinaryTarget
609
610
  module.exports.getDefaultMaxCacheSize = nativeBinding.getDefaultMaxCacheSize
610
611
  module.exports.getEventDimensions = nativeBinding.getEventDimensions
Binary file
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultTasksRunner = exports.RemoteCacheV2 = void 0;
4
4
  const task_orchestrator_1 = require("./task-orchestrator");
5
+ const performance_life_cycle_1 = require("./life-cycles/performance-life-cycle");
5
6
  const cache_directory_1 = require("../utils/cache-directory");
6
7
  const promises_1 = require("fs/promises");
7
8
  const path_1 = require("path");
@@ -54,13 +55,15 @@ class RemoteCacheV2 {
54
55
  }
55
56
  exports.RemoteCacheV2 = RemoteCacheV2;
56
57
  const defaultTasksRunner = async (tasks, options, context) => {
57
- const { total: threadCount } = (0, task_orchestrator_1.getThreadPoolSize)(options, context.taskGraph);
58
- await options.lifeCycle.startCommand(threadCount);
58
+ const { total: threadCount, discrete } = (0, task_orchestrator_1.getThreadPoolSize)(options, context.taskGraph);
59
+ // Total thread count drives the TUI; the discrete `--parallel` feeds the perf report.
60
+ await options.lifeCycle.startCommand(threadCount, discrete);
59
61
  try {
60
62
  return await runAllTasks(options, context);
61
63
  }
62
64
  finally {
63
- await options.lifeCycle.endCommand();
65
+ // Hand the TUI exit popup the perf report (multi-task TUI runs); other modes flush it.
66
+ await options.lifeCycle.endCommand((0, performance_life_cycle_1.getPerformanceReport)(tasks.length));
64
67
  }
65
68
  };
66
69
  exports.defaultTasksRunner = defaultTasksRunner;
@@ -15,7 +15,7 @@ async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, li
15
15
  const invokeRunnerTerminalLifecycle = new invoke_runner_terminal_output_life_cycle_1.InvokeRunnerTerminalOutputLifeCycle(tasks);
16
16
  const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
17
17
  const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
18
- ...(0, run_command_1.constructLifeCycles)(invokeRunnerTerminalLifecycle),
18
+ ...(0, run_command_1.constructLifeCycles)(invokeRunnerTerminalLifecycle, fullTaskGraph, nxJson),
19
19
  taskResultsLifecycle,
20
20
  lifeCycle,
21
21
  ]);
@@ -1,5 +1,5 @@
1
1
  import { Task } from '../config/task-graph';
2
- import { BatchInfo, BatchStatus, ExternalObject, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
2
+ import { BatchInfo, BatchStatus, ExternalObject, PerformanceSummaryPayload, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
3
3
  import { TaskStatus } from './tasks-runner';
4
4
  /**
5
5
  * The result of a completed {@link Task}.
@@ -21,8 +21,13 @@ export interface TaskMetadata {
21
21
  groupId: number;
22
22
  }
23
23
  export interface LifeCycle {
24
- startCommand?(parallel?: number): void | Promise<void>;
25
- endCommand?(): void | Promise<void>;
24
+ /**
25
+ * @param threadCount total thread-pool size (drives the TUI display)
26
+ * @param parallel resolved `--parallel` (discrete slots), for the performance report
27
+ */
28
+ startCommand?(threadCount?: number, parallel?: number): void | Promise<void>;
29
+ /** @param summary performance report payload for the TUI exit popup (TUI runs only) */
30
+ endCommand?(summary?: PerformanceSummaryPayload): void | Promise<void>;
26
31
  scheduleTask?(task: Task): void | Promise<void>;
27
32
  /**
28
33
  * @deprecated use startTasks
@@ -53,8 +58,8 @@ export interface LifeCycle {
53
58
  export declare class CompositeLifeCycle implements LifeCycle {
54
59
  private readonly lifeCycles;
55
60
  constructor(lifeCycles: LifeCycle[]);
56
- startCommand(parallel?: number): Promise<void>;
57
- endCommand(): Promise<void>;
61
+ startCommand(threadCount?: number, parallel?: number): Promise<void>;
62
+ endCommand(summary?: PerformanceSummaryPayload): Promise<void>;
58
63
  scheduleTask(task: Task): Promise<void>;
59
64
  startTask(task: Task): void;
60
65
  endTask(task: Task, code: number): void;
@@ -5,17 +5,17 @@ class CompositeLifeCycle {
5
5
  constructor(lifeCycles) {
6
6
  this.lifeCycles = lifeCycles;
7
7
  }
8
- async startCommand(parallel) {
8
+ async startCommand(threadCount, parallel) {
9
9
  for (let l of this.lifeCycles) {
10
10
  if (l.startCommand) {
11
- await l.startCommand(parallel);
11
+ await l.startCommand(threadCount, parallel);
12
12
  }
13
13
  }
14
14
  }
15
- async endCommand() {
15
+ async endCommand(summary) {
16
16
  for (let l of this.lifeCycles) {
17
17
  if (l.endCommand) {
18
- await l.endCommand();
18
+ await l.endCommand(summary);
19
19
  }
20
20
  }
21
21
  }
@@ -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[];