added Program.structureIsReused property, disallow reuse if target module

kind differs in old and new programs, move setting of resolvedModules
cache to the program, added tests
This commit is contained in:
Vladimir Matveev
2015-06-23 21:06:57 -07:00
parent 7a7d775f81
commit ba3eb0d0cf
5 changed files with 301 additions and 14 deletions

View File

@@ -138,7 +138,8 @@ var harnessSources = [
"services/patternMatcher.ts",
"versionCache.ts",
"convertToBase64.ts",
"transpile.ts"
"transpile.ts",
"reuseProgramStructure.ts"
].map(function (f) {
return path.join(unittestsDirectory, f);
})).concat([

View File

@@ -5634,8 +5634,6 @@ namespace ts {
// will immediately bail out of walking any subtrees when we can see that their parents
// are already correct.
let result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /* setParentNode */ true)
// pass set of modules that were resolved before so 'createProgram' can reuse previous resolution results
result.resolvedModules = sourceFile.resolvedModules;
return result;
}

View File

@@ -163,9 +163,13 @@ namespace ts {
let filesByName = createFileMap<SourceFile>(fileName => host.getCanonicalFileName(fileName));
let structureIsReused = oldProgram && host.hasChanges && tryReuseStructureFromOldProgram();
if (!structureIsReused) {
// if old program was provided by has different target module kind - assume that it cannot be reused
// different module kind can lead to different way of resolving modules
if (oldProgram && oldProgram.getCompilerOptions().module !== options.module) {
oldProgram = undefined;
}
if (!tryReuseStructureFromOldProgram()) {
forEach(rootNames, name => processRootFile(name, false));
// Do not process the default library if:
// - The '--noLib' flag is used.
@@ -218,9 +222,15 @@ namespace ts {
}
function tryReuseStructureFromOldProgram(): boolean {
if (!host.hasChanges) {
// host does not support method 'hasChanges'
return false;
}
if (!oldProgram) {
return false;
}
Debug.assert(!oldProgram.structureIsReused);
// there is an old program, check if we can reuse its structure
let oldRootNames = oldProgram.getRootFileNames();
@@ -258,6 +268,8 @@ namespace ts {
// imports has changed
return false;
}
// pass the cache of module resolutions from the old source file
newSourceFile.resolvedModules = oldSourceFile.resolvedModules;
}
else {
// file has no changes - use it as is
@@ -275,6 +287,8 @@ namespace ts {
files = newSourceFiles;
oldProgram.structureIsReused = true;
return true;
}
@@ -576,14 +590,26 @@ namespace ts {
function processImportedModules(file: SourceFile, basePath: string) {
collectExternalModuleReferences(file);
if (file.imports.length) {
let allImportsInTheCache = true;
// check that all imports are contained in resolved modules cache
// if at least one of imports in not in the cache - cache needs to be reinitialized
for (let moduleName of file.imports) {
if (!hasResolvedModuleName(file, moduleName)) {
allImportsInTheCache = false;
break;
if (file.imports.length) {
let allImportsInTheCache = false;
// try to grab existing module resolutions from the old source file
let oldSourceFile: SourceFile = oldProgram && oldProgram.getSourceFile(file.fileName);
if (oldSourceFile) {
file.resolvedModules = oldSourceFile.resolvedModules;
// check that all imports are contained in resolved modules cache
// if at least one of imports in not in the cache - cache needs to be reinitialized
checkImports: {
if (file.resolvedModules) {
for (let moduleName of file.imports) {
if (!hasResolvedModuleName(file, moduleName)) {
break checkImports;
}
}
allImportsInTheCache = true;
}
}
}

View File

@@ -1244,6 +1244,7 @@ namespace ts {
/* @internal */ getIdentifierCount(): number;
/* @internal */ getSymbolCount(): number;
/* @internal */ getTypeCount(): number;
/* @internal */ structureIsReused?: boolean;
}
export interface SourceMapSpan {

View File

@@ -0,0 +1,261 @@
/// <reference path="..\..\..\src\harness\external\mocha.d.ts" />
/// <reference path='..\..\..\src\harness\harness.ts' />
/// <reference path="..\..\..\src\harness\harnessLanguageService.ts" />
module ts {
const enum ChangedPart {
references = 1 << 0,
importsAndExports = 1 << 1,
program = 1 << 2
}
let newLine = "\r\n";
interface SourceFileWithText extends SourceFile {
sourceText?: SourceText;
}
interface NamedSourceText {
name: string;
text: SourceText
}
interface ProgramWithSourceTexts extends Program {
sourceTexts?: NamedSourceText[];
}
class SourceText implements IScriptSnapshot {
private fullText: string;
constructor(private references: string,
private importsAndExports: string,
private program: string,
private changedPart: ChangedPart = 0,
private version = 0) {
}
static New(references: string, importsAndExports: string, program: string): SourceText {
Debug.assert(references !== undefined);
Debug.assert(importsAndExports !== undefined);
Debug.assert(program !== undefined);
return new SourceText(references + newLine, importsAndExports + newLine, program || "");
}
public getVersion(): number {
return this.version;
}
public updateReferences(newReferences: string): SourceText {
Debug.assert(newReferences !== undefined);
return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1);
}
public updateImportsAndExports(newImportsAndExports: string): SourceText {
Debug.assert(newImportsAndExports !== undefined);
return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1);
}
public updateProgram(newProgram: string): SourceText {
Debug.assert(newProgram !== undefined);
return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1);
}
public getFullText() {
return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program);
}
public getText(start: number, end: number): string {
return this.getFullText().substring(start, end);
}
getLength(): number {
return this.getFullText().length;
}
getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange {
var oldText = <SourceText>oldSnapshot;
var oldSpan: TextSpan;
var newLength: number;
switch(oldText.changedPart ^ this.changedPart){
case ChangedPart.references:
oldSpan = createTextSpan(0, oldText.references.length);
newLength = this.references.length;
break;
case ChangedPart.importsAndExports:
oldSpan = createTextSpan(oldText.references.length, oldText.importsAndExports.length);
newLength = this.importsAndExports.length
break;
case ChangedPart.program:
oldSpan = createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length);
newLength = this.program.length;
break;
default:
Debug.assert(false, "Unexpected change");
}
return createTextChangeRange(oldSpan, newLength);
}
}
function createTestCompilerHost(texts: NamedSourceText[], target: ScriptTarget): CompilerHost {
let files: Map<SourceFileWithText> = {};
for (let t of texts) {
let file = <SourceFileWithText>createSourceFile(t.name, t.text.getFullText(), target);
file.sourceText = t.text;
files[t.name] = file;
}
return {
getSourceFile(fileName): SourceFile {
return files[fileName];
},
getDefaultLibFileName(): string {
return "lib.d.ts"
},
writeFile(file, text) {
throw new Error("NYI");
},
getCurrentDirectory(): string {
return "";
},
getCanonicalFileName(fileName): string {
return sys.useCaseSensitiveFileNames ? fileName: fileName.toLowerCase();
},
useCaseSensitiveFileNames(): boolean {
return sys.useCaseSensitiveFileNames;
},
getNewLine() : string {
return sys.newLine;
},
hasChanges(oldFile: SourceFileWithText): boolean {
let current = files[oldFile.fileName];
return !current || oldFile.sourceText.getVersion() !== current.sourceText.getVersion();
}
}
}
function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): Program {
var host = createTestCompilerHost(texts, options.target);
let program = <ProgramWithSourceTexts>createProgram(rootNames, options, host);
program.sourceTexts = texts;
return program;
}
function updateProgram(oldProgram: Program, rootNames: string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void) {
var texts: NamedSourceText[] = (<ProgramWithSourceTexts>oldProgram).sourceTexts.slice(0);
updater(texts);
var host = createTestCompilerHost(texts, options.target);
var program = <ProgramWithSourceTexts>createProgram(rootNames, options, host, oldProgram);
program.sourceTexts = texts;
return program;
}
function getSizeOfMap(map: Map<any>): number {
let size = 0;
for (let id in map) {
if (hasProperty(map, id)) {
size++;
}
}
return size;
}
function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: Map<string>): void {
let file = program.getSourceFile(fileName);
assert.isTrue(file !==undefined, `cannot find file ${fileName}`);
if (expectedContent === undefined) {
assert.isTrue(file.resolvedModules === undefined, "expected resolvedModules to be undefined");
}
else {
assert.isTrue(file.resolvedModules !== undefined, "expected resolvedModuled to be set");
let actualCacheSize = getSizeOfMap(file.resolvedModules);
let expectedSize = getSizeOfMap(expectedContent);
assert.isTrue(actualCacheSize === expectedSize, `expected actual size: ${actualCacheSize} to be equal to ${expectedSize}`);
for (let id in expectedContent) {
if (hasProperty(expectedContent, id)) {
assert.isTrue(hasProperty(file.resolvedModules, id), `expected ${id} to be found in resolved modules`);
assert.isTrue(expectedContent[id] === file.resolvedModules[id], `expected '${expectedContent[id]}' to be equal to '${file.resolvedModules[id]}'`);
}
}
}
}
describe("Reuse program structure", () => {
let target = ScriptTarget.Latest;
let files = [
{name: "a.ts", text: SourceText.New(`/// <reference path='b.ts'/>`, "", `var x = 1`)},
{name: "b.ts", text: SourceText.New(`/// <reference path='c.ts'/>`, "", `var y = 2`)},
{name: "c.ts", text: SourceText.New("", "", `var z = 1;`)},
]
it("successful if change does not affect imports", () => {
var program_1 = newProgram(files, ["a.ts"], {target});
var program_2 = updateProgram(program_1, ["a.ts"], {target}, files => {
files[0].text = files[0].text.updateProgram("var x = 100");
});
assert.isTrue(program_1.structureIsReused);
});
it("fails if change affects tripleslash references", () => {
var program_1 = newProgram(files, ["a.ts"], {target});
var program_2 = updateProgram(program_1, ["a.ts"], {target}, files => {
let newReferences = `/// <reference path='b.ts'/>
/// <reference path='c.ts'/>
`;
files[0].text = files[0].text.updateReferences(newReferences);
});
assert.isTrue(!program_1.structureIsReused);
});
it("fails if change affects imports", () => {
var program_1 = newProgram(files, ["a.ts"], {target});
var program_2 = updateProgram(program_1, ["a.ts"], {target}, files => {
files[2].text = files[2].text.updateImportsAndExports("import x from 'b'");
});
assert.isTrue(!program_1.structureIsReused);
});
it("fails if module kind changes", () => {
var program_1 = newProgram(files, ["a.ts"], {target, module: ModuleKind.CommonJS});
var program_2 = updateProgram(program_1, ["a.ts"], {target, module: ModuleKind.AMD}, files => void 0);
assert.isTrue(!program_1.structureIsReused);
});
it("resolution cache follows imports", () => {
let files = [
{ name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") },
{ name: "b.ts", text: SourceText.New("", "", "var y = 2") },
];
var options: CompilerOptions = {target};
var program_1 = newProgram(files, ["a.ts"], options);
checkResolvedModulesCache(program_1, "a.ts", { "b": "b.ts" });
checkResolvedModulesCache(program_1, "b.ts", undefined);
var program_2 = updateProgram(program_1, ["a.ts"], options, files => {
files[0].text = files[0].text.updateProgram("var x = 2");
});
assert.isTrue(program_1.structureIsReused);
// content of resolution cache should not change
checkResolvedModulesCache(program_1, "a.ts", { "b": "b.ts" });
checkResolvedModulesCache(program_1, "b.ts", undefined);
// imports has changed - program is not reused
var program_3 = updateProgram(program_2, ["a.ts"], options, files => {
files[0].text = files[0].text.updateImportsAndExports("");
});
assert.isTrue(!program_2.structureIsReused);
checkResolvedModulesCache(program_3, "a.ts", undefined);
var program_4 = updateProgram(program_3, ["a.ts"], options, files => {
let newImports = `import x from 'b'
import y from 'c'
`;
files[0].text = files[0].text.updateImportsAndExports(newImports);
});
assert.isTrue(!program_3.structureIsReused);
checkResolvedModulesCache(program_4, "a.ts", {"b": "b.ts", "c": undefined});
});
})
}