From 89267bcd6fb967a6273a41ad51fbbc7445d6df24 Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Sat, 14 Feb 2015 19:53:12 -0800 Subject: [PATCH] Move fileWatching logic to the server to allow for testing on non-node systems --- src/compiler/sys.ts | 9 -- src/harness/harnessLanguageService.ts | 30 +---- src/server/editorServices.ts | 179 +++++++------------------- src/server/server.ts | 119 +++++++++++++++++ 4 files changed, 169 insertions(+), 168 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c666ca991cf..5f10a747d42 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -18,8 +18,6 @@ module ts { readDirectory(path: string, extension?: string): string[]; getMemoryUsage? (): number; exit(exitCode?: number): void; - getModififedTime? (fileName: string): Date; - stat? (fileName: string, callback?: (err: any, stats: any) => any): void; } export interface FileWatcher { @@ -305,13 +303,6 @@ module ts { }, exit(exitCode?: number): void { process.exit(exitCode); - }, - getModififedTime(fileName: string): Date { - var stats = _fs.statSync(fileName); - return stats.mtime; - }, - stat(fileName: string, callback?: (err: any, stats: any) => any) { - _fs.stat(fileName, callback); } }; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 9f0062cfd71..712f0afe0aa 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -488,6 +488,10 @@ module Harness.LanguageService { } readFile(fileName: string): string { + if (fileName.indexOf(Harness.Compiler.defaultLibFileName) >= 0) { + fileName = Harness.Compiler.defaultLibFileName; + } + var snapshot = this.host.getScriptSnapshot(fileName); return snapshot && snapshot.getText(0, snapshot.getLength()); } @@ -525,29 +529,9 @@ module Harness.LanguageService { readDirectory(path: string, extension?: string): string[] { throw new Error("Not implemented Yet."); } - - getModififedTime(fileName: string): Date { - return new Date(); - } - - stat(path: string, callback?: (err: any, stats: any) => any) { - return 0; - } - - lineColToPosition(fileName: string, line: number, col: number): number { - return this.host.lineColToPosition(fileName, line, col); - } - - positionToZeroBasedLineCol(fileName: string, position: number): ts.LineAndCharacter { - return this.host.positionToZeroBasedLineCol(fileName, position); - } - - getFileLength(fileName: string): number { - return this.host.getScriptSnapshot(fileName).getLength(); - } - - getFileNames(): string[] { - return this.host.getScriptFileNames(); + + watchFile(fileName: string, callback: (fileName: string) => void): ts.FileWatcher { + return { close() { } }; } close(): void { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 85ee71adce5..9e6363b509f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -62,18 +62,15 @@ module ts.server { children: ScriptInfo[] = []; // files referenced by this file defaultProject: Project; // project to use by default for file - mtime: Date; - constructor(private host: ServerHost, public filename: string, public content: string, public isOpen = false) { + fileWatcher: FileWatcher; + + constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { this.svc = ScriptVersionCache.fromString(content); - if (!isOpen) { - this.mtime = this.host.getModififedTime(filename); - } } close() { this.isOpen = false; - this.mtime = this.host.getModififedTime(this.filename); } addChild(childInfo: ScriptInfo) { @@ -203,7 +200,7 @@ module ts.server { removeReferencedFile(info: ScriptInfo) { if (!info.isOpen) { - this.filenameToScript[info.filename] = undefined; + this.filenameToScript[info.fileName] = undefined; } } @@ -212,7 +209,7 @@ module ts.server { if (!scriptInfo) { scriptInfo = this.project.openReferencedFile(filename); if (scriptInfo) { - this.filenameToScript[scriptInfo.filename] = scriptInfo; + this.filenameToScript[scriptInfo.fileName] = scriptInfo; } } else { @@ -221,9 +218,9 @@ module ts.server { } addRoot(info: ScriptInfo) { - var scriptInfo = ts.lookUp(this.filenameToScript, info.filename); + var scriptInfo = ts.lookUp(this.filenameToScript, info.fileName); if (!scriptInfo) { - this.filenameToScript[info.filename] = info; + this.filenameToScript[info.fileName] = info; return info; } } @@ -363,7 +360,7 @@ module ts.server { } getSourceFile(info: ScriptInfo) { - return this.filenameToSourceFile[info.filename]; + return this.filenameToSourceFile[info.fileName]; } getSourceFileFromName(filename: string) { @@ -439,112 +436,6 @@ module ts.server { return copiedList; } - // REVIEW: for now this implementation uses polling. - // The advantage of polling is that it works reliably - // on all os and with network mounted files. - // For 90 referenced files, the average time to detect - // changes is 2*msInterval (by default 5 seconds). - // The overhead of this is .04 percent (1/2500) with - // average pause of < 1 millisecond (and max - // pause less than 1.5 milliseconds); question is - // do we anticipate reference sets in the 100s and - // do we care about waiting 10-20 seconds to detect - // changes for large reference sets? If so, do we want - // to increase the chunk size or decrease the interval - // time dynamically to match the large reference set? - export class WatchedFileSet { - watchedFiles: ScriptInfo[] = []; - nextFileToCheck = 0; - watchTimer: NodeJS.Timer; - - // average async stat takes about 30 microseconds - // set chunk size to do 30 files in < 1 millisecond - constructor(private host: ServerHost, public fileEvent: (info: ScriptInfo, eventName: string) => void, - public msInterval = 2500, public chunkSize = 30) { - } - - checkWatchedFileChanged(checkedIndex: number, stats: NodeJS.fs.Stats) { - var info = this.watchedFiles[checkedIndex]; - if (info && (!info.isOpen)) { - if (info.mtime.getTime() != stats.mtime.getTime()) { - info.svc.reloadFromFile(info.filename); - } - } - } - - fileDeleted(info: ScriptInfo) { - if (this.fileEvent) { - this.fileEvent(info, "deleted"); - } - } - - static fileDeleted = 34; - - poll(checkedIndex: number) { - var watchedFile = this.watchedFiles[checkedIndex]; - if (!watchedFile) { - return; - } - if (measurePerf) { - var start = process.hrtime(); - } - this.host.stat(watchedFile.filename,(err, stats) => { - if (err) { - var msg = err.message; - if (err.errno) { - msg += " errno: " + err.errno.toString(); - } - if (err.errno == WatchedFileSet.fileDeleted) { - this.fileDeleted(watchedFile); - } - } - else { - this.checkWatchedFileChanged(checkedIndex, stats); - } - }); - if (measurePerf) { - var elapsed = process.hrtime(start); - var elapsedNano = 1e9 * elapsed[0] + elapsed[1]; - } - } - - // this implementation uses polling and - // stat due to inconsistencies of fs.watch - // and efficiency of stat on modern filesystems - startWatchTimer() { - this.watchTimer = setInterval(() => { - var count = 0; - var nextToCheck = this.nextFileToCheck; - var firstCheck = -1; - while ((count < this.chunkSize) && (nextToCheck != firstCheck)) { - this.poll(nextToCheck); - if (firstCheck < 0) { - firstCheck = nextToCheck; - } - nextToCheck++; - if (nextToCheck == this.watchedFiles.length) { - nextToCheck = 0; - } - count++; - } - this.nextFileToCheck = nextToCheck; - }, this.msInterval); - } - - // TODO: remove watch file if opened by editor or no longer referenced - // assume normalized and absolute pathname - addFile(info: ScriptInfo) { - this.watchedFiles.push(info); - if (this.watchedFiles.length == 1) { - this.startWatchTimer(); - } - } - - removeFile(info: ScriptInfo) { - this.watchedFiles = copyListRemovingItem(info, this.watchedFiles); - } - } - interface ProjectServiceEventHandler { (eventName: string, project: Project): void; } @@ -556,20 +447,32 @@ module ts.server { openFilesReferenced: ScriptInfo[] = []; // projects covering open files inferredProjects: Project[] = []; - watchedFileSet: WatchedFileSet; constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { if (measurePerf) { calibrateTimer(); } ts.disableIncrementalParsing = true; - this.watchedFileSet = new WatchedFileSet(this.host,(info, eventName) => { - if (eventName == "deleted") { - this.fileDeletedInFilesystem(info); - } - }); } + watchedFileChanged(fileName: string) { + var info = this.filenameToScriptInfo[fileName]; + if (!info) { + this.psLogger.info("Error: got watch notification for unknown file: " + fileName); + } + + if (!this.host.fileExists(fileName)) { + // File was deleted + this.fileDeletedInFilesystem(info); + } + else { + if (info && (!info.isOpen)) { + info.svc.reloadFromFile(info.fileName); + } + } + } + + log(msg: string, type = "Err") { this.psLogger.msg(msg, type); } @@ -587,11 +490,15 @@ module ts.server { } fileDeletedInFilesystem(info: ScriptInfo) { - this.psLogger.info(info.filename + " deleted"); - this.watchedFileSet.removeFile(info); + this.psLogger.info(info.fileName + " deleted"); + + if (info.fileWatcher) { + info.fileWatcher.close(); + info.fileWatcher = undefined; + } if (!info.isOpen) { - this.filenameToScriptInfo[info.filename] = undefined; + this.filenameToScriptInfo[info.fileName] = undefined; var referencingProjects = this.findReferencingProjects(info); for (var i = 0, len = referencingProjects.length; i < len; i++) { referencingProjects[i].removeReferencedFile(info); @@ -697,13 +604,13 @@ module ts.server { /** * @param filename is absolute pathname */ - openFile(filename: string, openedByClient = false) { - filename = ts.normalizePath(filename); - var info = ts.lookUp(this.filenameToScriptInfo, filename); + openFile(fileName: string, openedByClient = false) { + fileName = ts.normalizePath(fileName); + var info = ts.lookUp(this.filenameToScriptInfo, fileName); if (!info) { var content: string; - if (this.host.fileExists(filename)) { - content = this.host.readFile(filename); + if (this.host.fileExists(fileName)) { + content = this.host.readFile(fileName); } if (!content) { if (openedByClient) { @@ -711,10 +618,10 @@ module ts.server { } } if (content !== undefined) { - info = new ScriptInfo(this.host, filename, content, openedByClient); - this.filenameToScriptInfo[filename] = info; + info = new ScriptInfo(this.host, fileName, content, openedByClient); + this.filenameToScriptInfo[fileName] = info; if (!info.isOpen) { - this.watchedFileSet.addFile(info); + info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); }); } } } @@ -800,11 +707,11 @@ module ts.server { } this.psLogger.info("Open file roots: ") for (var i = 0, len = this.openFileRoots.length; i < len; i++) { - this.psLogger.info(this.openFileRoots[i].filename); + this.psLogger.info(this.openFileRoots[i].fileName); } this.psLogger.info("Open files referenced: ") for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { - this.psLogger.info(this.openFilesReferenced[i].filename); + this.psLogger.info(this.openFilesReferenced[i].fileName); } this.psLogger.endGroup(); } diff --git a/src/server/server.ts b/src/server/server.ts index 974fb491674..6d210ff0d13 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -72,6 +72,102 @@ module ts.server { } } + interface WatchedFile { + fileName: string; + callback: (fileName: string) => void; + mtime: Date; + } + + class WatchedFileSet { + private watchedFiles: WatchedFile[] = []; + private nextFileToCheck = 0; + private watchTimer: NodeJS.Timer; + private static fileDeleted = 34; + + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + constructor(public interval = 2500, public chunkSize = 30) { + } + + private static copyListRemovingItem(item: T, list: T[]) { + var copiedList: T[] = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + + private static getModifiedTime(fileName: string): Date { + return fs.statSync(fileName).mtime; + } + + private poll(checkedIndex: number) { + var watchedFile = this.watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + + fs.stat(watchedFile.fileName,(err, stats) => { + if (err) { + var msg = err.message; + if (err.errno) { + msg += " errno: " + err.errno.toString(); + } + if (err.errno == WatchedFileSet.fileDeleted) { + watchedFile.callback(watchedFile.fileName); + } + } + else if (watchedFile.mtime.getTime() != stats.mtime.getTime()) { + watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName); + } + }); + } + + // this implementation uses polling and + // stat due to inconsistencies of fs.watch + // and efficiency of stat on modern filesystems + private startWatchTimer() { + this.watchTimer = setInterval(() => { + var count = 0; + var nextToCheck = this.nextFileToCheck; + var firstCheck = -1; + while ((count < this.chunkSize) && (nextToCheck != firstCheck)) { + this.poll(nextToCheck); + if (firstCheck < 0) { + firstCheck = nextToCheck; + } + nextToCheck++; + if (nextToCheck === this.watchedFiles.length) { + nextToCheck = 0; + } + count++; + } + this.nextFileToCheck = nextToCheck; + }, this.interval); + } + + addFile(fileName: string, callback: (fileName: string) => void ): WatchedFile { + var file: WatchedFile = { + fileName, + callback, + mtime: WatchedFileSet.getModifiedTime(fileName) + }; + + this.watchedFiles.push(file); + if (this.watchedFiles.length === 1) { + this.startWatchTimer(); + } + return file; + } + + removeFile(file: WatchedFile) { + this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); + } + } + class IOSession extends Session { constructor(host: ServerHost, logger: ts.server.Logger) { super(host, logger); @@ -95,6 +191,29 @@ module ts.server { // TODO: check that this location is writable var logger = new Logger(__dirname + "/.log" + process.pid.toString()); + + // REVIEW: for now this implementation uses polling. + // The advantage of polling is that it works reliably + // on all os and with network mounted files. + // For 90 referenced files, the average time to detect + // changes is 2*msInterval (by default 5 seconds). + // The overhead of this is .04 percent (1/2500) with + // average pause of < 1 millisecond (and max + // pause less than 1.5 milliseconds); question is + // do we anticipate reference sets in the 100s and + // do we care about waiting 10-20 seconds to detect + // changes for large reference sets? If so, do we want + // to increase the chunk size or decrease the interval + // time dynamically to match the large reference set? + var watchedFileSet = new WatchedFileSet(); + ts.sys.watchFile = function (fileName, callback) { + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: () => watchedFileSet.removeFile(watchedFile) + } + + }; + // Start listening new IOSession(ts.sys, logger).listen(); } \ No newline at end of file