Skip to content

Commit 67d0e33

Browse files
authored
feat(core): add spinners when graph compute takes long time (#28966)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> Sometimes graph computation can take longer than may be expected. In these cases we can show a spinner, and if only one plugin is still running **and the daemon is disabled** also show the plugin names.
1 parent f922e2b commit 67d0e33

File tree

6 files changed

+229
-37
lines changed

6 files changed

+229
-37
lines changed

packages/nx/src/daemon/client/client.ts

+17
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ import {
7777
FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
7878
type HandleFlushSyncGeneratorChangesToDiskMessage,
7979
} from '../message-types/flush-sync-generator-changes-to-disk';
80+
import {
81+
DelayedSpinner,
82+
SHOULD_SHOW_SPINNERS,
83+
} from '../../utils/delayed-spinner';
8084

8185
const DAEMON_ENV_SETTINGS = {
8286
NX_PROJECT_GLOB_CACHE: 'false',
@@ -194,6 +198,17 @@ export class DaemonClient {
194198
projectGraph: ProjectGraph;
195199
sourceMaps: ConfigurationSourceMaps;
196200
}> {
201+
let spinner: DelayedSpinner;
202+
if (SHOULD_SHOW_SPINNERS) {
203+
// If the graph takes a while to load, we want to show a spinner.
204+
spinner = new DelayedSpinner(
205+
'Calculating the project graph on the Nx Daemon',
206+
500
207+
).scheduleMessageUpdate(
208+
'Calculating the project graph on the Nx Daemon is taking longer than expected. Re-run with NX_DAEMON=false to see more details.',
209+
30_000
210+
);
211+
}
197212
try {
198213
const response = await this.sendToDaemonViaQueue({
199214
type: 'REQUEST_PROJECT_GRAPH',
@@ -208,6 +223,8 @@ export class DaemonClient {
208223
} else {
209224
throw e;
210225
}
226+
} finally {
227+
spinner?.cleanup();
211228
}
212229
}
213230

packages/nx/src/project-graph/build-project-graph.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
ConfigurationSourceMaps,
4545
mergeMetadata,
4646
} from './utils/project-configuration-utils';
47+
import { DelayedSpinner, SHOULD_SHOW_SPINNERS } from '../utils/delayed-spinner';
4748

4849
let storedFileMap: FileMap | null = null;
4950
let storedAllWorkspaceFiles: FileData[] | null = null;
@@ -313,14 +314,47 @@ async function updateProjectGraphWithPlugins(
313314
(plugin) => plugin.createDependencies
314315
);
315316
performance.mark('createDependencies:start');
317+
318+
let spinner: DelayedSpinner;
319+
const inProgressPlugins = new Set<string>();
320+
321+
function updateSpinner() {
322+
if (!spinner) {
323+
return;
324+
}
325+
if (inProgressPlugins.size === 1) {
326+
return `Creating project graph dependencies with ${
327+
inProgressPlugins.keys()[0]
328+
}`;
329+
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
330+
return [
331+
`Creating project graph dependencies with ${inProgressPlugins.size} plugins`,
332+
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
333+
].join('\n');
334+
} else {
335+
return `Creating project graph dependencies with ${inProgressPlugins.size} plugins`;
336+
}
337+
}
338+
339+
if (SHOULD_SHOW_SPINNERS) {
340+
spinner = new DelayedSpinner(
341+
`Creating project graph dependencies with ${plugins.length} plugins`
342+
);
343+
}
344+
316345
await Promise.all(
317346
createDependencyPlugins.map(async (plugin) => {
318347
performance.mark(`${plugin.name}:createDependencies - start`);
319-
348+
inProgressPlugins.add(plugin.name);
320349
try {
321-
const dependencies = await plugin.createDependencies({
322-
...context,
323-
});
350+
const dependencies = await plugin
351+
.createDependencies({
352+
...context,
353+
})
354+
.finally(() => {
355+
inProgressPlugins.delete(plugin.name);
356+
updateSpinner();
357+
});
324358

325359
for (const dep of dependencies) {
326360
builder.addDependency(
@@ -352,6 +386,7 @@ async function updateProjectGraphWithPlugins(
352386
`createDependencies:start`,
353387
`createDependencies:end`
354388
);
389+
spinner?.cleanup();
355390

356391
const graphWithDeps = builder.getUpdatedProjectGraph();
357392

@@ -396,15 +431,43 @@ export async function applyProjectMetadata(
396431
const errors: CreateMetadataError[] = [];
397432

398433
performance.mark('createMetadata:start');
434+
let spinner: DelayedSpinner;
435+
const inProgressPlugins = new Set<string>();
436+
437+
function updateSpinner() {
438+
if (!spinner) {
439+
return;
440+
}
441+
if (inProgressPlugins.size === 1) {
442+
return `Creating project metadata with ${inProgressPlugins.keys()[0]}`;
443+
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
444+
return [
445+
`Creating project metadata with ${inProgressPlugins.size} plugins`,
446+
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
447+
].join('\n');
448+
} else {
449+
return `Creating project metadata with ${inProgressPlugins.size} plugins`;
450+
}
451+
}
452+
453+
if (SHOULD_SHOW_SPINNERS) {
454+
spinner = new DelayedSpinner(
455+
`Creating project metadata with ${plugins.length} plugins`
456+
);
457+
}
458+
399459
const promises = plugins.map(async (plugin) => {
400460
if (plugin.createMetadata) {
401461
performance.mark(`${plugin.name}:createMetadata - start`);
462+
inProgressPlugins.add(plugin.name);
402463
try {
403464
const metadata = await plugin.createMetadata(graph, context);
404465
results.push({ metadata, pluginName: plugin.name });
405466
} catch (e) {
406467
errors.push(new CreateMetadataError(e, plugin.name));
407468
} finally {
469+
inProgressPlugins.delete(plugin.name);
470+
updateSpinner();
408471
performance.mark(`${plugin.name}:createMetadata - end`);
409472
performance.measure(
410473
`${plugin.name}:createMetadata`,
@@ -417,6 +480,8 @@ export async function applyProjectMetadata(
417480

418481
await Promise.all(promises);
419482

483+
spinner?.cleanup();
484+
420485
for (const { metadata: projectsMetadata, pluginName } of results) {
421486
for (const project in projectsMetadata) {
422487
const projectConfiguration: ProjectConfiguration =

packages/nx/src/project-graph/plugins/internal-api.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
CreateNodesContextV2,
1616
CreateNodesResult,
1717
NxPluginV2,
18+
ProjectsMetadata,
1819
} from './public-api';
1920
import { ProjectGraph } from '../../config/project-graph';
2021
import { loadNxPluginInIsolation } from './isolation';
@@ -25,6 +26,7 @@ import {
2526
isAggregateCreateNodesError,
2627
} from '../error-types';
2728
import { IS_WASM } from '../../native';
29+
import { RawProjectGraphDependency } from '../project-graph-builder';
2830

2931
export class LoadedNxPlugin {
3032
readonly name: string;
@@ -41,11 +43,11 @@ export class LoadedNxPlugin {
4143
];
4244
readonly createDependencies?: (
4345
context: CreateDependenciesContext
44-
) => ReturnType<CreateDependencies>;
46+
) => Promise<RawProjectGraphDependency[]>;
4547
readonly createMetadata?: (
4648
graph: ProjectGraph,
4749
context: CreateMetadataContext
48-
) => ReturnType<CreateMetadata>;
50+
) => Promise<ProjectsMetadata>;
4951

5052
readonly options?: unknown;
5153
readonly include?: string[];
@@ -110,12 +112,12 @@ export class LoadedNxPlugin {
110112
}
111113

112114
if (plugin.createDependencies) {
113-
this.createDependencies = (context) =>
115+
this.createDependencies = async (context) =>
114116
plugin.createDependencies(this.options, context);
115117
}
116118

117119
if (plugin.createMetadata) {
118-
this.createMetadata = (graph, context) =>
120+
this.createMetadata = async (graph, context) =>
119121
plugin.createMetadata(graph, this.options, context);
120122
}
121123
}

packages/nx/src/project-graph/plugins/isolation/messaging.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export interface PluginCreateDependenciesResult {
8484
type: 'createDependenciesResult';
8585
payload:
8686
| {
87-
dependencies: ReturnType<LoadedNxPlugin['createDependencies']>;
87+
dependencies: Awaited<ReturnType<LoadedNxPlugin['createDependencies']>>;
8888
success: true;
8989
tx: string;
9090
}
@@ -99,7 +99,7 @@ export interface PluginCreateMetadataResult {
9999
type: 'createMetadataResult';
100100
payload:
101101
| {
102-
metadata: ReturnType<LoadedNxPlugin['createMetadata']>;
102+
metadata: Awaited<ReturnType<LoadedNxPlugin['createMetadata']>>;
103103
success: true;
104104
tx: string;
105105
}

packages/nx/src/project-graph/utils/project-configuration-utils.ts

+70-27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { workspaceRoot } from '../../utils/workspace-root';
1313
import { minimatch } from 'minimatch';
1414
import { join } from 'path';
1515
import { performance } from 'perf_hooks';
16+
1617
import { LoadedNxPlugin } from '../plugins/internal-api';
1718
import {
1819
MergeNodesError,
@@ -30,6 +31,11 @@ import {
3031
} from '../error-types';
3132
import { CreateNodesResult } from '../plugins/public-api';
3233
import { isGlobPattern } from '../../utils/globs';
34+
import { isOnDaemon } from '../../daemon/is-on-daemon';
35+
import {
36+
DelayedSpinner,
37+
SHOULD_SHOW_SPINNERS,
38+
} from '../../utils/delayed-spinner';
3339

3440
export type SourceInformation = [file: string | null, plugin: string];
3541
export type ConfigurationSourceMaps = Record<
@@ -324,6 +330,32 @@ export async function createProjectConfigurations(
324330
): Promise<ConfigurationResult> {
325331
performance.mark('build-project-configs:start');
326332

333+
let spinner: DelayedSpinner;
334+
const inProgressPlugins = new Set<string>();
335+
336+
function updateSpinner() {
337+
if (!spinner) {
338+
return;
339+
}
340+
341+
if (inProgressPlugins.size === 1) {
342+
return `Creating project graph nodes with ${inProgressPlugins.keys()[0]}`;
343+
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
344+
return [
345+
`Creating project graph nodes with ${inProgressPlugins.size} plugins`,
346+
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
347+
].join('\n');
348+
} else {
349+
return `Creating project graph nodes with ${inProgressPlugins.size} plugins`;
350+
}
351+
}
352+
353+
if (SHOULD_SHOW_SPINNERS) {
354+
spinner = new DelayedSpinner(
355+
`Creating project graph nodes with ${plugins.length} plugins`
356+
);
357+
}
358+
327359
const results: Array<ReturnType<LoadedNxPlugin['createNodes'][1]>> = [];
328360
const errors: Array<
329361
| AggregateCreateNodesError
@@ -352,44 +384,55 @@ export async function createProjectConfigurations(
352384
exclude
353385
);
354386

387+
inProgressPlugins.add(pluginName);
355388
let r = createNodes(matchingConfigFiles, {
356389
nxJsonConfiguration: nxJson,
357390
workspaceRoot: root,
358-
}).catch((e: Error) => {
359-
const errorBodyLines = [
360-
`An error occurred while processing files for the ${pluginName} plugin.`,
361-
];
362-
const error: AggregateCreateNodesError = isAggregateCreateNodesError(e)
363-
? // This is an expected error if something goes wrong while processing files.
364-
e
365-
: // This represents a single plugin erroring out with a hard error.
366-
new AggregateCreateNodesError([[null, e]], []);
367-
368-
const innerErrors = error.errors;
369-
for (const [file, e] of innerErrors) {
370-
if (file) {
371-
errorBodyLines.push(` - ${file}: ${e.message}`);
372-
} else {
373-
errorBodyLines.push(` - ${e.message}`);
374-
}
375-
if (e.stack) {
376-
const innerStackTrace = ' ' + e.stack.split('\n')?.join('\n ');
377-
errorBodyLines.push(innerStackTrace);
391+
})
392+
.catch((e: Error) => {
393+
const errorBodyLines = [
394+
`An error occurred while processing files for the ${pluginName} plugin.`,
395+
];
396+
const error: AggregateCreateNodesError = isAggregateCreateNodesError(e)
397+
? // This is an expected error if something goes wrong while processing files.
398+
e
399+
: // This represents a single plugin erroring out with a hard error.
400+
new AggregateCreateNodesError([[null, e]], []);
401+
402+
const innerErrors = error.errors;
403+
for (const [file, e] of innerErrors) {
404+
if (file) {
405+
errorBodyLines.push(` - ${file}: ${e.message}`);
406+
} else {
407+
errorBodyLines.push(` - ${e.message}`);
408+
}
409+
if (e.stack) {
410+
const innerStackTrace =
411+
' ' + e.stack.split('\n')?.join('\n ');
412+
errorBodyLines.push(innerStackTrace);
413+
}
378414
}
379-
}
380415

381-
error.stack = errorBodyLines.join('\n');
416+
error.stack = errorBodyLines.join('\n');
382417

383-
// This represents a single plugin erroring out with a hard error.
384-
errors.push(error);
385-
// The plugin didn't return partial results, so we return an empty array.
386-
return error.partialResults.map((r) => [pluginName, r[0], r[1]] as const);
387-
});
418+
// This represents a single plugin erroring out with a hard error.
419+
errors.push(error);
420+
// The plugin didn't return partial results, so we return an empty array.
421+
return error.partialResults.map(
422+
(r) => [pluginName, r[0], r[1]] as const
423+
);
424+
})
425+
.finally(() => {
426+
inProgressPlugins.delete(pluginName);
427+
updateSpinner();
428+
});
388429

389430
results.push(r);
390431
}
391432

392433
return Promise.all(results).then((results) => {
434+
spinner?.cleanup();
435+
393436
const { projectRootMap, externalNodes, rootMap, configurationSourceMaps } =
394437
mergeCreateNodesResults(results, nxJson, errors);
395438

0 commit comments

Comments
 (0)