Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add spinners when graph compute takes long time #28966

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/nx/src/daemon/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ import {
FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK,
type HandleFlushSyncGeneratorChangesToDiskMessage,
} from '../message-types/flush-sync-generator-changes-to-disk';
import {
DelayedSpinner,
SHOULD_SHOW_SPINNERS,
} from '../../utils/delayed-spinner';

const DAEMON_ENV_SETTINGS = {
NX_PROJECT_GLOB_CACHE: 'false',
Expand Down Expand Up @@ -194,6 +198,17 @@ export class DaemonClient {
projectGraph: ProjectGraph;
sourceMaps: ConfigurationSourceMaps;
}> {
let spinner: DelayedSpinner;
if (SHOULD_SHOW_SPINNERS) {
// If the graph takes a while to load, we want to show a spinner.
spinner = new DelayedSpinner(
'Calculating the project graph on the Nx Daemon',
500
).scheduleMessageUpdate(
'Calculating the project graph on the Nx Daemon is taking longer than expected. Re-run with NX_DAEMON=false to see more details.',
30_000
);
}
try {
const response = await this.sendToDaemonViaQueue({
type: 'REQUEST_PROJECT_GRAPH',
Expand All @@ -208,6 +223,8 @@ export class DaemonClient {
} else {
throw e;
}
} finally {
spinner?.cleanup();
}
}

Expand Down
73 changes: 69 additions & 4 deletions packages/nx/src/project-graph/build-project-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
ConfigurationSourceMaps,
mergeMetadata,
} from './utils/project-configuration-utils';
import { DelayedSpinner, SHOULD_SHOW_SPINNERS } from '../utils/delayed-spinner';

let storedFileMap: FileMap | null = null;
let storedAllWorkspaceFiles: FileData[] | null = null;
Expand Down Expand Up @@ -313,14 +314,47 @@ async function updateProjectGraphWithPlugins(
(plugin) => plugin.createDependencies
);
performance.mark('createDependencies:start');

let spinner: DelayedSpinner;
const inProgressPlugins = new Set<string>();

function updateSpinner() {
if (!spinner) {
return;
}
if (inProgressPlugins.size === 1) {
return `Creating project graph dependencies with ${
inProgressPlugins.keys()[0]
}`;
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
return [
`Creating project graph dependencies with ${inProgressPlugins.size} plugins`,
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
].join('\n');
} else {
return `Creating project graph dependencies with ${inProgressPlugins.size} plugins`;
}
}

if (SHOULD_SHOW_SPINNERS) {
spinner = new DelayedSpinner(
`Creating project graph dependencies with ${plugins.length} plugins`
);
}

await Promise.all(
createDependencyPlugins.map(async (plugin) => {
performance.mark(`${plugin.name}:createDependencies - start`);

inProgressPlugins.add(plugin.name);
try {
const dependencies = await plugin.createDependencies({
...context,
});
const dependencies = await plugin
.createDependencies({
...context,
})
.finally(() => {
inProgressPlugins.delete(plugin.name);
updateSpinner();
});

for (const dep of dependencies) {
builder.addDependency(
Expand Down Expand Up @@ -352,6 +386,7 @@ async function updateProjectGraphWithPlugins(
`createDependencies:start`,
`createDependencies:end`
);
spinner?.cleanup();

const graphWithDeps = builder.getUpdatedProjectGraph();

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

performance.mark('createMetadata:start');
let spinner: DelayedSpinner;
const inProgressPlugins = new Set<string>();

function updateSpinner() {
if (!spinner) {
return;
}
if (inProgressPlugins.size === 1) {
return `Creating project metadata with ${inProgressPlugins.keys()[0]}`;
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
return [
`Creating project metadata with ${inProgressPlugins.size} plugins`,
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
].join('\n');
} else {
return `Creating project metadata with ${inProgressPlugins.size} plugins`;
}
}

if (SHOULD_SHOW_SPINNERS) {
spinner = new DelayedSpinner(
`Creating project metadata with ${plugins.length} plugins`
);
}

const promises = plugins.map(async (plugin) => {
if (plugin.createMetadata) {
performance.mark(`${plugin.name}:createMetadata - start`);
inProgressPlugins.add(plugin.name);
try {
const metadata = await plugin.createMetadata(graph, context);
results.push({ metadata, pluginName: plugin.name });
} catch (e) {
errors.push(new CreateMetadataError(e, plugin.name));
} finally {
inProgressPlugins.delete(plugin.name);
updateSpinner();
performance.mark(`${plugin.name}:createMetadata - end`);
performance.measure(
`${plugin.name}:createMetadata`,
Expand All @@ -417,6 +480,8 @@ export async function applyProjectMetadata(

await Promise.all(promises);

spinner?.cleanup();

for (const { metadata: projectsMetadata, pluginName } of results) {
for (const project in projectsMetadata) {
const projectConfiguration: ProjectConfiguration =
Expand Down
10 changes: 6 additions & 4 deletions packages/nx/src/project-graph/plugins/internal-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CreateNodesContextV2,
CreateNodesResult,
NxPluginV2,
ProjectsMetadata,
} from './public-api';
import { ProjectGraph } from '../../config/project-graph';
import { loadNxPluginInIsolation } from './isolation';
Expand All @@ -25,6 +26,7 @@ import {
isAggregateCreateNodesError,
} from '../error-types';
import { IS_WASM } from '../../native';
import { RawProjectGraphDependency } from '../project-graph-builder';

export class LoadedNxPlugin {
readonly name: string;
Expand All @@ -41,11 +43,11 @@ export class LoadedNxPlugin {
];
readonly createDependencies?: (
context: CreateDependenciesContext
) => ReturnType<CreateDependencies>;
) => Promise<RawProjectGraphDependency[]>;
readonly createMetadata?: (
graph: ProjectGraph,
context: CreateMetadataContext
) => ReturnType<CreateMetadata>;
) => Promise<ProjectsMetadata>;

readonly options?: unknown;
readonly include?: string[];
Expand Down Expand Up @@ -110,12 +112,12 @@ export class LoadedNxPlugin {
}

if (plugin.createDependencies) {
this.createDependencies = (context) =>
this.createDependencies = async (context) =>
plugin.createDependencies(this.options, context);
}

if (plugin.createMetadata) {
this.createMetadata = (graph, context) =>
this.createMetadata = async (graph, context) =>
plugin.createMetadata(graph, this.options, context);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface PluginCreateDependenciesResult {
type: 'createDependenciesResult';
payload:
| {
dependencies: ReturnType<LoadedNxPlugin['createDependencies']>;
dependencies: Awaited<ReturnType<LoadedNxPlugin['createDependencies']>>;
success: true;
tx: string;
}
Expand All @@ -99,7 +99,7 @@ export interface PluginCreateMetadataResult {
type: 'createMetadataResult';
payload:
| {
metadata: ReturnType<LoadedNxPlugin['createMetadata']>;
metadata: Awaited<ReturnType<LoadedNxPlugin['createMetadata']>>;
success: true;
tx: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { workspaceRoot } from '../../utils/workspace-root';
import { minimatch } from 'minimatch';
import { join } from 'path';
import { performance } from 'perf_hooks';

import { LoadedNxPlugin } from '../plugins/internal-api';
import {
MergeNodesError,
Expand All @@ -30,6 +31,11 @@ import {
} from '../error-types';
import { CreateNodesResult } from '../plugins/public-api';
import { isGlobPattern } from '../../utils/globs';
import { isOnDaemon } from '../../daemon/is-on-daemon';
import {
DelayedSpinner,
SHOULD_SHOW_SPINNERS,
} from '../../utils/delayed-spinner';

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

let spinner: DelayedSpinner;
const inProgressPlugins = new Set<string>();

function updateSpinner() {
if (!spinner) {
return;
}

if (inProgressPlugins.size === 1) {
return `Creating project graph nodes with ${inProgressPlugins.keys()[0]}`;
} else if (process.env.NX_VERBOSE_LOGGING === 'true') {
return [
`Creating project graph nodes with ${inProgressPlugins.size} plugins`,
...Array.from(inProgressPlugins).map((p) => ` - ${p}`),
].join('\n');
} else {
return `Creating project graph nodes with ${inProgressPlugins.size} plugins`;
}
}

if (SHOULD_SHOW_SPINNERS) {
spinner = new DelayedSpinner(
`Creating project graph nodes with ${plugins.length} plugins`
);
}

const results: Array<ReturnType<LoadedNxPlugin['createNodes'][1]>> = [];
const errors: Array<
| AggregateCreateNodesError
Expand Down Expand Up @@ -352,44 +384,55 @@ export async function createProjectConfigurations(
exclude
);

inProgressPlugins.add(pluginName);
let r = createNodes(matchingConfigFiles, {
nxJsonConfiguration: nxJson,
workspaceRoot: root,
}).catch((e: Error) => {
const errorBodyLines = [
`An error occurred while processing files for the ${pluginName} plugin.`,
];
const error: AggregateCreateNodesError = isAggregateCreateNodesError(e)
? // This is an expected error if something goes wrong while processing files.
e
: // This represents a single plugin erroring out with a hard error.
new AggregateCreateNodesError([[null, e]], []);

const innerErrors = error.errors;
for (const [file, e] of innerErrors) {
if (file) {
errorBodyLines.push(` - ${file}: ${e.message}`);
} else {
errorBodyLines.push(` - ${e.message}`);
}
if (e.stack) {
const innerStackTrace = ' ' + e.stack.split('\n')?.join('\n ');
errorBodyLines.push(innerStackTrace);
})
.catch((e: Error) => {
const errorBodyLines = [
`An error occurred while processing files for the ${pluginName} plugin.`,
];
const error: AggregateCreateNodesError = isAggregateCreateNodesError(e)
? // This is an expected error if something goes wrong while processing files.
e
: // This represents a single plugin erroring out with a hard error.
new AggregateCreateNodesError([[null, e]], []);

const innerErrors = error.errors;
for (const [file, e] of innerErrors) {
if (file) {
errorBodyLines.push(` - ${file}: ${e.message}`);
} else {
errorBodyLines.push(` - ${e.message}`);
}
if (e.stack) {
const innerStackTrace =
' ' + e.stack.split('\n')?.join('\n ');
errorBodyLines.push(innerStackTrace);
}
}
}

error.stack = errorBodyLines.join('\n');
error.stack = errorBodyLines.join('\n');

// This represents a single plugin erroring out with a hard error.
errors.push(error);
// The plugin didn't return partial results, so we return an empty array.
return error.partialResults.map((r) => [pluginName, r[0], r[1]] as const);
});
// This represents a single plugin erroring out with a hard error.
errors.push(error);
// The plugin didn't return partial results, so we return an empty array.
return error.partialResults.map(
(r) => [pluginName, r[0], r[1]] as const
);
})
.finally(() => {
inProgressPlugins.delete(pluginName);
updateSpinner();
});

results.push(r);
}

return Promise.all(results).then((results) => {
spinner?.cleanup();

const { projectRootMap, externalNodes, rootMap, configurationSourceMaps } =
mergeCreateNodesResults(results, nxJson, errors);

Expand Down
Loading
Loading