Yearly Node.js 2018

Node.js 2018 まとめ

この記事は HTML5j カンファレンスで発表した、 Node.js 2018 のまとめの話をブログに起こしたものです。

speakerdeck.com

ちょっとずるいですが、この記事一つで Node.js アドベントカレンダーJavaScript アドベントカレンダーの25日目の記事です。

10月にNode.js v11 がリリース

Node.js v11 は変更点はいくつかありますが、v11.0.0ではそんなに大きな機能はありません。代わりに性能向上と安定性向上を行っています。

これにはNode.jsのコア変更ポリシーが関わっています。

Node.js Core Policyである 「Less is More」

Less is More という言葉をNode.js の文脈で最初に使ったのはこの jsconf での発表が初めてですね。

www.youtube.com

要は「豊富な機能を追加するのではなく、最小限の機能でコアは小さくシンプルにする」という話です。 もともとは建築家の言葉で、「これ以上ないことは豊かなことである」とも訳されます。

さて、この発表の中でも大きな機能が少ない代わりに、フォーカスすることとして、安定性・性能・セキュリティを向上させることについて触れています。

今回はNode.js が2018年の中で、どういう形でこれらの非機能要件とも言える情報について改善を重ねてきたかを解説します。

安定性

安定性と一口に言ってもいろいろあるのでここでは、Node.jsが安定性を得るためにやってることを紹介します。

LTS

Node.js は長期間サポート(LTS)があります。

https://github.com/nodejs/Release/raw/master/schedule.png

このサポートはLTS対象のバージョンであれば2年間のパッチリリースが確約され、セキュリティのアップデートは3年間受けられるというものです。

今だとv6.xが4月までセキュリティアップデート期間、v8.xが4月までバグ修正のアップデート期間です。v10.xは予定では2020年の10月まで受けられます。

V8 の ABI 互換性サポート

Node.js の内部のJavaScriptエンジンであるV8はNode.jsに対しての非互換の修正があるときには事前の通知がされるようになっています。 また、V8自身はアップデートがある度に、Node.jsに対して最新のmasterを追加し、テストが落ちないかを定期的にモニタリングしています。

github.com

Jenkins によるCI実行

Node.jsでは、PR毎に各種CPU, OSのビルドをJenkins上でCIを回すことで壊れるかどうかを確認しています。

jenkins-node-ci
jenkins-ci

ただ、Node.jsのテストも膨大な数があるので、testが実行されると中には PASS したり、 FAIL したりする flaky なテストがあります。これらについてはレポートされる仕組みがあり、その中で詳細な調査を行いながら修正されていきます

flaky test
flaky-test

つい最近 flaky なテストが全部PASSして、グリーンになった!ということでコアメンバーが喜んでました。

(ただこのツイートにもあったとおり、明日にはすぐにイエローになってしまいましたが。)

アプリケーションを安定化させる試み

大きな機能追加はないですが、アプリケーション内でCPU負荷が高いときのinspectorや非同期処理実行時にtraceする async_hook といった機能が追加、改善されています。

Inspector | Node.js v11.5.0 Documentation

Async Hooks | Node.js v11.5.0 Documentation

これらの機能はnode.jsの内部状態をもとに解析するツールです。CPUのプロファイラはV8の内部プロファイラの機能を利用して状況を把握するためのツールです。こういった機能が増えることで、安定性を改善しています。

const inspector = require('inspector');
const fs = require('fs');
const session = new inspector.Session();
session.connect();

session.post('Profiler.enable', () => {
  session.post('Profiler.start', () => {
    // invoke business logic under measurement here...

    // some time later...
    session.post('Profiler.stop', (err, { profile }) => {
      // write profile to disk, upload, etc.
      if (!err) {
        fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
      }
    });
  });
});

性能

Node.js においては性能も重要な指標の一つです。これも維持するために色々やっています。

ベンチマークを常に測る

benchmarking グループというワーキンググループがNode.jsのパフォーマンスを常に計測しています。

benchmark
benchmark

これを見ながら大きなregressionが起きていないかとか、バージョン間で差を見ることもできます。基本的にV8がパフォーマンスを向上させているので、省メモリになっていたり、高速になっています。

また、マイクロベンチマークだけではなく、現実のアプリケーションでもどれくらい下がっているかを計測するためにExpressを使った航空機予約システムである、acmeair という模擬システムでも評価しています。

GitHub - acmeair/acmeair-nodejs: A Node.js implementation of the Acme Air Sample Application. With datastore support of MongoDB, Cloudant, Cassandra. With runtime support of Bluemix/CloudFoundry, Docker... With Micro-Services.

Worker

Node.js の昨今の利用例として、ネットワークサーバだけではなく、babel, webpackといったフロントエンドのツールとして使われることが多いです。この様な時には大量のファイルを変換したり、文字列連結をしたりするので、IOの時間よりもCPUの時間が支配的になります。結果としてマルチスレッド・マルチプロセスでの処理の方が効率的にCPUが利用できます。

Node.js: The Road to Workers

Turbo Boost Next Node.js - Speaker Deck

実際に筆者もbabelを使ってmulti-threadとmulti-process、シンプルに一つのプロセスを使ったもので比較してみました。筆者の計測結果を以下に載せます。

worker result
worker result

これを見ると、ファイル数が100以上であればマルチプロセスよりもマルチスレッドのほうが高速になるという結果が出ました。プロセスを起動するよりもスレッドを起動するほうがコスト的に若干安いので、こういう結果になりますが、まだSharedArrayBufferは利用していないので、メモリ共有をしだすとどうなるかはまだ考察していません。

ちなみに最近入った llhttp というパーサがやばい

最近入った Fedor Indutny 製の HTTP Parser ですね。

github.com

これ、えげつないっす。

HTTP Parser はこれまで C で書かれた http_parser が使われてました。 しかしながら、 Cのhttp_parserは中身を見るとメンテナンスしやすいとは言えず、またアクティブなメンテナもいなかったので徐々にブラックボックス化していました。

Fedor の作った llhttp は「JavaScriptで書いた処理をLLVMバイトコードC言語に変換することでHTTPのパーサを作ってしまう」というものです。正確にはTypeScriptで書かれており、TypeScriptで書いた処理をC言語LLVMバイトコードに変換しています。

これ、普通だと逆で、C言語 / LLVM で書かれたものを JavaScript でも呼べるように asm.js や wasm に変換する」というアプローチを取りそうですが、 Fedor は「 JavaScript で書いた処理を C言語 / LLVM に変換」しています。

中身を読むと分かりますが、実際にはCやLLVMのジェネレータがあります。Node.jsで試すなら、build optionで --experimental-http-parser を付けてビルドするか、 実行時に --http-parser=llhttp とやると最新の Node.js では実行できます。

llhttp
llhttp

セキュリティ

Node.js でも昨今問題になっているセキュリティについてもコアでの取り組みを紹介します。

セキュリティワーキンググループ

Node.js 内部では TSC と呼ばれるコアの内部で話し合いが行われています。毎回セキュリティのトピックは話されており、特に OpenSSLや V8 といった内部依存ライブラリの脆弱性があると事前に関係者だけに告知され、パッチの適用後、全体に通知されます。

nodejs.org

あんまり知られていませんが、 Bug Bounty プログラムも行われています。これにより、セキュリティの脆弱性をついたバグには報奨金が支払われるようになっています。

hackerone.com

セキュリティリリースがあると以下のように告知されます。

nodejs.org

セキュリティの取組み(npm, yarn)

コアのセキュリティではありませんが、3rd party製のライブラリでもセキュリティ障害が見られることがあります。 記憶にあたらしい所で行くと、 event-stream が別メンテナーによってセキュリティの問題を仕込まれた事がありました。

github.com

このような問題は急成長している npm のモジュールだと発生しがちです。対策としては今の所事後策で自分のリポジトリ内に問題があるかを調査することしかできません。そのようなコマンドを npm も yarn も用意しているので、積極的に使っていきましょう。

https://docs.npmjs.com/cli/audit

https://yarnpkg.com/lang/en/docs/cli/audit/

この手の npm|yarn audit コマンドを実行すると、自分の package-lock.json や yarn.lock 内にあるリポジトリ脆弱性の報告がないかを検証してくれます。

Web Standards

「Less is More」といっても例外があります。 Web 標準のAPI に関しては機能追加しようとする動きがあります。

コアコミッターの一人である James Snell さんの話にあった言葉を紹介します。

why node.js needs web standards
why node.js needs web standards

「Node.js は主にウェブアプリケーション開発者プラットフォームとしても今までも、今も存在している。一方で Node.js のコアは small core という哲学を表明している。この small core の中には Web Standards によるものも含まれている。」

これらの流れから、HTTP2 や ES Modules 、 Promise の改善といった機能追加は Web 標準の API と合わせるために行われています。

Promisify や fs.promises は Promise 改善の流れです。

util.promisify が追加された - from scratch

File System | Node.js v11.5.0 Documentation

また、 HTTP/2 から HTTP/3 までの流れも検討はされています。

ngtcp2をベースに HTTP/3 の JS 実装をしようという検討は書かれています。

今後の流れ: Unified JavaScript Platform

今後は JavaScript の共通プラットフォームとして統合していこうとする流れがあります。

Unified JavaScript Platform
Unified JavaScript Platform

現在は、 Web Standard API に関しては W3CWHATWG といったグループが作っています。Node.js の Standard API は我々 Node.js core memberが作っています。また、それらの中間にある JavaScript そのものの API や文法は ECMA/TC39 といったグループが作っています。

これらのグループには特にコンセンサスが取られているわけではなく、それぞれがそれぞれで緩く繋がっていましたが、今後はこの繋がりを強化して、もう少しお互いのコンセンサスを取りながら統合していきたいという話が NodeFest 2018 の Node Discussion でされていました。

W3C / WHATWG で作られたAPI と Node.js の API は歩み寄りをしていき、なるべく寄せていきます。また、その中央で ECMA/TC39 が仕様を固めるという風に三者がまとまりながら話を進めていくようにしていきたいという話がされていました。

Web API も Node.js APIECMAScript も求めているのは "ユースケース" です。さらにWeb API も Node APIECMAScriptも全部丸っと知っているのは仕様策定者よりも開発者になります。開発者、つまり僕らがライブラリやアプリケーションを作ってユースケースを作っていき、仕様策定者側にフィードバックしていく必要があります

つまり、リードしていくのは、仕様作成者だけではなく、我々です。

2019年は Node 学園祭は jsconf.jp として生まれ変わる予定ですが、そこでも仕様フィードバックの場を設けて今後の未来を一緒に作れるようにしていきたいとおもいます。

蛇足

この手のNode.js と JavaScriptアドベントカレンダーの活動も今後は JavaScript アドベントカレンダー一つにして、記事数が足りなくなったら「その2, その3」と増やせるようにしていきたいですね (というわけで Unified Advent Calendar Entry にしてみました)。

Chrome Dev Summitに参加しました!

Chrome Dev Summit に初参加しました!色々トピックとして気になったものを紹介してます。後直接 Addy Osmani とか Paul Irish とかに聞く機会があったので、色々ついでに聞いてきました。

Chrome も 10 周年なんですよねー。感慨深い。

1日目は「現在のChromeでできること、やってること」という感じで、ケーススタディやツールチェインの話が多めでした。 2日目は「未来のChromeでできること、今後やるべきこと」という感じでした。

1日目の熱かったもの

ProjectVisBug

github.com

特定のサイトに対して画像を変更したり、一部のコンテンツの内容を改変したりすることがGUIを通してできるツールですね。

f:id:yosuke_furukawa:20181116195400g:plain

意外と簡単にインストールして、ドラッグアンドドロップやフォントサイズの変更がGUIで変えられるので、オーサリングツールみたいな感じですね。普通にインストールしておくといいと思います。見た目をさっと変えてイメージ伝えるのに良いですね。

Squoosh

squoosh.app

画像のエンコード形式を変更したときの見た目をbefore after形式で見ながら自分の画像の最適なものに変更してくれるツールですね。ツールとしても有用ですが、ツールとしてというよりも中身がすごいですね。 WASMでwebpやmozjpegなどの各種エンコード形式をブラウザで変換してますし、それだけじゃなく、web workerを使っていたり、service workerを使っていたりとモダンな機能をふんだんに使ってますね。

f:id:yosuke_furukawa:20181116200037g:plain

ツールとしてはもちろん、今のOff the main thread や PWA や WebAssembly の全部入りのプロジェクトとして興味深かったです。

Performance Budget Case Studies

昨今のトピックであるPerformanceの話ですね。今回はPinterest だったり、Spotify だったりとパフォーマンスのケーススタディが多く、最近の仕事に近いので参考になりました。

特に Performance Budget という考え方が最近は主流になりつつあるので、各社の対応がどうなってるかが知ることができたのは非常に有用でした。

f:id:yosuke_furukawa:20181115225922p:plain

日経電子版以降、こういう事例が増えましたね。

ただ一方で、ちゃんと気をつけないといけないのは、「パフォーマンスを上げることが即ビジネスのKPIに繋がる」という話はビジネスのKPI次第なので危険ですね。いろんなファクターがビジネスKPIに関連する中でパフォーマンスを上げることはベースラインの品質を上げることはできてもビジネスKPIとは直接起因するファクターになるかは微妙です。

Chrome Dev Summitの話の中で面白かったのは、「Long Term のビジョンを持ちつつも、 Short Term での計画を作っていく」という点です。いきなりビジネスKPIを求めるのではなく、長期的なKPIを上げるというビジョンと短期的な成果を上げていくのが重要というのは水を浴びせられるような気持ちになりました。

f:id:yosuke_furukawa:20181115230703p:plain

この話が無茶苦茶刺さった。。。やっぱ一度パフォーマンス改善をやってみると分かりますが、そんな簡単にビジネスKPIに繋がらないんですよね。

『速くなったことでユーザ体験はよくなったとしてもユーザ体験を上げることが即売上につながるわけではない』、というか。ただし、じゃあやらなくていいかというと違っていて、それは目に見える成果につなげていくための階段の一歩目でしかなく、もっと着実にステップを踏んでいくことで最終的によくなるという話にしないといけないわけです。

2日目の熱かったもの

2日目からは Addy Osmani と直接話したり、色んな人と話に行っていた関係で、途中で退出したものもあったのですが、それにしても面白い発表ばかりでした。

Off the main thread

main threadを使わずにworker threadを如何にうまく使うか、というところに焦点を当てた話ですね。ここのトピックでもいくつか話はありましたが、まずはweb workerのpain pointの話からスタートします。

f:id:yosuke_furukawa:20181116180429p:plain

web worker はメッセージパッシングモデルで実施されていますが、メッセージのやり取りの際にserialize と deserialize が交互に発生します。このやり取りを thread hop と呼んでいて、thread hopがたくさん発生するとNGという話が書いてありました。

f:id:yosuke_furukawa:20181116180347p:plain

これに対して、 Tasklet と呼ばれるライブラリが提案されていました。これは Task 間での協調を簡単にするためのライブラリですね。まだ experimental とのことです。

f:id:yosuke_furukawa:20181116180317p:plain

更に言うと、 Worker にした所で手放しに良くなるわけじゃありません。

f:id:yosuke_furukawa:20181116180743p:plain

Worker にすると rendering はスムースになりますが、入力のレイテンシは遅れます。メインスレッドを阻害しないというだけで直接高速になるとかそういうものではない、ということですね。

worker dom や amp script と呼ばれる amp の仕組みでは、メインスレッドを阻害しないようにしており、このトレードオフをうまく使って実現されているとのことでした。

f:id:yosuke_furukawa:20181116181019p:plain

この他にも Actor Model などの考え方が紹介されており、UIをどう作るかが変わってきそうだなと感じました。

WebPackaging

f:id:yosuke_furukawa:20181116200329p:plain

WebPackaging の Signed Exchange というコンテンツに対して署名して、コンテンツオーナーであることが確認できればURLバーをコンテンツホルダーのURLに変更できるという仕組みが紹介されていました。

f:id:yosuke_furukawa:20181116173440p:plain

特にAMPで有用な技術ですね。AMPでは、Googleの保持するAMPのキャッシュサーバからコンテンツを配信することができますが、現状ではAMPのキャッシュサーバのURLは配信サーバのURLになってしまうため、これが問題視されています。

Signed Exchange はこのURLバーをコンテンツホルダーのURLに変更することが可能な技術です。コンテンツホルダーが署名し、その署名がコンテンツホルダーのものかどうかを検証することで実現されます。まだ仕様策定中の技術です。

Portals

次のページの navigation を seamless にするという話で Portals が紹介されていました。

f:id:yosuke_furukawa:20181116174216p:plain

SPA が提供するものの一つとして、部分レンダリング(header, footer等は変えず、main contents 部分のみをレンダリングする事)による、 seamless なページ遷移がありますが、これを SPA じゃなくても可能にしてしまう技術ですね。ページを先読みし、先読みしたことをイベントとして受け取ることができるのと、それを iframe のような形で表示することができます。別ページであっても部分レンダリングしてるかのように見せることができるのがすごいですね。

実際の活用例として、集英社の漫画を閲覧する機能を Portals で使った例が示されてました。

f:id:yosuke_furukawa:20181116173850p:plain

この他にも タスクの優先順序をスケジューリングすることができる、 Schedule API や Picture In Picture といった機能が紹介されており、 SPAじゃないとできなかったことが徐々にブラウザでやってくれるようになってきているなぁ と思いました。

WebPackaging と Portals を組み合わせることで「これまでサイト間やページ間で感じていたギャップを縮め、low friction から zero friction へ」という話がされていました。

f:id:yosuke_furukawa:20181116200516p:plain

インタビュー

折角の機会なので、色々な人に話を聞いてきました。Google の Paul Irish, Addy Osmani, Alex Russelと少しだけ話してきました。

Interview with Paul Irish

lighthouse などのパフォーマンスツールの開発者である Paul Irish と話してきました。

Q1. Performance Budget という話は多いが、既に React, React DOM, React Router, axios などを使っていると 60KB くらいは有り、さらにその上で moment.js などのライブラリや Polyfills が乗ってくると 300KB とかを超えてしまうが、 Paul Irish はどうしているか?

A1. 基本的にあまりライブラリは使わず、使うとしても重たくないのを選んで使うか、ブラウザの素の機能を使う。DateTime系の機能もdate-fnsにするか、もしくは intl の DateTime APIを使ってしまう。また、最近だとPolyfillsも限定的な環境 (IEとか)でしか使わないので、普段はbabelでtranspileもしない。

Q2. babelを使わないといけないのはいくつかあって、一つは JSX 、 もう一つは ES Modules なんだけど、どうしているか?

A2. 作ってるのがツール系だからあんまり使わないけど、しょうがないときは使う。

Q3. 社内で性能測定ハッカソン(ISUCONみたいなの) をやっていて、その時に headless chrome を high load performance tool として負荷かけつつ、チェックするのにも使おうと思ってるんだけど、プロセスが毎回起動するので負荷をかけるツールとしては使いにくい、もっと軽量なものを予定していないのか?

A3. あったほうが良いかも。ただやるなら Web SpeedTest みたいな感じでちゃんとしたクライアントから実行する仕組みでそれを複数台から実施する方が実際の環境には近いはず、作るのは大変かもしれないけど、作ってくれたら事例になる。

f:id:yosuke_furukawa:20181116174413p:plain

Interview with Addy Osmani

Q1. 会社の中でlighthouse をCIに使っているが、 lighthouse の点数を参考にしているだけで、他にメトリクスとして取るべき指標はあるか?

A1. JS Size, CSS size, image みたいなリソースのコストは取っておいた事例があるかも。

Q2. 事例で言うとJSはどれくらいのサイズにしている or するべきなのか?

A2. JS で言うと、 Pinterest は 200KB 以下という指標を持っている。昔は170KB以下が望ましいとされてたが、今は250KB程度までは問題ないと言われてる。 web.dev を有効活用して欲しい。

Q3. Guess.js みたいな考え方のものを使って先読みをしようと思ってるけどどう思うか?

A3. Guess.js は alpha 版だけど、あれは考え方みたいなものだし、そのまま使って欲しいと思っているわけじゃない。何か統計的な情報に基づいて先読みする仕組みは重要なのでやってみて事例にすると良さそう。

Q4. WebのフロントでPerfomanceを改善してみているが、最終的にビジネスKPIに繋がるような話にまでならないことが多い、この辺をどう考えているか。

A4. まさにそれはGoogle全体で課題だと思ってる。一方で「パフォーマンスを良くしたらビジネスKPIが上がる」というふうに短絡的に捉えるのは危険。パフォーマンスが悪ければ品質の問題になるが、良ければ必ずしも売上やユーザ数が上がるといったKPIに繋がるわけではない。いくつかのサブとなる指標を置いてやってみるのが良いと思う。

Q5. Pinterest がセッション時間が伸びて回遊時間が伸びたのを取ってたみたいな形?

A5. そのとおり。セッション時間が伸びたというのでユーザ 1人あたりが滞在する時間が増える、というのを指標にしている。

f:id:yosuke_furukawa:20181116174625p:plain

interview with Alex Russel

(帰りかけてる時にちょろっと話しただけで、全然ちゃんとは話せず・・・、また写真も取れず。)

Q1. ブログのエントリで「JS は CO2 」とか「DXはおとり商法」みたいな記事を読んだよ。あの話の中で理想とするJSのフレームワークはあるのか、または何も使わず plain なJSでやるのがいいのかはどう思ってる?

A1. JSのフレームワークは必要だと思ってる。ただブログに書いたとおりすごくtiny なもののが良いと思ってる。 preactとか svelte は理想の一つ。

まとめ

1日目は今のWebでできることを紹介しつつ、ケーススタディで非常に面白かったです。2日目は未来のWebでできることを紹介していました。どちらもワクワクする内容で、久しぶりにすごく刺激になるカンファレンスでした。

また、2日目の最後の方に Google の中で Web Performanceをやっている人たちと刺激的な話ができたのも面白かったです。来年もやるとのことですし、Google IO もあるのでまた行きたいですね!

Node.js における設計ミス By Ryan Dahl

Ryan Dahl は Node.js の original author ですが、彼の作ったプロダクト deno に関するトークが jsconf.eu 2018 でありました。 Node.js にずっと関わってきた僕が見て非常に興奮するような話だったので、しばらくぶりにブログに書き起こすことにしました。

背景

Ryan Dahl は2009年に Node.js の話を初めて公の場に公開しました。その時の「公の場」というのが「jsconf.eu 2009」です。

www.youtube.com

Video: Node.js by Ryan Dahl - JSConf.eu - 2009

この発表から Node.js が広まり、今やサーバのみならず、IoTデバイス、デスクトップアプリなど、様々なところで動作しています。

で、今回はその発表から9年の歳月が経過し、Node.jsに対しての設計不備について Ryan Dahl 自ら発表したという状況です。

2018.jsconf.eu

発表資料: http://tinyclouds.org/jsconf2018.pdf

動画:

www.youtube.com

今回の記事はこの話を超訳したものを紹介し、慣れてない方のために都度解説を挟みます。最後に古川の感想を書いて締めようかと思います。

当初のゴール

私 (Ryan Dahl) はNode.jsの初期開発と開発マネージメントを行っていました。当初の目標は「イベントドリブンなHTTP Serverを作れるようにすること」でした。 ある時点から Server side JS というゴールにスイッチしていきました。Server side JSにはイベントループモデルを取り入れることに成功しました。

WindowsでのIOCPと Linuxでのepoll、 OSXではkqueueを融和させ、libuvを作った事、それをcoreのJSレイヤでサポートしたり、npmを作ってユーザのコードを管理したりという一定の成功は得られました。

私は2012年にリーダーを引退し、Node.jsの開発を引き継ぐことにしました。『2012年の時点でもう既にゴールは達成された』と思っていました。しかし、今現在2018年にNode.jsを半年間使ってみたところそれは勘違いでした

Node に残っていたmission criticalなタスク

2012年頃にIsaacにリーダーを引き継いだ後、実際はまだまだいくつかのcriticalなタスクが残っていました。 それらのタスクは消化され、今のNode.jsは当時よりも良いプロジェクトになっています。

  • npmをnodeのcoreの中に入れること by Isaac
  • N-API というbinding API
  • libuv の構築 by Ben and Bert
  • governance と community の管理 by Mikeal Rogers
  • crypto周りのコードベースの大幅な改善 by Fedor Indutny

この他にもいろいろな物がありましたが、現メンバーの力によってかなり大幅な改善がされています。

しかし、私は「現時点の Node.js を半年間使ってみたが、自分の目的とは異なったものになってしまった」という感想を持っています。

mission critical なタスクはいくつかは解消されていましたが、いくつかのタスクはそもそもの設計の根幹に関わっており、解消しきれていませんでした。 このあとの話はそれらをリストアップし、どうやって deno が生まれたかについての話になります。

動的型付け

動的型付け言語は科学的計算を一度だけ行うのにはベストな言語です。 JavaScript は動的型付けの言語の中でもかなり良い方の言語だと今でも思っています。

ただ Node.js においては複数回トライエラーを繰り返しながら設計されることを想定しています。設計過程で起きたNode.jsのエラーの内容はわかりにくく、エラー時の解消方法もはっきりしていません。

Node.jsのこういう側面を見るたびに黒板を爪で引っ掻いた音を聞いたときのような嫌な印象を受けました。

せめて今ならもっとよくできるのではないか、という思いを持ちました。

古川注釈: 静的型付け

おそらく静的型付け言語のがエラーの内容がわかりやすく、解消方法もわかりやすいと言いたいのではないか、特にTypeScriptのように変換する事が現代は割と一般的なので。

Promise

Promiseは2009年に一度Node coreに記述されていました。しかしながら、2010年にはそれらをすべて消すということを決定しました

今でも愚かな決断だったと思っています。

async/await の抽象化を行うためには Promise は最初から入れておくべきでした。

Node.js のコアではPromiseが無いために今日まで非同期に関しての体験を悪くしてしまっています。

古川注釈: Promise

2009, 2010年のPromise騒動は覚えていて、Promiseを入れるという事が行われていたにも関わらず、当時はcallbackのほうがprimitiveでわかりやすく性能面でも利点が大きいという事になり、Promiseにするのはcoreの実装では不要という判断がされていました。

当時はここまでPromiseが今後のキーになるとRyanは思っていなかったのでしょう。

Security

V8それ自身はsecureなsandboxモデルを表現しています。この点についてもっと深く考察しておけば、もっと良いセキュリティの考察を得られ、それによって他の言語よりもより良いセキュリティを提供できたと思ってます。

例えば: ただのlinterなのに、networkアクセスやファイルへのフルアクセス権は不要ですよね。

古川注釈: Security

V8それ自身はただJavaScriptの実行エンジンであってファイルシステムへのアクセスやネットワークアクセスする機能は提供していません。標準出力を行う console.log ですら V8の対象の外です。それに対してNode.jsは fshttp といったファイル・ネットワークリソースへのアクセスを提供します。ただ現在のNode.jsの幅広いユースケースを鑑みると、Linterやbuildツールとして実行しているときにネットワークリソースへのアクセス権は不要であったり、ファイルの書き込み権限は不要であったりします。

これらをsandboxの中で適宜パーミッションを得ながら実行できればもっとより良いセキュリティモデルを提供できた、と言いたいのでしょう。

Build System (GYP)

GYPは大きな失敗でした。ビルドシステムは思ったよりも難しくて重要な根幹のシステムでした。

そもそもv8がgypを採用していて、Node.jsもその仕組に乗っかりましたが、その後 gyp から gn に build システムは移り、gypユーザーは取り残されてしまいました。

gyp のインタフェースはそこまで悪いものではないですが、JavaScriptのプロジェクト内で「JSONっぽいけどJSONじゃないPythonのsyntax」を使わされ、ユーザーにとってはひどい体験だったでしょう。

このまま gyp を未来永劫使い続けるのは Node.js のコアにとっては大きな失敗です。 V8 の C++ bindings を書くなら、今は FFI (Foreign Function Interface) を推奨します。

かつて、 FFI を推奨してくれた人たちが居ましたが、当時の私(Ryan)はそれを無視しました。

(ちなみに、 libuv が autotools をサポートしたことに関しては今でも残念に思ってます。)

古川注釈: gyp

FFI と gyp に関しては FFI のがポータブルである一方で当時はgypのが高速という事が言われていました。性能を優先したデザインを取ってgypをサポートしていました。

しかし、 gyp は python2 ベースですし、そもそも Chrome や v8 開発のために作られたビルドシステムです。Chrome や v8 のビルドシステムも現在は gn や bazel というビルドシステムに置き換わっていて、 Node.js がgypを使い続けるのはアーキテクチャ上負債になっています。

コアが特定プロジェクトのビルドシステムに依存するよりは FFI のような統一的な呼び出し方法のがまだ良かった、と言いたかったのではないかと思います。

(ちなみに Ryan は常に何かのビルドシステムに乗っかっては「失敗だったから変えるわ」って言うタイプで昔はWAFというビルドシステムにのって、それからgypに変えています。)

package.json

Node.js は package.json の中にある "main" フィールドを読んでそれを Node.js に require() でモジュールとして読み込ませることにしました。

最終的には npm は node.js の中のreleaseに含めることに成功し、それがデファクトスタンダードになっています。しかしながらそれは中央集権リポジトリを生み出してしまいました。

require("somemodule") と記述するのは明確な特定のモジュールを示しているわけではありません。 ローカルに定義されたモジュールの可能性もあります。npmのデータベースにあるモジュールなのかローカルに定義されたモジュールなのかは実は呼び出しているだけではわかりません。

f:id:yosuke_furukawa:20180604213609p:plain

また、 package.json はファイルを含んだディレクトリをモジュールの概念として扱っています。これは厳密的に言えば必要な抽象化ではありませんでした。 Web 上には少なくともその抽象化されたモジュールは存在しません。

また、不要な情報を多く含んでいます、ライセンス、リポジトリURL、description、こういう情報はプレーンなモジュールにおいては noise です。

import した時にファイルとURLsが使われていれば、パスにバージョンを定義できます。依存関係のリストも不要です。

古川注釈: package.json

いくつか示唆に富んだ話ですね。解説が難しいです。ここの話からモジュールの話やpackageの話が多く存在します。 現地で聞いていたときの印象としては Ryan Dahl は module は package.json 以下にあるディレクトリを指すのではなく、コアが提供するのは単一ファイルで十分なのではないかと思っているんだと思いました。

import の時に import foo from "https://example.com/foo/v2/index.js" とかで url で指定させたり、 import foo from "./foo/v2/index.js" などのファイルで指定できるようにするだけでよく、ディレクトリを指定させるのはアプリケーションごとにやれば良いと思っているのかと。

node_modules

f:id:yosuke_furukawa:20180604215229p:plain

moduleの解決アルゴリズムが相当複雑になってしまいました。 vendorのモジュールをデフォルトにした事は良いことではありますが、実際には $NODE_PATH は使われていません。

ブラウザのsemanticsからも外れてしまいました。

これは私のミスです、本当に申し訳ありません。でももうやり直すことは不可能です。

古川注釈: node_modules

module の解決アルゴリズムは node_modules 内の依存関係をたどって依存解決をします。また、 moduleは指定元からの相対パスでファイルを指定しますが、 $NODE_PATH 環境変数にパスがセットされているとそこからも読み込まれます。

この他にもいくつか hacky な方法がありますが、どの方法も微妙です。この状況を招いたのは node でもっとシンプルな方法を提供できなかったせいだと Ryan は語っていました。一方で今のNode.jsの仕組みをブラウザに逆輸入されているため、この意見については反対意見もあるようです。

".js" の拡張子なしで module を読み込ませられるようにしたこと

いわゆる require("somemodule").js の拡張子がなくても Node.js はモジュールとして読み込み可能です。 でも browser では src の指定に対して .js を省くようなことはしません。

module loader はユーザーの意図を表現するファイルシステムへのクエリーであるべきで、明示的な指定のが良いです。

index.js

index.js は require("./") で読み込めますが、これは require("./index.js") の省略です。 最初はこれが良いアイデアだと思ってました。ブラウザも //index.html を省略できます。

しかしながら、これも module loader を多少複雑にしました。

Node.js module 設計ミスサマリ

私の失敗の多くは module に関する部分です。どうやってユーザのコードを管理するかという部分の失敗が多くありました。

なぜこの状況になったかというと、イベントI/O に集中して注力してしまい、結果としてモジュールの設計を後回しになってしまった事にあります。

この重要性に早く気づいていればもっとこの状況をよくできたと思っています。

Deno

私はこれまでの設計ミスに基づいて新しいプロダクトを開発しました。それが deno です。

github.com

最初に言っておくと、全然まだまだプロトタイプレベルです。 動かないのが普通の状況なので、 lldb でデバッグして直したりしない限り動かないし、ましてやこれでなにか作るのは強くオススメしません。 (※ ちなみに古川はまだ osx で起動どころかビルドに成功していません。)

deno は v8 で動くセキュアなTypeScript 実行環境です。

Deno のゴール: Secure Model

deno は sandbox モデルになっています。デフォルトではネットワークアクセスもなければファイルの書き込み権限もありません。 (--allow-net --allow-writeをつけない限りはネットワークアクセスと書き込み権限が付きません。)

ユーザーが信頼していないツールをちょっと動かすみたいなケースではこれはセキュアなモデルです。(例えばlinterをちょっとだけ動かすとか)

また、 deno では「ダイレクトに任意のnative codeを実行すること」は許可していません。全ては protobuf の呼び出しによって間接的に実行されます。

以下の図を見てください。

f:id:yosuke_furukawa:20180604215242p:plain

古川解説: Secure Model

deno は OS のカーネルのごとく、 特権モードとユーザ空間を明示的に分けてるデザインなんですね。非常に面白いです。 Goの中で v8-worker と呼ばれる worker を起動して、 worker と main process の間で protocol buffer を経由して通信して特権のコードを実行するか決めてるんですね。

(面白いけど、ここまでのセキュアなものが本当に必要なのかは不明ですね。一方で最近 npm の脆弱性も増えているので必要な面もわかります。)

Deno Goal: モジュールシステムのシンプル化

Nodeの既存モジュールとの親和性は求めてません。

importは相対パス絶対パス、もしくはURLだけでしか指定できません。

import { test } from "https://unpkg.com/deno_testing@0.0.5/testing.ts"
import { log } from "./util.ts"

import には拡張子は必要で、基本的に一度読んだらキャッシュしますが、キャッシュを強制的に開放するときは --reload をつけて実行します。

vendoring に関してはデフォルトのキャッシュディレクトリの指定をしなければ実現できます。

古川解説: モジュールシステムのシンプル化

ファイルをモジュールの基本的な単位にするし、node.jsのnpmのことは完全に忘れて新しくするという潔さ。 また、 import には URL かファイルの相対・絶対パスしか用意しないので、 vendoring というかバージョン管理や固定は不要ですね。ファイルならそのままgitで管理できているし、URLの場合は絶対パスなので取得先が壊れたりしない限りは(理想的には)固定されます。

まぁただ本格的に使うなら vendoringとかpackage management 用の何かの仕組みを3rd partyが作ったりするんでしょうけど。 vgo とか bundler のように。

TypeScript コンパイラ

私はTypeScriptが大好きです。

TypeScript はとても美しく、プロトタイプレベルから巨大なシステムになっても構造化を保つことができます。

denoではTypeScriptのコンパイラをモジュールの参照解決とビルド成果物のインクリメンタルキャッシュに利用します。 TypeScriptのモジュールは変更されてない限りは再コンパイルしません。 通常のJavaScriptも使えるようになります(TypeScriptはJavaScriptのスーパーセットなのでそこまで難しくはありませんが)。

スタートアップを高速化するためにv8のsnapshotも利用する予定です(まだプロトタイプには入ってません)。

古川注釈: TypeScript コンパイラ

Ryan Dahl が TypeScript が好きなのは一つ前のRyan Dahl プロダクトである propel が TypeScript なところからも感じていました。ここはそこまで驚きはないです。 何個かのアプリケーションを作るうち、Ryan Dahl も型が必要だと思ったのでしょう。

v8 snapshot は ここで解説しましたが、所謂heapのsnapshotを事前に取っておいて起動を高速化するためのテクニックですね。

single executable file と最小限のリンク

deno それ自身が最小限の構成をすることを目指しています。 ldd で見てみると7つ程度しかファイルがありません。

 > ldd deno
linux-vdso.so.1 => (0x00007ffc6797a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f104fa47000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f104f6c5000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f104f3bc000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f104f1a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f104eddc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f104fc64000)

2018年であることを活用したモデル

Nodeのモジュールをコンパイルするのに Parcel をバンドルツールに使っています。Nodeで行っている事よりも最大限にシンプルです。

github.com

native code の中に 良いインフラストラクチャを作っています。httpサーバについても心配する必要はありません。既に動作は確認できています。 (Nodeの時はWeb Serverは今は手で自分で作る必要がありましたが、今は違います。)

また現時点ではJS以外のパートは Go を使っています。しかし、これはGoですべてやるという事でもありません。今は色々調査しています。 Rust や C++ も良いでしょう。

その他諸々

  • キャッチされない例外がPromiseで起きたら即座にシャットダウンします。
  • top-level await をサポートします。
  • 機能的に重なる場合はブラウザとの互換を取ります。

古川の感想

deno はまだまだプロトタイプレベルですが、今のNode.jsにもフィードバックできるところは多そうだなと思いました。そういう意味では使ってフィードバックをNode.jsにしていこうとは思いました。しかし今時点では積極的に使おうにも lldb 等のデバッガなしでは使えないので何かを作れるようになるのは先だなと思いました。

またNode.jsにおける module とは package のことであり、ディレクトリを指してます。一方で ES Modules や deno における module というのはファイルの事であり、最低限の単位しか持ちません。この時点で考え方において大きな違いがあると思いました。

Node.jsがディレクトリを単位とし、 package.json という定義ファイルがあったからこそ議論を進めた一方で、言語やコアのベーシックな機能として持つ最小限の module というのが何なのか、何であれば良かったのかについて考えさせられました。

いずれにせよ 9年越しに Ryan Dahl の話を聞けて大分面白かったです。他の jsconf と Node Collaborators Summit の話も面白かったのでいつか書きます

NaN === NaN が false な理由とutil.isDeepStrictEqual

NaN === NaN は false

NaN、つまりは Not a Number 同士の同値比較が false になるのは、よく JavaScript とかで罠だと言われていますが、罠でもなんでもないです。 false が返るという仕様です。仕様の経緯を追うとすぐに『 IEEE754 という浮動小数点の標準規格で決められているから』、という理由がヒットします。

では IEEE754 ではなんで NaN == NaN を false にしようという話になったのか、というのを調べてみました。 今回はそういう歴史の話です。

IEEE754

現在のプログラミング言語の処理系の多くが採用している浮動小数点の標準規格です。

この標準規格は以下のことを定義している。

- 基本形式: 二進および十進の浮動小数点数データの集合。有限な数(符号付ゼロと非正規化数を含む)、無限、特殊な「数ではない」値(NaN)から成る。二進形式3種類、十進形式2種類で、計5種類の基本形式が存在する。
- 交換形式: 浮動小数点数を効率的かつコンパクトな形で交換するのに使われる符号化形式(ビット列)
- 丸め規則: 算術や変換の際に数を丸める方式(端数処理)。5種類
- 演算: 基本形式に対する算術演算や他の演算
- 例外処理: 例外的状態の通知(ゼロ除算、オーバーフロー、その他)。5種類

またこの規格では、高度な例外処理、追加的な演算(三角関数など)、式評価、再現可能性などを強く推奨している。

wikipediaより。

wiki項目の中で NaN だけは特別な記述がされています。

NaNとの大小比較では、自分自身と比較した場合でも「大小不明な結果」を返す。

NaN - Wikipedia

ただ厳密に言うと、 NaN には signaling NaN と quiet NaN という二種類あり、 JavaScript の NaN はquiet NaN という 「NaNを比較した時に例外を上げない代わりに必ず false になる」というものですね。

なんで false なのか、という経緯

標準規格で決まっているのはわかったものの、なんで NaN === NaN を false にしたのか、というそもそもの経緯についても調べてみました。 NaNとは、『数学的に数字ではない、とされている値を計算機上で扱うときの便宜的な値』です。比較不可能な値同士を比較したという事でその時点で本来的には例外です。

ただし、 JavaScript では quiet NaN が採用されているため、例外はスローされず、 false になります。

同じように Infinity という『数学的に無限を表す便宜的な値』もあります。Infinity === Infinity は true になっていますが、これにもちゃんとした理由があります。

IEEE754 で表現しているのは丸めも含めた"近似値"になります。

IEEE754-2008 という改訂版では、 +∞や-∞への丸めも記述されています。

方向丸め
- +∞への丸め 正の無限大に近い側へ丸める。切り上げ (rounding up, ceiling) とも呼ばれる。
- −∞への丸め 負の無限大に近い側へ丸める。切り下げ (rounding down, floor) とも呼ばれる。

なので、 Infinity も他の数字と同じく丸められた近似された値です。『無限大』というNumber.MAX_VALUEですらない、上限を表すための近似値のため、 Infinity === Infinitytrue になる、それに対して、 NaN は近似された値ですらありません

近似されてもいない数字ではないもの同士の比較に対して、『 NaN === NaN が見かけ上同じものだからという理由で true になるのはおかしく、 false であるべき』、というのが IEEE 754 での主張です。ここまでが NaN === NaN が false な理由です。

+0-0 の比較演算

さて、 === の比較だと厳密等価という値比較になり、 NaN同士 を比較した場合は false になるというのは前述の通りですが、=== にも多少微妙な数字が有ります、それが +0 と -0 です。

IEEE754において、比較演算では +0-0 は等しいとされており、 +0 === -0 は true になります。しかし、実際の2進数上の表現は異なります(符号ビット部分)し、 1.0/0.01.0/-0.0 で得られる値も異なります(前者は Inifinity, 後者は -Inifinity )。ほとんどのケースでは +0 === -0 が trueでも困りませんが、比較以外の演算の時のみ+0と-0は分かれて扱われており、それらを無限小(無限大の逆)として扱うのであれば +0 === -0 が trueになるのはおかしいという話もあります。

IEEE 754における負のゼロ - Wikipedia

ES2015で追加された Object.is というのはこれを正しく処理するためのものです。 +0 と -0は Object.is() では false になります。

※ ただし、 NaN 同士の比較でも true になります。 Object.is(NaN, NaN) => true

IEEE754の制約を受けずに機能的に同一の値(SameValue)であれば true になる関数という事ですね。機能的に同一、という言葉だけでは分かりにくいのですが、要は NaN を含んだ配列で NaN が存在するかをチェックしようとするケースや、 Object.definePropertyで -0を指定するケースで利用するものです。

[1, NaN, 3].indexOf(NaN) // 必ず -1
[1, NaN, 3].findIndex((e) => Object.is(e, NaN)) //  1

Object.is は一般的な開発者が使うというよりも多少メタな領域で JavaScript を拡張したいライブラリーとかが使うもの、という認識ですが、憶えておいて損はないです。特に +0-0を分けておきたいときには有用です。

util.isDeepStrictEqual とは

object 同士の内容を比較する便利関数です。 v9 から追加されています。Node.js Advent Calendarでも紹介しました。

qiita.com

で、この util.isDeepStrictEqual が多少変な動きをするので、議論を重ねていたら、 NaN === NaN が false な理由とか JavaScript の同値には3種類あるとかそういう沼にハマって調べていた、というのがこの話を書こうと思った経緯です・・・(長かった)。

このツイートの元になったのは、 util.isDeepStrictEqualWeakMap|WeakSet の時に内容がどうであれ必ず true を返すという動きをするためです。

x y === Object.is util.isDeepStrictEqual
1 1 true true true
"foo" "foo" true true true
NaN NaN false true true
Infinity Infinity true true true
+0 -0 true false false
{ foo: 1} {foo: 1} false false true
{ foo: 0} {foo: -0} false false false
new Set([1, 2, 3]) new Set([1, 2, 3]) false false true
new Set([1, 2, 3]) new Set([4, 5, 6]) false false false
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 1}, {foo: 2}]) false false true
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 3}, {foo: 4}]) false false true

util.isDeepStrictEqualに関しては、Strict Equal という名前がついているので、厳密等価性 (=== )を表現するのかと思いきや、そうではなく、機能的に同一であるという方の Object.is と基本的には同じ動きをします(注意点1)。型が objectMapSetArray の時は中身を deep に比較するという動きを見せますが、 WeakMapWeakSetの時は内容が取れないので、 valueOf の値が空オブジェクトになるため、中身がどうであれ必ず true が返ってきます(注意点2)。

この動きについていくつか issue を上げてみて、様子を伺っていますが、 TSC としては議論中です。将来的に動きが変わるかもしれないので、v9時点では util.isDeepStrictEqualをヘビーに使うのは推奨しません。

個人的には StrictEqual という名前から想起しやすい動きをしてくれたほうが良いので、NaN 同士や +0, -0の動きも === と同じ動きのが良いです。このままにするのであれば、 util.isDeepSameValue という名前にしてほしいと思ってます。

WeakMap/WeakSet に関してはGCに依存した動きをしますし、そもそもコレクション同士を比較できるものじゃないので true よりは false もしくは例外をスローするか undefined みたいな特殊な値のが正しそうだな、と思っています。

参考資料

2017年振り返り

2017年振り返り

毎年激動の年ですが、今年も色々あったので振り返りをしていきます。

会社

リクルートテクノロジーズに転職して1年、マネージャーになりました。

codeiq.jp

正確にはマネージャーとシニアソフトウェアエンジニアの兼務なのですが、マネージャー側面の話はあまりしたこと無いのでこの CodeIQ のイベントは貴重でしたね。

チームビルディングの記事も書きました。 recruit-tech.co.jp

会社で仕事してるの?って聞かれること多いのですが、めっちゃやってます!!!!コードも書いてるし、マネジメントも一応ちゃんとしています。まだまだおもしろい仕事があるので興味あれば言って下さい!!!!

イベント・登壇

日本でのイベントや登壇はあまりカウントしてなくて、海外イベントの登壇は今回2回行いました。

How to create a local JavaScript Community in Node Interactive North America 2017 www.youtube.com speakerdeck.com

Turbo Boost Next Node.js in JSDC Taiwan 2017 www.youtube.com speakerdeck.com

英語での発表も苦手意識は薄くなってきましたが、やっぱりまだまだ緊張しますね。日本での登壇と違ってアドリブだったり失敗した時のリカバリができないので緊張しながらやってるのが分かります。

日本での登壇で一番評判がよかったのは以下の発表でしょうか。

You need to know SSR speakerdeck.com

PublicKey にも記事にしていただきました。 www.publickey1.jp

Node Community

Node 学園祭

今年も無事やれてよかったなと思いました。自分の思いが先行してていくつも失敗はあったのですが、発表・ワークショップ・ディスカッション・アフターパーティと結果としては大成功だったかなと。

yosuke-furukawa.hatenablog.com

Node Japan User Group 法人化

今年一番大変だったものの1つなんですが、Node Japan User Group を法人化し、 Japan Node.js Association にしました。

Japan Node.js Association

ぜんぜん慣れないながらも定款を書いたり、法務局を何度も訪問したりと色々やっていました。法人って時間がとにかくかかるのですが、去年からの宿題でずっとやらないといけないと思っていたのができたのがホント嬉しかったです。法人化すると法人データベースに名前が載るのですが、載った時はガッツポーズしましたね。

法人化もできて、今後も大きくなる土台作りが済んだのでNode.jsをますます発展できるようにこれからもがんばります。

インタビュー系記事

今年はインタビューで取り上げて貰う機会が多くてありがたかったです(なぜか緑の服着てることが多い)。

codezine.jp

freelance.levtech.jp

logmi.jp

codeiq.jp

app.codegrid.net

app.codegrid.net

執筆系

完全に色々滞りました。すいません、、、すいません、、、

React の記事を Web DB Press に納めましたが、こちらも厳密に言えば2016年の年末に頑張っていただけなので今年は執筆業としてはそこまで書いてこなかったかも。。。

gihyo.jp

一番大きな懸念であった法人化が終わったので来年は執筆も頑張っていきます。

OSS

Node.js の HTTP/2 に色々コントリビュートしてましたね。

https://github.com/nodejs/http2/commits?author=yosuke-furukawa

HTTP/2 周り全然最初動いてなかったので動くようにしてテスト追加してっていうのをやってたらv8でexperimentalながらリリースされました。(∩´∀`)∩ワーイ

でもこれもまだまだですねぇ、HTTP/2は次のv10までにexperimentalフラグが取れるように色々活動が進んでいますが、一番やりたいパフォーマンス面での貢献がまだまだ。。。

あと、Node.js のコアに関しては簡単なものは会社の若手ができるように社内で Node.js Core にコントリビュートさせる活動を開いています。この辺が活動の成果ですね。

https://github.com/nodejs/node/pull/17734 https://github.com/nodejs/node/pull/17699

活動していたら会社の若手が発表してくれました。嬉しかったです。

speakerdeck.com

Node.js コア以外だと agreed と呼ばれるライブラリをメンテナンスしたり、 redux 周りのライブラリをメンテナンスしたりしてました。

github.com

github.com

github.com

mizchiくんtwadaさんkoichikさん といったJavaScriptOSSが得意な同僚に囲まれて刺激的な環境で開発できるのでOSS開発を仕事として実施する事ができてすごく良い体験をしています。

ブログ系

この辺の記事がバズりましたね。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

書いている内容もどんどん専門的な話になっていて、難しくなってきているのですが、これまで以上に分かりやすく書けるように頑張ります。

まとめ

今年もブログを書いて、Node.jsに貢献して、発表して、という一年でしたが、会社での地位も大きくなってどんどん色々な事に貢献していけるようになりました。来年は会社でエンジニアファーストな技術グループを作っていくことに一層磨きをかけつつも、今年出来なかった執筆系にも力を入れつつ、これまで以上に技術的な所を頑張っていきたいと思います。

来年もよろしくお願いいたします。

【書評】『超速! Webページ速度改善ガイド 』を読みました。

ご恵贈頂いた本である『超速! Webページ速度改善ガイド 』を読み終わったので紹介します。

gihyo.jp

あほむさんと SSR Panel Talk した時の話

いきなり関係ない話をしますが、 著者のあほむさんとPixel Gridのりぃさんと一度 Node 学園でパネルトークをやらせていただきました。その時の話で印象的だったのが、下記の話でした。

  • セキュリティやパフォーマンスも非機能要件だが、セキュリティは『実施しなかったら会社に与えるダメージが大きい』のに対してパフォーマンスは『実施しなくても会社に与えるダメージは見えにくい』
  • ただだからといって疎かにしていいわけではない、セキュリティは損害が会社のリスクとして見えやすいが、パフォーマンスはユーザビリティといった売上部分の根幹に関わる
  • 幸い、あほむさんの会社にはたまたまパフォーマンスに気をつけてる人が多く、そういう人を育てる土壌もある

という話を10月にしてました。大分端折っていますが、単純に「いいなぁ」と思ったのを覚えています

そしたら、あほむさんと泉水さん(二人共同じ会社)からパフォーマンスの知見を本にした物が出るという話でした。これはまず買うしかない。

Webページにおけるパフォーマンスの本

Webページにおけるパフォーマンスを題材にした本というのはこれまでにも数多く出版されています。教科書的な話で言うと、古くはハイパフォーマンスWebサイトから、続・ハイパフォーマンスWebサイトハイパフォーマンスブラウザネットワーキングとそれぞれ出版されています。

僕も仕事柄このあたりの本は大体抑えています。3年以上前に出版された本ですが、ハイパフォーマンスブラウザネットワーキングはネットワークのレイヤでかなり解説されているので、今読んでも学びがあるかなり良い本だと思います。

ただ、『超速! Webページ速度改善ガイド』ほどプロトコルのレイヤから画像の圧縮形式の話、ブラウザの内部処理の話、JavaScriptGC、Service Workerの話までWebに関わる古今東西と未来をきちんと1冊にまとめている本は僕は知りません。

強いてあげるとすると、パフォーマンス向上のためのデザイン設計という本が多少近いでしょうか。でもこれもプロトコルレイヤの解説とブラウザの内部処理レベルの解説は超速本よりは見劣りします。

www.oreilly.co.jp

これだけの話をまとめているという点だけでも読む価値はあるかと思います。

超速本のターゲット層

細かい所も読もうとすると、玄人向けだと思います(自分が読んでも難しいと感じる所が多かったので)。とはいえ全く知らないという人でも手元において損する本ではありません。逆に全く知らない人でもパフォーマンスの職人が何を考えているのかを知るという意味では有意義だと思います。

読み方としては、『一般読者向けに概念を把握する』ところと『Webパフォーマンス職人向けに実践するところ』を分けて読むと良いと思います。最初に概念、次に実践という丁寧な構成になっているので、まずは概念から理解し、実践は分からなかったら一旦飛ばして深く知りたければ後で抑える、という読み方もできます。これ一冊だけで著者のあほむさん、泉水さんの知識と体験が凝縮されているので、全部読んで全てが分からなくてもしょうがないと思います。

超速本関心したところ

第四章・第五章のブラウザのレンダリングの所は先程述べた通り詳しく解説してる本自体が珍しいのでまずはそこが素晴らしいと思いました。

パフォーマンス改善本としては、よくあるのは「表示されるまで」を高速にするというのを解説しているところが多いのですが、表示されるだけじゃなく、アニメーションだったりスクロール時の計算だったりの「操作してから」のところに焦点を当てているのは素晴らしかったです。

超速本もうちょいこうした方が良さそうなところ

こういうテクニックめいたところよりも『組織の中でどうやって継続的にパフォーマンスをキープしてるか』だったり、『組織の中で発生したパフォーマンスの障害』だったりといった実例に即した内容を聞いてみたかったです。

どうしてもHTTPの基礎的な内容だったりJavaScriptの基礎的な内容で言うと専門的な本にページ数の関係では敵わないので、そこの解説にページを割くよりは概要にとどめた上で、より実際に起きたリアルワールドでの知見が読めるとより良い本になりそうだと思いました。

その辺に関しては「 MANABIYA.tech 」で著者の泉水さんとパネルディスカッションするのでそこで深ぼってみようかなと。

manabiya.tech

まとめ

何はともあれ、恵贈してもらったからというだけではなく、「買い」の本ですね。難しいと感じてしまうところもあるかもしれませんが、今を生きるパフォーマンス職人である、あほむさん、泉水さんの知識と体験が詰まった良い本です。

gihyo.jp

Node.js Performance 改善ガイド

Node.js Performance 改善ガイド

この記事は Node.js 2 Advent Calender の 5日目の記事です。

qiita.com

Node.js のパフォーマンスについての話がISUCON含めて徐々に増えてきているので、僕が業務でやってるパフォーマンス改善をやる際の流れとその時の方法について説明します。

まず「なんか遅いなぁ」と思って調べると、だいたい「Memory、CPU、FIle、Network」のいずれかが遅くなっていたり、負荷がかかっていることが多いです。このリソースのいずれに負荷がかかっているのかを調べるには top だったりなんだったりで調べてもらい、その後の状況を整理する所からが勝負です。

それぞれ微妙に改善するポイントが違うのでそれぞれどうやって原因を特定して、対策を講じるかについて書いていきます。

Memory の場合

ちなみに僕が負荷計測していて一番よくあるのがメモリです。 Node.js のメモリへの負荷が高まっており、ガベージコレクションが頻発してCPUにも負荷がかかって結果として遅くなる、ということが多いです。

Node.js でも他の言語と同様、一度使ったオブジェクトの開放漏れ、所謂メモリリークが発生するとメモリへの負荷が高まり、この状況に陥ります。

f:id:yosuke_furukawa:20171202233417p:plain

(もちろんメモリリークが原因じゃなく、正当にメモリを使いすぎている可能性もあります)

メモリリークかどうかを特定する

メモリリークが発生するのはどこかのオブジェクトがルートノードと呼ばれる所からアクセス可能なままの状態で放置されており、 v8 がオブジェクトを開放できない時に発生します。

https://dt-cdn.net/assets/images/content/ebook/javabook/gc-roots-1d9223317f.png

https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/

これを判別するには heapdump と呼ばれる手法で解析する必要があります。これはヒープメモリの領域を全て取得し、dumpするという手法です。

これには node-heapdump を利用します。

github.com

node-heapdump を使うと Node.js のヒープメモリ内の状況がわかります。

基本的には require('heapdump') としておくだけで良いです。UNIX系ならば SIGUSR2 のシグナルを送れば勝手に dump ファイルを取得してくれます。 もしも GC をかけてから取得したい場合など何らかの事前処理を伴ってからプログラマブルに取得したい場合は以下の要領で組み込むことも可能です。

const heapdump = require('heapdump')

process.on('SIGUSR1', () => {
  global.gc(); // gc関数は --expose-gc フラグを付ける必要があります。
  heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');
})

ここで取得したダンプファイルは Chrome の DevTools で解析できます。 DevTools のMemoryタブからアクセスして内容を確認します。

f:id:yosuke_furukawa:20171202235413p:plain

メモリ逼迫前と逼迫後の状態でheapdumpを取っておいてそれを比較します。DevToolsで比較操作をした結果が以下の状況です。

f:id:yosuke_furukawa:20171204122705p:plain

ちなみに最近実際に起きたので言うと、 React v15 の Server Side Rendering をしていた時に、React の createElement した時のHTMLオブジェクトがGCで回収されずにメモリを逼迫していました。 React v16にしたら解消されたのでそこまで深追いはしてませんが、特定には heapdump を使いました。

メモリリークではない場合

これは正当にメモリを多く利用するアプリということなので、メモリの拡張を検討しましょう。

Node.js では default で 1.5 GB までヒープメモリを保持しますがそれ以上になると Out of Memory でプロセスが終了します。これを拡張しましょう。

Frequently Asked Questions · nodejs/node Wiki · GitHub

A: By default, --max_old_space_size (which controls the upper limit of the V8 heap) is ~1.5GB. 

$ node --max_old_space_size=2048 index.js

この他にも --trace_gc--trace_gc_verbose といったGarbage Collectionが起きたときの詳細なログを取る方法もあります。これらを使って適切なタイミングで GC がかかっているかを調査するというのも手です。

--trace_gc // GCが起きた時にログを出力する

--trace_gc_verbose // GCが起きたときのログを詳細に出力する

これらの他にもGCのタイミングを調整する方法もあります。Node.jsではidleの時にGCを行いますが、それとは別のタイミング(メモリの状況に応じて)で実施することもできます。ただあまり使ったことはないです。

--min_semi_space_size (semispace(CopyGCにおいて、fromからtoへコピーされているがまだ残っているもの)の最小サイズ)

--max_semi_space_size (semispaceの最大サイズ)

--semi_space_growth_factor (semispaceの成長率)

CPU の場合

CPU高負荷で相談される事もメモリの次くらいに多いです。

僕がよく相談されるのは「開発中は気づかなかったけど、最大想定ユーザー数を見越して高負荷計測をした所、CPUに負荷がかかりすぎて想定ユーザー数をさばききれない」という問題です。高負荷測定ではCPUが高負荷になることも多いのですが、負荷がそこまでかかっていないのにCPUが100%になった場合はどこの処理に時間がかかっているのかを確認する必要があります。

どこの処理に時間がかかっているのかを確認する

いくつか紹介します。

  • Nodeでv8 simple profilerを使ってprofileする方法でまずは粗く計測する方法
  • flame graph を使ってどこにCPU負荷がかかっているのかを画像で確認する方法

これらで粗く問題領域を特定したあとは performance.now() だったり、 console.time だったりで関数の内部でどれだけ時間がかかるのかを計測して犯人探しをします。

v8 simple profiler

v8 が提供してくれているプロファイラをNode.jsから呼び出すという方法です。

Easy profiling for Node.js Applications | Node.js

$ node --prof index.js // これを実行すると isolate-0xNNNNNNNN-v8.log が出力される
$ node --prof-process isolate-0xNNNNNNNNN-v8.log // 注意: Node v9.0.0 - v9.2.0 では isolate-0xNNNNNNNNNN-v8.log のファイルの末尾に不正な行があり何も表示されない。最後の末尾の行を消せば動く。

これを使うとどこでCPUヘビーな処理が行われているかの概略を表示してくれます。

Statistical profiling result from isolate-0x103800000-v8.log,

 [Shared libraries]: 
   ticks  total  nonlib   name
     14    9.4%          /usr/lib/system/libsystem_platform.dylib
      9    6.0%          /usr/lib/system/libsystem_kernel.dylib
      2    1.3%          /usr/lib/system/libsystem_c.dylib
      1    0.7%          /usr/lib/system/libsystem_malloc.dylib

 [JavaScript]:
   ticks  total  nonlib   name
    109    1.2%    2.4%  LazyCompile: *EventEmitter events.js:11:22
     80    0.9%    1.8%  LazyCompile: *ReadableState _stream_readable.js:35:23
     75    0.8%    1.7%  LazyCompile: *IncomingMessage _http_incoming.js:20:25

 [C++]:
   ticks  total  nonlib   name
     24   16.1%   19.5%  t node::(anonymous namespace)::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)
     10    6.7%    8.1%  t node::Binding(v8::FunctionCallbackInfo<v8::Value> const&)
....

 [Summary]:
   ticks  total  nonlib   name
      2    1.3%    1.6%  JavaScript
     90   60.4%   73.2%  C++
      4    2.7%    3.3%  GC
     26   17.4%          Shared libraries
     31   20.8%          Unaccounted

 [C++ entry points]:
   ticks    cpp   total   name
     40   59.7%   26.8%  T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*)
     19   28.4%   12.8%  T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*)
....

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
     20   17.2%  UNKNOWN
      2   10.0%    T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*)
      1   50.0%      LazyCompile: ~startup bootstrap_node.js:13:19
      1  100.0%        Script: ~<anonymous> bootstrap_node.js:10:10
      1   50.0%      LazyCompile: ~emitNone events.js:113:18
      1  100.0%        LazyCompile: ~emit events.js:165:44
      1  100.0%          LazyCompile: ~resume_ _stream_readable.js:822:17
      1  100.0%            LazyCompile: ~_combinedTickCallback internal/process/next_tick.js:129:33
      1    5.0%    T v8::internal::Builtin_FunctionPrototypeBind(int, v8::internal::Object**, v8::internal::Isolate*)
      1  100.0%      Script: ~<anonymous> dns.js:1:11
      1  100.0%        LazyCompile: ~NativeModule.compile bootstrap_node.js:589:44
      1  100.0%          LazyCompile: ~NativeModule.require bootstrap_node.js:521:34
      1  100.0%            Script: ~<anonymous> net.js:1:11

....

このファイルの中で僕がまず見るのは [Summary] です。 JavaScript , C++, GC, Shared librariesのいずれで負荷がかかっているのかを見ます。 GCの場合はメモリを解析しますし、JavaScriptC++ の場合はそれぞれ [JavaScript] [C++] のカテゴリに書かれている内容を見てCPUを使ってる処理を特定します。

[JavaScript] のセクションは JavaScript の処理でCPU負荷がかかっているものがリストアップされます。その際に RegExpLazyCompile といったラベルが出てくることがありますが、これはそれぞれ処理の内容の概略を表しています。

  • RegExp: 正規表現が実行されています。正規表現は新しく RegExp オブジェクトを構築するのと、正規表現用のコンパイルが実行されるので、v8エンジンにとってのコストは軽くないです。普通に文字列を含むとかの計算をするだけなら indexOf とか includes とかを使う方がコスト的には安いです。
  • LazyCompile: 何らかの理由で遅延コンパイルになっています。v8の実行中に最適化がかかれば高速になりますがそれまでは高速化されません。ちなみに LazyCompile: *EventEmitter events.js:11:22 のように * が関数の先頭についていれば最適化された事を示し、 LazyCompile: ~NativeModule.compile bootstrap_node.js:589:44 のように ~ が関数の先頭についた場合は最適化が実行されなかったという事を示しています。
  • Builtin: ビルトインで組み込まれている JavaScript 関数です。Math.abs とか JSON.parse とかが該当します。ここがCPU 100%になってると標準関数の使い方が悪かったり、 JSON に巨大なオブジェクトを渡していたりする場合が多いです。

[C++] のセクションでは、 JavaScriptから C++に処理が遷移したあとのCPU実行状況を示しています。ここで出てくるのは v8 が実際に中で行っている処理の関数が出てくることが多いので、このレイヤで何かしようとしてもNode.jsのアプリケーションレベルでは厳しいときが多いです。

[Bottom up (heavy) profile] のカテゴリには実際にCPU負荷が高かったいくつかのポイントをリストアップし、それのコールスタックを表示しています。コールスタックが見れるのでどこの関数で負荷が高い処理をしているかが分かるようになっています。

flame graph を取得する

v8 simple profiler はどこの処理が負荷が高いのか概要をつかむときには使えますが、実際にどういう事をすると負荷がかかるのかわかっている時は flame graph でみたほうが分かりやすいです。

flame graph そのものが何なのかを分かっていない方はこの記事に詳しく書いてあるのでご一読ください。

www.brendangregg.com

日本語だとこの記事が分かりやすかったです。

http://deeeet.com/writing/2016/05/29/go-flame-graph/

Node.js には 0x と呼ばれる npm module があるのでそれを使うと MacOSUnix 環境では簡単にグラフを取得することができます。

www.npmjs.com

$ npm i 0x -g
$ 0x index.js
// sudo が必要なので password が求められることが有ります。

何回か処理をした後プログラムを終了すると下記のようなグラフが出力されます。

f:id:yosuke_furukawa:20171204180401p:plain

x軸の長さが登場した回数を表し、y軸がコールスタックの深さを示しています。コールスタックが深ければ深いほど関数呼び出しの回数が多いので負荷がかかります。ループの中で呼ばれていたりして何度も実行されているとx軸が長くなります。CPUが100%だったりして返ってこないという時はこのコールスタックの一番上が何かで特定すると分かりやすいです。

File の場合

File IOがボトルネックになるアプリケーションに遭遇したことはあまり無いです。たまに聞くケースで言うと、「Node.jsで大きなファイルの読み込み・書き込みをした時に遅い」と言われることがあります。例えば「ファイルアップロードするシステムを作っていて大きなサイズのファイルのアップロードが遅くなる」とか、「大きなサイズのファイルをダウンロードするのが遅い」とかそういう話ですね。

ただこういう大きなファイルを扱うアプリケーションはそもそもスケールさせるのが難しいのでバッチか何かで別途ファイルを生成して nginx であったり CDN に載せてアプリケーションサーバでサーブしないようにする方が良いです。アップロードもアプリケーションサーバを経由させず、S3に直接PUTするといった方法を取る事が多いです。

まずはそういう方法で回避できないか検討し、どうしてもアプリケーションサーバで扱う必要が出てきた時のみチューニングを実施します。

Node.js は御存知の通り非同期を基本としているので blocking することは基本的にはないのですが、容量が大きい場合は多少状況が異なります

大きなサイズのファイルをどうしても扱う時

Node.js は非同期を基本としていますが、大きなファイルを扱う場合は気をつける必要があります。

const http = require('http')
const fs = require('fs')
const { promisify } = require('util')
const readFile = promisify(fs.readFile)

const server = http.createServer(async (req, res) => {
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })
  // fsのreadFileを行ってdata.txtを読み込む
  // ただこのファイルが1GBとかある場合はこうすると、一気にメモリが消費されるとともに
  // File IOも負荷がかかってしまう。
  const data = await readFile(__dirname + '/data.txt')
  res.end(data)
});
server.listen(3000)

このコードを実際に動かし、並列でファイルをダウンロードしてみると、Node.jsのプロセスがメモリをどんどん使ってしまい、結果として File IOとメモリの両方に負荷がかかって遅くなるのを感じると思います。

そこでよく使われるのが少しずつ chunked でファイルをもってくる Stream です。

const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => { 
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })
  const stream = fs.createReadStream(__dirname + '/data.txt')
  stream.pipe(res) // pipe を使ってresponseに書き込む
})
server.listen(3000)

これならファイルサイズが多少大きくても少しずつファイルを読み込み、レスポンスに書き込むのでメモリにもFile I/Oにも優しいです。

ただこれもデフォルトでは64KBずつしかファイルを読み込まないので、ファイルサイズによっては遅く感じることもあります。この状況すらも避けたいのであれば、 StreamhighWaterMark パラメータを自分でチューニングし、バッファサイズを調整する必要があります。

https://nodejs.org/api/stream.html#stream_buffering

const http = require('http');
const fs = require('fs');
const bufferSize = 1000000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })

  // highWaterMark を設定する。ここでは 1MB ずつメモリにバッファリングする
  const stream = fs.createReadStream(__dirname + '/data.txt', { highWaterMark: bufferSize })
  stream.pipe(res)
})
server.listen(3000)

ここでのhighWaterMarkのサイズはレスポンス速度とリソース負荷とのトレードオフを取ることになります。

highWaterMark が増えればバッファリングされる量が増えてファイルの内容をメモリに貯めてから書き込むようになり、レスポンス速度の向上が期待できますが、その分メモリも増えますしFileIOも増えます。highWaterMarkを減らせば今度はレスポンス速度は下がりますが、 メモリやFile IOの負荷は軽減が見込めます。

Network の場合

Network がボトルネックになることは割と多いです。しかし Node.js のレイヤで何かチューニングすることはそんなに多くないです。シチュエーションとしては例えばBackendにAPIサーバが居て、そこに対してリクエストを送ってレスポンスが返るまで待つようなアプリケーション(いわゆるBFF)でBackendのAPIサーバがボトルネックになる、といったケースです。

このような状況の場合、まずは API サーバのチューニングから始まるので APIクライアントである Node.js のレイヤで何かすることはまだ無いです。あるとしたらキャッシュを持ってAPIサーバにリクエストを送る回数を減らすと行った措置でしょうか。

ただし、キャッシュは諸刃の剣です。またBackendのAPIサーバのチューニングをしたいと思っても自分達がハンドリングできない時もあります。そういった場合に Node.js のレイヤでもできることはあります。

keepalive を on にする

Node.js の http client はデフォルトでは keepalive を off に設定されています。

https://nodejs.org/api/http.html#http_new_agent_options

この状況下では、 http の接続を毎回実施している状況になります。httpkeepalive を有効にすることで接続するまでの時間を短縮することが可能です。

const http = require('http');
const keepAliveAgent = new http.Agent({ keepAlive: true });
options.agent = keepAliveAgent;
http.request(options, onResponseCallback);

弊社ではBFFから API サーバへの接続に axios を使っていますが、それも keepalive はデフォルトでは off になっているので下記の要領で on にする必要があります。

{
  url: '/user',
  baseURL: 'https://some-domain.com/api/',
  httpAgent: new http.Agent({ keepAlive: true }),
}

ちなみに https の場合も同様です。 httpsAPIを叩く場合はTLS接続の負荷を減らせるため、 keepalive の効果は特に大きいです。

しかしながら、 keepalive に対応していないAPIサーバもいますし、また1台に負荷が偏ってしまうのを防ぐため(接続するサーバを分散させるため)に keepalive を意図的に使わないようにしているAPIサーバも居たりします。そういったときにはこの施策は使えないので注意。

http.Agentkeepalive 以外にも下記のチューニングをすることが可能です。

new http.Agent({ 
  keepAlive: true,
  keepAliveMsecs: 10000 // keepaliveが有効になる期間 デフォルトは1秒 (1000)
  maxSockets: Infinity // 何個のsocketを最大同時に接続させるか デフォルトは Infinity
  maxFreeSockets: 256 // 接続している socket のうち、free 状態になってるsocketを最大何個まで確保するか デフォルトは 256
})

その他: 全体的にパフォーマンスを改善するためにやること

今挙げたリソースの観点からだけでなく、全体的にパフォーマンスを改善するために下記のことも検討します。

  • JITが効いているかを確認する
  • clusterが利用できないか
  • C++ addons vs JavaScript library

JIT が効いているかを確認する

v8 は内部で Just In Time compiler (通称JIT) を利用していますが実際に JIT が有効に働くかどうかは記述した JavaScript 次第です。

例えば

  • arguments を不適切に利用している場合
  • 引数に取り得るtypesが number だったり object だったりで一意に定まらない場合
  • ES2017+ などの新しいsyntaxを利用している場合

などなど、JITにとって好ましくない色々なケースが考えられます。

これらの JIT に好ましくないケースを回避してコードがうまく実行できるかどうかに関しては、いくつかのオプションを有効にしてログを取りながら確認する必要があります。

$ node --trace-opt --trace-deopt  --trace_ic index.js

それぞれ以下の意味があります。

--trace-opt JITがoptimizeしているかどうかをロギングする。

--trace-deopt JITがoptimizeできずにdeoptimizationが起きているかどうかをロギングする。deoptimizeが発生するとログが表示される

--trace_ic inline cache の状態をロギングする。

この他にも --print-opt-code といったoptimize後のコードを表示するオプションも存在します。

この中でよく見るのは --trace-deopt ですが、これを実行するとかなりの量のログが出力されます。 しかも Node.js の標準ライブラリ内のJavaScriptでも稀に deoptimization が発生しているので自分の関係のあるログだけフィルターして見ることをオススメします。

$ node --trace-deopt index.js | grep mysample

自分のコードであればなるべく deoptimization が発生しないようにコードを修正します。

よく見る deoptimizationの発生事例 で言うと、

  • babelREST/Spread operatorをトランスパイルした結果、 arguments に触るようになってしまい、deoptimization が起きるというケース
  • 関数が object の型のものも、 string の型のものも引数で受け付けるように柔軟な関数を定義しているケース

です。

前者の例ではServer Side では deoptimization が起きないように babel の設定を見直したりします。 後者の例は最近はFlowやTypeScriptなどで型が付けられるようになってきたので発生しにくくなりましたが、それでも union type などの柔軟な型を定義することができます。それによってdeoptimizationが発生した場合は関数定義を分けて対応するように修正します。

clusterが使えないか検討する

Node.js はシングルスレッドですが、clusterを使うとマルチプロセスで並列性が上がります。CPUコアが複数利用できるなら検討したほうがパフォーマンスが上がることが多いです。 clusterは普通のNode.jsのchild_processを使ったものでメモリの共有はできませんが、並列で計算させるのには使えます。また、最近では pm2cluster に簡単に変換させることもできるようにしているので、 pm2 を使っていれば簡単に cluster 化を行うことが可能です。

pm2.keymetrics.io

cluster も若干チューニングポイントが有ります。clusterは基本的に Round Robin されるように各種Worker プロセスに平等にリクエストを渡す作りになっています。リソース効率を上げるのであればこの方がオススメですが、どのWorkerに渡したかという情報をスケジューリングして上げる必要があるので前処理があります。この前処理すら不要でさっさと Worker に渡して速度を上げて欲しいという場合であれば、 clusterschedulingPolicy を調整する必要があります。

https://nodejs.org/dist/latest-v9.x/docs/api/cluster.html#cluster_cluster_schedulingpolicy

$ NODE_CLUSTER_SCHED_POLICY=none node cluster.js // こうすると Round Robin をやめてランダムに渡すようになる

ただ経験上、このようなスケジュールポリシーにまで手を入れるような事は無いです。むしろ Round Robin のままのが都合が良い事が多いです。

C++ addons vs JavaScript libraries

これは npm に上がっているライブラリを使う時に検討する事ですが、基本的に JavaScript で書かれたライブラリの方を選択するようにしています。

これは互換性だったり運用性もJavaScriptで書かれた物の方が上なのですが、パフォーマンスについてもよっぽどのことがない限りは Pure JavaScript のライブラリのが十分に高速ですC++ addons の場合、 JavaScript のレイヤから C++のレイヤを呼び出すときのオーバーヘッドがあるので、そのオーバーヘッドを無視できるくらい C++ で複雑な計算をする場合は利点があります。

しかしそうじゃない場合、特に頻繁にそこの計算処理を呼び出す場合においては普通に JavaScript で実装されている方がJITが効くことによって大体において高速です

まとめ

  • どの計算資源に負荷がかかっているかによって状況の整理方法は異なります。
  • Memory だったら heapdumpを取りましょう。
    • その後ヒープの差分を見ながら犯人を探しましょう。
  • CPUだったらまずは profile しましょう。
    • その後怪しい処理に目をつけたら Performance API か console.time で犯人を探しましょう。
  • File だったらまずは処理をアプリケーションの外に出せないか検討しましょう。
    • もしも外だしできなかったら Stream の利用を検討しましょう。
    • 必要に応じて Stream の highWaterMark でチューニングしましょう。
  • Network だったらまずは呼び出し先の状況を整理し、呼び出し先で解決できないか検討しましょう。
    • 呼び出し先の情報をキャッシュできないかを検討しましょう。
    • 必要に応じてkeepaliveオプションを有効にして接続にかかる負荷を軽減させましょう。
  • その他以下のことは余裕がある範囲で検討しましょう。
    • JIT が効いてるかどうか
    • cluster が使えるかどうか(これは設計時に検討したほうが良いです)
    • Pure JavaScript のモジュールを選んでいるかどうか (これも設計時に検討した方が良いです)

参考資料