Skip to content

Commit 7e5d753

Browse files
authored
[rush] Add --debug-build-cache-ids flag (#5173)
* [rush] Add `--debug-build-cache-ids` flag * Minor cleanup * Lexicographically sort by operation name * Sort operations by name for display * Add comments about hash salt --------- Co-authored-by: David Michon <dmichon-msft@users.noreply.github.com>
1 parent 90bd555 commit 7e5d753

15 files changed

Lines changed: 333 additions & 174 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Add a new CLI flag `--debug-build-cache-ids` to help with root-causing unexpected cache misses.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Sort all operations lexicographically by name for reporting purposes.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/reviews/api/rush-lib.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,8 @@ export interface _INpmOptionsJson extends IPackageManagerOptionsJsonBase {
585585
export interface IOperationExecutionResult {
586586
readonly cobuildRunnerId: string | undefined;
587587
readonly error: Error | undefined;
588+
getStateHash(): string;
589+
getStateHashComponents(): ReadonlyArray<string>;
588590
readonly logFilePaths: ILogFilePaths | undefined;
589591
readonly metadataFolderPath: string | undefined;
590592
readonly nonCachedDurationMs: number | undefined;

libraries/rush-lib/src/api/BuildCacheConfiguration.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import { createHash } from 'node:crypto';
45
import * as path from 'path';
6+
57
import {
68
JsonFile,
79
JsonSchema,
@@ -17,7 +19,11 @@ import { RushConstants } from '../logic/RushConstants';
1719
import type { ICloudBuildCacheProvider } from '../logic/buildCache/ICloudBuildCacheProvider';
1820
import { RushUserConfiguration } from './RushUserConfiguration';
1921
import { EnvironmentConfiguration } from './EnvironmentConfiguration';
20-
import { CacheEntryId, type GetCacheEntryIdFunction } from '../logic/buildCache/CacheEntryId';
22+
import {
23+
CacheEntryId,
24+
type IGenerateCacheEntryIdOptions,
25+
type GetCacheEntryIdFunction
26+
} from '../logic/buildCache/CacheEntryId';
2127
import type { CloudBuildCacheProviderFactory, RushSession } from '../pluginFramework/RushSession';
2228
import schemaJson from '../schemas/build-cache.schema.json';
2329

@@ -201,23 +207,41 @@ export class BuildCacheConfiguration {
201207
);
202208
const rushUserConfiguration: RushUserConfiguration = await RushUserConfiguration.initializeAsync();
203209

204-
let getCacheEntryId: GetCacheEntryIdFunction;
210+
let innerGetCacheEntryId: GetCacheEntryIdFunction;
205211
try {
206-
getCacheEntryId = CacheEntryId.parsePattern(buildCacheJson.cacheEntryNamePattern);
212+
innerGetCacheEntryId = CacheEntryId.parsePattern(buildCacheJson.cacheEntryNamePattern);
207213
} catch (e) {
208214
terminal.writeErrorLine(
209215
`Error parsing cache entry name pattern "${buildCacheJson.cacheEntryNamePattern}": ${e}`
210216
);
211217
throw new AlreadyReportedError();
212218
}
213219

220+
const { cacheHashSalt = '', cacheProvider } = buildCacheJson;
221+
const salt: string = `${RushConstants.buildCacheVersion}${cacheHashSalt ? `${RushConstants.hashDelimiter}${cacheHashSalt}` : ''}`;
222+
// Extend the cache entry id with to salt the hash
223+
// This facilitates forcing cache invalidation either when the build cache version changes (new version of Rush)
224+
// or when the user-side salt changes (need to purge bad cache entries, plugins including additional files)
225+
const getCacheEntryId: GetCacheEntryIdFunction = (options: IGenerateCacheEntryIdOptions): string => {
226+
const saltedHash: string = createHash('sha1')
227+
.update(salt)
228+
.update(options.projectStateHash)
229+
.digest('hex');
230+
231+
return innerGetCacheEntryId({
232+
phaseName: options.phaseName,
233+
projectName: options.projectName,
234+
projectStateHash: saltedHash
235+
});
236+
};
237+
214238
let cloudCacheProvider: ICloudBuildCacheProvider | undefined;
215239
// Don't configure a cloud cache provider if local-only
216-
if (buildCacheJson.cacheProvider !== 'local-only') {
240+
if (cacheProvider !== 'local-only') {
217241
const cloudCacheProviderFactory: CloudBuildCacheProviderFactory | undefined =
218-
rushSession.getCloudBuildCacheProviderFactory(buildCacheJson.cacheProvider);
242+
rushSession.getCloudBuildCacheProviderFactory(cacheProvider);
219243
if (!cloudCacheProviderFactory) {
220-
throw new Error(`Unexpected cache provider: ${buildCacheJson.cacheProvider}`);
244+
throw new Error(`Unexpected cache provider: ${cacheProvider}`);
221245
}
222246
cloudCacheProvider = await cloudCacheProviderFactory(buildCacheJson as ICloudBuildCacheJson);
223247
}

libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,14 @@ Object {
12601260
"required": false,
12611261
"shortName": undefined,
12621262
},
1263+
Object {
1264+
"description": "Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.",
1265+
"environmentVariable": undefined,
1266+
"kind": "Flag",
1267+
"longName": "--debug-build-cache-ids",
1268+
"required": false,
1269+
"shortName": undefined,
1270+
},
12631271
Object {
12641272
"description": "Selects a single instead of the default locale (en-us) for non-ship builds or all locales for ship builds.",
12651273
"environmentVariable": undefined,
@@ -1414,6 +1422,14 @@ Object {
14141422
"required": false,
14151423
"shortName": undefined,
14161424
},
1425+
Object {
1426+
"description": "Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.",
1427+
"environmentVariable": undefined,
1428+
"kind": "Flag",
1429+
"longName": "--debug-build-cache-ids",
1430+
"required": false,
1431+
"shortName": undefined,
1432+
},
14171433
Object {
14181434
"description": "Perform a production build, including minification and localization steps",
14191435
"environmentVariable": undefined,
@@ -1555,6 +1571,14 @@ Object {
15551571
"required": false,
15561572
"shortName": undefined,
15571573
},
1574+
Object {
1575+
"description": "Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.",
1576+
"environmentVariable": undefined,
1577+
"kind": "Flag",
1578+
"longName": "--debug-build-cache-ids",
1579+
"required": false,
1580+
"shortName": undefined,
1581+
},
15581582
Object {
15591583
"description": "Perform a production build, including minification and localization steps",
15601584
"environmentVariable": undefined,

libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { WeightedOperationPlugin } from '../../logic/operations/WeightedOperatio
5858
import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants';
5959
import { Selection } from '../../logic/Selection';
6060
import { NodeDiagnosticDirPlugin } from '../../logic/operations/NodeDiagnosticDirPlugin';
61+
import { DebugHashesPlugin } from '../../logic/operations/DebugHashesPlugin';
6162

6263
/**
6364
* Constructor parameters for PhasedScriptAction.
@@ -79,7 +80,10 @@ export interface IPhasedScriptActionOptions extends IBaseScriptActionOptions<IPh
7980
}
8081

8182
interface IInitialRunPhasesOptions {
82-
executionManagerOptions: Omit<IOperationExecutionManagerOptions, 'beforeExecuteOperations'>;
83+
executionManagerOptions: Omit<
84+
IOperationExecutionManagerOptions,
85+
'beforeExecuteOperations' | 'inputsSnapshot'
86+
>;
8387
initialCreateOperationsContext: ICreateOperationsContext;
8488
stopwatch: Stopwatch;
8589
terminal: ITerminal;
@@ -155,6 +159,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
155159
private readonly _variantParameter: CommandLineStringParameter | undefined;
156160
private readonly _noIPCParameter: CommandLineFlagParameter | undefined;
157161
private readonly _nodeDiagnosticDirParameter: CommandLineStringParameter;
162+
private readonly _debugBuildCacheIdsParameter: CommandLineFlagParameter;
158163
private readonly _includePhaseDeps: CommandLineFlagParameter | undefined;
159164

160165
public constructor(options: IPhasedScriptActionOptions) {
@@ -186,19 +191,20 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
186191
new WeightedOperationPlugin().apply(this.hooks);
187192
new ValidateOperationsPlugin(terminal).apply(this.hooks);
188193

189-
if (this._enableParallelism) {
190-
this._parallelismParameter = this.defineStringParameter({
191-
parameterLongName: '--parallelism',
192-
parameterShortName: '-p',
193-
argumentName: 'COUNT',
194-
environmentVariable: EnvironmentVariableNames.RUSH_PARALLELISM,
195-
description:
196-
'Specifies the maximum number of concurrent processes to launch during a build.' +
197-
' The COUNT should be a positive integer, a percentage value (eg. "50%%") or the word "max"' +
198-
' to specify a count that is equal to the number of CPU cores. If this parameter is omitted,' +
199-
' then the default value depends on the operating system and number of CPU cores.'
200-
});
201-
}
194+
this._parallelismParameter = this._enableParallelism
195+
? this.defineStringParameter({
196+
parameterLongName: '--parallelism',
197+
parameterShortName: '-p',
198+
argumentName: 'COUNT',
199+
environmentVariable: EnvironmentVariableNames.RUSH_PARALLELISM,
200+
description:
201+
'Specifies the maximum number of concurrent processes to launch during a build.' +
202+
' The COUNT should be a positive integer, a percentage value (eg. "50%%") or the word "max"' +
203+
' to specify a count that is equal to the number of CPU cores. If this parameter is omitted,' +
204+
' then the default value depends on the operating system and number of CPU cores.'
205+
})
206+
: undefined;
207+
202208
this._timelineParameter = this.defineFlagParameter({
203209
parameterLongName: '--timeline',
204210
description:
@@ -237,18 +243,18 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
237243
`Using "--impacted-by A --include-phase-deps" avoids that work by performing "_phase:test" only for downstream projects.`
238244
});
239245

240-
if (this._isIncrementalBuildAllowed) {
241-
this._changedProjectsOnly = this.defineFlagParameter({
242-
parameterLongName: '--changed-projects-only',
243-
parameterShortName: '-c',
244-
description:
245-
'Normally the incremental build logic will rebuild changed projects as well as' +
246-
' any projects that directly or indirectly depend on a changed project. Specify "--changed-projects-only"' +
247-
' to ignore dependent projects, only rebuilding those projects whose files were changed.' +
248-
' Note that this parameter is "unsafe"; it is up to the developer to ensure that the ignored projects' +
249-
' are okay to ignore.'
250-
});
251-
}
246+
this._changedProjectsOnly = this._isIncrementalBuildAllowed
247+
? this.defineFlagParameter({
248+
parameterLongName: '--changed-projects-only',
249+
parameterShortName: '-c',
250+
description:
251+
'Normally the incremental build logic will rebuild changed projects as well as' +
252+
' any projects that directly or indirectly depend on a changed project. Specify "--changed-projects-only"' +
253+
' to ignore dependent projects, only rebuilding those projects whose files were changed.' +
254+
' Note that this parameter is "unsafe"; it is up to the developer to ensure that the ignored projects' +
255+
' are okay to ignore.'
256+
})
257+
: undefined;
252258

253259
this._ignoreHooksParameter = this.defineFlagParameter({
254260
parameterLongName: '--ignore-hooks',
@@ -257,43 +263,44 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
257263
'Make sure you know what you are skipping.'
258264
});
259265

260-
if (this._watchPhases.size > 0 && !this._alwaysWatch) {
261-
// Only define the parameter if it has an effect.
262-
this._watchParameter = this.defineFlagParameter({
263-
parameterLongName: '--watch',
264-
description: `Starts a file watcher after initial execution finishes. Will run the following phases on affected projects: ${Array.from(
265-
this._watchPhases,
266-
(phase: IPhase) => phase.name
267-
).join(', ')}`
268-
});
269-
}
266+
// Only define the parameter if it has an effect.
267+
this._watchParameter =
268+
this._watchPhases.size > 0 && !this._alwaysWatch
269+
? this.defineFlagParameter({
270+
parameterLongName: '--watch',
271+
description: `Starts a file watcher after initial execution finishes. Will run the following phases on affected projects: ${Array.from(
272+
this._watchPhases,
273+
(phase: IPhase) => phase.name
274+
).join(', ')}`
275+
})
276+
: undefined;
270277

271278
// If `this._alwaysInstall === undefined`, Rush does not define the parameter
272279
// but a repository may still define a custom parameter with the same name.
273-
if (this._alwaysInstall === false) {
274-
this._installParameter = this.defineFlagParameter({
275-
parameterLongName: '--install',
276-
description:
277-
'Normally a phased command expects "rush install" to have been manually run first. If this flag is specified, ' +
278-
'Rush will automatically perform an install before processing the current command.'
279-
});
280-
}
281-
282-
if (this._alwaysInstall !== undefined) {
283-
this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER);
284-
}
285-
286-
if (
280+
this._installParameter =
281+
this._alwaysInstall === false
282+
? this.defineFlagParameter({
283+
parameterLongName: '--install',
284+
description:
285+
'Normally a phased command expects "rush install" to have been manually run first. If this flag is specified, ' +
286+
'Rush will automatically perform an install before processing the current command.'
287+
})
288+
: undefined;
289+
290+
this._variantParameter =
291+
this._alwaysInstall !== undefined ? this.defineStringParameter(VARIANT_PARAMETER) : undefined;
292+
293+
const isIpcSupported: boolean =
287294
this._watchPhases.size > 0 &&
288-
this.rushConfiguration.experimentsConfiguration.configuration.useIPCScriptsInWatchMode
289-
) {
290-
this._noIPCParameter = this.defineFlagParameter({
291-
parameterLongName: '--no-ipc',
292-
description:
293-
'Disables the IPC feature for the current command (if applicable to selected operations). Operations will not look for a ":ipc" suffixed script.' +
294-
'This feature only applies in watch mode and is enabled by default.'
295-
});
296-
}
295+
!!this.rushConfiguration.experimentsConfiguration.configuration.useIPCScriptsInWatchMode;
296+
this._noIPCParameter = isIpcSupported
297+
? this.defineFlagParameter({
298+
parameterLongName: '--no-ipc',
299+
description:
300+
'Disables the IPC feature for the current command (if applicable to selected operations). Operations will not look for a ":ipc" suffixed script.' +
301+
'This feature only applies in watch mode and is enabled by default.'
302+
})
303+
: undefined;
297304

298305
this._nodeDiagnosticDirParameter = this.defineStringParameter({
299306
parameterLongName: '--node-diagnostic-dir',
@@ -303,6 +310,12 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
303310
'This directory will contain a subdirectory for each project and phase.'
304311
});
305312

313+
this._debugBuildCacheIdsParameter = this.defineFlagParameter({
314+
parameterLongName: '--debug-build-cache-ids',
315+
description:
316+
'Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.'
317+
});
318+
306319
this.defineScriptParameters();
307320

308321
for (const [{ associatedPhases }, tsCommandLineParameter] of this.customParameters) {
@@ -468,6 +481,10 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
468481
cobuildConfiguration,
469482
terminal
470483
}).apply(this.hooks);
484+
485+
if (this._debugBuildCacheIdsParameter.value) {
486+
new DebugHashesPlugin(terminal).apply(this.hooks);
487+
}
471488
} else if (!this._disableBuildCache) {
472489
terminal.writeVerboseLine(`Incremental strategy: output preservation`);
473490
// Explicitly disabling the build cache also disables legacy skip detection.
@@ -525,11 +542,13 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
525542
projectsInUnknownState: projectSelection
526543
};
527544

528-
const executionManagerOptions: Omit<IOperationExecutionManagerOptions, 'beforeExecuteOperations'> = {
545+
const executionManagerOptions: Omit<
546+
IOperationExecutionManagerOptions,
547+
'beforeExecuteOperations' | 'inputsSnapshot'
548+
> = {
529549
quietMode: isQuietMode,
530550
debugMode: this.parser.isDebug,
531551
parallelism,
532-
changedProjectsOnly,
533552
beforeExecuteOperationAsync: async (record: OperationExecutionRecord) => {
534553
return await this.hooks.beforeExecuteOperation.promise(record);
535554
},
@@ -609,6 +628,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
609628

610629
const executionManagerOptions: IOperationExecutionManagerOptions = {
611630
...partialExecutionManagerOptions,
631+
inputsSnapshot: initialSnapshot,
612632
beforeExecuteOperationsAsync: async (records: Map<Operation, OperationExecutionRecord>) => {
613633
await this.hooks.beforeExecuteOperations.promise(records, initialExecuteOperationsContext);
614634
}
@@ -809,6 +829,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
809829
stopwatch,
810830
executionManagerOptions: {
811831
...executionManagerOptions,
832+
inputsSnapshot: state,
812833
beforeExecuteOperationsAsync: async (records: Map<Operation, OperationExecutionRecord>) => {
813834
await this.hooks.beforeExecuteOperations.promise(records, executeOperationsContext);
814835
}

0 commit comments

Comments
 (0)