Node.js の fs.watch() と fs.watchFile() の違い
Node.js のファイル監視の API には fs.watch()
と fs.watchFile()
の 2 つがある。
微妙に機能がかぶっているし、使い分けが分かりにくかったので調べてみた。
公式情報を見る
まずは公式のドキュメント (v0.8.0)を見てみた。
fs.watchFile(filename, [options], listener)
Stability: 2 - Unstable. Use fs.watch instead, if available.
Watch for changes on
filename
.fs.watch(filename, [options], [listener])
Stability: 2 - Unstable. Not available on all platforms.
Watch for changes on
filename
, wherefilename
is either a file or a directory.
と書いてある。
つまり
fs.watch()
の利用を推奨しているfs.watch()
は全てのプラットフォームで使えるわけではないfs.watch()
はファイルとディレクトリを監視できるが、fs.watchFile()
はファイルしか監視できない
ことが分かる。
歴史的見地から調べる
次に、過去をさかのぼるために ChangeLog を見てみた。
fs.watchFile()
は v0.1.18 でprocess.watchFile()
として登場した古い APIfs.watch()
は v0.5.9 で実装された新しい API
だと分かった。
しかし、これ以上の公式の情報が見つからなかったため、全体像が見えない。
ソースコードを見る
困ったらソースコードを見ろ、と昔の偉い人も言っている。いざ、コード リーディング!
fs.watch()
まずは、fs.watch()
の実装をみてみよう。(ソースコードは執筆時点で最新の v0.10.19 を利用する)
lib/fs.js
→ src/fs_event_wrap.cc
の順にたどっていくと、uv_fs_event_init()
関数が本丸だと分かった。
uv
で始まる関数は libuv で定義されたもの。libuv は Node.js のプラットフォーム間の差を吸収するためのライブラリらしく、IO やスレッド、タイマーなどの処理が実装されているようだ。
では、uv_fs_event_init()
の実装を見てみる。deps/uv/src
の下を grep してみると
- unix\aix.c
- unix\cygwin.c
- unix\kqueue.c
- unix\linux-inotify.c
- unix\sunos.c
- win\fs-event.c
が引っかかった。プラットフォームごとに実装が異なっているようだ。
ざっと読んでみると、
プラットフォーム | 実装方法 |
---|---|
Linux | inotify を利用 |
MacOS、*BSD | kqueue を利用 |
Windows | ReadDirectoryChangesW() を利用 |
Solaris | Event Ports を利用 |
AIX | (未対応) |
Cygwin | (未対応) |
となっていた。
つまり、fs.watch() はネイティブの監視処理を利用して、変更があったら OS から通知してもらっていることが分かった。
fs.watchFile()
一方の fs.watchFile()
を見てみる。Node.js には古くから実装されているが、現在では利用が推奨されていないほうの関数である。
lib/fs.js
→ src/node_stat_watcher.cc
の順にたどっていくと、uv_fs_poll_start()
関数が本丸だと分かった。
あとは、uv_fs_poll_start()
の流れをつかめばおしまい。deps/uv/src/fs-poll.c
を見てみよう。なんとなくポーリングをしていそうな名前である。
int uv_fs_poll_start(uv_fs_poll_t* handle,
uv_fs_poll_cb cb,
const char* path,
unsigned int interval) {
// 初期化処理は省略
if (uv_fs_stat(loop, &ctx->fs_req, ctx->path, poll_cb))
abort();
肝は uv_fs_stat()
の呼び出し。この関数は、Node.js で言うところの fs.stat()
相当の処理だろう。ファイルの更新日時の取得を依頼して、取得が完了したら、poll_cb()
が呼ばれる。
その poll_cb()
を見てみよう。
static void poll_cb(uv_fs_t* req) {
// 異常処理とか、前回と違ってたらイベント発行する処理とか
/* Reschedule timer, subtract the delay from doing the stat(). */
interval = ctx->interval;
interval -= (uv_now(ctx->loop) - ctx->start_time) % interval;
if (uv_timer_start(&ctx->timer_handle, timer_cb, interval, 0))
abort();
}
実行結果の解析が終わったら、interval
後に timer_cb()
が呼ばれるようにタイマーを開始している。
timer_cb()
では、再度 uv_fs_stat()
を実行している。つまり、定期的に fs.stat()
を実行している。
つまり、fs.watchFile() は定期的に fs.stat() をポーリングで実行して、変更されたらイベントを発行している ことが分かった。非常に原始的な実装になっている。
まとめ
fs.watch()
:
- 新しい API、利用が推奨されている
- push 型 (OS の監視機能を利用しているので、待機中に CPU を消費しない)
- 一部のプラットフォーム (AIX、Cygwin) では利用できない
fs.watchFile()
:
- 古い API、あまり使ってほしくなさそう
- poll 型 (定期的に stat を実行する)
- どのプラットフォームでも動く