WebAssemblyをNodeJS Native Addonの配布形式として使う

三行まとめ

  • Cライブラリzopfliをwasmにビルドして npmjs.com にリリースしてみた
  • wasmはポータブルなバイナリで、ネイティブコードと比較して半分程度の性能を期待できる
  • emscriptenは N-API と比べると出来ることが少なすぎるのが課題

背景

WebAssembly *1 の評価のために、Cで書かれたzlib互換の圧縮ライブラリ google/zopfli をemscriptenでwasmにビルドしてリリースしてみました。

評価だけでなくリリースまでしたのは、zopfliのnodejs native addonである node-zopfli をしばしばインストールできないことがあるという問題があってそれを解決したかったからです。nodejsのnative addonはnode-gypというビルドツールを使うことが多いようですが、これはPython 2.x などnodejsのエコシステム外のツールを利用するため、npm packageのインストールが難しいことがあるのです*2

WebAssemblyにビルドしたものを配布するのであれば、実質的にnaive addonに実行可能形式を含めてnpmjs.orgでま配信するということになり、nodejs native addonのもつビルド問題とは無縁でいられます。これでWebAssemblyの速度が十分であれば、 Cで書かれたライブラリのbindingをマシンに依存しない移植性のある実行可能形式で配布する という夢のようなことを実現できるというわけです。

成果物

github.com

npm install -D "@gfx/zopfli" でインストールできます。 compression-webpack-plugin + zopfli でgzip圧縮ファイルを用意する - Islands in the byte stream で示した require("node-zopfli")require("@gfx/zopfli") に置きかえるだけで動くはずです。

ランダムなバイト列を対象にベンチマークをしてみたところ、以下のようになりました(READMEから抜粋):

payload size: 1

  • universal-zopfli x 106 ops/sec ±0.79% (80 runs sampled)
  • node-zopfli x 201 ops/sec ±1.99% (82 runs sampled)

payload size: 1024

  • universal-zopfli x 1.37 ops/sec ±12.99% (11 runs sampled)
  • node-zopfli x 4.62 ops/sec ±3.34% (27 runs sampled)

payload size: 1038336

  • universal-zopfli x 0.26 ops/sec ±6.91% (6 runs sampled)
  • node-zopfli x 0.39 ops/sec ±1.35% (6 runs sampled)

ペイロードサイズによってパターンが違いますが、native binding版の node-zopfli の 30%から70%くらいの速度が出ています。 zopfli に関していえばこのくらいの速度で十分なので、そのままリリースすることにしました*3。しかもこれはNodeJS/V8にWebAssemblyが組み込まれたごく初期のもので、WebAssembly自体まだMVP (Minimum Viable Product) でしかありません。速度はこれから向上していくことでしょう。

なおemscriptenの共通ランタイムがかなり大きく、minifiedで50kbもあります。今回はNodeJSライブラリとして主に使うことを想定しているので関係ありませんが、ウェブ用として考えるとwasmのために50kbがもれなくついてくるというのは小さくない欠点です。この意味でも、現状ではNodeJS用としてはすでに実用可能でも、ちょっとしたウェブ用ライブラリのためにwasmを使える状態ではなさそうです。

しかしNodeJS用の技術としては、WebAssemblyは将来的に移植性のある実行可能形式として非常に有効である可能性を示せたと思います。

Binding Code

さてWebAssemblyは移植性のある実行可能形式としてすでに実用可能ではありますが、現状だと開発が非常に難しいという問題があります。

まず今回は、ある程度枯れているemscriptenを使うことにしました *4

emscriptenでzopfliをビルドする試みはすでに 2013年に imaya/zopfli.js として存在するため、Zopfli を Emscripten で移植した際の備忘録 : document やzopfli.jsのソースコードなどを参考にしつつ開発しました。

しかし開発は正直いってかなり難しかったです。wasm (ソースコード的にはC言語の関数)に対してはポインタも含む数値しか渡せないので、JSの文字列をutf8にencodeしつつ整数の配列にして *5、それをemscriptenのヒープにコピーしてポインタ(=ヒープのオフセット)を得てそれを引数とし、戻り値はこれまたポインタをいじってヒープの内容を Uint8Array に変換し、最後に確保したヒープを freeで解放し…という具合です。いかにもオーバーヘッドがありそうなことばかりしてますね。うまくいかないときのエラーメッセージもわかりにくく、単にbindingを作るならN-APIのほうが開発はしやすいでしょう。

コードはこんな感じです。

binding.c はともかく、 index.ts には苦戦の後がしのばれると思います。

さすがにこのままだとつらいので、そのうちemscripten的にもWebAssembly的にも何とかなるとは思います。とはいえ現状でも文字列(またはバイト列)を受け取りバイト列を返すことはがんばればできるので、すでにそれなりに面白いことはできそうです。

今後の展開

WebAssemblyは Web と付くのでブラウザ専用技術かと思いきや、NodeJSでもかなり有用な技術になる可能性があります。現時点でもNodeJSはスクリプト言語として高速な部類であり、WebAssemblyはそれを補完する技術として期待が持てます。

NodeJSの欠点としてよく語られてきたRDBMSと相性が悪いという特徴は、Promiseとasync/awaitによって解消されました。静的型がないという欠点も、TypeScriptによってある程度解消し、大規模な開発にも耐えられるようになりました。そしてこのWebAssemblyによって、一部の重い処理をCで実装しつつさらにそれをサーバーとウェブブラウザ両方で利用したりするということもできるようになりつつあります。これだけ好条件がそろえば、 NodeJSは今後、ウェブアプリケーション開発用言語として有力になっていくでしょうね!

そういうわけで、WebAssemblyは非常に注目している技術なのです。

*1:See also WebAssembly の基礎 - nmi.jp

*2:node-zopfliはさらにnode-pre-gypというビルド済バイナリをクラウドストレージに保存して利用可能であれば利用する、という仕組みなのですが、これのどこかでコケていてインストールできないことがあるようでした。ただし今回はnode-pre-gypには関心がないので深入りはしてません。

*3:ただしWebAssemblyが利用可能であるNodeJS 8.0以上が必要です。

*4:他、LLVMでwasmを生成したり Cheerp を使うという方法もあるようですが、現時点で一番安定しているのはおそらくemscriptenです。

*5:これはemscriptenが提供するユーティリティ intArrayFromString() でできますが…。

はてなブログのエントリにPlantUMLを埋め込む方法

PlantUML、便利ですよね。はてなブログでも使いたいですよね。ということでやってみました。

まずエントリの最後にこのスニペットを置きます:

<script>
  var a = Array.from(document.querySelectorAll("pre.code"));
  a.forEach(function (pre) {
    if (pre.attributes['data-lang'].value) return;
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "https://plantuml-service.herokuapp.com/svg");
    xhr.onload = function () {
      if (xhr.status === 200 && pre instanceof HTMLPreElement) {
        pre.parentNode.replaceChild(xhr.responseXML.documentElement, pre);
      }
    }
    xhr.send("@startuml\n" + pre.innerText + "\n@enduml");
  });
</script>

tagomorisさんのコメントをうけて追記: plantuml-service.herokuapp.com というのはビットジャーニー社が用意しているPlantUML描画用のendpointです*1

そうしたらこんな感じでPlantUML snippetを書きます(これはKibela Flavored Markdown 互換の記法です)。

```{plantuml}
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
```

すると出力はこんな感じになります。

Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response

本家PlantUML serverとちがって文字数に制限がないので安定して使えます。ただし、実際にはちょっとはてなブログの仕様上の制約があって {plantuml} というラベルを取れません。なので、 はてなブログのsyntax highlighterにとって有効な言語を指定していない code blocksをすべてPlantUMLとして処理してしまいます。まあ、そんなに大きな問題ではないでしょう。

*1:ソースはこちら: https://github.com/bitjourney/plantuml-service なお、このherokuにデプロイしてあるサービス自体は無保証、というかただのサンプルサービスなので、ずっと動いているという保証はありませんし、サーバーを増強する予定もありません。

AssemblyScriptでHello, world!してみた

github.com

AssemblyScriptという、TypeScriptのサブセットでありWebAssemblyにコンパイルできる言語があります。

※ WebAssemblyについては WebAssembly の基礎 - nmi.jp などをどうぞ

TSのサブセットとはいえ、WebAssemblyにコンパイルしやすくするために若干互換性がない部分もあります。たとえば、 ||&& といった演算子の結果はJSだと左辺ないし右辺の値ですが、ASの場合はboolになります。

とはいえ assembly.d.ts のおかげで普通にvscodeで開発できるので、生のWebAssemblyを書くよりはだいぶマシでしょう。

標準ライブラリは std/ 以下をみるといくらか定義されていて、たとえばstring.tsをみると文字列操作の様子がわかります。

https://github.com/AssemblyScript/assemblyscript/blob/master/std/string.ts

いまのところ usize を汎用ポインタ型として使うので非常に読みにくいですが、まあC言語を知っていれば読めないことはないでしょう。

…はい、TSのサブセットとはいうものの、ポインタ操作などは完全にC言語です。GCもないので、文字列操作をするにはmalloc()free() でメモリを操作するコードを書くことになりますし、free() をし忘れると当然メモリリークになります。

とりあえず触ってみようと思い、hello()とadd(x, y)だけするコードを書いてみました:

https://github.com/gfx/hello-assemblyscript

package.jsonとindex.json をみれば分かる通り、assemblyscript がコンパイラで assemblyscript-loader がコンパイルしたwasmのローダーです。

NodeJS v8.0 からWebAssemblyのサポートが入ったので、NodeJSがあれば手元ですぐ試せます。 npm install && make で実行できるかと思います。

WebAssemblyはemscriptenを使えばCのコードから生成できるので、ASの用途は限られるとは思います。ただemscriptenに比べるとランタイムがコンパクトで使い方もES modules準拠なのはメリットかもしれません。emscriptenにくらべると開発環境のセットアップも非常に楽です。

独自性の強い TSの皮をかぶったC言語で、しかも既存の資産もないという状況はかなり厳しいように見えますが、たとえばMessagePack encoder/decoderやJSON processorのような、小規模で速度が求められるようなケースでは使えるかもしれません。

ISUCON7 予選通過した!

スギャブロエックス(id:sugyan, id:kazeburo, id:gfx) で予選に出場して2日目2位でした。去年は予選敗退だったので2年ぶりの本戦出場です。

バランスの良い良問で大変楽しかったです。ISUCON運営チームに於かれましては大変おつかれさまでした&ありがとうございました。

isucon.net

スギャブロエックスのスコア推移:

f:id:gfx:20171023000645p:plain

repo: https://github.com/gfx/isucon7-qualify

言語

採用言語はnodejsでした。もともとはPerl界隈で知り合った3人ですが、最近だれも現役でPerlを書いておらず、go, ruby あたりでやるか〜みたいな話を最初していました。途中でnodejs実装が追加されることになったので、nodejsにしたい!と希望してこうなりました。

と驚かれる言語なんですが、 ぼくは 最近一番得意だし、速度的にもRubyよりも数倍速いのでgolangの次くらいに有利なはず、ということでnodejsにしたかったのでした。他の二人的には負荷が大きかったと思うけど、それが不利にならないくらいサポートは出来ていたと思う…!

前半

isubataというチャットサービス…という顔をして画像配信がボトルネックという感じでした。帯域が足りない…これは…ISUCON4で見たやつ…!

ISUCON4 本選の解説と講評 : ISUCON公式Blog

このへんはだいたいkazeburoさんとsugyanがいろいろやって解決してだいたい10万点突破するくらい。

ぼくはこのへんひたすらコードリーディングしてたりプチプチN+1を潰したりしていました。

後半

画像配信がそれほど問題ではなくなり、ウェブアプリの問題が大きくなってきたのでアプリケーションエンジニアとしてようやく活躍できるようになってとにかくアプリの変更を色々やってみるフェーズに。ひたすら試行錯誤です。

よくみるとnodejs実装は地味に無駄なクエリを発行してたりするのでとにかく潰す。難しいところは相談しながら、簡単なところはエイヤでやっていく感じに。このへんはISUCON1に出場したころは全然できなかったなーと感慨深い思いでした。

やったこと:

https://github.com/gfx/isucon7-qualify/commits?author=gfx

なんどかrevertしたりしてますね。

普段TypeScriptで補完バリバリの環境に慣れているせいか、変数のtypoが目立って苦しい感じでした。ローカルで動かせればそんなことには…と思うけどローカル環境のちゃんとしたセットアップはそれなりに大変なので判断がむずかしい…。しかしeslintのセットアップくらいすればよかった。

その他雑多な感想

  • はじめて組むメンバーだったけど、作業をいいかんじに分担できて、作業が重複したりコンフリクトしたりみたいなことがなくて効率よくできた
    • kazeburoさんがインフラ中心に全体をみて、ぼくとsugyanがアプリ側をみるという感じ
  • ボトルネックが「帯域(HTTP cache)」「クエリ」「CPU」それぞれ良い感じに設定してあって総合力が試される感じだった
  • WAFの置き換えまではやる余力がなかったので本戦までには選択肢として検討できるようにはしたい
    • nodejs実装でつかわれていたexpressは全然速いWAFではないので
  • 事前gzip圧縮をzopfliで…とかも余力があればやってよかったかも
  • redis, memcached, オンメモリキャッシュなどもまったくやらず
    • サーバー構成的にやる余地がなかったというのもあるけど、もっと時間があればオンメモリで1秒キャッシュみたいなのは試したかった気がする
  • サーバー三台でいいかんじにリクエストを受けるのは結局いい感じのアイデアを出せなくてちょっと悔しい
    • 全体構成はkazeburoさんの案でした
  • alp めっちゃいい
    • 今回だと alp -r --sum -f access_log.6.txt --aggregates '/history/\d+,/channel/\d+,/profile/\w+,/icons/\S+' とかで良い感じにまとめられたので最高

チームメイトのエントリ

compression-webpack-plugin + zopfli でgzip圧縮ファイルを用意する

sprocketsを使っているアセットは半ば自動的にgzip圧縮版ファイルが用意されるのでそれをnginxのgzip_staticなどでサーブすればいいわけですが、JSのビルドをwebpck化したときにそういえばgzipされたファイルを用意しなくなったなと。それでもまあ、nginxが圧縮はしてくれますが、nginx自身が行うon-the-flyよりも事前に時間をかけて圧縮するほうが圧縮効率はいいので、やらない手はありません。

というわけでcompression-webpack-pluginです。これはデフォルトだとgzip圧縮しますが、圧縮ルーチンをカスタマイズできるのでnode-zopfli *1を使うこともできます。こんな感じに:

const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require('node-zopfli');

// ...

webpack.plugins.push(
    // ...

    new CompressionPlugin({
      test: /\.js$/,
      algorithm: (content, options, fn) => {
        zopfli.gzip(content, options, fn);
      },
    }),
    
    // ...
);

これを自動でやってくれる zopfli-webpack-plugin というのもありますが、compression-webpack-pluginとメンテナが同じなわりにあまりメンテされてない様子なのでcwpにしました。

ちなみに元サイズ909kbのJSファイルは次のようになりました:

  • nginx on-the-fly: 331kb
  • zlib: 281kb
  • zopfli: 271kb

zopfliはわりと効果ありますね。なお node-zopfli は環境によってはインストールが難しいため、 WebAssemblyをNodeJS Native Addonの配布形式として使う - Islands in the byte stream で紹介したwasm版の @gfx/zopfli を使うほうが運用は楽かもしれません。

gzipよりも更に高い圧縮率をほこるbrotliもやりたいですが、nginxの設定をしないといけないのでまた後ほど。

cf. Brotli を用いた静的コンテンツ配信最適化と Accept-Encoding: br について | blog.jxck.io

*1:zopfliはzlibよりも高い圧縮率をほこるgzip実装です。

GraphQLの型を補完する Date, BigInt, Any を提供する graphql_types gem を書いた

github.com

クライアントサイドでもdecoderがないとただの文字列だったりオブジェクトだったりしますが、ひとまずRuby側だけでも。

Anyなんて使う機会あるの?って感じもしますが、「なんらかのオブジェクトの構造をもっているはずだが、クライアントサイドはその詳細を知る必要がない」ケースってあると思うんですよね。そういうときに使う想定です。多用するものではないと思いますが。

開発者向けにMarkdown JPというコミュニティを作ってみました

gitter.im

  • Markdown自体の仕様については、CommonMarkに期待しているので commonmark.org でよい
  • CommonMarkに収まらない拡張を日本語で議論できる場所がほしい
    • ルビや数式など
  • サービス間で(ある程度)互換性があることはMarkdownの大きな価値なので、その議論ができるといいと思っている
    • ビジネス的には競合だったりするもあると思うが、そこで差別化してもユーザーにとってメリットはないと思うので

という感じです。

8月にMarkdown Nightをやったら思いのほか反響があったので、それをうけてコミュニティの必要性を感じたというのもあります。

GraphiQL が "Mode graphql failed to advance stream." というエラーで起動しなったときのワークアラウンド

GraphiQL(グラフィクル)ってのはGraphQL(グラフキューエル)のAPI consoleです。GraphQL IDEと呼ばれることもあります。

github.com

(現行バージョン: v0.11.5, バグ確認バージョン: v0.11.2)

こいつには、どうも変なクエリを食わせると二度と起動しなくなることがあるという問題があります。このときdev consoleには "Uncaught Error: Mode graphql failed to advance stream." というメッセージがでてます。

LocalStorageへ保存しているのがダメなクエリだと思われるのですが、データを消してもすぐGraphiQL自体がLocalStorageに書き込んでしまってダメな状態を復活させてしまいどうしたものかという感じでした。

とりいそぎ「GraphiQLを閉じて、同じドメインの別ページでLocalStorageを消す」という方法がうまく回復できるっぽいのでそれでしのいでいます。


少し調べたところ、codemirror-graphqlかCodeMirrorのバグっぽいなと当たりをつけ、codemirror-graphqlで問題を再現できたので起票しました。

https://github.com/graphql/codemirror-graphql/issues/236

しかし、この起票した問題はテストが無限ループっぽい感じになって終了しなくなるというもので、例外が出るわけではないので違う問題かもしれません。

yarnpkgのenginesのバージョンチェックを無効にする方法

yarnpkg v1.0から、 package.jsonengines sectionのバージョンチェックが厳密になりました。これにより新しいnodejsやyarnpkgを試すのが面倒になります。

これにメリットを感じない場合は無効化しましょう。

具体的には、 ~/.yarnrcignore-engines true を書き足すと、 --ignore-engines が指定されたのと同じになりバージョンチェックが無視されます。

echo "ignore-engines true" >> ~/.yarnrc

Rubyの型定義ファイルを中央repoにしないほうがいい理由

あるいは私がDefinitelyTyped (DT) が失敗だと思っている理由、です。

あたりが話の発端です。


DTについては以前いまいちイケてない理由を書いたことがあります。

TypeScriptのDefinitelyTypedは「ダメでもともと、うまく使えればラッキー」くらいの距離感がよい - Islands in the byte stream

この時の話を一言でまとめると「ライブラリの作者ではない第三者がメンテしていることが多く、基本的に品質が低い」のがよくないというものでした。

それ以外にもいろいろ欠点があります。

また、 あらゆるライブラリの型情報をあつめる という野心的な試みなのはいいものの、そのせいでrepositoryをcloneするのに時間がかかり、パッチを作るのも大変です。

DTの場合はバージョニングも微妙で、型定義ファイルのバージョンはmajor versionだけオリジナルと一致させてteenyなどを自動的にインクリメントさせる方式をとっています。このため、ライブラリの特定バージョンを表す型定義ファイルというものは存在しません。このへんは本質的な欠点というよりは、DTのポリシーがイケてないという話かもしれませんが。

というわけで、いま私が関わっているプロジェクトではDTをあまり使っていません。DTは、基本的には使わないほうがいいと考えています。


追記:

TSはJavaScriptそのものではないのでいろいろ難しい面はあります。JS互換の静的型AltJSだけでもTypeScriptとflowtypeがあるし。