So we are back to polling.

This commit is contained in:
zhengbli 2016-04-19 14:25:57 -07:00
parent 6e812e59a5
commit 576e5f8055
2 changed files with 107 additions and 193 deletions

View file

@ -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<DirectoryWatcher> = {};
// One file can have multiple watchers
const fileWatcherCallbacks: Map<FileWatcherCallback[]> = {};
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

View file

@ -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) {