Skip to content

Commit 7c6521e

Browse files
authored
When building projects in watch mode, only schedule projects that need build or need update to bundle (#48865)
* Handle timesouts to reflect the time it was set with * Remove unused internal calls from solution builder * If the project doesnt need building or updating bundle, dont schedule it but do it right away * Reduce the time between project builds to 100ms * Some tests for projects building * Schedule builds such that when change is not detected 5 projects are built at a time * Fix tests in main
1 parent c592ee7 commit 7c6521e

File tree

66 files changed

+11243
-982
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+11243
-982
lines changed

src/compiler/tsbuildPublic.ts

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,6 @@ namespace ts {
145145
// Testing only
146146
/*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus;
147147
/*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void;
148-
/*@internal*/ buildNextInvalidatedProject(): void;
149-
/*@internal*/ getAllParsedConfigs(): readonly ParsedCommandLine[];
150148
/*@internal*/ close(): void;
151149
}
152150

@@ -250,7 +248,6 @@ namespace ts {
250248
allProjectBuildPending: boolean;
251249
needsSummary: boolean;
252250
watchAllProjectsPending: boolean;
253-
currentInvalidatedProject: InvalidatedProject<T> | undefined;
254251

255252
// Watch state
256253
readonly watch: boolean;
@@ -332,7 +329,6 @@ namespace ts {
332329
allProjectBuildPending: true,
333330
needsSummary: true,
334331
watchAllProjectsPending: watch,
335-
currentInvalidatedProject: undefined,
336332

337333
// Watch state
338334
watch,
@@ -684,7 +680,6 @@ namespace ts {
684680
projectPath: ResolvedConfigFilePath
685681
) {
686682
state.projectPendingBuild.delete(projectPath);
687-
state.currentInvalidatedProject = undefined;
688683
return state.diagnostics.has(projectPath) ?
689684
ExitStatus.DiagnosticsPresent_OutputsSkipped :
690685
ExitStatus.Success;
@@ -1171,19 +1166,22 @@ namespace ts {
11711166
!isIncrementalCompilation(config.options);
11721167
}
11731168

1174-
function getNextInvalidatedProject<T extends BuilderProgram>(
1169+
interface InvalidateProjectCreateInfo {
1170+
kind: InvalidatedProjectKind;
1171+
status: UpToDateStatus;
1172+
project: ResolvedConfigFileName;
1173+
projectPath: ResolvedConfigFilePath;
1174+
projectIndex: number;
1175+
config: ParsedCommandLine;
1176+
}
1177+
1178+
function getNextInvalidatedProjectCreateInfo<T extends BuilderProgram>(
11751179
state: SolutionBuilderState<T>,
11761180
buildOrder: AnyBuildOrder,
11771181
reportQueue: boolean
1178-
): InvalidatedProject<T> | undefined {
1182+
): InvalidateProjectCreateInfo | undefined {
11791183
if (!state.projectPendingBuild.size) return undefined;
11801184
if (isCircularBuildOrder(buildOrder)) return undefined;
1181-
if (state.currentInvalidatedProject) {
1182-
// Only if same buildOrder the currentInvalidated project can be sent again
1183-
return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ?
1184-
state.currentInvalidatedProject :
1185-
undefined;
1186-
}
11871185

11881186
const { options, projectPendingBuild } = state;
11891187
for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) {
@@ -1220,9 +1218,9 @@ namespace ts {
12201218
}
12211219

12221220
const status = getUpToDateStatus(state, config, projectPath);
1223-
verboseReportProjectStatus(state, project, status);
12241221
if (!options.force) {
12251222
if (status.type === UpToDateStatusType.UpToDate) {
1223+
verboseReportProjectStatus(state, project, status);
12261224
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
12271225
projectPendingBuild.delete(projectPath);
12281226
// Up to date, skip
@@ -1235,17 +1233,19 @@ namespace ts {
12351233

12361234
if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) {
12371235
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
1238-
return createUpdateOutputFileStampsProject(
1239-
state,
1236+
return {
1237+
kind: InvalidatedProjectKind.UpdateOutputFileStamps,
1238+
status,
12401239
project,
12411240
projectPath,
1242-
config,
1243-
buildOrder
1244-
);
1241+
projectIndex,
1242+
config
1243+
};
12451244
}
12461245
}
12471246

12481247
if (status.type === UpToDateStatusType.UpstreamBlocked) {
1248+
verboseReportProjectStatus(state, project, status);
12491249
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
12501250
projectPendingBuild.delete(projectPath);
12511251
if (options.verbose) {
@@ -1262,28 +1262,63 @@ namespace ts {
12621262
}
12631263

12641264
if (status.type === UpToDateStatusType.ContainerOnly) {
1265+
verboseReportProjectStatus(state, project, status);
12651266
reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config));
12661267
projectPendingBuild.delete(projectPath);
12671268
// Do nothing
12681269
continue;
12691270
}
12701271

1271-
return createBuildOrUpdateInvalidedProject(
1272-
needsBuild(state, status, config) ?
1272+
return {
1273+
kind: needsBuild(state, status, config) ?
12731274
InvalidatedProjectKind.Build :
12741275
InvalidatedProjectKind.UpdateBundle,
1275-
state,
1276+
status,
12761277
project,
12771278
projectPath,
12781279
projectIndex,
12791280
config,
1280-
buildOrder,
1281-
);
1281+
};
12821282
}
12831283

12841284
return undefined;
12851285
}
12861286

1287+
function createInvalidatedProjectWithInfo<T extends BuilderProgram>(
1288+
state: SolutionBuilderState<T>,
1289+
info: InvalidateProjectCreateInfo,
1290+
buildOrder: AnyBuildOrder,
1291+
) {
1292+
verboseReportProjectStatus(state, info.project, info.status);
1293+
return info.kind !== InvalidatedProjectKind.UpdateOutputFileStamps ?
1294+
createBuildOrUpdateInvalidedProject(
1295+
info.kind,
1296+
state,
1297+
info.project,
1298+
info.projectPath,
1299+
info.projectIndex,
1300+
info.config,
1301+
buildOrder as BuildOrder,
1302+
) :
1303+
createUpdateOutputFileStampsProject(
1304+
state,
1305+
info.project,
1306+
info.projectPath,
1307+
info.config,
1308+
buildOrder as BuildOrder
1309+
);
1310+
}
1311+
1312+
function getNextInvalidatedProject<T extends BuilderProgram>(
1313+
state: SolutionBuilderState<T>,
1314+
buildOrder: AnyBuildOrder,
1315+
reportQueue: boolean
1316+
): InvalidatedProject<T> | undefined {
1317+
const info = getNextInvalidatedProjectCreateInfo(state, buildOrder, reportQueue);
1318+
if (!info) return info;
1319+
return createInvalidatedProjectWithInfo(state, info, buildOrder);
1320+
}
1321+
12871322
function listEmittedFile({ write }: SolutionBuilderState, proj: ParsedCommandLine, file: string) {
12881323
if (write && proj.options.listEmittedFiles) {
12891324
write(`TSFILE: ${file}`);
@@ -1769,37 +1804,47 @@ namespace ts {
17691804
function invalidateProjectAndScheduleBuilds(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) {
17701805
state.reportFileChangeDetected = true;
17711806
invalidateProject(state, resolvedPath, reloadLevel);
1772-
scheduleBuildInvalidatedProject(state);
1807+
scheduleBuildInvalidatedProject(state, 250, /*changeDetected*/ true);
17731808
}
17741809

1775-
function scheduleBuildInvalidatedProject(state: SolutionBuilderState) {
1810+
function scheduleBuildInvalidatedProject(state: SolutionBuilderState, time: number, changeDetected: boolean) {
17761811
const { hostWithWatch } = state;
17771812
if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) {
17781813
return;
17791814
}
17801815
if (state.timerToBuildInvalidatedProject) {
17811816
hostWithWatch.clearTimeout(state.timerToBuildInvalidatedProject);
17821817
}
1783-
state.timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250, state);
1818+
state.timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, time, state, changeDetected);
17841819
}
17851820

1786-
function buildNextInvalidatedProject(state: SolutionBuilderState) {
1821+
function buildNextInvalidatedProject(state: SolutionBuilderState, changeDetected: boolean) {
17871822
state.timerToBuildInvalidatedProject = undefined;
17881823
if (state.reportFileChangeDetected) {
17891824
state.reportFileChangeDetected = false;
17901825
state.projectErrorsReported.clear();
17911826
reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation);
17921827
}
1828+
let projectsBuilt = 0;
17931829
const buildOrder = getBuildOrder(state);
17941830
const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false);
17951831
if (invalidatedProject) {
17961832
invalidatedProject.done();
1797-
if (state.projectPendingBuild.size) {
1798-
// Schedule next project for build
1799-
if (state.watch && !state.timerToBuildInvalidatedProject) {
1800-
scheduleBuildInvalidatedProject(state);
1833+
projectsBuilt++;
1834+
while (state.projectPendingBuild.size) {
1835+
// If already scheduled, skip
1836+
if (state.timerToBuildInvalidatedProject) return;
1837+
// Before scheduling check if the next project needs build
1838+
const info = getNextInvalidatedProjectCreateInfo(state, buildOrder, /*reportQueue*/ false);
1839+
if (!info) break; // Nothing to build any more
1840+
if (info.kind !== InvalidatedProjectKind.UpdateOutputFileStamps && (changeDetected || projectsBuilt === 5)) {
1841+
// Schedule next project for build
1842+
scheduleBuildInvalidatedProject(state, 100, /*changeDetected*/ false);
1843+
return;
18011844
}
1802-
return;
1845+
const project = createInvalidatedProjectWithInfo(state, info, buildOrder);
1846+
project.done();
1847+
if (info.kind !== InvalidatedProjectKind.UpdateOutputFileStamps) projectsBuilt++;
18031848
}
18041849
}
18051850
disableCache(state);
@@ -1961,11 +2006,6 @@ namespace ts {
19612006
return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath);
19622007
},
19632008
invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None),
1964-
buildNextInvalidatedProject: () => buildNextInvalidatedProject(state),
1965-
getAllParsedConfigs: () => arrayFrom(mapDefinedIterator(
1966-
state.configFileCache.values(),
1967-
config => isParsedCommandLine(config) ? config : undefined
1968-
)),
19692009
close: () => stopWatching(state),
19702010
};
19712011
}

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -279,18 +279,27 @@ interface Array<T> { length: number; [n: number]: T; }`
279279
}
280280
}
281281

282+
interface CallbackData {
283+
cb: TimeOutCallback;
284+
args: any[];
285+
ms: number | undefined;
286+
time: number;
287+
}
282288
class Callbacks {
283-
private map: TimeOutCallback[] = [];
289+
private map: { cb: TimeOutCallback; args: any[]; ms: number | undefined; time: number; }[] = [];
284290
private nextId = 1;
285291

292+
constructor(private host: TestServerHost) {
293+
}
294+
286295
getNextId() {
287296
return this.nextId;
288297
}
289298

290-
register(cb: (...args: any[]) => void, args: any[]) {
299+
register(cb: TimeOutCallback, args: any[], ms?: number) {
291300
const timeoutId = this.nextId;
292301
this.nextId++;
293-
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
302+
this.map[timeoutId] = { cb, args, ms, time: this.host.getTime() };
294303
return timeoutId;
295304
}
296305

@@ -308,9 +317,19 @@ interface Array<T> { length: number; [n: number]: T; }`
308317
return n;
309318
}
310319

320+
private invokeCallback({ cb, args, ms, time }: CallbackData) {
321+
if (ms !== undefined) {
322+
const newTime = ms + time;
323+
if (this.host.getTime() < newTime) {
324+
this.host.setTime(newTime);
325+
}
326+
}
327+
cb(...args);
328+
}
329+
311330
invoke(invokeKey?: number) {
312331
if (invokeKey) {
313-
this.map[invokeKey]();
332+
this.invokeCallback(this.map[invokeKey]);
314333
delete this.map[invokeKey];
315334
return;
316335
}
@@ -319,13 +338,13 @@ interface Array<T> { length: number; [n: number]: T; }`
319338
// so do not clear the entire callback list regardless. Only remove the
320339
// ones we have invoked.
321340
for (const key in this.map) {
322-
this.map[key]();
341+
this.invokeCallback(this.map[key]);
323342
delete this.map[key];
324343
}
325344
}
326345
}
327346

328-
type TimeOutCallback = () => any;
347+
type TimeOutCallback = (...args: any[]) => void;
329348

330349
export interface TestFileWatcher {
331350
cb: FileWatcherCallback;
@@ -380,8 +399,8 @@ interface Array<T> { length: number; [n: number]: T; }`
380399
private time = timeIncrements;
381400
getCanonicalFileName: (s: string) => string;
382401
private toPath: (f: string) => Path;
383-
private timeoutCallbacks = new Callbacks();
384-
private immediateCallbacks = new Callbacks();
402+
private timeoutCallbacks = new Callbacks(this);
403+
private immediateCallbacks = new Callbacks(this);
385404
readonly screenClears: number[] = [];
386405

387406
readonly watchedFiles = createMultiMap<Path, TestFileWatcher>();
@@ -478,6 +497,14 @@ interface Array<T> { length: number; [n: number]: T; }`
478497
return new Date(this.time);
479498
}
480499

500+
getTime() {
501+
return this.time;
502+
}
503+
504+
setTime(time: number) {
505+
this.time = time;
506+
}
507+
481508
private reloadFS(fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], options?: Partial<ReloadWatchInvokeOptions>) {
482509
Debug.assert(this.fs.size === 0);
483510
fileOrFolderOrSymLinkList = fileOrFolderOrSymLinkList.concat(this.withSafeList ? safeList : []);
@@ -935,8 +962,8 @@ interface Array<T> { length: number; [n: number]: T; }`
935962
}
936963

937964
// TOOD: record and invoke callbacks to simulate timer events
938-
setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) {
939-
return this.timeoutCallbacks.register(callback, args);
965+
setTimeout(callback: TimeOutCallback, ms: number, ...args: any[]) {
966+
return this.timeoutCallbacks.register(callback, args, ms);
940967
}
941968

942969
getNextTimeoutId() {
@@ -980,7 +1007,7 @@ interface Array<T> { length: number; [n: number]: T; }`
9801007
this.immediateCallbacks.invoke();
9811008
}
9821009

983-
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
1010+
setImmediate(callback: TimeOutCallback, ...args: any[]) {
9841011
return this.immediateCallbacks.register(callback, args);
9851012
}
9861013

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
"unittests/tsbuildWatch/noEmit.ts",
147147
"unittests/tsbuildWatch/noEmitOnError.ts",
148148
"unittests/tsbuildWatch/programUpdates.ts",
149+
"unittests/tsbuildWatch/projectsBuilding.ts",
149150
"unittests/tsbuildWatch/publicApi.ts",
150151
"unittests/tsbuildWatch/reexport.ts",
151152
"unittests/tsbuildWatch/watchEnvironment.ts",

src/testRunner/unittests/tsbuild/sample.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,11 @@ namespace ts {
338338
function verifyInvalidation(heading: string) {
339339
// Rebuild this project
340340
builder.invalidateProject(logicConfig.path as ResolvedConfigFilePath);
341-
builder.buildNextInvalidatedProject();
341+
builder.getNextInvalidatedProject()?.done();
342342
baselineState(`${heading}:: After rebuilding logicConfig`);
343343

344344
// Build downstream projects should update 'tests', but not 'core'
345-
builder.buildNextInvalidatedProject();
345+
builder.getNextInvalidatedProject()?.done();
346346
baselineState(`${heading}:: After building next project`);
347347
}
348348

src/testRunner/unittests/tsbuildWatch/demo.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ namespace ts.tscWatch {
4848
change: sys => sys.writeFile(coreFiles[0].path, coreFiles[0].content),
4949
timeouts: sys => {
5050
sys.checkTimeoutQueueLengthAndRun(1); // build core
51-
sys.checkTimeoutQueueLengthAndRun(1); // build animals
52-
sys.checkTimeoutQueueLengthAndRun(1); // build zoo
53-
sys.checkTimeoutQueueLengthAndRun(1); // build solution
51+
sys.checkTimeoutQueueLengthAndRun(1); // build animals, zoo and solution
5452
sys.checkTimeoutQueueLength(0);
5553
},
5654
}

src/testRunner/unittests/tsbuildWatch/moduleResolution.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ namespace ts.tscWatch {
4545
caption: "Append text",
4646
change: sys => sys.appendFile(`${projectRoot}/project1/index.ts`, "const bar = 10;"),
4747
timeouts: sys => {
48-
sys.checkTimeoutQueueLengthAndRun(1); // build project1
49-
sys.checkTimeoutQueueLengthAndRun(1); // Solution
48+
sys.checkTimeoutQueueLengthAndRun(1); // build project1 and solution
5049
sys.checkTimeoutQueueLength(0);
5150
}
5251
},

0 commit comments

Comments
 (0)