JavaScript
Node.js
Node.jsDay 2

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

この記事はNode.js Advent Calendar 2018の2日目の記事です。

ちょっとしたネタですが、Node.js v10.10から入ったfs.readdirwithFileTypesオプションとfs.Direntについて紹介したいと思います。

TL;DR

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

fs.statを使った判定方法

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

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.readdirfs.statを使うことでFile Systemを二回コールしています。
パフォーマンス的には改善の余地がありそうです。

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

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

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

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

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.Direntdirentですが、ディレクトリエントリと呼ばれるファイルやディレクトリの情報を持った構造体です。
UNIX系のOSを使っている方であればmanで説明を読むことができます。

$ 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です。
あと、標準出力すると時間がかかるので表示結果は/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.readdirwithFileTypesオプションを使ったコードの計測結果です。

$ 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.readdirwithFileTypesオプションを使っていくのが良いかと思います。

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