Node.jsで高速にファイル一覧を取得するfs.readdirのwithFileTypesオプション

この記事は[Node.js Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nodejs)の2日目の記事です。

ちょっとしたネタですが、Node.js v10.10から入った[fs.readdir](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback)の`withFileTypes`オプションと[fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent)について紹介したいと思います。

# TL;DR
- 特定のディレクトリ配下のファイル一覧を表示する方法を紹介
- 読み込んだディレクトリのファイル種別がファイルかディレクトリか判定する方法を紹介
  - `fs.stat`を使った従来の方法
  - Node.js v10.10.0から使える`fs.readdir`(または`fs.readdirSync`)の`withFileTypes`と`fs.Dirent`を使った方法

# fs.statを使った判定方法

例えばNode.jsを使ってディレクトリ配下のファイル一覧を表示したいとき以下のように`fs.readdir`と`fs.stat`を使って書くことができます。

```js
const fs = require('fs');
const path = require('path');

const showFiles = (dirpath, callback) => {
  fs.readdir(dirpath, (err, files) => {
    if (err) {
      console.error(err);
      return;
    }

    for (const file of files) {
      const fp = path.join(dirpath, file);
      fs.stat(fp, (err, stats) => {
        if (err) {
          console.error(err);
          return;	
        }
        if (stats.isDirectory()) {
          showFiles(fp, callback);
        } else {
          callback(fp);
        }
      });
    }
  });
}

showFiles(process.argv[2], console.log);
```

`fs.readdir`で特定のディレクトリ配下のファイルとディレクトリの一覧を取得し、それを`fs.stat`を使ってディレクトリかどうか判定しています。

もちろん上記のコードで問題ありません。
しかし、このコードは`fs.readdir`と`fs.stat`を使うことでFile Systemを二回コールしています。
パフォーマンス的には改善の余地がありそうです。

そこでNode.js v10.10.0から新しく`fs.readdir`に`withFileTypes`というオプション引数が追加されました。

# fs.readdirのwithFileTypesオプションを使った判定方法

追加されたオプション`withFileTypes`を`true`にするとこれまでのようにファイル名やディレクトリ名の文字列の配列を取得するのではなく、[fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent)オブジェクトの配列を取得することができます。
実装されたPRは→https://github.com/nodejs/node/pull/22020

で、結局何ができるのかというと上記のように`fs.readdir`と`fs.stat`より簡潔なコードになり、実行速度も高速なコードを書くことが可能になります。

```js
const fs = require('fs');
const path = require('path');

const showFiles = (dirpath, callback) => {
  fs.readdir(dirpath, {withFileTypes: true}, (err, dirents) => {
    if (err) {
      console.error(err);
      return;
    }

    for (const dirent of dirents) {
      const fp = path.join(dirpath, dirent.name);
      if (dirent.isDirectory()) {
        showFiles(fp, callback);
      } else {
        callback(fp);
      }
    }
  });
}

showFiles(process.argv[2], console.log);
```

# direntとは

これまでに出てきた`fs.Dirent`の`dirent`ですが、ディレクトリエントリと呼ばれるファイルやディレクトリの情報を持った構造体です。
UNIX系のOSを使っている方であれば`man`で説明を読むことができます。

```bash
$ man dirent
```

日本語の説明は https://nxmnpg.lemoda.net/ja/5/dirent を参考にしてみてください。


Node.jsの非同期I/Oやイベントループに使われているlibuvにも`uv_dirent_t`とうenumが存在していてます。
http://docs.libuv.org/en/v1.x/fs.html

`fs.readdir`で取得できる`fs.Dirent`クラスの`isDirectory`関数とかはこの`uv_dirent_t`が持つ値を使って判定しています。

# パフォーマンス

withFileTypesオプションを使うことでパフォーマンスはどれくらい良くなったのか計測してみました。
計測対象は筆者のローカルにcloneしている[nodejs/node](https://github.com/nodejs/node)です。
あと、標準出力すると時間がかかるので表示結果は`/dev/null`に捨てています。
5回計測した結果を載せます。

### fs.stat
まずは上記の`fs.stat`を使ったコードの計測結果です。

```
$ time node stat-test.js /path/to/node > /dev/null
```

```
real	0m1.425s
user	0m1.394s
sys	0m1.109s

real	0m1.188s
user	0m1.205s
sys	0m0.816s

real	0m1.302s
user	0m1.383s
sys	0m1.059s

real	0m1.242s
user	0m1.252s
sys	0m0.929s

real	0m1.420s
user	0m1.373s
sys	0m0.931s
```

### fs.readdir withFileTypes

次に上記の`fs.readdir`の`withFileTypes`オプションを使ったコードの計測結果です。

```
$ time node withFileTypes-test.js /path/to/node > /dev/null
```

```
real	0m0.666s
user	0m0.668s
sys	0m0.385s

real	0m0.628s
user	0m0.681s
sys	0m0.362s

real	0m0.694s
user	0m0.697s
sys	0m0.372s

real	0m0.657s
user	0m0.697s
sys	0m0.369s

real	0m0.732s
user	0m0.742s
sys	0m0.392s
```

手元で試した限りでは結構差がある結果になりました。

v10.10.0以降でファイルかディレクトリか判定するときは`fs.stat`ではなく`fs.readdir`の`withFileTypes`オプションを使っていくのが良いかと思います。

以上になります。最後までお読みいただきありがとうございました。
この記事の不備や質問についてはコメント欄またはTwitter([@shisama_](https://twitter.com/shisama_))までお願いいたします。