-
Notifications
You must be signed in to change notification settings - Fork 594
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
[rush] Split ProjectChangeAnalyzer, fix build cache hashes #4476
base: main
Are you sure you want to change the base?
[rush] Split ProjectChangeAnalyzer, fix build cache hashes #4476
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should do a pre-publish to verify that everything that is changing here doesn't break our flow, since we're very likely the largest customer on this tool.
common/changes/@microsoft/rush/split-project-change-analyzer_2024-01-09-22-11.json
Outdated
Show resolved
Hide resolved
common/changes/@microsoft/rush/split-project-change-analyzer_2024-01-09-22-06.json
Outdated
Show resolved
Hide resolved
libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts
Outdated
Show resolved
Hide resolved
c7d98ec
to
1836e05
Compare
1836e05
to
495f39f
Compare
f4a0556
to
5a6794d
Compare
"changes": [ | ||
{ | ||
"packageName": "@rushstack/lookup-by-path", | ||
"comment": "Add \"IReadonlyLookupByPath\" interface to help unit tests for functions that consume \"LookupByPath\".", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"comment": "Add \"IReadonlyLookupByPath\" interface to help unit tests for functions that consume \"LookupByPath\".", | |
"comment": "Add `IReadonlyLookupByPath` interface to help unit tests for functions that consume `LookupByPath`.", |
await projectChangeAnalyzer._ensureInitializedAsync(terminal); | ||
|
||
const analyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(this.rushConfiguration); | ||
const snapshotProvider: (() => Promise<IInputSnapshot | undefined>) | undefined = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const snapshotProvider: (() => Promise<IInputSnapshot | undefined>) | undefined = | |
const snapshotProvider: IInputSnapshotProvider | undefined = |
terminal.writeLine( | ||
`The Rush monorepo is not in a Git repository. Rush will proceed without incremental build support.` | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the only reason why the snapshot provider or initial snapshot would be undefined?
import { Colorize, type ITerminal } from '@rushstack/terminal'; | ||
|
||
import { Git } from './Git'; | ||
import { ProjectChangeAnalyzer } from './ProjectChangeAnalyzer'; | ||
import type { IInputSnapshot, IInputSnapshotProvider } from './snapshots/InputSnapshot'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "snapshot" terminology may be a little confusing because Jest snapshots are stored in a similarly-named folder.
* is responsible for change detection in all incremental builds) to determine what actually chanaged. | ||
* | ||
* Calling `waitForChange()` will return a promise that resolves when the package-deps of one or | ||
* more projects differ from the value the previous time it was invoked. The first time will always resolve with the full selection. | ||
*/ | ||
export class ProjectWatcher { | ||
private readonly _snapshotProvider: IInputSnapshotProvider; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private readonly _snapshotProvider: IInputSnapshotProvider; | |
private readonly _getSnapshotAsync: IInputSnapshotProvider; |
return options.buildCacheConfiguration.getCacheEntryId({ | ||
projectName: options.project.packageName, | ||
projectStateHash: options.operationStateHash, | ||
phaseName: options.phaseName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return options.buildCacheConfiguration.getCacheEntryId({ | |
projectName: options.project.packageName, | |
projectStateHash: options.operationStateHash, | |
phaseName: options.phaseName | |
const { buildCacheConfiguration, project: { packageName }, operationStateHash, phaseName } = options; | |
return buildCacheConfiguration.getCacheEntryId({ | |
projectName: packageName, | |
projectStateHash: operationStateHash, | |
phaseName |
import * as path from 'path'; | ||
import { createHash, type Hash } from 'crypto'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import * as path from 'path'; | |
import { createHash, type Hash } from 'crypto'; | |
import * as path from 'node:path'; | |
import { createHash, type Hash } from 'node:crypto'; |
for (const projectDeps of projectHashDeps.values()) { | ||
if (shrinkwrapHash) { | ||
projectDeps.set(shrinkwrapFile, shrinkwrapHash); | ||
for (const project of rushConfiguration.projects) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always analyzing all projects?
* The methods on this interface are idempotent and will return the same result regardless of when they are executed. | ||
* @beta | ||
*/ | ||
export interface IInputSnapshot { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export interface IInputSnapshot { | |
export interface IInputsSnapshot { |
* | ||
* @internal | ||
*/ | ||
export class InputSnapshot implements IInputSnapshot { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export class InputSnapshot implements IInputSnapshot { | |
export class InputsSnapshot implements IInputsSnapshot { |
Summary
Fixes #4400
Makes #3994 more feasible.
Details
Splits the functionality of ProjectChangeAnalyzer into two components:
ProjectChangeAnalyzer#_tryGetSnapshotProviderAsync
- Performs all the async I/O operations to determine the current state of the repository, including taking a snapshot ofprocess.env
so that per-operation hashes can be lazily computed in a stable manner.InputSnapshot
- Synchronous data structure representing the state of the repository, which can be queried for operation-level state hashes, or tracked file lists. Performs the evaluation of input/output collisions as part of evaluating said queries. Includes evaluation ofincrementalBuildIgnoredGlobs
anddependsOnEnvVars
, but does not include the hashes from dependencies, since the Operation graph is not provided to this API.Moves build cache hash computation to the
beforeExecuteOperations
tap inCacheableOperationPlugin
and updates the computation to factor in the runtime operation graph, rather than only the npm dependencies.For a specific example of how this alters the build cache, consider a repository with one project and two phases in
rush build
:Project
A
Phases
_phase:first
and_phase:second
, with_phase:second
depends onself: ['_phase:first']
Define a command line parameter
--some-flag-for-first
that is forwarded only to_phase:first
.Before this change, running
rush build
andrush build --some-flag-for-first
would produce different cache IDs forA (_phase:first)
, but the same cache id forA (_phase:second)
.With this change the cache IDs for
A (_phase:second)
are also impacted by the presence of the--some-flag-for-first
, since it can change the output ofA (_phase:first)
.How it was tested
For the splitting, added unit tests for
InputSnapshot
andProjectChangeAnalyzer#_tryGetSnapshotProviderAsync
.For the build cache IDs, temporarily removed applicability of the
--production
CLI parameter from_phase:test
and validated that runningnode ./apps/rush/lib/start-dev.js test
andnode ./apps/rush/lib/start-dev.js test --production
still produced different cache keys for_phase:test
.For comparison, validated that the command-line.json alternation and existing
rush test
andrush test --production
had a collision on the cache keys for_phase:test
.Impacted documentation
Any documentation about how build cache keys are computed.