Skip to content

Commit 7b290fd

Browse files
committed
Update the timestamps of outputs that dont need to be written because of incremental build
This ensures that after `tsbuild` after incremental build of `tsbuild -w` doesnt result in unnecessary rebuilds
1 parent f1949bb commit 7b290fd

File tree

5 files changed

+226
-118
lines changed

5 files changed

+226
-118
lines changed

Diff for: src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -3945,6 +3945,10 @@
39453945
"category": "Error",
39463946
"code": 6370
39473947
},
3948+
"Updating unchanged output timestamps of project '{0}'...": {
3949+
"category": "Message",
3950+
"code": 6371
3951+
},
39483952

39493953
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
39503954
"category": "Message",

Diff for: src/compiler/tsbuild.ts

+78-19
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ namespace ts {
119119
newestDeclarationFileContentChangedTime?: Date;
120120
newestOutputFileTime?: Date;
121121
newestOutputFileName?: string;
122-
oldestOutputFileName?: string;
122+
oldestOutputFileName: string;
123123
}
124124

125125
/**
@@ -332,6 +332,9 @@ namespace ts {
332332
// TODO: To do better with watch mode and normal build mode api that creates program and emits files
333333
// This currently helps enable --diagnostics and --extendedDiagnostics
334334
afterProgramEmitAndDiagnostics?(program: T): void;
335+
336+
// For testing
337+
now?(): Date;
335338
}
336339

337340
export interface SolutionBuilderHost<T extends BuilderProgram> extends SolutionBuilderHostBase<T> {
@@ -991,16 +994,40 @@ namespace ts {
991994
return;
992995
}
993996

997+
if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) {
998+
// Fake build
999+
updateOutputTimestamps(proj);
1000+
return;
1001+
}
1002+
9941003
const buildResult = buildSingleProject(resolved);
995-
const dependencyGraph = getGlobalDependencyGraph();
996-
const referencingProjects = dependencyGraph.referencingProjectsMap.getValue(resolved);
1004+
if (buildResult & BuildResultFlags.AnyErrors) return;
1005+
1006+
const { referencingProjectsMap, buildQueue } = getGlobalDependencyGraph();
1007+
const referencingProjects = referencingProjectsMap.getValue(resolved);
9971008
if (!referencingProjects) return;
1009+
9981010
// Always use build order to queue projects
999-
for (const project of dependencyGraph.buildQueue) {
1011+
for (let index = buildQueue.indexOf(resolved) + 1; index < buildQueue.length; index++) {
1012+
const project = buildQueue[index];
10001013
const prepend = referencingProjects.getValue(project);
1001-
// If the project is referenced with prepend, always build downstream projectm,
1002-
// otherwise queue it only if declaration output changed
1003-
if (prepend || (prepend !== undefined && !(buildResult & BuildResultFlags.DeclarationOutputUnchanged))) {
1014+
if (prepend !== undefined) {
1015+
// If the project is referenced with prepend, always build downstream project,
1016+
// If declaration output is changed changed, build the project
1017+
// otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps
1018+
const status = projectStatus.getValue(project);
1019+
if (prepend || !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) {
1020+
if (status && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes)) {
1021+
projectStatus.setValue(project, {
1022+
type: UpToDateStatusType.OutOfDateWithUpstream,
1023+
outOfDateOutputFileName: status.oldestOutputFileName,
1024+
newerProjectName: resolved
1025+
});
1026+
}
1027+
}
1028+
else if (status && status.type === UpToDateStatusType.UpToDate) {
1029+
status.type = UpToDateStatusType.UpToDateWithUpstreamTypes;
1030+
}
10041031
addProjToQueue(project);
10051032
}
10061033
}
@@ -1110,6 +1137,7 @@ namespace ts {
11101137
let declDiagnostics: Diagnostic[] | undefined;
11111138
const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d);
11121139
const outputFiles: OutputFile[] = [];
1140+
// TODO:: handle declaration diagnostics in incremental build.
11131141
emitFilesAndReportErrors(program, reportDeclarationDiagnostics, writeFileName, /*reportSummary*/ undefined, (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }));
11141142
// Don't emit .d.ts if there are decl file errors
11151143
if (declDiagnostics) {
@@ -1118,6 +1146,7 @@ namespace ts {
11181146

11191147
// Actual Emit
11201148
const emitterDiagnostics = createDiagnosticCollection();
1149+
const emittedOutputs = createFileMap<true>(toPath as ToPath);
11211150
outputFiles.forEach(({ name, text, writeByteOrderMark }) => {
11221151
let priorChangeTime: Date | undefined;
11231152
if (!anyDtsChanged && isDeclarationFile(name)) {
@@ -1131,6 +1160,7 @@ namespace ts {
11311160
}
11321161
}
11331162

1163+
emittedOutputs.setValue(name, true);
11341164
writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark);
11351165
if (priorChangeTime !== undefined) {
11361166
newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime);
@@ -1143,9 +1173,13 @@ namespace ts {
11431173
return buildErrors(emitDiagnostics, BuildResultFlags.EmitErrors, "Emit");
11441174
}
11451175

1176+
// Update time stamps for rest of the outputs
1177+
newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs);
1178+
11461179
const status: UpToDateStatus = {
11471180
type: UpToDateStatusType.UpToDate,
1148-
newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime
1181+
newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime,
1182+
oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile)
11491183
};
11501184
diagnostics.removeKey(proj);
11511185
projectStatus.setValue(proj, status);
@@ -1175,23 +1209,34 @@ namespace ts {
11751209
if (options.dry) {
11761210
return reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!);
11771211
}
1212+
const priorNewestUpdateTime = updateOutputTimestampsWorker(proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0);
1213+
projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
1214+
}
11781215

1179-
if (options.verbose) {
1180-
reportStatus(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!);
1181-
}
1182-
1183-
const now = new Date();
1216+
function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap<true>) {
11841217
const outputs = getAllProjectOutputs(proj);
1185-
let priorNewestUpdateTime = minimumDate;
1186-
for (const file of outputs) {
1187-
if (isDeclarationFile(file)) {
1188-
priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime);
1218+
if (!skipOutputs || outputs.length !== skipOutputs.getSize()) {
1219+
if (options.verbose) {
1220+
reportStatus(verboseMessage, proj.options.configFilePath!);
11891221
}
1222+
const now = host.now ? host.now() : new Date();
1223+
for (const file of outputs) {
1224+
if (skipOutputs && skipOutputs.hasKey(file)) {
1225+
continue;
1226+
}
1227+
1228+
if (isDeclarationFile(file)) {
1229+
priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime);
1230+
}
11901231

1191-
host.setModifiedTime(file, now);
1232+
host.setModifiedTime(file, now);
1233+
if (proj.options.listEmittedFiles) {
1234+
writeFileName(`TSFILE: ${file}`);
1235+
}
1236+
}
11921237
}
11931238

1194-
projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
1239+
return priorNewestUpdateTime;
11951240
}
11961241

11971242
function getFilesToClean(): string[] {
@@ -1368,6 +1413,20 @@ namespace ts {
13681413
}
13691414
}
13701415

1416+
function getFirstProjectOutput(project: ParsedCommandLine): string {
1417+
if (project.options.outFile || project.options.out) {
1418+
return first(getOutFileOutputs(project));
1419+
}
1420+
1421+
for (const inputFile of project.fileNames) {
1422+
const outputs = getOutputFileNames(inputFile, project);
1423+
if (outputs.length) {
1424+
return first(outputs);
1425+
}
1426+
}
1427+
return Debug.fail(`project ${project.options.configFilePath} expected to have atleast one output`);
1428+
}
1429+
13711430
export function formatUpToDateStatus<T>(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) {
13721431
switch (status.type) {
13731432
case UpToDateStatusType.OutOfDateWithSelf:

Diff for: src/harness/fakes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ namespace fakes {
377377

378378
export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
379379
createProgram = ts.createAbstractBuilder;
380+
now() {
381+
return new Date(this.sys.vfs.time());
382+
}
380383

381384
diagnostics: ts.Diagnostic[] = [];
382385

Diff for: src/testRunner/unittests/tsbuild.ts

+33-26
Original file line numberDiff line numberDiff line change
@@ -234,39 +234,46 @@ namespace ts {
234234
// Update a timestamp in the middle project
235235
tick();
236236
touch(fs, "/src/logic/index.ts");
237+
const originalWriteFile = fs.writeFileSync;
238+
const writtenFiles = createMap<true>();
239+
fs.writeFileSync = (path, data, encoding) => {
240+
writtenFiles.set(path, true);
241+
originalWriteFile.call(fs, path, data, encoding);
242+
};
237243
// Because we haven't reset the build context, the builder should assume there's nothing to do right now
238244
const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic"));
239245
assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date");
246+
verifyInvalidation(/*expectedToWriteTests*/ false);
240247

241248
// Rebuild this project
242-
tick();
243-
builder.invalidateProject("/src/logic");
244-
builder.buildInvalidatedProject();
245-
// The file should be updated
246-
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
247-
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
248-
249-
// Does not build tests or core because there is no change in declaration file
250-
tick();
251-
builder.buildInvalidatedProject();
252-
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt");
253-
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
254-
255-
// Rebuild this project
256-
tick();
257249
fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")}
258250
export class cNew {}`);
259-
builder.invalidateProject("/src/logic");
260-
builder.buildInvalidatedProject();
261-
// The file should be updated
262-
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
263-
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
264-
265-
// Build downstream projects should update 'tests', but not 'core'
266-
tick();
267-
builder.buildInvalidatedProject();
268-
assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt");
269-
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
251+
verifyInvalidation(/*expectedToWriteTests*/ true);
252+
253+
function verifyInvalidation(expectedToWriteTests: boolean) {
254+
// Rebuild this project
255+
tick();
256+
builder.invalidateProject("/src/logic");
257+
builder.buildInvalidatedProject();
258+
// The file should be updated
259+
assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt");
260+
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
261+
assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt");
262+
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
263+
writtenFiles.clear();
264+
265+
// Build downstream projects should update 'tests', but not 'core'
266+
tick();
267+
builder.buildInvalidatedProject();
268+
if (expectedToWriteTests) {
269+
assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt");
270+
}
271+
else {
272+
assert.equal(writtenFiles.size, 0, "Should not write any new files");
273+
}
274+
assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp");
275+
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
276+
}
270277
});
271278
});
272279

0 commit comments

Comments
 (0)