UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

194 lines (193 loc) 8.33 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PerformanceLifeCycle = void 0; exports.getPerformanceSummaryPayload = getPerformanceSummaryPayload; exports.getPerformanceReport = getPerformanceReport; exports.flushPerformanceReport = flushPerformanceReport; const node_fs_1 = require("node:fs"); const performance_report_1 = require("./performance-report"); const is_tui_enabled_1 = require("../is-tui-enabled"); const performance_analysis_1 = require("./performance-analysis"); /** * Measures how much wall-clock a run loses to parallelism contention versus its * critical-path floor, and reports it at the end of a run. Added on every run by * `constructLifeCycles`, but only emitted where the report is flushed (the CLI * `invokeTasksRunner` path) — the programmatic `init-tasks-runner` path collects * timings but never displays them. * * overhead = runDuration − criticalPathDuration, split by CAUSE off the occupancy * timeline: slot-queued time (recoverable by parallelism / machines) versus * coordinator time (hashing, scheduling, continuous-dep waits). * * Scope: discrete tasks only. Continuous tasks (no end time) are excluded; a * discrete task's wait for a continuous dependency to start is eligibility, not * contention. */ class PerformanceLifeCycle { constructor(taskGraph, options = {}) { this.taskGraph = taskGraph; this.options = options; this.timings = new Map(); /** taskId → terminal status (cache hit vs ran), for the cache summary. */ this.statuses = new Map(); /** taskId → other tasks in its batch (batches run sequentially). */ this.batchSiblings = new Map(); /** Resolved `--parallel`, set by the runner via {@link startCommand}'s second arg once the thread pool is sized. */ this.parallel = 1; activePerformanceLifeCycle = this; } // === Lifecycle hooks (called by the orchestrator as the run progresses) === /** * The runner passes the resolved `--parallel` (getThreadPoolSize's `discrete`) as the * second arg; the first (thread count) is for the TUI and ignored here. */ startCommand(_threadCount, parallel) { if (parallel != null) { this.parallel = parallel; } } registerRunningBatch(_batchId, batchInfo) { for (const id of batchInfo.taskIds) { this.batchSiblings.set(id, batchInfo.taskIds.filter((other) => other !== id)); } } endTasks(taskResults) { // Called incrementally (per group/batch); accumulate so the last call sees every timing. for (const { task, status } of taskResults) { const entry = this.entry(task.id); // `!= null`, not truthiness: synthetic/relative timelines can legitimately start at 0. if (task.startTime != null) { entry.startTime = task.startTime; } if (task.endTime != null) { entry.endTime = task.endTime; } if (status != null) { this.statuses.set(task.id, status); } } } entry(taskId) { let entry = this.timings.get(taskId); if (!entry) { entry = { continuous: this.taskGraph.tasks[taskId]?.continuous ?? false }; this.timings.set(taskId, entry); } return entry; } /** Analyze the collected timings into a structured summary, or `null` when no discrete task timings were recorded. */ getSummary() { return new performance_analysis_1.PerformanceAnalysis(this.timings, this.statuses, this.taskGraph, this.batchSiblings, this.parallel, this.options).summary(); } } exports.PerformanceLifeCycle = PerformanceLifeCycle; /** The most recently constructed performance lifecycle, read after the run. Cleared once consumed. */ let activePerformanceLifeCycle = null; /** * Structured report for the TUI's exit-countdown popup, or null when nothing to * show. Clears the active lifecycle so the popup owns the report and a later terminal * flush can't re-print it. Best-effort: a throw degrades to null. */ function getPerformanceSummaryPayload() { const lifeCycle = activePerformanceLifeCycle; if (!lifeCycle) { return null; } try { const summary = lifeCycle.getSummary(); if (!summary) { return null; } activePerformanceLifeCycle = null; return (0, performance_report_1.buildExitSummaryPayload)(summary); } catch (e) { // Best-effort: the report must never break the run. Surface the cause only under // verbose logging so a missing report stays debuggable. if (process.env.NX_VERBOSE_LOGGING === 'true') { console.error(e); } return null; } } /** * The performance report payload for `endCommand`'s TUI exit popup, or undefined when the * report should instead be flushed to the terminal — non-TUI runs, or a single task (the * complement of run-command's flush gate). Reading it consumes the report so the flush * won't reprint it. */ function getPerformanceReport(taskCount) { if (!(0, is_tui_enabled_1.isTuiEnabled)() || taskCount <= 1) { return undefined; } return getPerformanceSummaryPayload() ?? undefined; } /** * Print the performance report (if enabled) after the run summary. Called once the * terminal is restored, so it appears in every output mode including the TUI. */ function flushPerformanceReport() { const lifeCycle = activePerformanceLifeCycle; activePerformanceLifeCycle = null; if (!lifeCycle) { return; } // Cosmetic report; a throw (e.g. EPIPE to a closed pipe) must never mask the // real task error or fail an otherwise successful run. try { const summary = lifeCycle.getSummary(); if (!summary) { return; } // restore_terminal cooks the terminal back post-TUI, so console.log's plain \n // renders fine; it also supplies the single trailing newline formatReport omits. console.log((0, performance_report_1.formatReport)(summary)); // In GitHub Actions, also append the report to the job summary page — the same stats // as above, led by the run's outcome (a failed-tasks list, or a success line). // Independent of the console.log above so neither masks the other. writePerformanceReportToGitHubActions(summary); } catch (e) { // Best-effort report; never let it affect the run's exit behavior. Surface the // cause only under verbose logging. if (process.env.NX_VERBOSE_LOGGING === 'true') { console.error(e); } } } /** * Append the performance report to the GitHub Actions job summary page when running in * Actions (`$GITHUB_STEP_SUMMARY` is set there and nowhere else). No-op otherwise. The * Markdown is rendered below the guard, so non-CI runs don't pay to format a report * nothing reads. Best-effort: a write failure must never affect the run. * * Skipped for a nested run (one nx command invoked by another nx task's command), so only * the outermost run writes to the summary. Nx sets `NX_TASK_TARGET_PROJECT` on every task's * environment, which a nested nx inherits; its absence marks the top-level invocation (the * same "NX is already running" signal `nx exec` uses). */ function writePerformanceReportToGitHubActions(summary) { const summaryPath = process.env.GITHUB_STEP_SUMMARY; if (process.env.GITHUB_ACTIONS !== 'true' || !summaryPath || process.env.NX_TASK_TARGET_PROJECT) { return; } try { const report = (0, performance_report_1.formatReportMarkdown)(summary, currentNxCommand()); (0, node_fs_1.appendFileSync)(summaryPath, `${report}\n`); } catch (e) { if (process.env.NX_VERBOSE_LOGGING === 'true') { console.error(e); } } } /** * The nx command as typed — everything after the nx bin (`process.argv[0]` is node, * `[1]` is the bin). Only read on the CLI flush path, where argv is always the real nx * invocation, so it identifies the run in the summary heading. */ function currentNxCommand() { return process.argv.slice(2).join(' ').trim(); }