Skip to content

Commit 0f18cf8

Browse files
committed
Merge pull request microsoft#8196 from zhengbli/fileWatcherBackToPolling
Change file watching back to polling.
2 parents 4039146 + a09b001 commit 0f18cf8

File tree

3 files changed

+130
-113
lines changed

3 files changed

+130
-113
lines changed

src/compiler/sys.ts

Lines changed: 29 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
namespace ts {
44
export type FileWatcherCallback = (fileName: string, removed?: boolean) => void;
55
export type DirectoryWatcherCallback = (directoryName: string) => void;
6+
export interface WatchedFile {
7+
fileName: string;
8+
callback: FileWatcherCallback;
9+
mtime?: Date;
10+
}
611

712
export interface System {
813
args: string[];
@@ -26,12 +31,6 @@ namespace ts {
2631
exit(exitCode?: number): void;
2732
}
2833

29-
interface WatchedFile {
30-
fileName: string;
31-
callback: FileWatcherCallback;
32-
mtime?: Date;
33-
}
34-
3534
export interface FileWatcher {
3635
close(): void;
3736
}
@@ -230,83 +229,7 @@ namespace ts {
230229
const _os = require("os");
231230
const _crypto = require("crypto");
232231

233-
// average async stat takes about 30 microseconds
234-
// set chunk size to do 30 files in < 1 millisecond
235-
function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) {
236-
let watchedFiles: WatchedFile[] = [];
237-
let nextFileToCheck = 0;
238-
let watchTimer: any;
239-
240-
function getModifiedTime(fileName: string): Date {
241-
return _fs.statSync(fileName).mtime;
242-
}
243-
244-
function poll(checkedIndex: number) {
245-
const watchedFile = watchedFiles[checkedIndex];
246-
if (!watchedFile) {
247-
return;
248-
}
249-
250-
_fs.stat(watchedFile.fileName, (err: any, stats: any) => {
251-
if (err) {
252-
watchedFile.callback(watchedFile.fileName);
253-
}
254-
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
255-
watchedFile.mtime = getModifiedTime(watchedFile.fileName);
256-
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0);
257-
}
258-
});
259-
}
260-
261-
// this implementation uses polling and
262-
// stat due to inconsistencies of fs.watch
263-
// and efficiency of stat on modern filesystems
264-
function startWatchTimer() {
265-
watchTimer = setInterval(() => {
266-
let count = 0;
267-
let nextToCheck = nextFileToCheck;
268-
let firstCheck = -1;
269-
while ((count < chunkSize) && (nextToCheck !== firstCheck)) {
270-
poll(nextToCheck);
271-
if (firstCheck < 0) {
272-
firstCheck = nextToCheck;
273-
}
274-
nextToCheck++;
275-
if (nextToCheck === watchedFiles.length) {
276-
nextToCheck = 0;
277-
}
278-
count++;
279-
}
280-
nextFileToCheck = nextToCheck;
281-
}, interval);
282-
}
283-
284-
function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile {
285-
const file: WatchedFile = {
286-
fileName,
287-
callback,
288-
mtime: getModifiedTime(fileName)
289-
};
290-
291-
watchedFiles.push(file);
292-
if (watchedFiles.length === 1) {
293-
startWatchTimer();
294-
}
295-
return file;
296-
}
297-
298-
function removeFile(file: WatchedFile) {
299-
watchedFiles = copyListRemovingItem(file, watchedFiles);
300-
}
301-
302-
return {
303-
getModifiedTime: getModifiedTime,
304-
poll: poll,
305-
startWatchTimer: startWatchTimer,
306-
addFile: addFile,
307-
removeFile: removeFile
308-
};
309-
}
232+
const useNonPollingWatchers = process.env["TSC_NONPOLLING_WATCHER"];
310233

311234
function createWatchedFileSet() {
312235
const dirWatchers: Map<DirectoryWatcher> = {};
@@ -389,26 +312,11 @@ namespace ts {
389312
}
390313
}
391314
}
392-
393-
// REVIEW: for now this implementation uses polling.
394-
// The advantage of polling is that it works reliably
395-
// on all os and with network mounted files.
396-
// For 90 referenced files, the average time to detect
397-
// changes is 2*msInterval (by default 5 seconds).
398-
// The overhead of this is .04 percent (1/2500) with
399-
// average pause of < 1 millisecond (and max
400-
// pause less than 1.5 milliseconds); question is
401-
// do we anticipate reference sets in the 100s and
402-
// do we care about waiting 10-20 seconds to detect
403-
// changes for large reference sets? If so, do we want
404-
// to increase the chunk size or decrease the interval
405-
// time dynamically to match the large reference set?
406-
const pollingWatchedFileSet = createPollingWatchedFileSet();
407315
const watchedFileSet = createWatchedFileSet();
408316

409317
function isNode4OrLater(): boolean {
410-
return parseInt(process.version.charAt(1)) >= 4;
411-
}
318+
return parseInt(process.version.charAt(1)) >= 4;
319+
}
412320

413321
const platform: string = _os.platform();
414322
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
@@ -501,7 +409,7 @@ namespace ts {
501409
const files = _fs.readdirSync(path || ".").sort();
502410
const directories: string[] = [];
503411
for (const current of files) {
504-
// This is necessary because on some file system node fails to exclude
412+
// This is necessary because on some file system node fails to exclude
505413
// "." and "..". See https://github.com/nodejs/node/issues/4002
506414
if (current === "." || current === "..") {
507415
continue;
@@ -535,15 +443,26 @@ namespace ts {
535443
readFile,
536444
writeFile,
537445
watchFile: (fileName, callback) => {
538-
// Node 4.0 stabilized the `fs.watch` function on Windows which avoids polling
539-
// and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649
540-
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
541-
// if the current node.js version is newer than 4, use `fs.watch` instead.
542-
const watchSet = isNode4OrLater() ? watchedFileSet : pollingWatchedFileSet;
543-
const watchedFile = watchSet.addFile(fileName, callback);
544-
return {
545-
close: () => watchSet.removeFile(watchedFile)
546-
};
446+
if (useNonPollingWatchers) {
447+
const watchedFile = watchedFileSet.addFile(fileName, callback);
448+
return {
449+
close: () => watchedFileSet.removeFile(watchedFile)
450+
};
451+
}
452+
else {
453+
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged);
454+
return {
455+
close: () => _fs.unwatchFile(fileName, fileChanged)
456+
};
457+
}
458+
459+
function fileChanged(curr: any, prev: any) {
460+
if (+curr.mtime <= +prev.mtime) {
461+
return;
462+
}
463+
464+
callback(fileName);
465+
}
547466
},
548467
watchDirectory: (directoryName, callback, recursive) => {
549468
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows

src/compiler/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,7 +2126,7 @@ namespace ts {
21262126
jsxFlags?: JsxFlags; // flags for knowing what kind of element/attributes we're dealing with
21272127
resolvedJsxType?: Type; // resolved element attributes type of a JSX openinglike element
21282128
hasSuperCall?: boolean; // recorded result when we try to find super-call. We only try to find one if this flag is undefined, indicating that we haven't made an attempt.
2129-
superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing
2129+
superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing
21302130
}
21312131

21322132
export const enum TypeFlags {
@@ -2507,7 +2507,7 @@ namespace ts {
25072507
// When options come from a config file, its path is recorded here
25082508
configFilePath?: string;
25092509
/* @internal */
2510-
// Path used to used to compute primary search locations
2510+
// Path used to used to compute primary search locations
25112511
typesRoot?: string;
25122512
types?: string[];
25132513

@@ -2824,7 +2824,7 @@ namespace ts {
28242824
*/
28252825
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
28262826
/**
2827-
* This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files
2827+
* This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files
28282828
*/
28292829
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
28302830
}

src/server/server.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,98 @@ namespace ts.server {
153153
// This places log file in the directory containing editorServices.js
154154
// TODO: check that this location is writable
155155

156+
// average async stat takes about 30 microseconds
157+
// set chunk size to do 30 files in < 1 millisecond
158+
function createPollingWatchedFileSet(interval = 2500, chunkSize = 30) {
159+
let watchedFiles: WatchedFile[] = [];
160+
let nextFileToCheck = 0;
161+
let watchTimer: any;
162+
163+
function getModifiedTime(fileName: string): Date {
164+
return fs.statSync(fileName).mtime;
165+
}
166+
167+
function poll(checkedIndex: number) {
168+
const watchedFile = watchedFiles[checkedIndex];
169+
if (!watchedFile) {
170+
return;
171+
}
172+
173+
fs.stat(watchedFile.fileName, (err: any, stats: any) => {
174+
if (err) {
175+
watchedFile.callback(watchedFile.fileName);
176+
}
177+
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
178+
watchedFile.mtime = getModifiedTime(watchedFile.fileName);
179+
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0);
180+
}
181+
});
182+
}
183+
184+
// this implementation uses polling and
185+
// stat due to inconsistencies of fs.watch
186+
// and efficiency of stat on modern filesystems
187+
function startWatchTimer() {
188+
watchTimer = setInterval(() => {
189+
let count = 0;
190+
let nextToCheck = nextFileToCheck;
191+
let firstCheck = -1;
192+
while ((count < chunkSize) && (nextToCheck !== firstCheck)) {
193+
poll(nextToCheck);
194+
if (firstCheck < 0) {
195+
firstCheck = nextToCheck;
196+
}
197+
nextToCheck++;
198+
if (nextToCheck === watchedFiles.length) {
199+
nextToCheck = 0;
200+
}
201+
count++;
202+
}
203+
nextFileToCheck = nextToCheck;
204+
}, interval);
205+
}
206+
207+
function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile {
208+
const file: WatchedFile = {
209+
fileName,
210+
callback,
211+
mtime: getModifiedTime(fileName)
212+
};
213+
214+
watchedFiles.push(file);
215+
if (watchedFiles.length === 1) {
216+
startWatchTimer();
217+
}
218+
return file;
219+
}
220+
221+
function removeFile(file: WatchedFile) {
222+
watchedFiles = copyListRemovingItem(file, watchedFiles);
223+
}
224+
225+
return {
226+
getModifiedTime: getModifiedTime,
227+
poll: poll,
228+
startWatchTimer: startWatchTimer,
229+
addFile: addFile,
230+
removeFile: removeFile
231+
};
232+
}
233+
234+
// REVIEW: for now this implementation uses polling.
235+
// The advantage of polling is that it works reliably
236+
// on all os and with network mounted files.
237+
// For 90 referenced files, the average time to detect
238+
// changes is 2*msInterval (by default 5 seconds).
239+
// The overhead of this is .04 percent (1/2500) with
240+
// average pause of < 1 millisecond (and max
241+
// pause less than 1.5 milliseconds); question is
242+
// do we anticipate reference sets in the 100s and
243+
// do we care about waiting 10-20 seconds to detect
244+
// changes for large reference sets? If so, do we want
245+
// to increase the chunk size or decrease the interval
246+
// time dynamically to match the large reference set?
247+
const pollingWatchedFileSet = createPollingWatchedFileSet();
156248
const logger = createLoggerFromEnv();
157249

158250
const pending: string[] = [];
@@ -176,6 +268,12 @@ namespace ts.server {
176268

177269
// Override sys.write because fs.writeSync is not reliable on Node 4
178270
ts.sys.write = (s: string) => writeMessage(s);
271+
ts.sys.watchFile = (fileName, callback) => {
272+
const watchedFile = pollingWatchedFileSet.addFile(fileName, callback);
273+
return {
274+
close: () => pollingWatchedFileSet.removeFile(watchedFile)
275+
};
276+
};
179277

180278
const ioSession = new IOSession(ts.sys, logger);
181279
process.on("uncaughtException", function(err: Error) {

0 commit comments

Comments
 (0)