Skip to content

Commit 012f12b

Browse files
committed
To handle cancellation token, remove changed/affected files from the changeset only after getting the result
1 parent ffa64e8 commit 012f12b

4 files changed

Lines changed: 141 additions & 35 deletions

File tree

src/compiler/builder.ts

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace ts {
2222
* This api is only for internal use
2323
*/
2424
/*@internal*/
25-
getFilesAffectedBy(programOfThisState: Program, path: Path): ReadonlyArray<SourceFile>;
25+
getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken): ReadonlyArray<SourceFile>;
2626
}
2727

2828
/**
@@ -86,7 +86,7 @@ namespace ts {
8686
* Get the files affected by the source file.
8787
* This is dependent on whether its a module emit or not and hence function expression
8888
*/
89-
let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string> | undefined) => ReadonlyArray<SourceFile>;
89+
let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) => ReadonlyArray<SourceFile>;
9090

9191
/**
9292
* Cache of semantic diagnostics for files with their Path being the key
@@ -272,38 +272,65 @@ namespace ts {
272272
/**
273273
* Gets the files affected by the path from the program
274274
*/
275-
function getFilesAffectedBy(programOfThisState: Program, path: Path, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
275+
function getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
276+
// Since the operation could be cancelled, the signatures are always stored in the cache
277+
// They will be commited once it is safe to use them
278+
// eg when calling this api from tsserver, if there is no cancellation of the operation
279+
// In the other cases the affected files signatures are commited only after the iteration through the result is complete
280+
const signatureCache = cacheToUpdateSignature || createMap();
276281
const sourceFile = programOfThisState.getSourceFileByPath(path);
277282
if (!sourceFile) {
278283
return emptyArray;
279284
}
280285

281-
if (!updateShapeSignature(programOfThisState, sourceFile, cacheToUpdateSignature)) {
286+
if (!updateShapeSignature(programOfThisState, sourceFile, signatureCache, cancellationToken)) {
282287
return [sourceFile];
283288
}
284289

285-
return getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, cacheToUpdateSignature);
290+
const result = getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, signatureCache, cancellationToken);
291+
if (!cacheToUpdateSignature) {
292+
// Commit all the signatures in the signature cache
293+
updateSignaturesFromCache(signatureCache);
294+
}
295+
return result;
296+
}
297+
298+
/**
299+
* Updates the signatures from the cache
300+
* This should be called whenever it is safe to commit the state of the builder
301+
*/
302+
function updateSignaturesFromCache(signatureCache: Map<string>) {
303+
signatureCache.forEach((signature, path) => {
304+
fileInfos.get(path).signature = signature;
305+
hasCalledUpdateShapeSignature.set(path, true);
306+
});
286307
}
287308

288-
function getNextAffectedFile(programOfThisState: Program): SourceFile | Program | undefined {
309+
/**
310+
* This function returns the next affected file to be processed.
311+
* Note that until doneAffected is called it would keep reporting same result
312+
* This is to allow the callers to be able to actually remove affected file only when the operation is complete
313+
* eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained
314+
*/
315+
function getNextAffectedFile(programOfThisState: Program, cancellationToken: CancellationToken | undefined): SourceFile | Program | undefined {
289316
while (true) {
290317
if (affectedFiles) {
291318
while (affectedFilesIndex < affectedFiles.length) {
292319
const affectedFile = affectedFiles[affectedFilesIndex];
293-
affectedFilesIndex++;
294320
if (!seenAffectedFiles.has(affectedFile.path)) {
295321
// Set the next affected file as seen and remove the cached semantic diagnostics
296-
seenAffectedFiles.set(affectedFile.path, true);
297322
semanticDiagnosticsPerFile.delete(affectedFile.path);
298323
return affectedFile;
299324
}
325+
seenAffectedFiles.set(affectedFile.path, true);
326+
affectedFilesIndex++;
300327
}
301328

302329
// Remove the changed file from the change set
303330
changedFilesSet.delete(currentChangedFilePath);
304331
currentChangedFilePath = undefined;
305332
// Commit the changes in file signature
306-
currentAffectedFilesSignatures.forEach((signature, path) => fileInfos.get(path).signature = signature);
333+
updateSignaturesFromCache(currentAffectedFilesSignatures);
307334
currentAffectedFilesSignatures.clear();
308335
affectedFiles = undefined;
309336
}
@@ -320,21 +347,37 @@ namespace ts {
320347
// so operations are performed directly on program, return program
321348
if (compilerOptions.outFile || compilerOptions.out) {
322349
Debug.assert(semanticDiagnosticsPerFile.size === 0);
323-
changedFilesSet.clear();
324350
return programOfThisState;
325351
}
326352

327353
// Get next batch of affected files
354+
currentAffectedFilesSignatures.clear();
355+
affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, cancellationToken, currentAffectedFilesSignatures);
328356
currentChangedFilePath = nextKey.value as Path;
357+
semanticDiagnosticsPerFile.delete(currentChangedFilePath);
329358
affectedFilesIndex = 0;
330-
affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, currentAffectedFilesSignatures);
359+
}
360+
}
361+
362+
/**
363+
* This is called after completing operation on the next affected file.
364+
* The operations here are postponed to ensure that cancellation during the iteration is handled correctly
365+
*/
366+
function doneWithAffectedFile(programOfThisState: Program, affected: SourceFile | Program) {
367+
if (affected === programOfThisState) {
368+
changedFilesSet.clear();
369+
}
370+
else {
371+
seenAffectedFiles.set((<SourceFile>affected).path, true);
372+
affectedFilesIndex++;
331373
}
332374
}
333375

334376
/**
335377
* Returns the result with affected file
336378
*/
337-
function toAffectedFileResult<T>(result: T, affected: SourceFile | Program): AffectedFileResult<T> {
379+
function toAffectedFileResult<T>(programOfThisState: Program, result: T, affected: SourceFile | Program): AffectedFileResult<T> {
380+
doneWithAffectedFile(programOfThisState, affected);
338381
return { result, affected };
339382
}
340383

@@ -343,14 +386,15 @@ namespace ts {
343386
* Returns undefined when iteration is complete
344387
*/
345388
function emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult<EmitResult> {
346-
const affectedFile = getNextAffectedFile(programOfThisState);
389+
const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken);
347390
if (!affectedFile) {
348391
// Done
349392
return undefined;
350393
}
351394
else if (affectedFile === programOfThisState) {
352395
// When whole program is affected, do emit only once (eg when --out or --outFile is specified)
353396
return toAffectedFileResult(
397+
programOfThisState,
354398
programOfThisState.emit(/*targetSourceFile*/ undefined, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers),
355399
programOfThisState
356400
);
@@ -359,6 +403,7 @@ namespace ts {
359403
// Emit the affected file
360404
const targetSourceFile = affectedFile as SourceFile;
361405
return toAffectedFileResult(
406+
programOfThisState,
362407
programOfThisState.emit(targetSourceFile, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers),
363408
targetSourceFile
364409
);
@@ -370,14 +415,15 @@ namespace ts {
370415
*/
371416
function getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult<ReadonlyArray<Diagnostic>> {
372417
while (true) {
373-
const affectedFile = getNextAffectedFile(programOfThisState);
418+
const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken);
374419
if (!affectedFile) {
375420
// Done
376421
return undefined;
377422
}
378423
else if (affectedFile === programOfThisState) {
379424
// When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified)
380425
return toAffectedFileResult(
426+
programOfThisState,
381427
programOfThisState.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken),
382428
programOfThisState
383429
);
@@ -387,10 +433,12 @@ namespace ts {
387433
const targetSourceFile = affectedFile as SourceFile;
388434
if (ignoreSourceFile && ignoreSourceFile(targetSourceFile)) {
389435
// Get next affected file
436+
doneWithAffectedFile(programOfThisState, targetSourceFile);
390437
continue;
391438
}
392439

393440
return toAffectedFileResult(
441+
programOfThisState,
394442
getSemanticDiagnosticsOfFile(programOfThisState, targetSourceFile, cancellationToken),
395443
targetSourceFile
396444
);
@@ -505,46 +553,34 @@ namespace ts {
505553
* Returns if the shape of the signature has changed since last emit
506554
* Note that it also updates the current signature as the latest signature for the file
507555
*/
508-
function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string> | undefined) {
556+
function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) {
509557
Debug.assert(!!sourceFile);
510558

511559
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
512-
if (hasCalledUpdateShapeSignature.has(sourceFile.path)) {
560+
if (hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) {
513561
return false;
514562
}
515563

516-
Debug.assert(!cacheToUpdateSignature || !cacheToUpdateSignature.has(sourceFile.path));
517-
hasCalledUpdateShapeSignature.set(sourceFile.path, true);
518564
const info = fileInfos.get(sourceFile.path);
519565
Debug.assert(!!info);
520566

521567
const prevSignature = info.signature;
522568
let latestSignature: string;
523569
if (sourceFile.isDeclarationFile) {
524570
latestSignature = sourceFile.version;
525-
setLatestSigature();
526571
}
527572
else {
528-
const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true);
573+
const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken);
529574
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
530575
latestSignature = options.computeHash(emitOutput.outputFiles[0].text);
531-
setLatestSigature();
532576
}
533577
else {
534578
latestSignature = prevSignature;
535579
}
536580
}
581+
cacheToUpdateSignature.set(sourceFile.path, latestSignature);
537582

538583
return !prevSignature || latestSignature !== prevSignature;
539-
540-
function setLatestSigature() {
541-
if (cacheToUpdateSignature) {
542-
cacheToUpdateSignature.set(sourceFile.path, latestSignature);
543-
}
544-
else {
545-
info.signature = latestSignature;
546-
}
547-
}
548584
}
549585

550586
/**
@@ -652,7 +688,7 @@ namespace ts {
652688
/**
653689
* When program emits modular code, gets the files affected by the sourceFile whose shape has changed
654690
*/
655-
function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string> | undefined) {
691+
function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined) {
656692
if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) {
657693
return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape);
658694
}
@@ -675,7 +711,7 @@ namespace ts {
675711
if (!seenFileNamesMap.has(currentPath)) {
676712
const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath);
677713
seenFileNamesMap.set(currentPath, currentSourceFile);
678-
if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature)) {
714+
if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken)) {
679715
queue.push(...getReferencedByPaths(currentPath));
680716
}
681717
}

src/compiler/program.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,7 @@ namespace ts {
11621162
// This is because in the -out scenario all files need to be emitted, and therefore all
11631163
// files need to be type checked. And the way to specify that all files need to be type
11641164
// checked is to not pass the file to getEmitResolver.
1165-
const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile);
1165+
const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile, cancellationToken);
11661166

11671167
performance.mark("beforeEmit");
11681168

src/harness/unittests/builder.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,39 @@ namespace ts {
4141
program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }");
4242
assertChanges(["/b.js", "/a.js"]);
4343
});
44+
45+
it("keeps the file in affected files if cancellation token throws during the operation", () => {
46+
const files: NamedSourceText[] = [
47+
{ name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") },
48+
{ name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") },
49+
{ name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") },
50+
{ name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") },
51+
{ name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") },
52+
];
53+
54+
let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {});
55+
const assertChanges = makeAssertChangesWithCancellationToken(() => program);
56+
// No cancellation
57+
assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]);
58+
59+
// cancel when emitting a.ts
60+
program = updateProgramFile(program, "/a.ts", "export function foo() { }");
61+
assertChanges(["/a.js"], 0);
62+
// Change d.ts and verify previously pending a.ts is emitted as well
63+
program = updateProgramFile(program, "/d.ts", "export function bar() { }");
64+
assertChanges(["/a.js", "/d.js"]);
65+
66+
// Cancel when emitting b.js
67+
program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }");
68+
program = updateProgramFile(program, "/d.ts", "export function bar2() { }");
69+
assertChanges(["/d.js", "/b.js", "/a.js"], 1);
70+
// Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts
71+
program = updateProgramFile(program, "/e.ts", "export function bar3() { }");
72+
assertChanges(["/b.js", "/a.js", "/e.js"]);
73+
});
4474
});
4575

46-
function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray<string>) => void {
76+
function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray<string>) => void {
4777
const builder = createEmitAndSemanticDiagnosticsBuilder({
4878
getCanonicalFileName: identity,
4979
computeHash: identity
@@ -59,6 +89,46 @@ namespace ts {
5989
};
6090
}
6191

92+
function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: ReadonlyArray<string>, cancelAfterEmitLength?: number) => void {
93+
const builder = createEmitAndSemanticDiagnosticsBuilder({
94+
getCanonicalFileName: identity,
95+
computeHash: identity
96+
});
97+
let cancel = false;
98+
const cancellationToken: CancellationToken = {
99+
isCancellationRequested: () => cancel,
100+
throwIfCancellationRequested: () => {
101+
if (cancel) {
102+
throw new OperationCanceledException();
103+
}
104+
},
105+
};
106+
return (fileNames, cancelAfterEmitLength?: number) => {
107+
cancel = false;
108+
let operationWasCancelled = false;
109+
const program = getProgram();
110+
builder.updateProgram(program);
111+
const outputFileNames: string[] = [];
112+
try {
113+
// tslint:disable-next-line no-empty
114+
do {
115+
assert.isFalse(cancel);
116+
if (outputFileNames.length === cancelAfterEmitLength) {
117+
cancel = true;
118+
}
119+
} while (builder.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName), cancellationToken));
120+
}
121+
catch (e) {
122+
assert.isFalse(operationWasCancelled);
123+
assert.isTrue(e instanceof OperationCanceledException, e.toString());
124+
operationWasCancelled = true;
125+
}
126+
assert.equal(cancel, operationWasCancelled);
127+
assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength);
128+
assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength));
129+
};
130+
}
131+
62132
function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts {
63133
return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => {
64134
updateProgramText(files, fileName, fileContent);

0 commit comments

Comments
 (0)