Skip to content

Commit

Permalink
Update the timestamps of outputs that dont need to be written because…
Browse files Browse the repository at this point in the history
… of incremental build

This ensures that after `tsbuild` after incremental build of `tsbuild -w` doesnt result in unnecessary rebuilds
  • Loading branch information
sheetalkamat committed Dec 21, 2018
1 parent f1949bb commit 7b290fd
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 118 deletions.
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3945,6 +3945,10 @@
"category": "Error",
"code": 6370
},
"Updating unchanged output timestamps of project '{0}'...": {
"category": "Message",
"code": 6371
},

"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",
Expand Down
97 changes: 78 additions & 19 deletions src/compiler/tsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ namespace ts {
newestDeclarationFileContentChangedTime?: Date;
newestOutputFileTime?: Date;
newestOutputFileName?: string;
oldestOutputFileName?: string;
oldestOutputFileName: string;
}

/**
Expand Down Expand Up @@ -332,6 +332,9 @@ namespace ts {
// TODO: To do better with watch mode and normal build mode api that creates program and emits files
// This currently helps enable --diagnostics and --extendedDiagnostics
afterProgramEmitAndDiagnostics?(program: T): void;

// For testing
now?(): Date;
}

export interface SolutionBuilderHost<T extends BuilderProgram> extends SolutionBuilderHostBase<T> {
Expand Down Expand Up @@ -991,16 +994,40 @@ namespace ts {
return;
}

if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) {
// Fake build
updateOutputTimestamps(proj);
return;
}

const buildResult = buildSingleProject(resolved);
const dependencyGraph = getGlobalDependencyGraph();
const referencingProjects = dependencyGraph.referencingProjectsMap.getValue(resolved);
if (buildResult & BuildResultFlags.AnyErrors) return;

const { referencingProjectsMap, buildQueue } = getGlobalDependencyGraph();
const referencingProjects = referencingProjectsMap.getValue(resolved);
if (!referencingProjects) return;

// Always use build order to queue projects
for (const project of dependencyGraph.buildQueue) {
for (let index = buildQueue.indexOf(resolved) + 1; index < buildQueue.length; index++) {
const project = buildQueue[index];
const prepend = referencingProjects.getValue(project);
// If the project is referenced with prepend, always build downstream projectm,
// otherwise queue it only if declaration output changed
if (prepend || (prepend !== undefined && !(buildResult & BuildResultFlags.DeclarationOutputUnchanged))) {
if (prepend !== undefined) {
// If the project is referenced with prepend, always build downstream project,
// If declaration output is changed changed, build the project
// otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps
const status = projectStatus.getValue(project);
if (prepend || !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) {
if (status && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes)) {
projectStatus.setValue(project, {
type: UpToDateStatusType.OutOfDateWithUpstream,
outOfDateOutputFileName: status.oldestOutputFileName,
newerProjectName: resolved
});
}
}
else if (status && status.type === UpToDateStatusType.UpToDate) {
status.type = UpToDateStatusType.UpToDateWithUpstreamTypes;
}
addProjToQueue(project);
}
}
Expand Down Expand Up @@ -1110,6 +1137,7 @@ namespace ts {
let declDiagnostics: Diagnostic[] | undefined;
const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d);
const outputFiles: OutputFile[] = [];
// TODO:: handle declaration diagnostics in incremental build.
emitFilesAndReportErrors(program, reportDeclarationDiagnostics, writeFileName, /*reportSummary*/ undefined, (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }));
// Don't emit .d.ts if there are decl file errors
if (declDiagnostics) {
Expand All @@ -1118,6 +1146,7 @@ namespace ts {

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

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

// Update time stamps for rest of the outputs
newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs);

const status: UpToDateStatus = {
type: UpToDateStatusType.UpToDate,
newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime
newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime,
oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile)
};
diagnostics.removeKey(proj);
projectStatus.setValue(proj, status);
Expand Down Expand Up @@ -1175,23 +1209,34 @@ namespace ts {
if (options.dry) {
return reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!);
}
const priorNewestUpdateTime = updateOutputTimestampsWorker(proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0);
projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
}

if (options.verbose) {
reportStatus(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!);
}

const now = new Date();
function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap<true>) {
const outputs = getAllProjectOutputs(proj);
let priorNewestUpdateTime = minimumDate;
for (const file of outputs) {
if (isDeclarationFile(file)) {
priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime);
if (!skipOutputs || outputs.length !== skipOutputs.getSize()) {
if (options.verbose) {
reportStatus(verboseMessage, proj.options.configFilePath!);
}
const now = host.now ? host.now() : new Date();
for (const file of outputs) {
if (skipOutputs && skipOutputs.hasKey(file)) {
continue;
}

if (isDeclarationFile(file)) {
priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime);
}

host.setModifiedTime(file, now);
host.setModifiedTime(file, now);
if (proj.options.listEmittedFiles) {
writeFileName(`TSFILE: ${file}`);
}
}
}

projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus);
return priorNewestUpdateTime;
}

function getFilesToClean(): string[] {
Expand Down Expand Up @@ -1368,6 +1413,20 @@ namespace ts {
}
}

function getFirstProjectOutput(project: ParsedCommandLine): string {
if (project.options.outFile || project.options.out) {
return first(getOutFileOutputs(project));
}

for (const inputFile of project.fileNames) {
const outputs = getOutputFileNames(inputFile, project);
if (outputs.length) {
return first(outputs);
}
}
return Debug.fail(`project ${project.options.configFilePath} expected to have atleast one output`);
}

export function formatUpToDateStatus<T>(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) {
switch (status.type) {
case UpToDateStatusType.OutOfDateWithSelf:
Expand Down
3 changes: 3 additions & 0 deletions src/harness/fakes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ namespace fakes {

export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
createProgram = ts.createAbstractBuilder;
now() {
return new Date(this.sys.vfs.time());
}

diagnostics: ts.Diagnostic[] = [];

Expand Down
59 changes: 33 additions & 26 deletions src/testRunner/unittests/tsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,39 +234,46 @@ namespace ts {
// Update a timestamp in the middle project
tick();
touch(fs, "/src/logic/index.ts");
const originalWriteFile = fs.writeFileSync;
const writtenFiles = createMap<true>();
fs.writeFileSync = (path, data, encoding) => {
writtenFiles.set(path, true);
originalWriteFile.call(fs, path, data, encoding);
};
// Because we haven't reset the build context, the builder should assume there's nothing to do right now
const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic"));
assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date");
verifyInvalidation(/*expectedToWriteTests*/ false);

// Rebuild this project
tick();
builder.invalidateProject("/src/logic");
builder.buildInvalidatedProject();
// The file should be updated
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");

// Does not build tests or core because there is no change in declaration file
tick();
builder.buildInvalidatedProject();
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");

// Rebuild this project
tick();
fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")}
export class cNew {}`);
builder.invalidateProject("/src/logic");
builder.buildInvalidatedProject();
// The file should be updated
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");

// Build downstream projects should update 'tests', but not 'core'
tick();
builder.buildInvalidatedProject();
assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt");
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
verifyInvalidation(/*expectedToWriteTests*/ true);

function verifyInvalidation(expectedToWriteTests: boolean) {
// Rebuild this project
tick();
builder.invalidateProject("/src/logic");
builder.buildInvalidatedProject();
// The file should be updated
assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt");
assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt");
assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt");
assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt");
writtenFiles.clear();

// Build downstream projects should update 'tests', but not 'core'
tick();
builder.buildInvalidatedProject();
if (expectedToWriteTests) {
assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt");
}
else {
assert.equal(writtenFiles.size, 0, "Should not write any new files");
}
assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp");
assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt");
}
});
});

Expand Down
Loading

0 comments on commit 7b290fd

Please sign in to comment.