From 576e5f8055c88776182b4a6ac3456ca089b08144 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Tue, 19 Apr 2016 14:25:57 -0700 Subject: [PATCH] So we are back to polling. --- src/compiler/sys.ts | 196 +------------------------------------------ src/server/server.ts | 104 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 193 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c391560bef..bdefe6fa34 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -26,12 +26,6 @@ namespace ts { exit(exitCode?: number): void; } - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } - export interface FileWatcher { close(): void; } @@ -230,185 +224,9 @@ namespace ts { const _os = require("os"); const _crypto = require("crypto"); - // average async stat takes about 30 microseconds - // set chunk size to do 30 files in < 1 millisecond - function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) { - let watchedFiles: WatchedFile[] = []; - let nextFileToCheck = 0; - let watchTimer: any; - - function getModifiedTime(fileName: string): Date { - return _fs.statSync(fileName).mtime; - } - - function poll(checkedIndex: number) { - const watchedFile = watchedFiles[checkedIndex]; - if (!watchedFile) { - return; - } - - _fs.stat(watchedFile.fileName, (err: any, stats: any) => { - if (err) { - watchedFile.callback(watchedFile.fileName); - } - else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { - watchedFile.mtime = getModifiedTime(watchedFile.fileName); - watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); - } - }); - } - - // this implementation uses polling and - // stat due to inconsistencies of fs.watch - // and efficiency of stat on modern filesystems - function startWatchTimer() { - watchTimer = setInterval(() => { - let count = 0; - let nextToCheck = nextFileToCheck; - let firstCheck = -1; - while ((count < chunkSize) && (nextToCheck !== firstCheck)) { - poll(nextToCheck); - if (firstCheck < 0) { - firstCheck = nextToCheck; - } - nextToCheck++; - if (nextToCheck === watchedFiles.length) { - nextToCheck = 0; - } - count++; - } - nextFileToCheck = nextToCheck; - }, interval); - } - - function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { - const file: WatchedFile = { - fileName, - callback, - mtime: getModifiedTime(fileName) - }; - - watchedFiles.push(file); - if (watchedFiles.length === 1) { - startWatchTimer(); - } - return file; - } - - function removeFile(file: WatchedFile) { - watchedFiles = copyListRemovingItem(file, watchedFiles); - } - - return { - getModifiedTime: getModifiedTime, - poll: poll, - startWatchTimer: startWatchTimer, - addFile: addFile, - removeFile: removeFile - }; - } - - function createWatchedFileSet() { - const dirWatchers: Map = {}; - // One file can have multiple watchers - const fileWatcherCallbacks: Map = {}; - return { addFile, removeFile }; - - function reduceDirWatcherRefCountForFile(fileName: string) { - const dirName = getDirectoryPath(fileName); - if (hasProperty(dirWatchers, dirName)) { - const watcher = dirWatchers[dirName]; - watcher.referenceCount -= 1; - if (watcher.referenceCount <= 0) { - watcher.close(); - delete dirWatchers[dirName]; - } - } - } - - function addDirWatcher(dirPath: string): void { - if (hasProperty(dirWatchers, dirPath)) { - const watcher = dirWatchers[dirPath]; - watcher.referenceCount += 1; - return; - } - - const watcher: DirectoryWatcher = _fs.watch( - dirPath, - { persistent: true }, - (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) - ); - watcher.referenceCount = 1; - dirWatchers[dirPath] = watcher; - return; - } - - function addFileWatcherCallback(filePath: string, callback: FileWatcherCallback): void { - if (hasProperty(fileWatcherCallbacks, filePath)) { - fileWatcherCallbacks[filePath].push(callback); - } - else { - fileWatcherCallbacks[filePath] = [callback]; - } - } - - function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { - addFileWatcherCallback(fileName, callback); - addDirWatcher(getDirectoryPath(fileName)); - - return { fileName, callback }; - } - - function removeFile(watchedFile: WatchedFile) { - removeFileWatcherCallback(watchedFile.fileName, watchedFile.callback); - reduceDirWatcherRefCountForFile(watchedFile.fileName); - } - - function removeFileWatcherCallback(filePath: string, callback: FileWatcherCallback) { - if (hasProperty(fileWatcherCallbacks, filePath)) { - const newCallbacks = copyListRemovingItem(callback, fileWatcherCallbacks[filePath]); - if (newCallbacks.length === 0) { - delete fileWatcherCallbacks[filePath]; - } - else { - fileWatcherCallbacks[filePath] = newCallbacks; - } - } - } - - function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - const fileName = typeof relativeFileName !== "string" - ? undefined - : ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath); - // Some applications save a working file via rename operations - if ((eventName === "change" || eventName === "rename") && hasProperty(fileWatcherCallbacks, fileName)) { - for (const fileCallback of fileWatcherCallbacks[fileName]) { - fileCallback(fileName); - } - } - } - } - - // 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? - const pollingWatchedFileSet = createPollingWatchedFileSet(); - const watchedFileSet = createWatchedFileSet(); - function isNode4OrLater(): boolean { - return parseInt(process.version.charAt(1)) >= 4; - } + return parseInt(process.version.charAt(1)) >= 4; + } const platform: string = _os.platform(); // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive @@ -535,15 +353,7 @@ namespace ts { readFile, writeFile, watchFile: (fileName, callback) => { - // Node 4.0 stabilized the `fs.watch` function on Windows which avoids polling - // and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649 - // and https://github.com/Microsoft/TypeScript/issues/4643), therefore - // if the current node.js version is newer than 4, use `fs.watch` instead. - const watchSet = isNode4OrLater() ? watchedFileSet : pollingWatchedFileSet; - const watchedFile = watchSet.addFile(fileName, callback); - return { - close: () => watchSet.removeFile(watchedFile) - }; + return _fs.watchFile(fileName, callback); }, watchDirectory: (directoryName, callback, recursive) => { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows diff --git a/src/server/server.ts b/src/server/server.ts index 3ffd64fe07..26ab01adc2 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -111,6 +111,12 @@ namespace ts.server { detailLevel?: string; } + interface WatchedFile { + fileName: string; + callback: FileWatcherCallback; + mtime?: Date; + } + function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { const logEnv: LogOptions = {}; const args = logEnvStr.split(" "); @@ -153,6 +159,98 @@ namespace ts.server { // This places log file in the directory containing editorServices.js // TODO: check that this location is writable + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) { + let watchedFiles: WatchedFile[] = []; + let nextFileToCheck = 0; + let watchTimer: any; + + function getModifiedTime(fileName: string): Date { + return fs.statSync(fileName).mtime; + } + + function poll(checkedIndex: number) { + const watchedFile = watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + + fs.stat(watchedFile.fileName, (err: any, stats: any) => { + if (err) { + watchedFile.callback(watchedFile.fileName); + } + else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { + watchedFile.mtime = getModifiedTime(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); + } + }); + } + + // this implementation uses polling and + // stat due to inconsistencies of fs.watch + // and efficiency of stat on modern filesystems + function startWatchTimer() { + watchTimer = setInterval(() => { + let count = 0; + let nextToCheck = nextFileToCheck; + let firstCheck = -1; + while ((count < chunkSize) && (nextToCheck !== firstCheck)) { + poll(nextToCheck); + if (firstCheck < 0) { + firstCheck = nextToCheck; + } + nextToCheck++; + if (nextToCheck === watchedFiles.length) { + nextToCheck = 0; + } + count++; + } + nextFileToCheck = nextToCheck; + }, interval); + } + + function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { + const file: WatchedFile = { + fileName, + callback, + mtime: getModifiedTime(fileName) + }; + + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); + } + return file; + } + + function removeFile(file: WatchedFile) { + watchedFiles = copyListRemovingItem(file, watchedFiles); + } + + return { + getModifiedTime: getModifiedTime, + poll: poll, + startWatchTimer: startWatchTimer, + addFile: addFile, + removeFile: removeFile + }; + } + + // 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? + const pollingWatchedFileSet = createPollingWatchedFileSet(); const logger = createLoggerFromEnv(); const pending: string[] = []; @@ -176,6 +274,12 @@ namespace ts.server { // Override sys.write because fs.writeSync is not reliable on Node 4 ts.sys.write = (s: string) => writeMessage(s); + ts.sys.watchFile = (fileName, callback) => { + const watchedFile = pollingWatchedFileSet.addFile(fileName, callback); + return { + close: () => pollingWatchedFileSet.removeFile(watchedFile) + }; + }; const ioSession = new IOSession(ts.sys, logger); process.on("uncaughtException", function(err: Error) {