テストの実行時間を2倍速くした話
webpack-dev-serverのテストを高速化しました。
jestを使っていて、--runInBand
を今までは使っていましたが、それを外しました。
--runInBand
jestはデフォルトでワーカーを使い並列実行を行います。
しかし、このオプションをつけるとそれが直列実行できます。
理由としては、serverのlistenするテストが多く、mochaで書かれていたため、急にjestに移行してもコード自体が並列実行できるものではなかったからです。
PR
このPRはベネチアで書かれました:)
— 蝉丸ファン (@about_hiroppy) 2019年6月9日
結果
直列実行
Test Suites: 1 skipped, 49 passed, 49 of 50 total Tests: 9 skipped, 419 passed, 428 total Snapshots: 152 passed, 152 total Time: 113.313s, estimated 173s Ran all test suites.
並列実行
Test Suites: 1 skipped, 49 passed, 49 of 50 total Tests: 9 skipped, 419 passed, 428 total Snapshots: 152 passed, 152 total Time: 60.5s Ran all test suites.
約2倍、速くなりました🎉
戦略
当たり前ですが、ポートを富豪的に使うことにより、並列実行をさせます。
ポートマップ
当初は個数じゃなくて、ポート番号にしてたのですが、柔軟性がなかったため、個数に変更しました。
また、この書き方だと手書きのミスによる重複が絶対に発生しません。
const portsList = { cli: 2, // cliのテストでは、2個ポートを使う sockJSClient: 1, // ファイル名が小文字だったので、別PRで直す SockJSServer: 1, Client: 1, ClientOptions: 3, MultiCompiler: 1, ... }; let startPort = 8079; const ports = {}; Object.entries(portsList).forEach(([key, value]) => { // no-plusplusの影響で ++ はなし ports[key] = value === 1 ? (startPort += 1) : [...new Array(value)].map(() => (startPort += 1)); }); module.exports = ports; // const [port1, port2] = require('./ports-map')['cli'];
起動時
jestには、初回起動時に一回だけ実行できる、globalSetup
というキーが存在します。
そして、起動時に使用するすべてのポートが空いているかを確認するスクリプトを実行させます。
// jest.config.js globalSetup: '<rootDir>/globalSetupTest.js' // globalSetupTest.js // node.jsのネイティブでポート確認するのめんどくさいのでこのモジュールを使う const tcpPortUsed = require('tcp-port-used'); const ports = require('./test/ports-map'); async function validatePorts() { const samples = []; Object.entries(ports).forEach(([key, value]) => { const arr = Array.isArray(value) ? value : [value]; arr.forEach((port) => { const check = tcpPortUsed.check(port, 'localhost').then((inUse) => { if (inUse) throw new Error(`${port} has already used. [${key}]`); }); samples.push(check); }); }); try { await Promise.all(samples); } catch (e) { console.error(e); process.exit(1); } } module.exports = validatePorts;
これで、テスト実行前にテストで使用するすべてのポートが使用されていない場合のみテストを実行できるようになります。
テストコード
各テストが以下のようにポートを引用すれば競合しないです。
const [port1, port2, port3] = require('../ports-map').ClientOptions;
ただ、ポート込みのsnapshotを取っている場合、結構更新が走りやすいので注意してください。
一つ真ん中とかでポート増やすとずれ込みます。
さいごに
まず、serverが起動する多くのテストを並列実行する場合のポート管理のベストプラクティスがいまいちわからないです。
サーバー起動ばっかりやってる参考になるプロダクトがあったら教えてください。
一旦、他のメンテナの意見を待ちつつもっといい方法があれば考えます。
この戦略は、レビューによって変わる可能性があります。
けど、一番何が言いたかったかって言うと、並列実行はCPUをめっちゃ使うけどくっそ速くなるぞ!!!ってことでした。(当たり前)
追記
Node6をサポート対象外にしたので、async/awaitが使えるようになりました😁