@@ -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 {
0 commit comments