最近やっていること2022年夏

概要

直近1年くらいでやっていたことをまとめる

全体の方向性

  • 持続可能な開発組織づくり
    • 開発生産性の向上
    • 各チームが自律的に開発できるように責任と権限を整備
    • 開発チームのフィードバックサイクルの向上
    • SREチームのトイル削減

具体的にやったこと

  • 課題の洗い出し
  • 定常作業のドキュメンテーション
  • ドキュメント削除会
  • トイル削減のための調整
  • リリースフローの整備
  • ワークフローのSlack化
  • DesignDocの提案とそれを用いたコミュニケーション
  • TerraformのCI改善
  • GitHub ActionsのOIDC連携
  • モノレポの設計・実装
  • コンテナのビルド時間削減
  • MongoDBのスロークエリ改善
  • 社内LT
  • 社外登壇
  • 壁打ち(アプリケーション設計、テスト設計)

まとめ

振り返るタイミングがあったのでまとめた。
粒度があれだけど一旦書き出してみた。
掘り下げるといろいろかけることがありそう。
今後を考えるいいきっかけになった。

too many open filesエラーが出たときにlsofコマンドで確認する

概要

too many open filesエラーが出たときにlsofコマンドで確認するときの手順を説明する機会があったのでまとめる。

環境

node -v
v16.16.0
docker -v
Docker version 20.10.17, build 100c701

用意するもの

Dockerfile
file.txt
index.js

Dockerfile

FROM node:16.16.0-bullseye-slim
COPY . .
RUN apt update && apt install -y lsof procps linux-perf
CMD ["node","index.js"]

サンプルプログラム index.js

const fs = require('fs');

const openFileAndDontClose = () => {
  fs.open('file.txt', 'r', (err, fd) => {
    console.log(`fd: ${fd}`)
    console.log(`err: ${err}`)
  });
}

setInterval(openFileAndDontClose, 1000)

1秒ごとにfile.txtを開き続ける。

検証

docker buildx build -t test .
docker run --name test test
docker exec -it test /bin/bash

nodeプロセスのPIDを確認

ps aux            
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4100  3340 pts/0    Ss   13:41   0:00 /bin/bash
root        10  0.1  0.4 352596 33644 pts/0    Sl+  13:41   0:00 node index.js
root        21  0.0  0.0   4100  3520 pts/1    Ss   13:41   0:00 /bin/bash
root        34  0.0  0.0   6700  3012 pts/1    R+   13:42   0:00 ps aux
ulimit -Sn
1048576
ulimit -Hn
1048576

ファイルディスクリプタの確認

/proc/PID/fd

ls -l /proc/1/fd
total 0
lrwx------ 1 root root 64 Jul 16 14:00 0 -> /dev/null
l-wx------ 1 root root 64 Jul 16 14:00 1 -> 'pipe:[56376]'
lr-x------ 1 root root 64 Jul 16 14:00 10 -> 'pipe:[55593]'
l-wx------ 1 root root 64 Jul 16 14:00 11 -> 'pipe:[55593]'
lrwx------ 1 root root 64 Jul 16 14:00 12 -> 'anon_inode:[eventfd]'
lrwx------ 1 root root 64 Jul 16 14:00 13 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Jul 16 14:00 14 -> 'pipe:[50868]'
l-wx------ 1 root root 64 Jul 16 14:00 15 -> 'pipe:[50868]'
lrwx------ 1 root root 64 Jul 16 14:00 16 -> 'anon_inode:[eventfd]'
lr-x------ 1 root root 64 Jul 16 14:00 17 -> /file.txt
lr-x------ 1 root root 64 Jul 16 14:00 18 -> /dev/null
lr-x------ 1 root root 64 Jul 16 14:00 19 -> /file.txt
l-wx------ 1 root root 64 Jul 16 14:00 2 -> 'pipe:[56377]'
lr-x------ 1 root root 64 Jul 16 14:02 20 -> /file.txt
lr-x------ 1 root root 64 Jul 16 14:02 21 -> /file.txt
lr-x------ 1 root root 64 Jul 16 14:02 22 -> /file.txt
lr-x------ 1 root root 64 Jul 16 14:02 23 -> /file.txt
...

23 -> /file.txt
fdの番号 -> 開いている対象

lsofコマンド

lsof -p PID
# もしくは
# lsof -c プロセス名
lsof -p 1
COMMAND PID USER   FD      TYPE DEVICE SIZE/OFF    NODE NAME
node      1 root  cwd       DIR  0,142     4096  270825 /
node      1 root  rtd       DIR  0,142     4096  270825 /
node      1 root  txt       REG  0,142 81180048 2890255 /usr/local/bin/node
node      1 root  mem       REG  254,1          2890255 /usr/local/bin/node (path dev=0,142)
node      1 root  mem       REG  254,1          2885690 /lib/x86_64-linux-gnu/libc-2.31.so (path dev=0,142)
node      1 root  mem       REG  254,1          2885735 /lib/x86_64-linux-gnu/libpthread-2.31.so (path dev=0,142)
node      1 root  mem       REG  254,1          2885704 /lib/x86_64-linux-gnu/libgcc_s.so.1 (path dev=0,142)
node      1 root  mem       REG  254,1          2885711 /lib/x86_64-linux-gnu/libm-2.31.so (path dev=0,142)
node      1 root  mem       REG  254,1          2886466 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28 (path dev=0,142)
node      1 root  mem       REG  254,1          2885698 /lib/x86_64-linux-gnu/libdl-2.31.so (path dev=0,142)
node      1 root  mem       REG  254,1          2885678 /lib/x86_64-linux-gnu/ld-2.31.so (path dev=0,142)
node      1 root    0u      CHR    1,3      0t0       5 /dev/null
node      1 root    1w     FIFO   0,11      0t0   56376 pipe
node      1 root    2w     FIFO   0,11      0t0   56377 pipe
node      1 root    3u  a_inode   0,12        0   12340 [eventpoll]
node      1 root    4r     FIFO   0,11      0t0   50866 pipe
node      1 root    5w     FIFO   0,11      0t0   50866 pipe
node      1 root    6r     FIFO   0,11      0t0   50867 pipe
node      1 root    7w     FIFO   0,11      0t0   50867 pipe
node      1 root    8u  a_inode   0,12        0   12340 [eventfd]
node      1 root    9u  a_inode   0,12        0   12340 [eventpoll]
node      1 root   10r     FIFO   0,11      0t0   55593 pipe
node      1 root   11w     FIFO   0,11      0t0   55593 pipe
node      1 root   12u  a_inode   0,12        0   12340 [eventfd]
node      1 root   13u  a_inode   0,12        0   12340 [eventpoll]
node      1 root   14r     FIFO   0,11      0t0   50868 pipe
node      1 root   15w     FIFO   0,11      0t0   50868 pipe
node      1 root   16u  a_inode   0,12        0   12340 [eventfd]
node      1 root   17r      REG  0,142        0 3807111 /file.txt
node      1 root   18r      CHR    1,3      0t0       5 /dev/null
node      1 root   19r      REG  0,142        0 3807111 /file.txt
node      1 root   20r      REG  0,142        0 3807111 /file.txt
node      1 root   21r      REG  0,142        0 3807111 /file.txt
...
lsof -p 1
COMMAND PID USER   FD      TYPE DEVICE SIZE/OFF    NODE NAME
...
node      1 root   20r      REG  0,142        0 3807111 /file.txt
  • FDは20番をreadで開いているという意味
  • TYPEのREGはregular file
  • NAMEは開いている対象

今回のプログラムはfile.txtを開き続けるので、file.txtが多く表示される。

総数だけ見たいときは下記

lsof -p 1 | wc -l

ulimitを変更する

docker run --name test --ulimit nofile=100:100 test
ulimit -Sn 
100
ulimit -Hn
100

上限を30にすると29まで使った次に、too many open filesのエラーが出ることを確認

docker run --name test --ulimit nofile=30:30 test
fd: 17
err: null
fd: 19
err: null
fd: 20
err: null
fd: 21
err: null
fd: 22
err: null
fd: 23
err: null
fd: 24
err: null
fd: 25
err: null
fd: 26
err: null
fd: 27
err: null
fd: 28
err: null
fd: 29
err: null
fd: undefined
err: Error: EMFILE: too many open files, open 'file.txt'

まとめ

too many open filesのエラーが出たとき、どのプロセスが、なんのファイルを開こうとしているのか確認する手順は下記

  1. ps auxでプロセス番号確認
  2. lsof -p PIDでファイルの上表を表示
  3. NAME欄で多く表示されているものを確認する

対応として、

  • プログラムで不具合はないか確認
  • ulimitで開くことのできるファイルの上限を上げる

参考情報

man lsof
...
  FD is followed by one of these  characters,  describing  the
                  mode under which the file is open:

                       r for read access;
                       w for write access;
                       u for read and write access;
                       space if mode unknown and no lock
                            character follows;
                       `-' if mode unknown and lock
                            character follows.
...
man lsof
...
TYPE       is  the  type  of  the node associated with the file - e.g.,
                  GDIR, GREG, VDIR, VREG, etc...
...
                  or ``REG'' for a regular file;
...

https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback

マイクロサービス移行とモノレポを選択した理由について

概要

仕事でマイクロサービス移行にともなってモノレポを採用した。 その理由をまとめる。 具体的な内容には触れない。

課題

長らくモノリスで開発してきたことによる

  • コード量の増加
  • テスト時間の増加
  • 依存関係によるデプロイの遅れ
  • 開発者への依存

が課題になっていた。

改善方針

これに対して、

  • マイクロサービス分離
  • モノレポによる開発体験の統一

によって解決を図った。

具体策

サービス分離

  • 機能単位でモノリスアプリケーションを分離し、サービスごとに開発サイクルを回せるように
    コードの変更差分に対してのみCI実行、サービスごとにCI並列実行

  • サービスごとに独立したデプロイ
    Gitタグで任意のタイミングでサービスごとにDockerイメージを作成し、デプロイ

これにより

  • CI実行時間の削減
  • ワークフローの統一化
  • デプロイの依存関係の解消

を実現した。

モノレポ

npm workspaceを利用、モノレポフレームワークなし

  • 開発ワークフローの統一

  • 差分実行、並列実行
    GitHub Actionsで変更差分に対してのみCI実行、サービスごとにCIを並列実行

  • 開発ツール・バージョンの統一
    jestなどの各設定ファイルを共通化
    npm packageとしてワークスペースから参照

  • セットアップの自動化
    Makefileを実行するだけで開発をはじめられるように

なぜモノレポか?

工夫するとポリレポでもモノレポのメリットの一部は実現できる

  • GitHub Template Repository(マイクロサービスのテンプレート)
  • GitHub Actions Reusable Workflow(CIワークフローの共通化)
  • GitHub Actions Repository Dispatch(リポジトリ間のCIトリガー)
  • npm packageによる設定ファイルの共通化
チームのコラボレーションのため
  • 特定の機能が特定の開発者に依存しつつある
  • 特定の開発者しかメンテナンスしないリポジトリがある
  • コードを共有する仕組みが整っていない

これらの課題を解決するため

セットアップの仕組み化と適切なサービス分離により、認知不可を低めつつ
担当していないプロダクトコードも目に入りやすい状況を作る。

また、プロダクトコードはすべて一つのリポジトリにある状況を作ることで、リポジトリの管理が不要になる。

依存関係の解決のため

現状OpenAPIを別リポジトリで開発しており、 開発時の依存関係解決のワークフローを簡潔にしたい

今後必要なこと

  • CIプロセスの継続的な改善
    開発が進むにつれビルドや静的解析の速度が課題になる

  • CDプロセスの改善
     プログレッシブデリバリー

  • マイクロサービス化に向けた組織構成について
     現状の組織構成のままやっていけるかチーム内で検討

まとめ

開発ワークフローの課題をサービス分離にともなって解決していっているという話を書いた。
一番の目的は開発サイクルを早め、市場の変化に対応すること
現在、開発基盤を大方作り終え、今後フィードバックを受けながら改善していく

  • マイクロサービスや開発チームが増えてきたときに、開発体験が悪くならない仕組みを作る
     CIの差分実行・並列実行、CIの共通化、セットアップの簡略化などで実現

  • モノレポを採用することでチームのコラボレーションを促進する

npm workspaceとnpm installの挙動確認

環境

% node -v
v16.13.0
% npm -v
8.11.0

概要

npm workspaceでnpm installを実行すると
 1. ワークスペースルート
 2. (バージョンが異なるものは)各々のワークスペース
の順にインストールされる

構成

npm-workspace % tree . -a -I node_modules 
.
├── .gitignore
├── .npmrc
├── app-a
│   └── package.json
├── app-b
│   └── package.json
├── package-lock.json
└── package.json
save-exact=true
{
  "name": "npm-workspace",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "workspaces": [
    "app-a",
    "app-b"
  ]
}

挙動の確認

 %npm i rimraf -w app-a
 %npm i rimraf@2 -w app-b

結果A

 % tree .     
.
├── app-a
│   └── package.json
├── app-b
│   ├── node_modules
│   │   └── rimraf
│   │       ├── LICENSE
│   │       ├── README.md
│   │       ├── bin.js
│   │       ├── package.json
│   │       └── rimraf.js
│   └── package.json
├── node_modules
│   ├── app-a -> ../app-a
│   ├── app-b -> ../app-b
│   ├── balanced-match
│   │   ├── LICENSE.md
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── brace-expansion
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── concat-map
│   │   ├── LICENSE
│   │   ├── README.markdown
│   │   ├── example
│   │   │   └── map.js
│   │   ├── index.js
│   │   ├── package.json
│   │   └── test
│   │       └── map.js
│   ├── fs.realpath
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── old.js
│   │   └── package.json
│   ├── glob
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── common.js
│   │   ├── glob.js
│   │   ├── package.json
│   │   └── sync.js
│   ├── inflight
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inflight.js
│   │   └── package.json
│   ├── inherits
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inherits.js
│   │   ├── inherits_browser.js
│   │   └── package.json
│   ├── minimatch
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── minimatch.js
│   │   └── package.json
│   ├── once
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── once.js
│   │   └── package.json
│   ├── path-is-absolute
│   │   ├── index.js
│   │   ├── license
│   │   ├── package.json
│   │   └── readme.md
│   ├── rimraf
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── bin.js
│   │   ├── package.json
│   │   └── rimraf.js
│   └── wrappy
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       └── wrappy.js
├── package-lock.json
└── package.json

21 directories, 65 files

package-lock.jsonを削除し、コマンドの実行順を変えてみる

% rm package-lock.json 
% rm -rf node_modules && rm -rf app-b/node_modules
%npm i rimraf@2 -w app-b
 %npm i rimraf -w app-a

結果

% tree .
.
├── app-a
│   ├── node_modules
│   │   └── rimraf
│   │       ├── CHANGELOG.md
│   │       ├── LICENSE
│   │       ├── README.md
│   │       ├── bin.js
│   │       ├── package.json
│   │       └── rimraf.js
│   └── package.json
├── app-b
│   └── package.json
├── node_modules
│   ├── app-a -> ../app-a
│   ├── app-b -> ../app-b
│   ├── balanced-match
│   │   ├── LICENSE.md
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── brace-expansion
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── concat-map
│   │   ├── LICENSE
│   │   ├── README.markdown
│   │   ├── example
│   │   │   └── map.js
│   │   ├── index.js
│   │   ├── package.json
│   │   └── test
│   │       └── map.js
│   ├── fs.realpath
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── old.js
│   │   └── package.json
│   ├── glob
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── common.js
│   │   ├── glob.js
│   │   ├── package.json
│   │   └── sync.js
│   ├── inflight
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inflight.js
│   │   └── package.json
│   ├── inherits
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inherits.js
│   │   ├── inherits_browser.js
│   │   └── package.json
│   ├── minimatch
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── minimatch.js
│   │   └── package.json
│   ├── once
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── once.js
│   │   └── package.json
│   ├── path-is-absolute
│   │   ├── index.js
│   │   ├── license
│   │   ├── package.json
│   │   └── readme.md
│   ├── rimraf
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── bin.js
│   │   ├── package.json
│   │   └── rimraf.js
│   └── wrappy
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       └── wrappy.js
├── package-lock.json
└── package.json

以上の結果より、コマンドが実行された順番でワークスペースルートにインストールされることを確認できた。

npm installの実行順について

下記のような状態で

app-a/package.json

...
  "dependencies": {
    "rimraf": "3.0.2"
  },
...

app-b/package.json

...
  "dependencies": {
    "rimraf": "2.7.1"
  },
...
  • node_modulesとpackage-lock.jsonを削除してnpm iしたところAと同じ構造になることを確認
  • package.jsonのworkspaceの順序を入れ替えてnpm iしてもAと同じ構造になることを確認

package.json

  "workspaces": [
    "app-b",
    "app-a"
  ]
  • app-bの名前を変えてnpm iを実行すると、ワークスペースルートにrimraf@2.7.1がインストールされることを確認
%mv app-b app
% npm i 

よってnpm iの実行順序はディレクトリ名順で実行されることがわかった。

まとめ

npm workspaceとnpm installの挙動について初めて見たときよくわからなかったのでまとめた。

monorepoでpackage.jsonの初期値を設定する

概要

npm workspaceを利用するとき、package.jsonの初期値を設定することで構築の手間を減らす方法を示す。
これによりnpm initしたときに、npm scriptsやnpmパッケージが初期設定される。

環境

% node -v
v16.13.2
% npm -v
8.10.0

実装

% tree -a -I '.git|.gitignore' 
.
├── .npm-init.js
├── .npmrc
└── package.json

package.json

{
  "name": "hoge",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "workspaces": [],
  "dependencies": {
    "yargs": "17.5.1"
  }
}

.npm-init.js

const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const argv = yargs(hideBin(process.argv)).argv;
const scope = "@tom-256";
module.exports = {
  name: `${scope}/${argv.w}`,
  version: "0.0.0",
  description: "",
  main: "dist/index.js",
  scripts: {
    test: "jest",
    build: "tsc",
  },
  keywords: [],
  author: "",
  license: "ISC",
  dependencies: {
    express: "4.18.1",
  },
  devDependencies: {
    jest: "28.1.0",
  },
};

.npmrc

init-module=.npm-init.js

この状態でワークスペースを作ると下記のようになる。

npm init -y -w package-a
Wrote to /Users/.../hoge/package-a/package.json:

{
  "name": "@tom-256/package-a",
  "version": "0.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "test": "jest",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "4.18.1"
  },
  "devDependencies": {
    "jest": "28.1.0"
  }
}

まとめ

npm init時のscriptsやnpmパッケージの初期値を設定する方法を示した。
ただしこのままだと、初期値のnpmパッケージのバージョンが古くなってしまう。
npmパッケージのバージョン管理はRenovate等でおこなうことを想定したときに、.npm-init.jsに記述してあるバージョンをどのように管理にするか考える必要がある。
また、tscofnig.jsonや.eslinrc.jsなどの設定ファイルも構築するように.npm-init.jsに記述しておくことで更に手間を減らし、設定を統一することができる。
モノレポで構築の手間を減らしたり、設定を統一するケースで有用なのでメモした。

参考情報

https://docs.npmjs.com/cli/v8/commands/npm-init
https://docs.npmjs.com/cli/v8/using-npm/config#init-module
https://docs.npmjs.com/creating-a-package-json-file

補足

npm scriptsの設定を揃えたい場合はnpm set scriptを利用する。
https://docs.npmjs.com/cli/v8/commands/npm-set-script
例:npm set-script test "jest" -ws

Node.jsでパッケージが参照しているnode_modulesのパスを取得する

結論

require.resolveを使う

Use the internal require() machinery to look up the location of a module, but rather than loading the module, just return the resolved filename.

const path = require.resolve(<PACKAGE>)

https://nodejs.org/dist/latest-v16.x/docs/api/modules.html#requireresolverequest-options

環境

% node -v
v16.13.2
% npm -v
8.10.0

動作確認

npm init -y
npm init -y -w apps/api-a
npm i cowsay --save-exact
npm i express --save-exact
npm i express@4.0.0 --save-exact -w apps/api-a
require-resolve % tree -L 3 -I node_modules
.
├── apps
│   └── api-a
│       ├── index.js
│       └── package.json
├── package-lock.json
└── package.json

apps/api-a/package.json

{
  "name": "api-a",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "4.0.0"
  }
}

package.json

{
  "name": "require-resolve",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "workspaces": [
    "apps/api-a"
  ],
  "dependencies": {
    "cowsay": "1.5.0",
    "express": "4.18.1"
  }
}

CommonJSの場合

apps/api-a/index.js

const cowsay = require("cowsay");
console.log(require.resolve("cowsay"));

const express = require("express");
console.log(require.resolve("express"));

実行結果

node apps/api-a/index.js
/.../require-resolve/node_modules/cowsay/index.js # さかのぼって参照
/.../require-resolve/apps/api-a/node_modules/express/index.js # 最も近いもの 

挙動の説明

https://nodejs.org/dist/latest-v16.x/docs/api/modules.html#loading-from-node_modules-folders

上のドキュメントにある通り、node_modulesをさかのぼって参照する。
ディレクトリルートにexpress@4.18.1、exapi-aにexpress@4.0.0がインストールされており、
apps/api-a/index.jsは一番近いexpressを見に行く。
どのnode_modulesを利用しているかはrequrie.resolveで取得できる。

ES Modulesの場合

apps/api-a/index.js

console.log(await import.meta.resolve("cowsay"));
console.log(await import.meta.resolve("express"));

apps/api-a/package.json

  "type": "module",

実行結果

% node --experimental-import-meta-resolve apps/api-a/index.js
file:///.../require-resolve/node_modules/cowsay/index.js
file:///.../require-resolve/apps/api-a/node_modules/express/index.js

もしくは

import { createRequire } from "module";
const require = createRequire(import.meta.url);
console.log(require.resolve("cowsay"));
console.log(require.resolve("express"));
% node apps/api-a/index.js
/.../require-resolve/node_modules/cowsay/index.js
/.../require-resolve/apps/api-a/node_modules/express/index.js

参考ドキュメント

https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#importmetaresolvespecifier-parent https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#no-requireresolve

まとめ

どのnode_modulesを参照しているのかパスを取得する方法をまとめた。
モノレポはnode_modulesが複数できるので、node_module内のファイルを参照するときにパスを考慮する必要がある。

ESLintのパフォーマンスを調査する

環境

ESLint v8.13.0

調査方法

ルールごとにかかっている時間の割合を計測する

TIMINGオプションを付けると、実行時間が長い順のトップ10と、lint対象全体に対してのトップ10の割合が表示される

$ TIMING=1 eslint lib
Rule                    | Time (ms) | Relative
:-----------------------|----------:|--------:
no-multi-spaces         |    52.472 |     6.1%
camelcase               |    48.684 |     5.7%
no-irregular-whitespace |    43.847 |     5.1%
valid-jsdoc             |    40.346 |     4.7%
handle-callback-err     |    39.153 |     4.6%
space-infix-ops         |    35.444 |     4.1%
no-undefined            |    25.693 |     3.0%
no-shadow               |    22.759 |     2.7%
no-empty-class          |    21.976 |     2.6%
semi                    |    19.359 |     2.3%

https://eslint.org/docs/developer-guide/working-with-rules#per-rule-performance

debugオプションを使う

eslint src/**/*.ts --debug
eslint --cache src/**/*.ts --debug

下記のような情報が表示される。

  • Loading rule...読み込んでいるルール
  • eslint:linter Linting code for...どのファイルを読み込んでLintしているか
  • Cache hit/No cache found

上記をもとに不要なファイルが読み込まれていないか、キャッシュが効いているかを確認する。

https://eslint.org/docs/user-guide/command-line-interface#--debug

改善方法例

不要なルール、不要なファイルの見直し

キャッシュを使う

--cacheオプションを付けることでキャッシュファイルが生成され、
キャッシュファイルと差分があるもののみlintされる。

timeコマンドで計測した結果は下記

time eslint --cache src/**/*.ts
  • 初回実行時
    8.15s user
  • 二回目
    1.53s user

検証には適当な700行程度のTypeScriptのリポジトリを利用。
eslint-plugin-prettierが入っていたのでもともと遅い。

https://eslint.org/docs/user-guide/command-line-interface#caching

まとめ

ESLintのパフォーマンスを調査する手順をまとめた。
キャッシュオプションに関してはCIでも使えそうなので、今後必要が出たら調査していく。