Skip to content

fix(timeline,cobuilds): cobuilt operations should reflect the cobuild time and not cache restore time #4680

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

Merged
merged 29 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
99024b4
fix: show cobuild timings for all agents
aramissennyeydd May 6, 2024
090ce43
stopwatch timing isn't correct
aramissennyeydd May 7, 2024
6adcbdf
cleanup
aramissennyeydd May 7, 2024
872b12a
fixing when cache is flushed to disk
aramissennyeydd May 7, 2024
944d13a
add changeset
aramissennyeydd May 7, 2024
3ddde09
fix snapshot
aramissennyeydd May 7, 2024
523e08f
fix linting
aramissennyeydd May 7, 2024
dd67e3f
adjust the check to check for cobuild disabled
aramissennyeydd May 7, 2024
48e2361
make it not async
aramissennyeydd May 10, 2024
bdeffcb
make operation tracking async
aramissennyeydd May 10, 2024
77df07b
fix binding
aramissennyeydd May 10, 2024
cad6b5b
small adjustments to make the operations look better in timeline view
aramissennyeydd May 10, 2024
54d9265
Apply suggestions from code review
aramissennyeydd May 14, 2024
19f98fd
adjust comment
aramissennyeydd May 14, 2024
41e9cd5
flip stopwatch usage
aramissennyeydd May 21, 2024
088141d
add a comment
aramissennyeydd May 21, 2024
c1d2f2d
Un-async _onOperationComplete.
iclanton May 25, 2024
e6f821e
un-async onOperationComplete
aramissennyeydd Dec 26, 2024
93e2aa8
fix build
aramissennyeydd Dec 26, 2024
c4306ea
rebase fixes
aramissennyeydd Dec 26, 2024
f2c3c26
address PR feedback, move cobuild state to operation metadata manager
aramissennyeydd Dec 30, 2024
570d43f
fix lint warnings
aramissennyeydd Jan 3, 2025
462c36d
remove 1 more hard coded wasCobuilt calculation
aramissennyeydd Jan 3, 2025
fb06b5e
fix more lint issues + revert stopwatch changes
aramissennyeydd Jan 3, 2025
ea0d8c6
revert stopwatch back
aramissennyeydd Jan 3, 2025
31047c1
fix merge issue
aramissennyeydd Mar 5, 2025
0176f99
fix snapshot
aramissennyeydd Mar 5, 2025
fabfa57
Apply suggestions from code review
aramissennyeydd Mar 5, 2025
2dfa48e
fix typedef
aramissennyeydd Mar 5, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@
{
"operationName": "_phase:build",
"outputFolderNames": ["dist"],
"allowCobuildOrchestration": true,
"disableBuildCacheForOperation": true,
"sharding": {
"count": 75
}
},
{
"operationName": "_phase:build:shard",
"weight": 10,
"allowCobuildOrchestration": true
"weight": 10
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Operations that were cobuilt now have the cobuild time correctly reflected across all agents.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
9 changes: 7 additions & 2 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,6 @@ export interface _INpmOptionsJson extends IPackageManagerOptionsJsonBase {

// @alpha
export interface IOperationExecutionResult {
readonly cobuildRunnerId: string | undefined;
readonly error: Error | undefined;
getStateHash(): string;
getStateHashComponents(): ReadonlyArray<string>;
Expand Down Expand Up @@ -958,11 +957,17 @@ export class _OperationMetadataManager {
// (undocumented)
readonly stateFile: _OperationStateFile;
// (undocumented)
tryRestoreAsync({ terminal, terminalProvider, errorLogPath }: {
tryRestoreAsync({ terminal, terminalProvider, errorLogPath, cobuildContextId, cobuildRunnerId }: {
terminalProvider: ITerminalProvider;
terminal: ITerminal;
errorLogPath: string;
cobuildContextId?: string;
cobuildRunnerId?: string;
}): Promise<void>;
// (undocumented)
tryRestoreStopwatch(originalStopwatch: IStopwatchResult): IStopwatchResult;
// (undocumented)
wasCobuilt: boolean;
}

// @internal
Expand Down
18 changes: 11 additions & 7 deletions libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,9 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
` Press <${quitKey}> to gracefully exit.`,
` Press <${toggleWatcherKey}> to ${isPaused ? 'resume' : 'pause'}.`,
` Press <${invalidateKey}> to invalidate all projects.`,
` Press <${changedProjectsOnlyKey}> to ${this._changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).`
` Press <${changedProjectsOnlyKey}> to ${
this._changedProjectsOnly ? 'disable' : 'enable'
} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).`
];
if (isPaused) {
promptLines.push(` Press <${buildOnceKey}> to build once.`);
Expand Down Expand Up @@ -820,8 +822,9 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
// eslint-disable-next-line no-constant-condition
while (!abortSignal.aborted) {
// On the initial invocation, this promise will return immediately with the full set of projects
const { changedProjects, inputsSnapshot: state } =
await projectWatcher.waitForChangeAsync(onWaitingForChanges);
const { changedProjects, inputsSnapshot: state } = await projectWatcher.waitForChangeAsync(
onWaitingForChanges
);

if (abortSignal.aborted) {
return;
Expand Down Expand Up @@ -897,7 +900,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
executionManagerOptions
);

const { isInitial, isWatch, cobuildConfiguration } = options.executeOperationsContext;
const { isInitial, isWatch } = options.executeOperationsContext;

let success: boolean = false;
let result: IExecutionResult | undefined;
Expand Down Expand Up @@ -997,14 +1000,15 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
continue;
}

const { _operationMetadataManager: operationMetadataManager } =
operationResult as OperationExecutionRecord;

const { startTime, endTime } = operationResult.stopwatch;
jsonOperationResults[operation.name!] = {
startTimestampMs: startTime,
endTimestampMs: endTime,
nonCachedDurationMs: operationResult.nonCachedDurationMs,
wasExecutedOnThisMachine:
!operationResult.cobuildRunnerId ||
operationResult.cobuildRunnerId === cobuildConfiguration?.cobuildRunnerId,
wasExecutedOnThisMachine: operationMetadataManager?.wasCobuilt !== true,
result: operationResult.status,
dependencies: Array.from(getNonSilentDependencies(operation)).sort()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin {
await operationMetadataManager?.tryRestoreAsync({
terminalProvider,
terminal: buildCacheTerminal,
errorLogPath
errorLogPath,
cobuildContextId: cobuildConfiguration?.cobuildContextId,
cobuildRunnerId: cobuildConfiguration?.cobuildRunnerId
});
},
{ createLogFile: false }
Expand Down
60 changes: 48 additions & 12 deletions libraries/rush-lib/src/logic/operations/ConsoleTimelinePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
import type { IExecutionResult } from './IOperationExecutionResult';
import { OperationStatus } from './OperationStatus';
import type { CobuildConfiguration } from '../../api/CobuildConfiguration';
import type { OperationExecutionRecord } from './OperationExecutionRecord';

const PLUGIN_NAME: 'ConsoleTimelinePlugin' = 'ConsoleTimelinePlugin';

Expand Down Expand Up @@ -127,20 +128,25 @@ interface ITimelineRecord {
export interface IPrintTimelineParameters {
terminal: ITerminal;
result: IExecutionResult;
cobuildConfiguration: CobuildConfiguration | undefined;
cobuildConfiguration?: CobuildConfiguration;
}

interface ICachedDuration {
cached?: number;
uncached: number;
}

/**
* Print a more detailed timeline and analysis of CPU usage for the build.
* @internal
*/
export function _printTimeline({ terminal, result, cobuildConfiguration }: IPrintTimelineParameters): void {
export function _printTimeline({ terminal, result }: IPrintTimelineParameters): void {
//
// Gather the operation records we'll be displaying. Do some inline max()
// finding to reduce the number of times we need to loop through operations.
//

const durationByPhase: Map<IPhase, number> = new Map();
const durationByPhase: Map<IPhase, ICachedDuration> = new Map();

const data: ITimelineRecord[] = [];
let longestNameLength: number = 0;
Expand All @@ -155,17 +161,33 @@ export function _printTimeline({ terminal, result, cobuildConfiguration }: IPrin
}

const { stopwatch } = operationResult;
const { _operationMetadataManager: operationMetadataManager } =
operationResult as OperationExecutionRecord;

let { startTime } = stopwatch;
const { endTime } = stopwatch;

const { startTime, endTime } = stopwatch;
const duration: ICachedDuration = { cached: undefined, uncached: stopwatch.duration };

if (startTime && endTime) {
const nameLength: number = operation.name?.length || 0;
if (nameLength > longestNameLength) {
longestNameLength = nameLength;
}
const wasCobuilt: boolean = !!operationMetadataManager?.wasCobuilt;
if (
wasCobuilt &&
operationResult.status !== OperationStatus.FromCache &&
operationResult.nonCachedDurationMs
) {
duration.cached = stopwatch.duration;
startTime = Math.max(0, endTime - operationResult.nonCachedDurationMs);
duration.uncached = (endTime - startTime) / 1000;
}

workDuration += stopwatch.duration;

const { duration } = stopwatch;
const durationString: string = duration.toFixed(1);
const durationString: string = duration.uncached.toFixed(1);
const durationLength: number = durationString.length;
if (durationLength > longestDurationLength) {
longestDurationLength = durationLength;
Expand All @@ -177,21 +199,31 @@ export function _printTimeline({ terminal, result, cobuildConfiguration }: IPrin
if (startTime < allStart) {
allStart = startTime;
}
workDuration += duration;

const { associatedPhase } = operation;

durationByPhase.set(associatedPhase, (durationByPhase.get(associatedPhase) || 0) + duration);
if (associatedPhase) {
let durationRecord: ICachedDuration | undefined = durationByPhase.get(associatedPhase);
if (!durationRecord) {
durationRecord = {
cached: undefined,
uncached: 0
};
durationByPhase.set(associatedPhase, durationRecord);
}
if (duration.cached !== undefined) {
durationRecord.cached = (durationRecord.cached ?? 0) + duration.cached;
}
durationRecord.uncached += duration.uncached;
}

data.push({
startTime,
endTime,
durationString,
name: operation.name,
status: operationResult.status,
isExecuteByOtherCobuildRunner:
!!operationResult.cobuildRunnerId &&
operationResult.cobuildRunnerId !== cobuildConfiguration?.cobuildRunnerId
isExecuteByOtherCobuildRunner: wasCobuilt
});
}
}
Expand Down Expand Up @@ -310,7 +342,11 @@ export function _printTimeline({ terminal, result, cobuildConfiguration }: IPrin
}

for (const [phase, duration] of durationByPhase.entries()) {
terminal.writeLine(` ${Colorize.cyan(phase.name.padStart(maxPhaseName))} ${duration.toFixed(1)}s`);
const cachedDurationString: string = duration.cached
? `, from cache: ${duration.cached.toFixed(1)}s`
: '';
const durationString: string = `${duration.uncached.toFixed(1)}s${cachedDurationString}`;
terminal.writeLine(` ${Colorize.cyan(phase.name.padStart(maxPhaseName))} ${durationString}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ export interface IOperationExecutionResult {
* The value indicates the duration of the same operation without cache hit.
*/
readonly nonCachedDurationMs: number | undefined;
/**
* The id of the runner which actually runs the building process in cobuild mode.
*/
readonly cobuildRunnerId: string | undefined;
/**
* The relative path to the folder that contains operation metadata. This folder will be automatically included in cache entries.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { type IOperationExecutionRecordContext, OperationExecutionRecord } from
import type { IExecutionResult } from './IOperationExecutionResult';
import type { IEnvironment } from '../../utilities/Utilities';
import type { IInputsSnapshot } from '../incremental/InputsSnapshot';
import type { IStopwatchResult } from '../../utilities/Stopwatch';

export interface IOperationExecutionManagerOptions {
quietMode: boolean;
Expand Down Expand Up @@ -295,8 +296,8 @@ export class OperationExecutionManager {
const status: OperationStatus = this._hasAnyFailures
? OperationStatus.Failure
: this._hasAnyNonAllowedWarnings
? OperationStatus.SuccessWithWarning
: OperationStatus.Success;
? OperationStatus.SuccessWithWarning
: OperationStatus.Success;

return {
operationResults: this._executionRecords,
Expand Down Expand Up @@ -331,7 +332,9 @@ export class OperationExecutionManager {
* Handles the result of the operation and propagates any relevant effects.
*/
private _onOperationComplete(record: OperationExecutionRecord): void {
const { runner, name, status, silent } = record;
const { runner, name, status, silent, _operationMetadataManager: operationMetadataManager } = record;
const stopwatch: IStopwatchResult =
operationMetadataManager?.tryRestoreStopwatch(record.stopwatch) || record.stopwatch;

switch (status) {
/**
Expand Down Expand Up @@ -413,7 +416,7 @@ export class OperationExecutionManager {
case OperationStatus.Success: {
if (!silent) {
record.collatedWriter.terminal.writeStdoutLine(
Colorize.green(`"${name}" completed successfully in ${record.stopwatch.toString()}.`)
Colorize.green(`"${name}" completed successfully in ${stopwatch.toString()}.`)
);
}
break;
Expand All @@ -422,7 +425,7 @@ export class OperationExecutionManager {
case OperationStatus.SuccessWithWarning: {
if (!silent) {
record.collatedWriter.terminal.writeStderrLine(
Colorize.yellow(`"${name}" completed with warnings in ${record.stopwatch.toString()}.`)
Colorize.yellow(`"${name}" completed with warnings in ${stopwatch.toString()}.`)
);
}
this._hasAnyNonAllowedWarnings = this._hasAnyNonAllowedWarnings || !runner.warningsAreAllowed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ import { OperationMetadataManager } from './OperationMetadataManager';
import type { IPhase } from '../../api/CommandLineConfiguration';
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
import { CollatedTerminalProvider } from '../../utilities/CollatedTerminalProvider';
import type { IOperationExecutionResult } from './IOperationExecutionResult';
import type { IInputsSnapshot } from '../incremental/InputsSnapshot';
import { RushConstants } from '../RushConstants';
import type { IEnvironment } from '../../utilities/Utilities';
import {
getProjectLogFilePaths,
type ILogFilePaths,
initializeProjectLogFilesAsync
} from './ProjectLogWritable';
import type { IOperationExecutionResult } from './IOperationExecutionResult';
import type { IInputsSnapshot } from '../incremental/InputsSnapshot';
import { RushConstants } from '../RushConstants';
import type { IEnvironment } from '../../utilities/Utilities';

export interface IOperationExecutionRecordContext {
streamCollator: StreamCollator;
Expand Down Expand Up @@ -381,21 +381,24 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera
this.status = this.operation.enabled
? await this.runner.executeAsync(this)
: this.runner.isNoOp
? OperationStatus.NoOp
: OperationStatus.Skipped;
? OperationStatus.NoOp
: OperationStatus.Skipped;
}
// Make sure that the stopwatch is stopped before reporting the result, otherwise endTime is undefined.
this.stopwatch.stop();
// Delegate global state reporting
await onResult(this);
} catch (error) {
this.status = OperationStatus.Failure;
this.error = error;
// Make sure that the stopwatch is stopped before reporting the result, otherwise endTime is undefined.
this.stopwatch.stop();
// Delegate global state reporting
await onResult(this);
} finally {
if (this.isTerminal) {
this._collatedWriter?.close();
this.stdioSummarizer.close();
this.stopwatch.stop();
}
}
}
Expand Down
Loading
Loading