Skip to content

Commit

Permalink
Watch directories instead of individual files (#1279)
Browse files Browse the repository at this point in the history
This prevents EMFILE errors on large projects caused by running out of file descriptors.
  • Loading branch information
devongovett committed May 3, 2018
1 parent f75941c commit d67b76c
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 10 deletions.
17 changes: 7 additions & 10 deletions src/Bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const Parser = require('./Parser');
const WorkerFarm = require('./workerfarm/WorkerFarm');
const Path = require('path');
const Bundle = require('./Bundle');
const {FSWatcher} = require('chokidar');
const Watcher = require('./Watcher');
const FSCache = require('./FSCache');
const HMRServer = require('./HMRServer');
const Server = require('./Server');
Expand Down Expand Up @@ -107,7 +107,9 @@ class Bundler extends EventEmitter {
hmr:
target === 'node'
? false
: typeof options.hmr === 'boolean' ? options.hmr : watch,
: typeof options.hmr === 'boolean'
? options.hmr
: watch,
https: options.https || false,
logLevel: isNaN(options.logLevel) ? 3 : options.logLevel,
entryFiles: this.entryFiles,
Expand Down Expand Up @@ -321,12 +323,7 @@ class Bundler extends EventEmitter {
this.options.env = process.env;

if (this.options.watch) {
// FS events on macOS are flakey in the tests, which write lots of files very quickly
// See https://github.com/paulmillr/chokidar/issues/612
this.watcher = new FSWatcher({
useFsEvents: process.env.NODE_ENV !== 'test'
});

this.watcher = new Watcher();
this.watcher.on('change', this.onChange.bind(this));
}

Expand All @@ -344,7 +341,7 @@ class Bundler extends EventEmitter {
}

if (this.watcher) {
this.watcher.close();
this.watcher.stop();
}

if (this.hmr) {
Expand Down Expand Up @@ -378,7 +375,7 @@ class Bundler extends EventEmitter {
}

if (!this.watchedAssets.has(path)) {
this.watcher.add(path);
this.watcher.watch(path);
this.watchedAssets.set(path, new Set());
}

Expand Down
124 changes: 124 additions & 0 deletions src/Watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
const {FSWatcher} = require('chokidar');
const Path = require('path');

/**
* This watcher wraps chokidar so that we watch directories rather than individual files.
* This prevents us from hitting EMFILE errors when running out of file descriptors.
*/
class Watcher {
constructor() {
// FS events on macOS are flakey in the tests, which write lots of files very quickly
// See https://github.com/paulmillr/chokidar/issues/612
this.shouldWatchDirs = process.env.NODE_ENV !== 'test';
this.watcher = new FSWatcher({
useFsEvents: this.shouldWatchDirs,
ignoreInitial: true
});

this.watchedDirectories = new Map();
}

/**
* Find a parent directory of `path` which is already watched
*/
getWatchedParent(path) {
path = Path.dirname(path);

let root = Path.parse(path).root;
while (path !== root) {
if (this.watchedDirectories.has(path)) {
return path;
}

path = Path.dirname(path);
}

return null;
}

/**
* Find a list of child directories of `path` which are already watched
*/
getWatchedChildren(path) {
path = Path.dirname(path) + Path.sep;

let res = [];
for (let dir of this.watchedDirectories.keys()) {
if (dir.startsWith(path)) {
res.push(dir);
}
}

return res;
}

/**
* Add a path to the watcher
*/
watch(path) {
if (this.shouldWatchDirs) {
// If there is no parent directory already watching this path, add a new watcher.
let parent = this.getWatchedParent(path);
if (!parent) {
// Find watchers on child directories, and remove them. They will be handled by the new parent watcher.
let children = this.getWatchedChildren(path);
let count = 1;

for (let dir of children) {
count += this.watchedDirectories.get(dir);
this.watcher.unwatch(dir);
this.watchedDirectories.delete(dir);
}

let dir = Path.dirname(path);
this.watcher.add(dir);
this.watchedDirectories.set(dir, count);
} else {
// Otherwise, increment the reference count of the parent watcher.
this.watchedDirectories.set(
parent,
this.watchedDirectories.get(parent) + 1
);
}
} else {
this.watcher.add(path);
}
}

/**
* Remove a path from the watcher
*/
unwatch(path) {
if (this.shouldWatchDirs) {
let dir = this.getWatchedParent(path);
if (dir) {
// When the count of files watching a directory reaches zero, unwatch it.
let count = this.watchedDirectories.get(dir) - 1;
if (count === 0) {
this.watchedDirectories.delete(dir);
this.watcher.unwatch(dir);
} else {
this.watchedDirectories.set(dir, count);
}
}
} else {
this.watcher.unwatch(path);
}
}

/**
* Add an event handler
*/
on(event, callback) {
this.watcher.on(event, callback);
}

/**
* Stop watching all paths
*/
stop() {
this.watcher.close();
}
}

module.exports = Watcher;

0 comments on commit d67b76c

Please sign in to comment.