0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

コードリーディングAdvent Calendar 2022

Day 9

【コードリーディング】Node.js Pathモジュールのjoinメソッド成長記録(2009年〜2022年)後編

Posted at

前編では、joinメソッドの最新版のソースコードを読んで内部処理の実装を見ていきました。後編ではいよいよコミット履歴をたどりながらjoinメソッドがどのように成長していったのかを見ていきます。

joinメソッドの全コミット履歴

日付 メッセージ 修正内容 行数
Apr 15, 2009 everything is changed. i've waited much too long to commit. src/main.jsに実装 16
May 14, 2009 Rename main.js to node.js. src/main.jsからsrc/node.jsへリネーム 16
Jun 8, 2009 Module system refactor 引数.および./を空白文字へ置換 20
Jul 17, 2009 Move node.inherit, node.path, node.cat to new file: util.js src/node.jsからsrc/util.jsへ移動 20
Nov 1, 2009 Module refactor - almost CommonJS compatible now src/util.jsからsrc/node.jsへ移動 20
Dec 23, 2009 Fix require("../blah") issues normalizeArray新設によるリファクタリング 3
Apr 22, 2010 refactor path module to lib/path.js node/src/node.jsからnode/lib/path.jsへ独立 3
Nov 15, 2010 Implement new path.join behavior ワンライナーから多様なケースへ対応 12
Dec 2, 2010 more lint 表記統一などの修正 11
Jan 6, 2011 Remove keepBlanks flag from path functions 状態管理に使っていたkeepBlanks廃止 4
Jan 7, 2011 Path.resolve, path module windows compatibility windowsとposixに分けて2つ実装 16, 7
Jan 7, 2011 Lint Lint。表記等の修正 18, 8
Aug 10, 2012 path: small speed improvements パフォーマンス改善のためのメソッド置き換え 18, 7
Nov 21, 2012 windows: fix normalization of UNC paths UNCパス対応 28, 7
Feb 22, 2013 path: join throws TypeError on non-string args TypeErrorの処理を追加 31, 10
Feb 22, 2013 Throw TypeError on non-string args to path.resolve TypeErrorメッセージ修正、リファクタリング 31, 10
Jul 25, 2013 lib: macro-ify type checks 型確認の処理を新設したマクロ関数へ置き換え 31, 10
Aug 2, 2013 src: Replace macros with util functions マクロ関数をutilモジュールのメソッドへ置換 31, 10
Jan 22, 2014 path: improve POSIX path.join() performance posixバージョンのパフォーマンス改善 31, 18
Nov 21, 2014 path: allow calling platform specific methods path.win32, path.posixとしてエクスポート 30, 18
Nov 23, 2014 Merge remote-tracking branch 'joyent/v0.12' into v0.12 内容的には上記コミットと同じ 30, 18
Feb 1, 2015 lib: reduce util.is*() usage utilモジュールの型確認関数をtypeofへ置換 30, 18
Jul 4, 2015 path: refactor for performance and consistency パフォーマンス改善と一貫性のリファクタリング 31, 18
Feb 10, 2016 path: performance improvements on all platforms パフォーマンス改善に伴う大規模修正 71, 18
Feb 13, 2018 path: replace "magic" numbers by readable constants ハードコーディングの文字コードを定数へ置換 71, 18
Feb 17, 2018 path: replace duplicate conditions by functions 繰り返し登場する判別処理を関数へ置換 66, 18
Dec 7, 2018 path: replace assertPath() with validator 引数のバリデーション関数を共通化のため置換 66, 18
Mar 1, 2019 path: minor refactoring 細かいリファクタリング 66, 18
Mar 1, 2019 path: more small refactorings 同じく細かいリファクタリング 66, 18
Mar 1, 2019 path: refactor more path code for simplicity ifによるネストを1階層減らすリファクタリング 64, 18
Nov 10, 2019 path: replace var with let in lib/path.js varをletに修正(修正漏れの修正) 64, 18
Apr 29, 2020 path: fix comment grammar コメントの文法(英語)の間違いを修正 64, 18
Dec 3, 2020 path: refactor to use more primordials premordialsを使ったリファクタリング 65, 18
Apr 15, 2021 typings: add JSDoc types to lib/path JSDoc用コメント追加 69, 22

2009/4/15に初めてコミットされてから現在に至るまでの34回分のコミットをまとめてみました。前編でも見たとおり、joinメソッドはWindows用とPOSIX用の2つの実装があるため、行数の欄にもWindows用、POSIX用の順に記載しています。

1つのメソッドも13年以上の開発期間となると非常に多くの改善が行われています。それぞれのソースコードを見ていくと、4つの期間に分けることができそうです。古い方から順番に紹介していきます。

創成期 (2009/4/15-2011/1/6)

js src/main.js
node.path = new function () {
    this.join = function () {
        var joined = "";
        for (var i = 0; i < arguments.length; i++) {
            var part = arguments[i].toString();
            if (i === 0) {
                part = part.replace(/\/*$/, "/");
            } else if (i === arguments.length - 1) {
                part = part.replace(/^\/*/, "");
            } else {
                part = part.replace(/^\/*/, "")
                           .replace(/\/*$/, "/");
            }
            joined += part;
        }
        return joined;
    };

};

joinメソッドの一番最初の実装が上記のコードになっています。前編で最新版のjoinメソッドのソースコードを紹介しましたが、受け取った引数をパスとして連結して返すというシンプルな機能は変わりないものの、ソースコードの中身はかなりの違いがあります。

引数の扱いには...argsのような残余引数(可変長引数)ではなく、argumentsオブジェクト1を使っています。これは2019/3/1のpath: more small refactoringsというコミットまで続きます。主な処理内容は、引数に含まれる特殊文字の置き換えになっています。

date message 修正内容 行数
Apr 15, 2009 everything is changed. i've waited much too long to commit. src/main.jsに実装 16
May 14, 2009 Rename main.js to node.js. src/main.jsからsrc/node.jsへリネーム 16
Jun 8, 2009 Module system refactor 引数.および./を空白文字へ置換 20
Jul 17, 2009 Move node.inherit, node.path, node.cat to new file: util.js src/node.jsからsrc/util.jsへ移動 20
Nov 1, 2009 Module refactor - almost CommonJS compatible now src/util.jsからsrc/node.jsへ移動 20
Dec 23, 2009 Fix require("../blah") issues normalizeArray新設によるリファクタリング 3
Apr 22, 2010 refactor path module to lib/path.js src/node.jsからlib/path.jsへ独立 3
Nov 15, 2010 Implement new path.join behavior ワンライナーから多様なケースへ対応 12
Dec 2, 2010 more lint 表記統一などの修正 11
Jan 6, 2011 Remove keepBlanks flag from path functions 状態管理に使っていたkeepBlanks廃止 4

10回のコミットが行われた2009/4/15から2011/1/6(11回目のコミットの直前)までを創成期としました。初めてnode/src/main.jsというファイルが出来た2009/4/15から、node/src/main.js -> node/src/node.js -> node/src/util.js -> node/src/node.js -> node/lib/path.jsと4回の移動(またはファイルのリネーム)を繰り返し、現在と同じnode/lib/path.jsへたどり着いています。ちなみに2009/4/15に生成されたmain.jsの先頭に定義されているメソッドがこのjoinメソッドでした。

js node/lib/path.js
exports.join = function() {
  var args = Array.prototype.slice.call(arguments);
  return exports.normalizeArray(args).join('/');
};

創成期の最後には、2009/12/23に導入されたnormalizeArrayメソッドに大半の処理を預け、argumentsオブジェクトを配列に置き換えてから処理を行うように変更されて、4行という非常に短いコードになっています。

OS適応期 (2011/1/7-2016/2/9)

js node/lib/path.js
if (isWindows) {

  // windows version
  exports.join = function() {
    var paths = Array.prototype.slice.call(arguments, 0).filter(function(p) {
          return p && typeof p === 'string';
        }),
        joined = paths.join('\\');

    // Make sure that the joined path doesn't start with two slashes
    // - it will be mistaken for an unc path by normalize() -
    // unless the paths[0] also starts with two slashes
    if (/^[\\\/]{2}/.test(joined) && !/^[\\\/]{2}/.test(paths[0])) {
      joined = joined.slice(1);
    }

    return exports.normalize(joined);
  }

} else /* posix */ {

  // posix version
  exports.join = function() {
    var paths = Array.prototype.slice.call(arguments, 0);
    return exports.normalize(paths.filter(function(p, index) {
      return p && typeof p === 'string'
    }).join('/'));
  }
}

2011/1/7のコミットPath.resolve, path module windows compatibilityから始まり、2016/2/9まで続く期間をOS適応期とします。この時期の最大の変化は、Windows用とPOSIX用に分けた2つの実装が誕生したことです。その要因となるのは、WindowsにおけるUNCパス23への対応です。先頭に2つのバックスラッシュが続くパスという特徴を考慮した処理がWindowsバージョンの実装にのみ追加されています。

date message 修正内容 行数
Jan 7, 2011 Path.resolve, path module windows compatibility windowsとposixに分けて2つ実装 16, 7
Jan 7, 2011 Lint Lint。表記等の修正 18, 8
Aug 10, 2012 path: small speed improvements パフォーマンス改善のためのメソッド置き換え 18, 7
Nov 21, 2012 windows: fix normalization of UNC paths UNCパス対応 28, 7
Feb 22, 2013 path: join throws TypeError on non-string args TypeErrorの処理を追加 31, 10
Feb 22, 2013 Throw TypeError on non-string args to path.resolve TypeErrorメッセージ修正、リファクタリング 31, 10
Jul 25, 2013 lib: macro-ify type checks 型確認の処理を新設したマクロ関数へ置き換え 31, 10
Aug 2, 2013 src: Replace macros with util functions マクロ関数をutilモジュールのメソッドへ置換 31, 10
Jan 22, 2014 path: improve POSIX path.join() performance posixバージョンのパフォーマンス改善 31, 18
Nov 21, 2014 path: allow calling platform specific methods path.win32, path.posixとしてエクスポート 30, 18
Nov 23, 2014 Merge remote-tracking branch 'joyent/v0.12' into v0.12 内容的には上記コミットと同じ 30, 18
Feb 1, 2015 lib: reduce util.is*() usage utilモジュールの型確認関数をtypeofへ置換 30, 18
Jul 4, 2015 path: refactor for performance and consistency パフォーマンス改善と一貫性のリファクタリング 31, 18

OS適応期は13回のコミットが行われ、UNCパス対応以外にもパフォーマンス改善や引数のバリデーションなどの修正が繰り返されています。コードの行数もWindowsバージョンは16行から31行へ、POSIXバージョンは7行から18行へと増加しています。OS適応期の最終形は、以下のような実装となりました。

js node/lib/path.js
var win32 = {};

win32.join = function() {
  var paths = [];
  for (var i = 0; i < arguments.length; i++) {
    var arg = arguments[i];
    assertPath(arg);
    if (arg) {
      paths.push(arg);
    }
  }

  var joined = paths.join('\\');

  // Make sure that the joined path doesn't start with two slashes, because
  // normalize() will mistake it for an UNC path then.
  //
  // This step is skipped when it is very clear that the user actually
  // intended to point at an UNC path. This is assumed when the first
  // non-empty string arguments starts with exactly two slashes followed by
  // at least one more non-slash character.
  //
  // Note that for normalize() to treat a path as an UNC path it needs to
  // have at least 2 components, so we don't filter for that here.
  // This means that the user can use join to construct UNC paths from
  // a server name and a share name; for example:
  //   path.join('//server', 'share') -> '\\\\server\\share\')
  if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
    joined = joined.replace(/^[\\\/]{2,}/, '\\');
  }

  return win32.normalize(joined);
};

var posix = {};

// posix version
posix.join = function() {
  var path = '';
  for (var i = 0; i < arguments.length; i++) {
    var segment = arguments[i];
    assertPath(segment);
    if (segment) {
      if (!path) {
        path += segment;
      } else {
        path += '/' + segment;
      }
    }
  }
  return posix.normalize(path);
};

Windowsバージョンのコード行数が大きく変化した要因は、UNCパスに関する実装説明の長いコメントの追加です。POSIXバージョンのコード行数が大きく変化した要因は、2014/1/22のコミットpath: improve POSIX path.join() performanceによるパフォーマンス改善のためであり、Array.prototype.joinによって配列要素の文字列を/で結合するという処理をfor文内の+=に置き換えています。4倍(1.5us)相当の改善効果があったとのことです。

またこの時期は引数として受け取った文字列(パスの一部)のバリデーションの手段が、typeof -> IS_STRING(マクロ関数) -> utils.isString(utilモジュールのメソッド) -> typeofと何度も変更になっています。最終的にassertPathというバリデーション用の関数の内部でtypeofを使う方法に落ち着いています。

パフォーマンス向上期 (2016/2/10-2022/12現在)

js node/lib/path.js
const win32 = {

  join: function join() {
    if (arguments.length === 0)
      return '.';

    var joined;
    var firstPart;
    for (var i = 0; i < arguments.length; ++i) {
      var arg = arguments[i];
      assertPath(arg);
      if (arg.length > 0) {
        if (joined === undefined)
          joined = firstPart = arg;
        else
          joined += '\\' + arg;
      }
    }

    if (joined === undefined)
      return '.';

    // Make sure that the joined path doesn't start with two slashes, because
    // normalize() will mistake it for an UNC path then.
    //
    // This step is skipped when it is very clear that the user actually
    // intended to point at an UNC path. This is assumed when the first
    // non-empty string arguments starts with exactly two slashes followed by
    // at least one more non-slash character.
    //
    // Note that for normalize() to treat a path as an UNC path it needs to
    // have at least 2 components, so we don't filter for that here.
    // This means that the user can use join to construct UNC paths from
    // a server name and a share name; for example:
    //   path.join('//server', 'share') -> '\\\\server\\share\\')
    //var firstPart = paths[0];
    var needsReplace = true;
    var slashCount = 0;
    var code = firstPart.charCodeAt(0);
    if (code === 47/*/*/ || code === 92/*\*/) {
      ++slashCount;
      const firstLen = firstPart.length;
      if (firstLen > 1) {
        code = firstPart.charCodeAt(1);
        if (code === 47/*/*/ || code === 92/*\*/) {
          ++slashCount;
          if (firstLen > 2) {
            code = firstPart.charCodeAt(2);
            if (code === 47/*/*/ || code === 92/*\*/)
              ++slashCount;
            else {
              // We matched a UNC path in the first part
              needsReplace = false;
            }
          }
        }
      }
    }
    if (needsReplace) {
      // Find any more consecutive slashes we need to replace
      for (; slashCount < joined.length; ++slashCount) {
        code = joined.charCodeAt(slashCount);
        if (code !== 47/*/*/ && code !== 92/*\*/)
          break;
      }

      // Replace the slashes if needed
      if (slashCount >= 2)
        joined = '\\' + joined.slice(slashCount);
    }

    return win32.normalize(joined);
  },

};

const posix = {

  join: function join() {
    if (arguments.length === 0)
      return '.';
    var joined;
    for (var i = 0; i < arguments.length; ++i) {
      var arg = arguments[i];
      assertPath(arg);
      if (arg.length > 0) {
        if (joined === undefined)
          joined = arg;
        else
          joined += '/' + arg;
      }
    }
    if (joined === undefined)
      return '.';
    return posix.normalize(joined);
  },

}

2016/2/10のコミットpath: performance improvements on all platformsから現在に至るまでをパフォーマンス向上期としました。2016/2/10のコミットによって上記のようにコードが大幅にリニューアルされています。前述したOS適応期の2014/1/22のコミットpath: improve POSIX path.join() performanceに続いてWindowsバージョンでもパフォーマンス改善の取り組みが行われています。

Windowsバージョンのパフォーマンス改善のために、POSIXバージョンと同じくArray.prototype.joinを使った\\による文字列の結合をfor文と+=を組み合わせた文字列結合に変更しています。またUNCパス対応の処理も、正規表現によって\\を検出する処理から、/\を文字コードで比較して判別する処理(コミットコメントではmanual parsersと表記)に変更しています。

date message 修正内容 行数
Feb 10, 2016 path: performance improvements on all platforms パフォーマンス改善に伴う大規模修正 71, 18
Feb 13, 2018 path: replace "magic" numbers by readable constants ハードコーディングの文字コードを定数へ置換 71, 18
Feb 17, 2018 path: replace duplicate conditions by functions 繰り返し登場する判別処理を関数へ置換 66, 18
Dec 7, 2018 path: replace assertPath() with validator 引数のバリデーション関数を共通化のため置換 66, 18
Mar 1, 2019 path: minor refactoring 細かいリファクタリング 66, 18
Mar 1, 2019 path: more small refactorings 同じく細かいリファクタリング 66, 18
Mar 1, 2019 path: refactor more path code for simplicity ifによるネストを1階層減らすリファクタリング 64, 18
Nov 10, 2019 path: replace var with let in lib/path.js varをletに修正(修正漏れの修正) 64, 18
Apr 29, 2020 path: fix comment grammar コメントの文法(英語)の間違いを修正 64, 18
Dec 3, 2020 path: refactor to use more primordials premordialsを使ったリファクタリング 65, 18
Apr 15, 2021 typings: add JSDoc types to lib/path JSDoc用コメント追加 69, 22

パフォーマンス向上期は現在までに11回のコミットが行われています。大きなパフォーマンス改善がなされて、やるべきことをほぼやりきったのか、この時期のコミット内容はコメント内の英語の文法ミスの修正や変数宣言のvarletに変える修正など、実は単純なものが多かった印象です。 

おわりに

非常に長くなってしまいましたが、Node.jsのPathモジュールで定義されているjoinメソッドの13年を超える成長を見てきました。ここ1年半ほどは特に変更がありませんが、13年を超える開発期間を経て、単に引数として渡された文字列をパスとして結合するというシンプルな機能であっても、たくさんの改善がなされてきたことがわかります。2022/12時点の最新版のソースコードは前編で詳しく紹介していますので、気になる方はそちらもご確認ください。

  1. https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/arguments

  2. https://e-words.jp/w/UNC.html

  3. https://learn.microsoft.com/ja-jp/dotnet/standard/io/file-path-formats#unc-paths

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?