Skip to content

Commit 5798706

Browse files
committed
feat: refact non-cached duration logic according to code review
1 parent e3c3726 commit 5798706

File tree

9 files changed

+79
-54
lines changed

9 files changed

+79
-54
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ export interface _INpmOptionsJson extends IPackageManagerOptionsJsonBase {
371371

372372
// @alpha
373373
export interface IOperationExecutionResult {
374-
readonly durationInSecondsWithoutCache: number | undefined;
375374
readonly error: Error | undefined;
375+
readonly nonCachedDurationMs: number | undefined;
376376
readonly status: OperationStatus;
377377
readonly stdioSummarizer: StdioSummarizer;
378378
readonly stopwatch: IStopwatchResult;
@@ -400,8 +400,12 @@ export interface IOperationRunner {
400400
export interface IOperationRunnerContext {
401401
collatedWriter: CollatedWriter;
402402
debugMode: boolean;
403+
// Warning: (ae-forgotten-export) The symbol "OperationStateFile" needs to be exported by the entry point index.d.ts
404+
operationStateFile?: OperationStateFile;
403405
quietMode: boolean;
404406
stdioSummarizer: StdioSummarizer;
407+
// Warning: (ae-forgotten-export) The symbol "Stopwatch" needs to be exported by the entry point index.d.ts
408+
stopwatch: Stopwatch;
405409
}
406410

407411
// @public
@@ -508,8 +512,8 @@ export interface ITelemetryMachineInfo {
508512
// @beta (undocumented)
509513
export interface ITelemetryOperationResult {
510514
dependencies: string[];
511-
durationInSecondsWithoutCache?: number;
512515
endTimestampMs?: number;
516+
nonCachedDurationMs?: number;
513517
result: string;
514518
startTimestampMs?: number;
515519
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
578578
operationResults[operation.name!] = {
579579
startTimestampMs: startTime,
580580
endTimestampMs: endTime,
581-
durationInSecondsWithoutCache: operationResult.durationInSecondsWithoutCache,
581+
nonCachedDurationMs: operationResult.nonCachedDurationMs,
582582
result: operationResult.status,
583583
dependencies: Array.from(getNonSilentDependencies(operation)).sort()
584584
};

libraries/rush-lib/src/logic/Telemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ITelemetryOperationResult {
6868
endTimestampMs?: number;
6969

7070
/**
71-
* Duration in seconds when the operation does not hit cache
71+
* Duration in milliseconds when the operation does not hit cache
7272
*/
7373
nonCachedDurationMs?: number;
7474
}

libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import * as path from 'path';
55
import * as crypto from 'crypto';
6-
import { FileSystem, Path, ITerminal, FolderItem, InternalError } from '@rushstack/node-core-library';
6+
import { FileSystem, Path, ITerminal, FolderItem, InternalError, Async } from '@rushstack/node-core-library';
77

88
import { RushConfigurationProject } from '../../api/RushConfigurationProject';
99
import { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer';
@@ -19,6 +19,7 @@ export interface IProjectBuildCacheOptions {
1919
buildCacheConfiguration: BuildCacheConfiguration;
2020
projectConfiguration: RushProjectConfiguration;
2121
projectOutputFolderNames: ReadonlyArray<string>;
22+
additionalProjectOutputFilePaths?: ReadonlyArray<string>;
2223
command: string;
2324
trackedProjectFiles: string[] | undefined;
2425
projectChangeAnalyzer: ProjectChangeAnalyzer;
@@ -44,18 +45,23 @@ export class ProjectBuildCache {
4445
private readonly _buildCacheEnabled: boolean;
4546
private readonly _cacheWriteEnabled: boolean;
4647
private readonly _projectOutputFolderNames: ReadonlyArray<string>;
47-
private readonly _additionalOutputFilePaths: string[];
48+
private readonly _additionalProjectOutputFilePaths: ReadonlyArray<string>;
4849
private _cacheId: string | undefined;
4950

5051
private constructor(cacheId: string | undefined, options: IProjectBuildCacheOptions) {
51-
const { buildCacheConfiguration, projectConfiguration, projectOutputFolderNames } = options;
52+
const {
53+
buildCacheConfiguration,
54+
projectConfiguration,
55+
projectOutputFolderNames,
56+
additionalProjectOutputFilePaths
57+
} = options;
5258
this._project = projectConfiguration.project;
5359
this._localBuildCacheProvider = buildCacheConfiguration.localCacheProvider;
5460
this._cloudBuildCacheProvider = buildCacheConfiguration.cloudCacheProvider;
5561
this._buildCacheEnabled = buildCacheConfiguration.buildCacheEnabled;
5662
this._cacheWriteEnabled = buildCacheConfiguration.cacheWriteEnabled;
5763
this._projectOutputFolderNames = projectOutputFolderNames || [];
58-
this._additionalOutputFilePaths = [];
64+
this._additionalProjectOutputFilePaths = additionalProjectOutputFilePaths || [];
5965
this._cacheId = cacheId;
6066
}
6167

@@ -118,22 +124,14 @@ export class ProjectBuildCache {
118124
if (inputOutputFiles.length > 0) {
119125
terminal.writeWarningLine(
120126
'Unable to use build cache. The following files are used to calculate project state ' +
121-
`and are considered project output: ${inputOutputFiles.join(', ')}`
127+
`and are considered project output: ${inputOutputFiles.join(', ')}`
122128
);
123129
return false;
124130
} else {
125131
return true;
126132
}
127133
}
128134

129-
public get additionalOutputFilePaths(): string[] {
130-
return this._additionalOutputFilePaths;
131-
}
132-
133-
public addAdditionalOutputFilePaths(outputFilePath: string): void {
134-
this._additionalOutputFilePaths.push(outputFilePath);
135-
}
136-
137135
public async tryRestoreFromCacheAsync(terminal: ITerminal): Promise<boolean> {
138136
const cacheId: string | undefined = this._cacheId;
139137
if (!cacheId) {
@@ -261,14 +259,14 @@ export class ProjectBuildCache {
261259
} else {
262260
terminal.writeWarningLine(
263261
`"tar" exited with code ${tarExitCode} while attempting to create the cache entry. ` +
264-
`See "${logFilePath}" for logs from the tar process.`
262+
`See "${logFilePath}" for logs from the tar process.`
265263
);
266264
return false;
267265
}
268266
} else {
269267
terminal.writeWarningLine(
270268
`Unable to locate "tar". Please ensure that "tar" is on your PATH environment variable, or set the ` +
271-
`${EnvironmentVariableNames.RUSH_TAR_BINARY_PATH} environment variable to the full path to the "tar" binary.`
269+
`${EnvironmentVariableNames.RUSH_TAR_BINARY_PATH} environment variable to the full path to the "tar" binary.`
272270
);
273271
return false;
274272
}
@@ -370,13 +368,17 @@ export class ProjectBuildCache {
370368
}
371369

372370
// Add additional output file paths
373-
await Async.foreEachAsync(this._additionalOutputFilePaths, async (additionalOutputFilePath) => {
374-
const fullPath: string = `${projectFolderPath}/${addAdditionalOutputFilePath}`;
375-
const pathExists: boolean = await FileSystem.existsAsync(fullPath);
376-
if (pathExists) {
377-
outputFilePaths.push(addAdditionalOutputFilePath);
378-
}
379-
}, { concurrency: 10 });
371+
await Async.forEachAsync(
372+
this._additionalProjectOutputFilePaths,
373+
async (additionalProjectOutputFilePath) => {
374+
const fullPath: string = `${projectFolderPath}/${additionalProjectOutputFilePath}`;
375+
const pathExists: boolean = await FileSystem.existsAsync(fullPath);
376+
if (pathExists) {
377+
outputFilePaths.push(additionalProjectOutputFilePath);
378+
}
379+
},
380+
{ concurrency: 10 }
381+
);
380382

381383
// Ensure stable output path order.
382384
outputFilePaths.sort();

libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export interface IOperationExecutionResult {
3333
readonly stdioSummarizer: StdioSummarizer;
3434
/**
3535
* The value indicates the duration of the same operation without cache hit.
36-
* It is specified if current operation hits cache
3736
*/
3837
readonly nonCachedDurationMs: number | undefined;
3938
}

libraries/rush-lib/src/logic/operations/IOperationRunner.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { StdioSummarizer } from '@rushstack/terminal';
55
import type { CollatedWriter } from '@rushstack/stream-collator';
66

77
import type { OperationStatus } from './OperationStatus';
8+
import type { OperationStateFile } from './OperationStateFile';
9+
import type { Stopwatch } from '../../utilities/Stopwatch';
810

911
/**
1012
* Information passed to the executing `IOperationRunner`
@@ -28,6 +30,14 @@ export interface IOperationRunnerContext {
2830
* Object used to report a summary at the end of the Rush invocation.
2931
*/
3032
stdioSummarizer: StdioSummarizer;
33+
/**
34+
* Object used to record state of the operation.
35+
*/
36+
operationStateFile?: OperationStateFile;
37+
/**
38+
* Object used to track elapsed time.
39+
*/
40+
stopwatch: Stopwatch;
3141
}
3242

3343
/**

libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ export class OperationExecutionRecord implements IOperationRunnerContext {
8787

8888
private _collatedWriter: CollatedWriter | undefined = undefined;
8989

90-
public nonCachedDurationMs: number | undefined = undefined;
91-
9290
public constructor(operation: Operation, context: IOperationExecutionRecordContext) {
9391
const { runner } = operation;
9492

@@ -129,6 +127,11 @@ export class OperationExecutionRecord implements IOperationRunnerContext {
129127
return this._collatedWriter;
130128
}
131129

130+
public get nonCachedDurationMs(): number | undefined {
131+
// Lazy calculated because the state file is created/restored later on
132+
return this.operationStateFile?.state?.nonCachedDurationMs;
133+
}
134+
132135
public async executeAsync(onResult: (record: OperationExecutionRecord) => void): Promise<void> {
133136
this.status = OperationStatus.Executing;
134137
this.stopwatch.start();

libraries/rush-lib/src/logic/operations/OperationStateFile.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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 * as path from 'path';
54
import { FileSystem, InternalError, JsonFile } from '@rushstack/node-core-library';
65
import { RushConstants } from '../RushConstants';
76

@@ -17,21 +16,32 @@ export interface IOperationStateJson {
1716
nonCachedDurationMs: number;
1817
}
1918

19+
/**
20+
* A helper class for managing the state file of a operation.
21+
*
22+
* @internal
23+
*/
2024
export class OperationStateFile {
2125
private readonly _rushProject: RushConfigurationProject;
2226
private readonly _filename: string;
27+
private _state: IOperationStateJson | undefined;
2328

2429
public constructor(options: IOperationStateFileOptions) {
2530
const { rushProject, phase } = options;
2631
this._rushProject = rushProject;
27-
this._filename = OperationStateFile.getFilename(phase, rushProject);
32+
this._filename = OperationStateFile._getFilename(phase, rushProject);
2833
}
2934

30-
public static getFilename(phase: IPhase, project: RushConfigurationProject): string {
35+
private static _getFilename(phase: IPhase, project: RushConfigurationProject): string {
3136
const relativeFilename: string = OperationStateFile.getFilenameRelativeToProjectRoot(phase);
3237
return `${project.projectFolder}/${relativeFilename}`;
3338
}
3439

40+
/**
41+
* ProjectBuildCache expects the relative path for better logging
42+
*
43+
* @internal
44+
*/
3545
public static getFilenameRelativeToProjectRoot(phase: IPhase): string {
3646
const identifier: string = `${phase.logFilenameIdentifier}`;
3747
return `${RushConstants.projectRushFolderName}/${RushConstants.rushTempFolderName}/operation/${identifier}/state.json`;
@@ -44,20 +54,26 @@ export class OperationStateFile {
4454
return this._filename;
4555
}
4656

57+
public get state(): IOperationStateJson | undefined {
58+
return this._state;
59+
}
60+
4761
public async writeAsync(json: IOperationStateJson): Promise<void> {
4862
await JsonFile.saveAsync(json, this._filename, { ensureFolderExists: true, updateExistingFile: true });
63+
this._state = json;
4964
}
5065

51-
public async tryReadAsync(): Promise<IOperationStateJson | undefined> {
66+
public async tryRestoreAsync(): Promise<IOperationStateJson | undefined> {
5267
try {
53-
return await JsonFile.loadAsync(this._filename);
68+
this._state = await JsonFile.loadAsync(this._filename);
5469
} catch (error) {
5570
if (FileSystem.isNotExistError(error as Error)) {
56-
return undefined;
71+
this._state = undefined;
5772
} else {
5873
// This should not happen
5974
throw new InternalError(error);
6075
}
6176
}
77+
return this._state;
6278
}
6379
}

libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import { CollatedTerminalProvider } from '../../utilities/CollatedTerminalProvid
3434
import { RushConstants } from '../RushConstants';
3535
import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration';
3636
import { OperationStateFile } from './OperationStateFile';
37-
import { OperationExecutionRecord } from './OperationExecutionRecord';
3837

3938
import type { RushConfiguration } from '../../api/RushConfiguration';
4039
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
@@ -275,11 +274,8 @@ export class ShellOperationRunner implements IOperationRunner {
275274
await projectBuildCache?.tryRestoreFromCacheAsync(terminal);
276275

277276
if (restoreFromCacheSuccess) {
278-
// Restore the original state of the operation without cache if hit cache
279-
if (context instanceof OperationExecutionRecord) {
280-
context.durationInSecondsWithoutCache =
281-
context.operationStateFile?.tryRead()?.durationInSecondsWithoutCache;
282-
}
277+
// Restore the original state of the operation without cache
278+
await context.operationStateFile?.tryRestoreAsync();
283279
return OperationStatus.FromCache;
284280
}
285281
}
@@ -378,16 +374,10 @@ export class ShellOperationRunner implements IOperationRunner {
378374
});
379375

380376
// If the operation without cache was successful, we can save the state to disk
381-
if (context instanceof OperationExecutionRecord) {
382-
if (context.operationStateFile) {
383-
const { duration } = context.stopwatch;
384-
if (duration) {
385-
context.operationStateFile.write({
386-
durationInSecondsWithoutCache: duration
387-
});
388-
}
389-
}
390-
}
377+
const { duration: durationInSeconds } = context.stopwatch;
378+
await context.operationStateFile?.writeAsync({
379+
nonCachedDurationMs: durationInSeconds * 1000
380+
});
391381

392382
// If the command is successful, we can calculate project hash, and no dependencies were skipped,
393383
// write a new cache entry.
@@ -451,19 +441,20 @@ export class ShellOperationRunner implements IOperationRunner {
451441
} else {
452442
const projectOutputFolderNames: ReadonlyArray<string> =
453443
operationSettings.outputFolderNames || [];
444+
const additionalProjectOutputFilePaths: ReadonlyArray<string> = [
445+
OperationStateFile.getFilenameRelativeToProjectRoot(this._phase)
446+
];
454447
this._projectBuildCache = await ProjectBuildCache.tryGetProjectBuildCache({
455448
projectConfiguration,
456449
projectOutputFolderNames,
450+
additionalProjectOutputFilePaths,
457451
buildCacheConfiguration: this._buildCacheConfiguration,
458452
terminal,
459453
command: this._commandToRun,
460454
trackedProjectFiles: trackedProjectFiles,
461455
projectChangeAnalyzer: this._projectChangeAnalyzer,
462456
phaseName: this._phase.name
463457
});
464-
this._projectBuildCache?.addAdditionalOutputFilePaths(
465-
OperationStateFile.getFilenameRelativeToProjectRoot(this._phase)
466-
);
467458
}
468459
}
469460
} else {

0 commit comments

Comments
 (0)