Skip to content

Commit fe7ae5f

Browse files
authored
Merge pull request #3649 from chengcyber/feat-operation-metadata
[rush] restore the duration of the same operation without cache hit as telemetry data
2 parents 0c3ad57 + c192317 commit fe7ae5f

File tree

12 files changed

+214
-13
lines changed

12 files changed

+214
-13
lines changed
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": "Include the operation duration in the telemetry data that was recorded during the non-cached run when a cache hit occurs.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ export interface _INpmOptionsJson extends IPackageManagerOptionsJsonBase {
372372
// @alpha
373373
export interface IOperationExecutionResult {
374374
readonly error: Error | undefined;
375+
readonly nonCachedDurationMs: number | undefined;
375376
readonly status: OperationStatus;
376377
readonly stdioSummarizer: StdioSummarizer;
377378
readonly stopwatch: IStopwatchResult;
@@ -399,8 +400,25 @@ export interface IOperationRunner {
399400
export interface IOperationRunnerContext {
400401
collatedWriter: CollatedWriter;
401402
debugMode: boolean;
403+
// @internal
404+
_operationStateFile?: _OperationStateFile;
402405
quietMode: boolean;
403406
stdioSummarizer: StdioSummarizer;
407+
stopwatch: IStopwatchResult;
408+
}
409+
410+
// @internal (undocumented)
411+
export interface _IOperationStateFileOptions {
412+
// (undocumented)
413+
phase: IPhase;
414+
// (undocumented)
415+
rushProject: RushConfigurationProject;
416+
}
417+
418+
// @internal (undocumented)
419+
export interface _IOperationStateJson {
420+
// (undocumented)
421+
nonCachedDurationMs: number;
404422
}
405423

406424
// @public
@@ -471,7 +489,7 @@ export interface IRushSessionOptions {
471489
terminalProvider: ITerminalProvider;
472490
}
473491

474-
// @alpha
492+
// @beta
475493
export interface IStopwatchResult {
476494
get duration(): number;
477495
get endTime(): number | undefined;
@@ -508,6 +526,7 @@ export interface ITelemetryMachineInfo {
508526
export interface ITelemetryOperationResult {
509527
dependencies: string[];
510528
endTimestampMs?: number;
529+
nonCachedDurationMs?: number;
511530
result: string;
512531
startTimestampMs?: number;
513532
}
@@ -582,6 +601,19 @@ export class Operation {
582601
weight: number;
583602
}
584603

604+
// @internal
605+
export class _OperationStateFile {
606+
constructor(options: _IOperationStateFileOptions);
607+
get filename(): string;
608+
static getFilenameRelativeToProjectRoot(phase: IPhase): string;
609+
// (undocumented)
610+
get state(): _IOperationStateJson | undefined;
611+
// (undocumented)
612+
tryRestoreAsync(): Promise<_IOperationStateJson | undefined>;
613+
// (undocumented)
614+
writeAsync(json: _IOperationStateJson): Promise<void>;
615+
}
616+
585617
// @beta
586618
export enum OperationStatus {
587619
Blocked = "BLOCKED",

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

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

libraries/rush-lib/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,8 @@ export { ICredentialCacheOptions, ICredentialCacheEntry, CredentialCache } from
118118
export type { ITelemetryData, ITelemetryMachineInfo, ITelemetryOperationResult } from './logic/Telemetry';
119119

120120
export { IStopwatchResult } from './utilities/Stopwatch';
121+
export {
122+
OperationStateFile as _OperationStateFile,
123+
IOperationStateFileOptions as _IOperationStateFileOptions,
124+
IOperationStateJson as _IOperationStateJson
125+
} from './logic/operations/OperationStateFile';

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export interface ITelemetryOperationResult {
6666
* If the operation was blocked, will be `undefined`.
6767
*/
6868
endTimestampMs?: number;
69+
70+
/**
71+
* Duration in milliseconds when the operation does not hit cache
72+
*/
73+
nonCachedDurationMs?: number;
6974
}
7075

7176
/**

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

Lines changed: 26 additions & 5 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,16 +45,23 @@ export class ProjectBuildCache {
4445
private readonly _buildCacheEnabled: boolean;
4546
private readonly _cacheWriteEnabled: boolean;
4647
private readonly _projectOutputFolderNames: ReadonlyArray<string>;
48+
private readonly _additionalProjectOutputFilePaths: ReadonlyArray<string>;
4749
private _cacheId: string | undefined;
4850

4951
private constructor(cacheId: string | undefined, options: IProjectBuildCacheOptions) {
50-
const { buildCacheConfiguration, projectConfiguration, projectOutputFolderNames } = options;
52+
const {
53+
buildCacheConfiguration,
54+
projectConfiguration,
55+
projectOutputFolderNames,
56+
additionalProjectOutputFilePaths
57+
} = options;
5158
this._project = projectConfiguration.project;
5259
this._localBuildCacheProvider = buildCacheConfiguration.localCacheProvider;
5360
this._cloudBuildCacheProvider = buildCacheConfiguration.cloudCacheProvider;
5461
this._buildCacheEnabled = buildCacheConfiguration.buildCacheEnabled;
5562
this._cacheWriteEnabled = buildCacheConfiguration.cacheWriteEnabled;
5663
this._projectOutputFolderNames = projectOutputFolderNames || [];
64+
this._additionalProjectOutputFilePaths = additionalProjectOutputFilePaths || [];
5765
this._cacheId = cacheId;
5866
}
5967

@@ -116,7 +124,7 @@ export class ProjectBuildCache {
116124
if (inputOutputFiles.length > 0) {
117125
terminal.writeWarningLine(
118126
'Unable to use build cache. The following files are used to calculate project state ' +
119-
`and are considered project output: ${inputOutputFiles.join(', ')}`
127+
`and are considered project output: ${inputOutputFiles.join(', ')}`
120128
);
121129
return false;
122130
} else {
@@ -251,14 +259,14 @@ export class ProjectBuildCache {
251259
} else {
252260
terminal.writeWarningLine(
253261
`"tar" exited with code ${tarExitCode} while attempting to create the cache entry. ` +
254-
`See "${logFilePath}" for logs from the tar process.`
262+
`See "${logFilePath}" for logs from the tar process.`
255263
);
256264
return false;
257265
}
258266
} else {
259267
terminal.writeWarningLine(
260268
`Unable to locate "tar". Please ensure that "tar" is on your PATH environment variable, or set the ` +
261-
`${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.`
262270
);
263271
return false;
264272
}
@@ -359,6 +367,19 @@ export class ProjectBuildCache {
359367
return undefined;
360368
}
361369

370+
// Add additional output file paths
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+
);
382+
362383
// Ensure stable output path order.
363384
outputFilePaths.sort();
364385

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface IOperationExecutionResult {
3131
* Object used to report a summary at the end of the Rush invocation.
3232
*/
3333
readonly stdioSummarizer: StdioSummarizer;
34+
/**
35+
* The value indicates the duration of the same operation without cache hit.
36+
*/
37+
readonly nonCachedDurationMs: number | undefined;
3438
}
3539

3640
/**

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

Lines changed: 12 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 { IStopwatchResult } from '../../utilities/Stopwatch';
810

911
/**
1012
* Information passed to the executing `IOperationRunner`
@@ -28,6 +30,16 @@ 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+
* @internal
37+
*/
38+
_operationStateFile?: OperationStateFile;
39+
/**
40+
* Object used to track elapsed time.
41+
*/
42+
stopwatch: IStopwatchResult;
3143
}
3244

3345
/**

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { OperationStatus } from './OperationStatus';
99
import { IOperationRunner, IOperationRunnerContext } from './IOperationRunner';
1010
import { Operation } from './Operation';
1111
import { Stopwatch } from '../../utilities/Stopwatch';
12+
import { OperationStateFile } from './OperationStateFile';
1213

1314
export interface IOperationExecutionRecordContext {
1415
streamCollator: StreamCollator;
@@ -80,6 +81,7 @@ export class OperationExecutionRecord implements IOperationRunnerContext {
8081

8182
public readonly runner: IOperationRunner;
8283
public readonly weight: number;
84+
public readonly _operationStateFile: OperationStateFile | undefined;
8385

8486
private readonly _context: IOperationExecutionRecordContext;
8587

@@ -96,6 +98,12 @@ export class OperationExecutionRecord implements IOperationRunnerContext {
9698

9799
this.runner = runner;
98100
this.weight = operation.weight;
101+
if (operation.associatedPhase && operation.associatedProject) {
102+
this._operationStateFile = new OperationStateFile({
103+
phase: operation.associatedPhase,
104+
rushProject: operation.associatedProject
105+
});
106+
}
99107
this._context = context;
100108
}
101109

@@ -119,6 +127,11 @@ export class OperationExecutionRecord implements IOperationRunnerContext {
119127
return this._collatedWriter;
120128
}
121129

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+
122135
public async executeAsync(onResult: (record: OperationExecutionRecord) => void): Promise<void> {
123136
this.status = OperationStatus.Executing;
124137
this.stopwatch.start();
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { FileSystem, InternalError, JsonFile } from '@rushstack/node-core-library';
5+
import { RushConstants } from '../RushConstants';
6+
7+
import type { IPhase } from '../../api/CommandLineConfiguration';
8+
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
9+
10+
/**
11+
* @internal
12+
*/
13+
export interface IOperationStateFileOptions {
14+
rushProject: RushConfigurationProject;
15+
phase: IPhase;
16+
}
17+
18+
/**
19+
* @internal
20+
*/
21+
export interface IOperationStateJson {
22+
nonCachedDurationMs: number;
23+
}
24+
25+
/**
26+
* A helper class for managing the state file of a operation.
27+
*
28+
* @internal
29+
*/
30+
export class OperationStateFile {
31+
private readonly _rushProject: RushConfigurationProject;
32+
private readonly _filename: string;
33+
private _state: IOperationStateJson | undefined;
34+
35+
public constructor(options: IOperationStateFileOptions) {
36+
const { rushProject, phase } = options;
37+
this._rushProject = rushProject;
38+
this._filename = OperationStateFile._getFilename(phase, rushProject);
39+
}
40+
41+
private static _getFilename(phase: IPhase, project: RushConfigurationProject): string {
42+
const relativeFilename: string = OperationStateFile.getFilenameRelativeToProjectRoot(phase);
43+
return `${project.projectFolder}/${relativeFilename}`;
44+
}
45+
46+
/**
47+
* ProjectBuildCache expects the relative path for better logging
48+
*
49+
* @internal
50+
*/
51+
public static getFilenameRelativeToProjectRoot(phase: IPhase): string {
52+
const identifier: string = phase.logFilenameIdentifier;
53+
return `${RushConstants.projectRushFolderName}/${RushConstants.rushTempFolderName}/operation/${identifier}/state.json`;
54+
}
55+
56+
/**
57+
* Returns the filename of the metadata file.
58+
*/
59+
public get filename(): string {
60+
return this._filename;
61+
}
62+
63+
public get state(): IOperationStateJson | undefined {
64+
return this._state;
65+
}
66+
67+
public async writeAsync(json: IOperationStateJson): Promise<void> {
68+
await JsonFile.saveAsync(json, this._filename, { ensureFolderExists: true, updateExistingFile: true });
69+
this._state = json;
70+
}
71+
72+
public async tryRestoreAsync(): Promise<IOperationStateJson | undefined> {
73+
try {
74+
this._state = await JsonFile.loadAsync(this._filename);
75+
} catch (error) {
76+
if (FileSystem.isNotExistError(error as Error)) {
77+
this._state = undefined;
78+
} else {
79+
// This should not happen
80+
throw new InternalError(error);
81+
}
82+
}
83+
return this._state;
84+
}
85+
}

0 commit comments

Comments
 (0)