Skip to content

Commit 5daa100

Browse files
author
Zhengbo Li
committed
unify the node filewatcher in sys.ts and server.ts
1 parent 0bc5c14 commit 5daa100

2 files changed

Lines changed: 107 additions & 125 deletions

File tree

src/compiler/sys.ts

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ namespace ts {
2121
exit(exitCode?: number): void;
2222
}
2323

24+
interface WatchedFile {
25+
fileName: string;
26+
callback: (fileName: string) => void;
27+
mtime: Date;
28+
}
29+
2430
export interface FileWatcher {
2531
close(): void;
2632
}
@@ -193,7 +199,104 @@ namespace ts {
193199
const _path = require("path");
194200
const _os = require("os");
195201
const _process = require("process");
196-
202+
203+
class WatchedFileSet {
204+
private watchedFiles: WatchedFile[] = [];
205+
private nextFileToCheck = 0;
206+
private watchTimer: NodeJS.Timer;
207+
208+
// average async stat takes about 30 microseconds
209+
// set chunk size to do 30 files in < 1 millisecond
210+
constructor(public interval = 2500, public chunkSize = 30) {
211+
}
212+
213+
private static copyListRemovingItem<T>(item: T, list: T[]) {
214+
var copiedList: T[] = [];
215+
for (var i = 0, len = list.length; i < len; i++) {
216+
if (list[i] != item) {
217+
copiedList.push(list[i]);
218+
}
219+
}
220+
return copiedList;
221+
}
222+
223+
private static getModifiedTime(fileName: string): Date {
224+
return _fs.statSync(fileName).mtime;
225+
}
226+
227+
private poll(checkedIndex: number) {
228+
var watchedFile = this.watchedFiles[checkedIndex];
229+
if (!watchedFile) {
230+
return;
231+
}
232+
233+
_fs.stat(watchedFile.fileName, (err: any, stats: any) => {
234+
if (err) {
235+
watchedFile.callback(watchedFile.fileName);
236+
}
237+
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
238+
watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName);
239+
watchedFile.callback(watchedFile.fileName);
240+
}
241+
});
242+
}
243+
244+
// this implementation uses polling and
245+
// stat due to inconsistencies of fs.watch
246+
// and efficiency of stat on modern filesystems
247+
private startWatchTimer() {
248+
this.watchTimer = setInterval(() => {
249+
var count = 0;
250+
var nextToCheck = this.nextFileToCheck;
251+
var firstCheck = -1;
252+
while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) {
253+
this.poll(nextToCheck);
254+
if (firstCheck < 0) {
255+
firstCheck = nextToCheck;
256+
}
257+
nextToCheck++;
258+
if (nextToCheck === this.watchedFiles.length) {
259+
nextToCheck = 0;
260+
}
261+
count++;
262+
}
263+
this.nextFileToCheck = nextToCheck;
264+
}, this.interval);
265+
}
266+
267+
addFile(fileName: string, callback: (fileName: string) => void): WatchedFile {
268+
var file: WatchedFile = {
269+
fileName,
270+
callback,
271+
mtime: WatchedFileSet.getModifiedTime(fileName)
272+
};
273+
274+
this.watchedFiles.push(file);
275+
if (this.watchedFiles.length === 1) {
276+
this.startWatchTimer();
277+
}
278+
return file;
279+
}
280+
281+
removeFile(file: WatchedFile) {
282+
this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles);
283+
}
284+
}
285+
286+
// REVIEW: for now this implementation uses polling.
287+
// The advantage of polling is that it works reliably
288+
// on all os and with network mounted files.
289+
// For 90 referenced files, the average time to detect
290+
// changes is 2*msInterval (by default 5 seconds).
291+
// The overhead of this is .04 percent (1/2500) with
292+
// average pause of < 1 millisecond (and max
293+
// pause less than 1.5 milliseconds); question is
294+
// do we anticipate reference sets in the 100s and
295+
// do we care about waiting 10-20 seconds to detect
296+
// changes for large reference sets? If so, do we want
297+
// to increase the chunk size or decrease the interval
298+
// time dynamically to match the large reference set?
299+
var watchedFileSet = new WatchedFileSet();
197300

198301
function isNode4OrLater(): Boolean {
199302
return parseInt(_process.version.charAt(1)) >= 4;
@@ -291,28 +394,17 @@ namespace ts {
291394
readFile,
292395
writeFile,
293396
watchFile: (fileName, callback) => {
294-
295-
// Node 4.0 stablized the `fs.watch` function which avoids polling
397+
// Node 4.0 stablized the `fs.watch` function on Windows which avoids polling
296398
// and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649
297399
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
298400
// if the current node.js version is newer than 4, use `fs.watch` instead.
299401
if (isNode4OrLater()) {
300402
return _fs.watch(fileName, (eventName: string, path: string) => callback(path));
301403
}
302404

303-
// watchFile polls a file every 250ms, picking up file notifications.
304-
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged);
305-
405+
var watchedFile = watchedFileSet.addFile(fileName, callback);
306406
return {
307-
close() { _fs.unwatchFile(fileName, fileChanged); }
308-
};
309-
310-
function fileChanged(curr: any, prev: any) {
311-
if (+curr.mtime <= +prev.mtime) {
312-
return;
313-
}
314-
315-
callback(fileName);
407+
close: () => watchedFileSet.removeFile(watchedFile)
316408
}
317409
},
318410
resolvePath: function (path: string): string {

src/server/server.ts

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -83,95 +83,6 @@ namespace ts.server {
8383
}
8484
}
8585

86-
interface WatchedFile {
87-
fileName: string;
88-
callback: (fileName: string) => void;
89-
mtime: Date;
90-
}
91-
92-
class WatchedFileSet {
93-
private watchedFiles: WatchedFile[] = [];
94-
private nextFileToCheck = 0;
95-
private watchTimer: NodeJS.Timer;
96-
97-
// average async stat takes about 30 microseconds
98-
// set chunk size to do 30 files in < 1 millisecond
99-
constructor(public interval = 2500, public chunkSize = 30) {
100-
}
101-
102-
private static copyListRemovingItem<T>(item: T, list: T[]) {
103-
var copiedList: T[] = [];
104-
for (var i = 0, len = list.length; i < len; i++) {
105-
if (list[i] != item) {
106-
copiedList.push(list[i]);
107-
}
108-
}
109-
return copiedList;
110-
}
111-
112-
private static getModifiedTime(fileName: string): Date {
113-
return fs.statSync(fileName).mtime;
114-
}
115-
116-
private poll(checkedIndex: number) {
117-
var watchedFile = this.watchedFiles[checkedIndex];
118-
if (!watchedFile) {
119-
return;
120-
}
121-
122-
fs.stat(watchedFile.fileName,(err, stats) => {
123-
if (err) {
124-
watchedFile.callback(watchedFile.fileName);
125-
}
126-
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
127-
watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName);
128-
watchedFile.callback(watchedFile.fileName);
129-
}
130-
});
131-
}
132-
133-
// this implementation uses polling and
134-
// stat due to inconsistencies of fs.watch
135-
// and efficiency of stat on modern filesystems
136-
private startWatchTimer() {
137-
this.watchTimer = setInterval(() => {
138-
var count = 0;
139-
var nextToCheck = this.nextFileToCheck;
140-
var firstCheck = -1;
141-
while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) {
142-
this.poll(nextToCheck);
143-
if (firstCheck < 0) {
144-
firstCheck = nextToCheck;
145-
}
146-
nextToCheck++;
147-
if (nextToCheck === this.watchedFiles.length) {
148-
nextToCheck = 0;
149-
}
150-
count++;
151-
}
152-
this.nextFileToCheck = nextToCheck;
153-
}, this.interval);
154-
}
155-
156-
addFile(fileName: string, callback: (fileName: string) => void ): WatchedFile {
157-
var file: WatchedFile = {
158-
fileName,
159-
callback,
160-
mtime: WatchedFileSet.getModifiedTime(fileName)
161-
};
162-
163-
this.watchedFiles.push(file);
164-
if (this.watchedFiles.length === 1) {
165-
this.startWatchTimer();
166-
}
167-
return file;
168-
}
169-
170-
removeFile(file: WatchedFile) {
171-
this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles);
172-
}
173-
}
174-
17586
class IOSession extends Session {
17687
constructor(host: ServerHost, logger: ts.server.Logger) {
17788
super(host, Buffer.byteLength, process.hrtime, logger);
@@ -243,28 +154,7 @@ namespace ts.server {
243154
// TODO: check that this location is writable
244155

245156
var logger = createLoggerFromEnv();
246-
247-
// REVIEW: for now this implementation uses polling.
248-
// The advantage of polling is that it works reliably
249-
// on all os and with network mounted files.
250-
// For 90 referenced files, the average time to detect
251-
// changes is 2*msInterval (by default 5 seconds).
252-
// The overhead of this is .04 percent (1/2500) with
253-
// average pause of < 1 millisecond (and max
254-
// pause less than 1.5 milliseconds); question is
255-
// do we anticipate reference sets in the 100s and
256-
// do we care about waiting 10-20 seconds to detect
257-
// changes for large reference sets? If so, do we want
258-
// to increase the chunk size or decrease the interval
259-
// time dynamically to match the large reference set?
260-
var watchedFileSet = new WatchedFileSet();
261-
ts.sys.watchFile = function (fileName, callback) {
262-
var watchedFile = watchedFileSet.addFile(fileName, callback);
263-
return {
264-
close: () => watchedFileSet.removeFile(watchedFile)
265-
}
266157

267-
};
268158
var ioSession = new IOSession(ts.sys, logger);
269159
process.on('uncaughtException', function(err: Error) {
270160
ioSession.logError(err, "unknown");

0 commit comments

Comments
 (0)