Quantcast
Channel: from scratch
Viewing all 105 articles
Browse latest View live

JavaScriptの文化とleftpadの話とpadStartについて

$
0
0

無駄にラノベみたいに長いタイトル書いちゃったんですが、まぁやっぱり一言くらいは残しておくかと思ったので書きます。長いのでまとめだけでも見てもらえると良いかもしれません。

leftpadの話はかなり大事になっていて、Node.js界隈を中心としてその他のOSSをやっている全体的に話が波及しています。幾つかの記事を読みました。今回はJSの文化と歴史についてちょっとずつ書いていこうかなと思います。

本の虫: npmからkikとその他諸々が消されたまとめ

江添さんの話はすごくよくまとまっていて、ネタも含めた上で一番面白い話になっていました、ここで言われている下記の疑問に答えていこうと思います。

もっと憂うべきパッケージがある。isArrayだ。このパッケージは一日88万回もダウンロードされていて、2016年2月だけの一ヶ月間に1800万回もダウンロードされていて、72個ものNPMパッケージが依存している。

isArrayの中身はこうだ。

var toString = {}.toString;

module.exports = Array.isArray || function (arr) {return toString.call(arr) == '[object Array]';
};

結局、このコードは本質的にたった一行のコードである。

その他、is-positive-integerという整数が正の整数かどうか判定するパッケージがあるが、これも本質的には4行のコードである。ところが、このパッケージは昨日まで3個もの依存を持っていた(今は0個になっている)

なぜこんなにも多数のパッケージに依存をするのだ?

さて、これは JavaScriptエンジニアじゃなければ当然の疑問だと思う。
つまり、『isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか』『なぜこんなにも多数のパッケージに依存するのか』である。

isArray のこれまで

さて、行数に注目する前にisArrayについて深掘りしてみましょう。

昔々に存在していた JavaScript GoodParts という古典がある、これについては今日の JavaScriptではほとんどが過去の knowhow になってしまっているが、昔を知る上では非常に重要だと思う。

www.oreilly.co.jp

JavaScript GoodParts にはなんと Array かどうかをチェックする『 is_array という処理はこう書け』という指南が付いている。
「6.5 配列かどうか」より抜粋

var is_array = function (value) {return value &&
        typeof value === 'object'&&
        typeof value.length === 'number'&&
        typeof value.splice === 'function'&&
        !(value.propertyIsEnumerable('length'));
};

つまり、値が存在し、値がtypeof で 'object'と判定され、 length というフィールドが数字であり、 splice メソッドを持っていて、 length が列挙可能なものであれば、それは配列であるというわけだ。ここまで判定しないとちゃんとした配列なのかどうかは 昔のJavaScriptでは分からなかった。

さて、この方法が編み出されてから少し時間が経過してさらにカジュアルなチェックが存在するようになった。それが上に貼っていた、Object.prototype.toString.callメソッドの内容で判定する方法だ。

var is_array = function (arr) {returnObject.prototype.toString.call(arr) == '[object Array]';
};
is_array(['foo', 'bar', 'buz']); // true

さて、そもそも配列かどうかを判定するのにここまで必要な理由がわからないという指摘もあると思う。現代のブラウザでは上のようなチェックも既に過去のノウハウとなっている。今ECMAScript 5 が実装されているブラウザが手元にあれば、これだけで良い。

Array.isArray(['foo', 'bar', 'buz']); // true

JavaScript GoodParts が出版された頃のブラウザの世界では初期のようなチェックまでやらないといけなかったのかもしれないが、現代のブラウザではだいたいArray.isArrayでのチェックをしておけば大丈夫なはずだ。

JavaScriptでArrayの判定 - 思ったこと

isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか

さて、話を元に戻そう、過去を振り返った結果として、最初は複数行あったコードが時代とともに洗練されていくのが分かったかと思う。実はここに『isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか』と『なぜこんなにも多数のパッケージに依存するのか』の理由が隠れている。

JavaScriptの出自がWebブラウザで主に実装されている、という特性上、クロスブラウザでの対応が求められることが多い、その場合に Array.isArrayだけでチェックしていた場合、古いブラウザでは動作しなくなってしまう

そこで、 isArray ライブラリの出番になる、Array.isArrayが存在するかどうかを調べて、無かったら内部的に Object.prototype.toString.call()でのチェックをする。こういう『foo機能が存在するかどうかを調べて無かったら内部的に代替手段を用意する』事を polyfillと呼んだりponyfillと言ったりする。polyfill/ponyfillの細かい定義はおそらく誰か別な人が書くと思うのでそちらに任せる。

レガシーブラウザも含めた広い環境で動作することが要求されるアプリケーションであったり、Node.js v0.10でも動くことを求められているライブラリで全て polyfill をわざわざ手書きしていたら生産性が落ちてしまうのは想像しやすいと思う。そこで OSSライブラリに頼ってなんとかするような状況になる。

この polyfill を使う文化こそがJavaScriptOSSのコードをロードする文化であり、多数のパッケージに依存する原因である。

『たかだか1行とか11行の〜』という話だが、行数自体は関係なく、 polyfill/ponyfill を使って書いていたほうが全体的に整合が取りやすいことが多いから書いていることが多い。もちろん自分でpolyfillを書いたほうがコスト的に安いことも多いので、その場合はわざわざ OSSに頼ったりはしないが、ある程度成熟したpolyfillは自分で書くよりも実績があるのでそれを使っておく方が安心する事も多い。

また、 polyfill というのは言い換えれば『新しい関数が環境に行き渡ったら不要になる』ものだ。

いらなくなったら該当のpolyfillは削除できる。そうして徐々に新しい関数に適用していく事で今日のウェブは急に壊れることなく使えている

翻って leftpadの話

leftpadが unpublish された件 はコミュニケーションのミスや npm の仕様などの不幸が重なった結果発生した事故で、あんまり頻繁に起きるようなことじゃない、とはいえ、かなり大きなニュースになってしまった。もう npm からは今回の反省点も含めてちゃんと声明を出しているので個人的にはnpmの対応には納得している。

The npm Blog — kik, left-pad, and npm

『急に壊れることなく使えている』という話をしたが、今回は不幸が重なって一部のOSSが壊れてしまった。こういう事態を避けたかったら自前で書くほうが良いと言えるだろう、もっと言えば npm に限った話ではなく githubであったり bitbucket であったりが急な不幸に見舞われることだって無いとはいえない、それをどうしても避けたいのであれば、もう自分で管理するしか無い気はする。OSSに依存するっていうのはそういう事だと割りきって使っている。

さて、leftpad も isArray と同じようなたった 11行のコードである、頑張れば1行でも書ける。

leftpad のコードがまずいとかそういう話で色々語られてはいたが、確かに愚直にループを回して足していくより今は String.prototype.repeatというメソッドがあるのでそれを使ったほうが綺麗にコードが書けるし、高速である。

paulownia.hatenablog.com

ただ String.prototype.repeat自体が ES2015から新しく定義された関数であり、leftpadのような幅広い環境で動かすような事を目的としたライブラリでは利用できない。最速な実装とは言えないが、単純に書くなら現状維持でも十分問題ないと思われる。

これも ES2015 が十分に行き渡れば中のコードを String.prototype.repeatで書き換えてしまって問題ないはずだ。

padStart の話

話を未来方向に向けるとpadStart/padEnd と呼ばれる仕様が ECMAScript で提案されている。実際のインタフェースはleftpadとは少し違うが下記のように書くことができる。

'hello'.padStart(10); // '     hello'

さらによいニュースとして、実際に v8 では実装が始まっている、ChromeやelectronやNode.jsではこれが使えるようになる事もそんなに先の話ではない。

https://chromium.googlesource.com/v8/v8/+/1a272ba23ec490f73349201c014537c851f3c964

つまりは leftpadのようなコードも時代とともにこう変わっていくだろう。

現在:

function leftpad (str, len, ch) {
  str = String(str);
  var i = -1;
  if (!ch && ch !== 0) ch = '';
  len = len - str.length;
  while (++i < len) {
    str = ch + str;
  }return str;
}
leftpad('hello', 10);

ちょっと先の未来

function leftpad (str, len, ch) {
  str = String(str);
  if (!ch && ch !== 0) ch = '';
  returnString(ch).repeat(len - str.length) + str;
}
leftpad('hello', 10);

更に先の未来

'hello'.padStart(10);


さて、 leftpad を padStart の polyfill として見た場合には、 isArray と Array.isArray と同じ状況であることがわかると思う。この手の話はたくさんある。

僕はJavaScriptというのは我々言語ユーザーと言語仕様策定者が歩みを寄せながら差を埋めつつ育てていくものだと思っている。polyfillだったり、痒いところに手が届くような小さいモジュールを作ってnpmに公開しておく行為は差を埋める一つの手段である。

そういうライブラリが浸透していくことで言語仕様策定者側はユーザーにとって必要な機能がわかる、ライブラリを元に仕様を決めていくための指針ができる。ライブラリを公開/利用するのは無理矢理言ってしまえば未来のJavaScriptへの貢献にもなっている。

まとめ

  • isArrayの歴史(Goodparts => Object.toString => Array.isArrayの変遷)
  • leftpad も実際はisArrayと同様
  • JavaScriptは言語ユーザーと言語使用者が歩みを寄せて差を埋めつつ育てる言語、polyfillもnpmへの公開もそれを使うのも、その差を埋めるための手段

DeNAを卒業します

$
0
0

この週末にこの話を書こうと思っていたのですが、筆不精なもので土日を普通に過ごしてしまいました。

さて、表題の通りDeNAを卒業することになりました。

今回は技術的な話ではなく、単純にライフステージの変化の話なので気にならない人は読み飛ばしてください。

DeNAに入社したのは3年半前

yosuke-furukawa.hatenablog.com

だいぶ昔の記事ですね。この時は同年代や年下の若いエンジニア達がメキメキと力をつけていく中で「なんとかしなければ」という焦燥感で常に何かしらをウォッチしつつ、新しい技術があれば飛びついて調べ、アウトプットする、というようなことをやってました。(今でもアンテナは張っているつもりです)

そんな折に DeNAに誘われて入社できたのは非常にラッキーだったと思っています。新しい技術を積極的に採用しつつも、ある程度冷静かつ成熟した判断をしている部分があり、しかも事業としても色んな事を多方面でやっているので面白かった。

yosuke-furukawa.hatenablog.com

初めて入って、 Play Framework x Javaで海外の人たちと英語でコミュニケーションしながら一緒に海外向けのゲームを作ったり、 PerlでHTMLもCSSもJSもほぼ全部一人でウェブサイト作ったり、 かと思ったら Node.js でリアルタイムゲームのR&Dをやったり、QuizNowっていうリアルタイムゲームを作ってYAPCで宣伝させてもらったりもしました。つい最近まではNBPFと呼ばれる新しいmobageのプラットフォーム周りの仕事をしてました。有意義なキャリア開発ができました。

DeNAという会社でもともとやりたかった下記のことはだいたい実現できました。

  • ウェブアプリの作り方の基礎を身につける
  • WebSocket等の新しいプロトコルでゲームを作る
  • JavaScript(Node.js)を使って仕事をする
  • 英語でコミュニケーションをする

次はどこに行くのか

あんまり隠してないので、言ってしまうとリクルートテクノロジーズっていうところに行きます。 Node.js をサービスでも使っているのと、 Node.js のOSS活動・コミュニティ活動を奨励する、という話だったので、OSS活動、コミュニティ活動も継続しようと思っています。特に Node.js committer/evangelism 活動はやり続けたいと思っています。

ただ誤解のないように言っておくと、 DeNAでもその手の活動は別に禁止されていません。むしろ奨励してくれています。

最近だとkazuho さんの h2oとかが良い例じゃないかなと思います。 他にもsonots さんみたいにRuby committer/Fluentd committerとしても活躍している例もあります。

じゃあなんで転職するのか

これには僕のライフステージが変化したのが大きいと思います。簡単に言ってしまうと、子どもが生まれました。 しばらくは子ども中心の生活になるのと後は自分のキャリアとか条件を鑑みて総合的に判断しました。

子どもが生まれるというのは非常に大きな変化でして、今は子どもにかかりきりです。ソフトウェアを作るのとは全然違った楽しさがあります。この記事も子どもが寝てくれたので隣で寝顔を見ながら書いてます。

f:id:yosuke_furukawa:20160418175847j:plain

しばらくは、OSS committerをしながら Node.js ユーザーグループ代表をしながら、サービスも作りながら、 Fulltime パパ committer をする予定です。

f:id:yosuke_furukawa:20160414143427j:plain

細かい話は飲みとかランチとかに行った時にでも話します。皆さん、よろしくお願いいたします。

謝辞

業務用アプリ作り続けて、ソーシャルゲームとかウェブアプリケーションのことを何も知らないで入ってきた門外漢に対して懇切丁寧に教えていただきありがとうございました。3年半という長いようで短い時間でしたが DeNAの皆さんと過ごした時間はかけがえの無いものでした。狭いIT業界なのでまたお会いすることもあるかと思いますが、その時はどうぞよろしくお願いいたします。

wishlist

一応置いておきます。(何か頂けるのであればベビー用品・生活家電系があると助かります)

www.amazon.co.jp

今後ともどうぞよろしくお願いいたします。

Node.js v6.0 (Current) がリリースされました。

$
0
0

さて、とうとう皆さん待望の Node.js v6.0 がリリースされました!次のLTS候補です。LTSになるのは2016年の10月からの予定です。v6 の LTS 期間は明示化されてないですが、ルールに照らし合わせれば、LTSになってから 2年半がサポート期間なので、おそらく 2019年4月まではサポートされます。

Node v6.0.0 (Current) | Node.js

Node.js v6.0 の主な変更点

  • ES2015 support の改善
  • module load性能の改善
  • Buffer APIの new Buffer() コンストラクタの廃止 (セキュリティ上の理由から)

ES2015 support の改善

やっぱりこれが一番大きな変化ですね。

node.greenを見てもらえればわかるかもしれませんが、 ES2015 のサポートがこれまでは 58% だったのが 96% まで大幅に拡大されました。

f:id:yosuke_furukawa:20160427021442p:plain

細かいところはnode.greenを見てもらうとして、いくつかのデフォルトで有効になった機能を紹介します。

デフォルトパラメータ

これは、関数に引数を渡す際に渡されない引数(undefinedな引数)にはデフォルトで特定の値にしてもらう、という機能です

function foo(a = 1, b = 2) {return a === 3 && b === 2;
}

foo(3) // true
// 引数の値をデフォルト引数の値として使うことも可能function f(list, indexA = 0, indexB = list.length) {return[list, indexA, indexB];
}
console.log(f([1,2,3]); // [1,2,3], 0, 3
console.log(f([1,2,3],1); // [1,2,3], 1, 3
console.log(f([1,2,3],1,2); // [1,2,3], 1, 2

デフォルトパラメータが書けるようになったので、いままでやっていたような もしも引数 a がundefinedだったらデフォルトで値をセットするという処理が読みやすく、書きやすくなりました。

デストラクチャ

デストラクチャリング、和訳すると分配束縛と呼ばれる機能です。Clojureにある機能ですね。 これを利用すると配列やオブジェクトで設定した値を取り出しやすくなります。 一番良く使うのは値をswapさせる時かと思います。

具体的には以下のとおり。

var hoge = 123;
var fuga =456;

// 値をswapするvar[fuga, hoge] = [hoge, fuga];

console.log(hoge); // 456
console.log(fuga); // 123var[a, [b], [c], d] = ['hello', [', ', 'junk'], ['world']];

console.log(a + b + c); //hello, world (aに"hello", bに",", cに"world"が入ってる )var pt = {x: 123, y: 444};
var{x, y} = pt;
console.log(x, y); // 123 444

Rest パラメータ

可変長パラメータを持つ関数を作る時に argumentsを使わずに作れるようになりました。

// ...itemsでRestパラメータfunction push(array, ...items) {// それをarrayにpush (ここはspread operator)
  array.push(...items);
}var list = [1, 2, 3];
//list変数に数字をpush
push(list, 4, 5, 6);
console.log(list); //[1, 2, 3, 4, 5, 6]

正規表現に sticky option と unicode option が付く

sticky option は直前に実行した正規表現のlastindexを覚えておき、次に検索する時はその lastindex から検索するというオプションです。

var text = "First line\nsecond line";
var regex = /(\S+) line\n?/y;

var match = regex.exec(text);
console.log(match[1]);  // "First"
console.log(regex.lastIndex); // 11var match2 = regex.exec(text);
console.log(match2[1]); // "Second"
console.log(regex.lastIndex); // 22

unicode option は正規表現のマッチ時にユニコードリテラルを使って書けるようになったり、サロゲートペアにマッチできるようになるためのフラグです。

"𠮷".match(/^.$/u)[0].length === 2

Proxy API

Proxyが提供してくれるのは、"ちゃんとした"メタプログラミングです。Proxyに関しては、Brendan Eich の構想を描いた発表資料があります。

www.slideshare.net

これが 2010 年という事で、Node.jsが流行りはじめたくらいに提案されたというのが歴史の深さを物語っていますが、Proxyは割りと色んな事ができるAPIです。5年経て、上のスライドで紹介されているものとは APIが異なりますが、以下のことに使えます。

  • Getter/Setter への インジェクションを行うことで、プロパティの変更や参照をされた時に処理を変えることができる。
  • method_missing みたいな存在しないメソッドを呼び出した時に呼ばれるメソッドを定義できる。

などなど、名前の通り、代理オブジェクトとして呼び出されたときの処理を肩代わりすることができます。

Proxy APIで Getter/Setter にインジェクトする。

こんな感じのことができます。

var handler = {
    get: function(target, name){return name in target?
            target[name] :
            37;
    }};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c'in p, p.c); // false, 37

存在しないプロパティを呼び出した時のデフォルトの振る舞いを定義できる。

const obj = {abc: 123};
const PropertyChecker = new Proxy(obj, {
  get(target, propKey, receiver) {if (!(propKey in target)) {thrownew ReferenceError('Unknown property: '+propKey);
    }return Reflect.get(target, propKey, receiver);
  }});
console.log(PropertyChecker.abc); // 123
console.log(PropertyChecker.def); // Reference Error

これをうまく応用することで例えば method_missing 的な存在しないメソッドがあった時の振る舞いを定義できます。

Reflect API

一方で Reflect APIは Proxy APIと対になって使われることが多い APIで、 Proxy が処理にインジェクトして、代理オブジェクトとして振る舞うのに対して、 Reflect APIはそれのユーティリティオブジェクトで、対象となるオブジェクトに対して処理を反映(Reflect)させる動きをします。

最初にReflectを僕が知った時は対象となるオブジェクトが取れるならそれを直接変更すればいいので、いまいち使いドコロが分かってなかったんですが、ある程度使ってみると使い所が分かってきました。

使い所 その1 演算子じゃなくて、関数にしたい時

in演算子を考えてみましょう

'assign'inObject// Object のプロパティに `assign` があるかどうか

これは演算子を使った式です、Reflect を使うと下記のように書くことができます。

Reflect.has(Object, 'assign'); // true

演算子じゃなくて関数で表現されているのがわかります。

他にも delete演算子を Reflect を使って書き換えることが可能です。

// delete 演算子var a = {abc: '123', def: '456'};
delete a['abc']; // {def: '456'}
// Reflectを使った場合var a = {abc: '123', def: '456'};
Reflect.deleteProperty(a, 'abc'); // {def: '456'}

専用の演算子を使ったほうが短く書けますが、関数を使って代用できるようにしておくと読みやすさや反映する対象がわかりやすくなります。

使い所その2 例外よりも戻り値として扱いたい時

Object.definePropertyというObjectに対してプロパティを定義する専用の関数がありますが、これは definePropertyに失敗すると例外がthrowされます。そのため下記のように書かなくてはいけません。

try{Object.defineProperty(obj, name, desc);
  // 成功時の処理}catch (e) {// 失敗時}

Reflectができるようになると下記のように書けます

if (Reflect.defineProperty(obj, name, desc)) {// 成功時の処理}else{// 失敗時の処理}

--es_staging flag で有効になるもの

--es_staging flag を付けるとES2015の機能がさらに増えます。ただし、 --es_staging flag はまだ unstable の状態の機能を試すのに使うためのフラグなのであまり本番で積極的に使うものではありません。

末尾再帰呼び出し最適化

再帰呼び出しをした時に再起かどうかを内部的に検知して、再帰呼び出しを普通のループに変換します。 詳しくは下記の teppeis さんのブログが参考になります。

teppeis.hatenablog.com

そこから持ってきた下記のスクリプトで試します。

'use strict'; // 今のところ strict mode でのみ有効function factorial(n, acc) {if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}[0,1,2,3,4,5,10,100,1000,10000,100000].forEach(function(n) {console.log(n, factorial(n, 1))});

階上計算処理ですが、これを普通に実行すると、 100000 の所で stack size オーバーフローでエラーになります。

$ node fact.js

0 1
1 1
2 2
3 6
4 24
5 120
10 3628800
100 9.332621544394418e+157
1000 Infinity
10000 Infinity
/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:2
function factorial(n, acc) {
                  ^

RangeError: Maximum call stack size exceeded
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:2:19)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)
    at factorial (/Users/yosuke/go/src/github.com/yosuke-furukawa/node/fact.js:4:10)

しかし、 --es_stagingで実行すると通るようになります。

$ node --es_staging fact.js

0 1
1 1
2 2
3 6
4 24
5 120
10 3628800
100 9.332621544394418e+157
1000 Infinity
10000 Infinity
100000 Infinity

Function#name

今までは無名関数だと .nameプロパティから関数名を取れませんでした、これが よくある無名関数を左辺に代入する書き方だと問題がありました。

// function.jsvar abc = () => {};
const def = function(){};
console.log(abc.name); // abc
console.log(def.name); // def// ただし、この場合は 普通に名前付き関数の名前が採用されるlet ghi = funciton jkl() {};
console.log(ghi.name); //jkl
$ node function.js


jkl

これを --es_stagingを付けると下記のようになります。

$ node --es_staging function.js
abc
def
jkl

逆に有効になっていないもの

一番有名所としては import/export でしょうか。 これに関しては、ただいま絶賛議論が紛糾している所です。

ES modules を Node に持ってくる、というのはかなり難しい問題で、今のところまだ有効な打開策が出ていません。そもそもまだ v8 でもパースできないのでパースできるようになったとして、どうやって解決するかという議論が始まったばかりです。

議論のまとめはこちら

github.com

module load 性能の向上

benchmark WG が継続してベンチマークを取るようにしてくれました。

Node.js Benchmarking

benchmark working group では requireや起動時間といった性能を計測するマイクロベンチと acmeairと呼ばれるチケット予約管理アプリを使ったマクロベンチの2種類を実行しています。 v4.x と比較した時に require した時の性能が初期ロードで 3倍高速に、二回目以降のロード(cache付きロード)で5倍高速になるようになりました。

ops/sec higher is better f:id:yosuke_furukawa:20160427100426p:plain

また acmeair を使った全体のスループットに関しても若干の向上が見られます。

f:id:yosuke_furukawa:20160427101015p:plain

Buffer APIの new Buffer() コンストラクタの廃止

セキュリティ上の理由から、 new Buffer() コンストラクタは廃止されました。 詳しくはこの辺りを見てもらうと良いかもしれません。

おまけ

Windows XP/Vistaで node.js のサポートが終了しました。

まとめ

他にも色々と変更はありますが、全てを紹介しようとすると膨大な量なので一旦ここまでに停めておきます、すべての変更をちゃんと追いたい人は以下のChangelogを参考にしてください。

node/CHANGELOG.md at master · nodejs/node · GitHub

アップグレードするかどうか

まず v0.10 / v0.12 を使っている場合は今年の年末でサポートが切れてしまうので v4 もしくは v6 への upgradeを推奨します。

f:id:yosuke_furukawa:20160427102953p:plain

v4 を使っているユーザーは 2018年までサポートは続くのでまだアップグレードしなくても構いません、とはいえ上にあげたような新しい機能を使いたいユーザーはアップグレードを検討してください。

v5 を使っているユーザーはあと2ヶ月はサポートされますが、v5はLTS対象じゃないので、アップグレードを推奨します。v6 にアップグレードすることを検討してください。

Stable ラベルから Current ラベルへ

今までは Stableというラベルが最新のバージョンにはついていましたが、 LTS と混同されやすいのでラベル名が Currentに変わりました。v6がLTSになったらその時はおそらく LTSラベルが付き、 v7 が Currentへと変わります。

v7 のリリースもまだ明示化されてないですが、今のところの計画では今から半年後の2016年の10月には次のv7.0がリリースされる予定です。

まとめ

Node.js v6.0 がリリースされました。

  • 主な変更点
  • ES2015 サポート拡充
  • 性能向上
  • new Buffer APIの廃止
  • アップグレード検討有無
  • Stable から Current へ

という話をしました。

ES Modules と Node.js について

$
0
0

書こう書こうと思いながらこのタイミングまでのがしてしまいました。 今一番 Node.js の中で hot な discussion の一つと言えるでしょう、『ES Modules が Node.js の中でどうなるか』です。

ES Modules 現況

ES2015 が発刊されてそろそろ一年です。 ES2015 にある機能は Node.js v6でも 93% 程度カバーされています。モダンブラウザでも大体が90%を超えています。しかし、 ES Modules だけはまだどのブラウザも実装しきれていません(kangax compat table は ES Modules は省かれてます)。

f:id:yosuke_furukawa:20160509010917p:plain

そもそも ECMAScript 2015 自身で定義されたのは構文だけなので、構文はともかく、どうやってモジュールを取ってくるかという Loader の部分がまだ決まりきっていません。

https://whatwg.github.io/loader/

現時点はいくつも決めなきゃいけないポイントがあって

  • 参照解決処理
  • 取得処理
  • script タグでどう書くのか
  • メモ化処理(所謂caching)

の全てを決めて一旦ロードマップ上のMilestone 0 が達成されるような状況です。

https://github.com/whatwg/loader/blob/master/roadmap.md

scriptタグでどう書くのか、参照解決処理など、ある程度決まっている処理はありますが、どの項目もまだ議論中です(少なくとも github 上ではまだ milestone 0 を discussion している最中に見える)。各種ブラウザでも、実装が始まっているところはありますが、仕様の方針待ちなところが多いです。

なんで Node.js に ES Modules が必要なのか

ES Modules の仕様が定義されるよりも前に Node.js は CommonJS と呼ばれるモジュールシステムを採用しました*1。それと npm というパッケージマネージャの組み合わせでエコシステムを作っています。結果として npm のエコシステムは Node.js にとどまらず、Browserify や webpack を組み合わせてフロントエンドにとっても大きなエコシステムになっています。

『CommonJS で既に育ってしまった生態系の中で ES Modules という標準仕様とどうやって相互運用性(interoperability)を取るのか』これが ES Modules が定義され始めた最初からずっと Node.js / npm で語られてる事でした。

相互運用性がないとこれまでのエコシステムと乖離(friction)ができてしまいます。せっかく Browserify や webpack で埋めたfrontend browserとNode.js との乖離がこれでまた起きることになります。

Node.js ではじゃあどうしようとしているのか

これではイカン、という事で Bradley Meck氏が interopを取ろうと Proposal を書き起こしました。最初に Proposal を書いた時は議論がいくつもあったので ものすごくたくさんの話が巻き起こってまとまらなかったのですが、何度も何度も議論を重ねて今やっと DRAFTというステータスになっています。

node-eps/002-es6-modules.md at master · nodejs/node-eps · GitHub

一応言っておくと DRAFTというのはやると決めた訳じゃないです。議論のテーブルに乗った状態DRAFTです。ステータスとしては DRAFT =>ACCEPTDRAFT =>REJECTの2通りがあるので REJECTされて無かったことになる可能性もあります。

ES Modules on Node.js 概要

おおまかな解決アルゴリズムを記述します。

まず、 DynamicModuleRecordと呼ばれる CommonJS 用の module 置き場を定義します。これは ES Modules から CommonJS の module.exportsで定義されたものを importで読み込めるようにするためのレジストリです。

その上で下記のアルゴリズムで読み込みます。

1.  読み込もうとしているファイルが CommonJS で定義されているのか ES Modules で定義されているのかを確認する(※)
2. もし CommonJS なら
  2-1. ファイルを即時評価する(今まで通り)
  2-2. DynamicModuleRecord に `module.exports` で読み込んだものを入れる
3. もし ES Modules なら
  3-1. ファイルをパースする(import/export でファイルを取得して、bindingを作るため)
  3-2. 再帰的に全ての依存関係のあるファイルを持ってくる
  3-3. 全ての依存関係のファイルから `import`  の binding を作る
  3-4. 評価する

簡易フローチャートで書くとこうですね。

f:id:yosuke_furukawa:20160509230807p:plain

この後さらにケースとしてはファイルが循環参照されてたらどうするかとかの話がありますが、一旦そこは置いておきます。

読み込もうとしているファイルが CommonJS で定義されているのか ES Modules で定義されているのかを確認するここが今のところ最大の議論のポイントです。

CommonJS なのか ES Modules なのかの確認方法ですが、読み込もうとしているファイルが.mjs拡張子だったら ES Modules、それ以外の .js等であれば普通に CommonJS として判断しようとしています。

つまり、 CommonJS でも ES Modules でも両方共読み込ませたいモジュールを作る場合、 package.jsonに下記のように記述し、

{"name": "test",
  "version": "0.0.1",
  "description": "",
  "main": "./index", // 拡張子なしで定義する}

index.mjsindex.jsを定義します、片方には ES Modules 形式で書きます。

// index.mjsexportdefaultclass foo {//..}

もう片方には CommonJS 形式で書きます。

// index.jsclass foo {// ...}
module.exports = foo;

こうすると読み込む側が .mjs形式に対応している Node.js であれば、 先に .mjsで解決しに行きます。見つからなければ .jsの方を解決する、という動きになります。

import で書く場合の path 解決方式

本筋からはそれますが、 importの重要なポイントなので記載しておきます。 ES Modules では Node.js が暗黙的にやっているようなスマートなパスの解決をしてくれない(現時点のローダーでは)ので気をつけましょう。

例えば、 requireで書いた場合、 require('./foo')のように .jsを削除して記載することが可能でした。

// ./foo.js を解決する
require('./foo');

importでモジュール参照解決をする場合、ES Modules の仕様としては暗黙的に .jsを保管してくれたりしないので気をつけましょう。

// ./foo だけ参照解決する// ./foo.js や ./foo.mjs や ./foo/index.js は参照解決しないimport'./foo';

./fooのようにローカルパス付きで読み込む場合は必ず .jsもしくは .mjsのように拡張子を付けて参照解決させる事になるでしょう。

import'./foo.js';
import'./bar.mjs';

ES Modules => CJS

これまでの説明だけでも分かりにくいと思うので例を上げて説明していきます。ここでは ES Modules から CommonJS で定義されたモジュールを読み込む場合です。ES Modules から CJS を「名前付きで」読み込んだ場合、 defaultというプロパティが入ります(これめっちゃ分かりにくい)。

// cjs.js
module.exports = {default:'my-default',
  thing:'stuff'};
// es.mjs// 名前付き(as baz)で ./cjs.js のファイルを import する// bindings なので中の値は外から書き換えられない。import * as baz from './cjs.js';
// baz = {//   get default() {return module.exports;},//   get thing() {return this.default.thing}.bind(baz)// }// console.log(baz.default.default); // my-default// default のオブジェクトを import して foo にアサインするimport foo from './cjs.js';
// foo = {default:'my-default', thing:'stuff'};// default プロパティを明示して読み込むimport{default as bar} from './cjs.js';
// bar = {default:'my-default', thing:'stuff'};

値を export して、 default の値としてアサインされる例:

// cjs.js
module.exports = null;
// es.mjsimport foo from './cjs.js';
// foo = null;import * as bar from './cjs.js';
// bar = {default:null};

関数を export する例:

// cjs.js
module.exports = function two() {return 2;
};
// es.mjsimport foo from './cjs.js';
foo(); // 2import * as bar from './cjs.js';
bar.name; // 'two' (関数名が取れる)
bar.default(); // 2 (default 関数に assign される)
bar(); // throws, bar is not a function

CJS => ES Modules

反対に CommonJS から ES Modules を読み込む時は下記のようになります。こちらは export defaultで export した場合は .defaultプロパティにアサインされます。

export default を利用する例:

// es.mjslet foo = {bar:'my-default'};
// note:exportdefault foo;
foo = null; // これは import 側に影響しない、 export した時点の値が返る、何故なら binding じゃなくて値だから
// cjs.jsconst es_namespace = require('./es');
// es_namespace ~= {//   get default() {//     return result_from_evaluating_foo;//   }// }
console.log(es_namespace.default);
// {bar:'my-default'}

export を利用する例:

// es.mjsexportlet foo = {bar:'my-default'};
export{foo as bar};
exportfunction f() {};
exportclass c {};
// cjs.jsconst es_namespace = require('./es');
// es_namespace ~= {//   get foo() {return foo;}//   get bar() {return foo;}//   get f() {return f;}//   get c() {return c;}// }

今のところの議論

ちょうど今盛り上がってるのには理由があって、仕様が DRAFTになって議論が始まった後に新しく Counter Proposal (反対提案) が書かれました。それが defense of dot jsという Proposal です。

これは .mjsという拡張子で解決するのではなく、 package.jsonにフィールドを足すだけで解決させるようにしたいという Proposal です。

defense of dot js の内容

こちらの仕様では、基本的に完全な互換性を取るのは諦め、 ES Modules で読み込むことをベースとします。 package.jsonmainフィールドがある時だけはすべてのファイルが CommonJS で読み込まれます。これが基本的な仕様です。

明示的に ES Modules で読み込ませたければ package.jsonmoduleフィールドでエントリーポイントを書きます。

// package.json{"main": "index.js",
  "module": "module.js"}

こうすると、 moduleフィールドがあれば Node.js は ES Module としてエントリポイントを見に行きます。古いバージョンの Node.js は mainフィールドのエントリポイントを見るだけなので古いバージョンにも対応されます。

しかし、このやり方には問題が1つあります。 module以外のエントリポイントを requireから指定できません。例えば、 lodashとかでよく見る require('lodash/array')みたいな読み込み方ができません。そこでこれを解決するために modules.rootというフィールドを利用します。

// package.json{"main": "index.js",
  "module": "module.js",
  "modules.root": "lib"}

上記のように"modules.root": "lib"フィールドがあると lib/*以下を requireから読み込めるようになります。つまり、 ES Modules で書いてあっても require('lodash/array')みたいな書き方ができるようになり、ある程度互換性を保てるようになります。

とはいえ、新しいバージョンで mainmoduleも無い package.jsonでは暗黙的には必ず ES Modules になってしまうので、かなり breaking changes です、その代わり拡張子での解決は要らないので、 .mjsなどの拡張子を検討する必要はありません。なので、 "Defense of dot js"なわけです。

CommonJS と ES Modules 両方対応するなら?

基本的には ES Modules に傾けるのがこの仕様のポイントですが、人気のあるモジュールはそうはいきません。 ES Modules と CommonJS の両対応させる必要のあるモジュールは存在するでしょう。この時は諦めて transpile して、 ES Modules => CommonJS のファイルも用意しておきます。 transpile した JavaScriptも一緒に package に入れておきます。 mainフィールドと moduleフィールドを書いておけば古い Node.js と新しい Node.js で読み込み先を変えてくれます。

Bradley Meck仕様との違い

Defense of dot js 側は将来的に ES Modules に全て揃えようという姿勢です。互換性をある程度損なっているし、それが起こす混乱はある程度受け入れる考えです。それに対して Bradley Meck 氏の仕様は互換性重視です。あくまで今のCommonJSと相互運用性を取るというのを主軸に添えて語られています。拡張子で ES ModulesなのかCommonJSなのかが変わるというのは言い換えれば一つ一つのファイル単位で ModuleなのかCommonJSなのかを切り替えられる柔軟な仕様です。過渡期は .mjsと .jsが混じるかもしれませんが、将来的にユーザーの判断でどっちがデファクトになるかによっては .mjsだけ残る可能性はあります。

hard choice (.mjs vs package.json)

これに対してさらに Counter で Bradley Meck 氏はブログを書いています。

medium.com

なんで .mjsを選んだのか、という理由が書いてあります。 ブログの基本的な論調としてはみんな .jsという拡張子に頼りすぎているという話です。

JavaScriptには様々な"方言"があります。CommonJS, UMD, AMD、これらの全ての JavaScriptは全て .jsになってます。

ES Modules というのはもうそれ単体で評価可能な Script ではないし、パースして他のファイルをロードしてチェックするという処理が必要な以上、もういっそ拡張子ごと変えるというアイデアも分からなくはないです。しかも Module だと暗黙的に strict mode で動くので、普通のScript とは別物だと思ったほうがいいです。

他の言語の例を挙げるなら、 .plPerlスクリプト) と .pmPerlモジュール)のように拡張子を変える事を是とする文化もあります。また、ファイルを開く前に拡張子だけ見れば Module なのか Script なのかが分かるので、人間にとっても意味はあります。

この仕様はまだまだ議論中です。また今週の TSCミーティングで議論があるでしょう。先週もありましたが、話はまとまりきらずに終わりました。今のうちに何かこうしたいという意志があれば issuePRに書くことをおすすめします。

まとめ

  • ES Modules の現時点の状況
  • ES Modules を Node.js はどうするのか
  • ES Modules on Node.js Proposalの詳細
  • Counter Proposal である Defense of dot js の話

*1:(正確には CommonJS の仕様からは大分外れてます。今やあれが CommonJS という事になっちゃってますが・・・)

OpenCV + Google Cloud Vision API + Intel Edison で笑った瞬間を撮るカメラを作る

$
0
0

やりたいこと

最近娘が生まれて二ヶ月経過し、そろそろ笑ったりするようになりました。今回のテーマは娘が笑った瞬間を逃さずにカメラで撮影する事です。ちなみにこういう子どもをネタにして行うハック、僕はこれを『親バカハック』と呼んでます。

f:id:yosuke_furukawa:20160608084338p:plain

TL; DR

  1. Intel Edison でカメラをセット、一定のタイミングで撮影しつつ
  2. OpenCVで粗く笑顔認識させてから
  3. Google Cloud Vision APIで表情解析
  4. 笑顔だと判定された画像を Slack で飛ばして画像をいつでも見れるようにする。

f:id:yosuke_furukawa:20160608102239p:plain

かわいい笑顔が撮れたので最高でした。

ハードウェアセットアップ

Intel Edisonを手に入れたのでそれを使って作ります。Edison は Arduino拡張ボードなら普通のUSB web camera 対応しているので、それをただぶっさして使います。

f:id:yosuke_furukawa:20160608084105p:plain

Intel Edison はSDカードほど小さくて、 x86Intel Atomというプロセッサーを積んでます。あんまり詳しくないのですが、筆者が大学の頃、太古の昔の教科書では x86CISC系は組み込み系に弱くて、 ARM の RISC系のが組み込みに有利という認識だったのですが、どうやら時代は変わって今ならどっちもどっちでx86なのに組み込み系に乗る時代になったようです。

ともかく、 Edison にしたのはただ『そこにあったから』です。Noderとしては Intel Edison を初期化した時に最初からnode.js v0.12 が入ってるので嬉しい(頼むからv4+にしてほしい)。本当は Intel Edison じゃなくても良くて、一番試してみたかったのは Tessel 2 なんですが、日本で手に入れるのは難しいという話だったので、一旦 Edison でトライ。

Edison がうまくつながっていれば USB web camera をただ USB port にさすだけで使えるのですが、初期の頃はさしただけで使えなくてあれこれ調べて SW1 スイッチを USB 側にしないと使えないようです(ハマりどころ)。

dev.classmethod.jp

ちなみにほぼこれを参考にすればハードウェアのセットアップは終わります。

github.com

笑顔認識

顔画像を認識させるのは OpenCVで簡単にできるんですが、さらに笑顔認識までしようとすると、多少の小細工が必要です。 OpenCVCascade Classifier という機能を持っています。これは名前の通り、 分類器(classifier)を連結(cascade)させて特定するという機能です。笑顔認識で言えば、

  1. 顔画像領域を特定(画像の中から眼や鼻や口といった特徴のある領域を抜き出)し

f:id:yosuke_furukawa:20160608105656p:plain

  1. その顔画像領域の中で笑顔かどうか(顔の下半分に半月/三日月形の領域があるか)を特定する

f:id:yosuke_furukawa:20160608105722p:plain

という二段構えの分類機能の組み合わせで成立しています。 今回の OpenCVでやっているのは非常に単純かつ強力な仕組みで、2001年くらいの論文で解説されている Viola Jones Object Detectionと呼ばれるものです。2001年の頃の論文が今ではライブラリとして簡単に扱えるので良い時代になったなと思ったのですが、残念ながらこの方法はそこまで精度が良くないです。構造が単純で解析に時間がかからないのが特徴です。

笑顔検出は npm モジュールから使えるようにライブラリにしています。

github.com

この smile-face-detectorを使ってリアルタイムでデモする動画をとってみました。 ちなみに web 屋っぽく websocket で配信する仕組みです。

smile

github.com

Google Cloud Vision APIを使って表情解析する

OpenCVで笑ったと判断したとしても、やっていることは『顔の下半分に半月形の領域があるかどうか』、なので本当に笑っているかどうかはまだ難しいです。そこは表情解析に定評のある Google Cloud Vision APIというクラウドの力を借ります。

Google Cloud Vision APIは表情解析、風景解析、画像内のOCR、画像内のオブジェクト認識ととんでもなく強力な機能を持った GoogleAPIです。これを使っただけでも何かできそうな気がしてきます。

Google Cloud Vision APIは表情解析して、その表情から読み取れる感情が happy なのかそれとも sad なのか、はたまた surprised なのかといった結果を返してくれます。

http://cdn.mos.techradar.com/art/internet/Google/Cloud_Vision_API-970-80.jpg

ただやはりリアルタイムに解析するにはネットワーク通信のコストがかかるのとどうしても時間がかかる、あと、無料枠では1日1000リクエストまでなので、やたらめったらに送る訳にはいかないという制限があります。なのでOpenCVでの一旦フィルタを使って OpenCVで笑ってると判断された画像だけを Google Cloud Vision APIに送るという仕組みにしています。

笑顔が検出されたら Slack に転送する

Google Cloud Vision APIが happy だと判定してくれたら後は楽で、その画像を Slack に送ります。

f:id:yosuke_furukawa:20160608084338p:plain

github.com

外出中とか仕事中でもかわいい画像が見れて便利です。

はーかわいい

ただし

何日間か試して思いましたが、以下の点でこの仕組のままでは厳しいです。

1. Intel Edison がそこまで高速ではない

Intel Edison は 500MHz で dual coreと組み込み系の中では高速ですが、さすがに1,2秒間隔で OpenCVを回し続けると不安定になります。 そこはやはり Raspberry PI 3 とかだと 1.2GHz quad coreになるとのことなのでハードウェア側をもう少しCPUリソースが使えるものにする必要があります。もしくはC++とかで直接OpenCVを使うといいのかもしれません。

2. 首の座っていない赤ちゃんは顔画像特定が難しい

これはもうどうしようもないんですけど、まだ赤ちゃんの首が座ってないので OpenCVでは正面をちゃんと向いていないと顔画像として認識してくれないので難しいです。なので顔が写ってても取りこぼすこともしばしば。これを解決するには画像を少しずつ角度を変えて実行する必要があるんですが、そうするとどうしてもさらに時間がかかってしまいます。

3. 親が笑ってると思ってもGoogle Cloud Vision APIは笑ってないと判定する

自分から見ると笑ってて可愛い笑顔だなーと思って Google Cloud Vision APIにかけると全くの「無表情」として帰ってきます。 Google Cloud Vision APIが判定してくれるのはもっと分かりやすい笑顔なので、そこまではっきりとした笑顔になるためにはもう少し赤ちゃんの成長が必要です。

とはいえ

一旦丸3日位やってみたら、こういう可愛い笑顔が撮れました。

f:id:yosuke_furukawa:20160608102239p:plain

今はもう少し赤ちゃんの状況に合わせてリアルタイムに笑顔を撮るんじゃなくて、90秒間隔くらいで Google Cloud Vision APIにかけながらSlackで様子を見てます。

f:id:yosuke_furukawa:20160608143537p:plain

まとめ

OpenCV + Google Cloud Vision API + Intel Edison で笑った瞬間を撮って、 Slack に送るカメラを作ってみました。 もう少し時間があったらEdisonじゃないハードウェアとかで試してみようかなと思います。

また Node.js で全部できそうだったので、Nodeで実現したけど、リソース効率を考えるともう少しシステムプログラミングよりの言語(CとかC++とかRust(?))を使ったほうがいいのかもしれません。

この辺も時間があったら試してみます。

Node.js における Promise を使った例外処理

$
0
0

さて、 Node.js のエラーハンドリングは難しいと言われてますが、 2016年現在、つまりNodeの v4 とか v6 が主流になり、 Promise が基本的な処理として採用されている状況ではどうでしょうか。ちょっと考えてみます。 一応これの補足です。

qiita.com

TL;DR

未だに難しい。ただし、 Promise で改善されている。async-await や zone まで来たらかなり楽になる。 あと、 unhandledRejection が uncaughtException よりも酷いことにならないので、大分マシになっている。

Node.js のエラーハンドリングの難しさ

まず JavaScriptには同期と非同期のエラーハンドリングのやり方があります。前者は所謂 try-catchによる方法、後者は callbackを使って第一引数で実現する方法や emit('error') - on('error')でイベントとして渡す方法等が存在します。

難しいとされるのはいくつかあって、同期と非同期が混じった時に画一的にハンドリングする方法が無いので、特に問題が起きます。

また JavaScriptJavaとかと違って try-catchを書かなくてもコンパイルエラーにならない言語なので、 catchの書き漏れが起きやすい言語なわけです。catchを書き漏らすと Node.js のさらに上のレイヤで補足され、誰も catch しない場合に 最終的に processuncaughtExceptionとしてcatchされる事となり、そこではログに書いて落とす以外の選択肢が取れません(後述)。

非同期は非同期で on('error')って書くスタイルや callbackのスタイルで混じります。前者は Stream というか EventEmitter を使った時の標準的な方法です、後者は最もベーシックなやり方で callback 関数の第一引数を必ず errorに当てるというものです。

つまりは画一的なエラーハンドリング処理が存在しないというのが JavaScriptにおける難しいところの1つです。

Promise

Promise を使うと上の状況を少しだけ回避できます。Promise が提供してくれるのはこの画一的なエラーハンドリングであり、 Promise の中で起きた例外は同期の throwであろうと rejectであろうと全て catch されて、Promise の .catchの関数に来てくれます。よく callback-hellの解決策として登場する Promise ですが、Promiseが解決するのはエラーハンドリングのやり方でもあります。

.catchを書き忘れた場合には unhandledRejectionという形で uncaughtExceptionのように最終的に processの所でキャッチされます。ただし、これは uncaughtExceptionほど絶望的な状況じゃありません。

ただ別に Promise も例外処理を雑に扱える銀の弾丸じゃありません。 Stream / EventEmitter のように 連続でイベントが発生するようなものは Promise で表現できないので結局 .on('error')でのハンドリングは残ると思います。

uncaughtException と unhandledRejection について

uncaughtExceptionunhandledRejectionもどちらも同じようにキャッチされなかった例外が processまで来てしまった時の例外ですが、認識しておいてもらいたいのは unhandledRejectionuncaughtExceptionでは処理の方法が全く異なる、という事です。

そもそもなんで uncaughtException ではprocessを落とさないといけないのか

uncaughtExceptionで落ちないといけないのは、簡単にいえば、例外を v8(C++) の層でキャッチしているから、です。

github.com

github.com

Node.js はイベントループモデルで作られていますが、イベントループの状態や実行中のタスクがどうなるかとか無関係に uncaughtExceptionが起きると v8 の C++レイヤまで一気に突き抜けてキャッチされます。こうなってしまうともう継続不能です。イベントループがおかしくなって、結果エラーが出続ける可能性もあるし、EventEmitter で積んだはずのイベントがちゃんと処理されない可能性もあります。なので、継続するのは推奨されておらず、ログに書いて、プロセスを落としてから再起動させる以外の手段がありません。

ただ、もう少し深ぼると、 Node.js のJSレイヤで全体を try-catchで括れば C++レイヤまで来ないようにすることも可能です。こうすると、少なくとも イベントループ がコントロールできない状況は防げる可能性はあります。これをしなかったのは、 v8 の最適化が try-catchのブロックが書かれた関数をJITで最適化しないという制約が存在するためです。性能を優先して、Node.js 内部ではこの方式は取られませんでした。

unhandledRejection ではなんでprocessは落ちなくていいのか

uncaughtExceptionC++のレイヤでキャッチしているという話をしましたが、 unhandledRejectionはJSレイヤで処理されています。

v8 の中のコードを見ると分かりますが、 Promise はほぼすべてのコードが JavaScriptで記述されています。

github.com

uncaughtExceptionの時と違って C++のレイヤまで突き抜けることはありません。 Promise が中で try-catchしているので、unhandledRejectionが起きた所でそれは JS の中のレイヤで起きている『try-catchで例外をcatchしたけど無視されている状況』です。少なくともイベントループの状態はおかしくなったりしません

また、 Promise としては unhandledRejection になった状態を別に禁止していません。その時点で unhandled な状態であったとしても別な時に catch される可能性があるためです。

結局のところ unhandledRejectionが起きた所でそれをどうするかはユーザーのアプリケーションに依存する訳です。

process を継続したくない時のエラー

エラーの中には process が動き続けることでより深刻な状況になるエラーも存在します。『あり得ない状況』になっているのに無理に動き続けた結果、より深刻な問題になってしまう事もあります。

『この状況になったらアプリケーションが落ちる以外に打つ手がない』という時、これまでは例外を投げて uncaughtException として落とすことができましたが、 Promise で括ってしまうと、『プロセスを継続したくない時のエラー』だろうとなんでも catch されてしまいます。

結局のところ、 JavaScriptには Javaで言うところの非検査例外(RuntimeException)も無ければ、 golangで言うところの panic に相当するような処理もありません。

少し前に node symposiumsという有識者だけで行われたイベントでこの辺りの『検査例外(checked exception)と非検査例外(unchecked exception)』を Promise でどう扱うかの話があったのですが、結局 JS の try-catchシンタックスを拡張するしかない上に、今のところこの手の話が TC39 で話されてるのも僕は知らないので、throw側 か catch側で工夫するしか無さそうです。

github.com

デフォルトの unhandledRejection の動き

今のところ、 Node.js は unhandledRejection が起きたとしてもデフォルトでは何も言いません。これは議論の最中です。

github.com

issue opener の意見としては、『unhandledRejectionが起きた時に何も言わないのはさすがにどうなのか、各種ブラウザの動きとも異なる』という話です。今のところ決定策は出てません。

ちなみに 各種ブラウザ動きが若干異なります。

  • Chromeは unhandledRejection が起きた時点にエラーとして出力して終わりです
  • FirefoxGCが発生した時にその段階で Promise が回収され、 rejection がハンドリングされなかったらエラーとして出力します
  • IE Edge は unhandledRejection で hook はできるけど、何も言わない??(未検証)

今後どうなりそうかで言うと

  • 単純に標準エラー出力に warn を吐くだけ (chrome案)
  • GCで Promise に該当するオブジェクトが 回収された時にprocessを落とす (Firefox& uncaughtException の動きに合わせる案)
  • 何もしない (現状維持)

のあたりの案がありますが、個人的には現状維持かやっても chrome案くらいかなと思っています。GCの時にデフォルトで落とすのは Promise.rejectで最初からunhandledな例外オブジェクトを作れる以上、やり過ぎ感があるかなと。。

何も言わないのは一見不親切に見えるかもしれませんが、 unhandledRejectionの動きをどうするべきかは、ユーザーのアプリケーションでどうするべきかを決めることであって、 Node.js が積極的に決めることではないので、デフォルトをどうするべきか難しいのです。こういう話は『機構と方針の分離』という話もあり、機構は用意するが、方針としては何も意見を出さないという姿勢を持つような処理系の哲学でもあります。

ちなみに Domain

Domain は中で uncaughtExceptionを使って処理しているだけなので、もう使わないでください。 結局domainの領域でエラーがcatchできた所で uncaughtExceptionが起きていると、再起動する以外に策はありません。

今後としては Zone に期待しましょう。

docs.google.com

まだ目下のところ stage 0 ですが、 domain 的に発生した exception を領域ごとにまとめて掴んで処理することができるようになるので、 unhandledRejectionよりも細やかなエラーハンドリングが可能になります。Zone は中の実装がどうなるかまだ決まりきっていませんが、おそらく Promise のような機構を使って、 JSの内部で処理をすると考えられるので、 domain よりは使えるものになる可能性が高いです。

実際の所どうするべきか

ここから先は個人的な意見で、『自分がウェブアプリケーションサーバを Node.js で作るとしたらどういう風にエラーハンドリングをするべきか』を記述しておきます。

  • 基本的に Promise を使う
  • Web Application Framework のミドルウェアのレイヤできちんと例外を補足する
  • もしミドルウェアの外でエラーが起きても unhandledRejection を使ってエラーを補足する

基本的に Promise を使う

速度が問題になるのであれば、 これまでのスタイル(callback etc)をそのまま使ったほうがいいでしょう。ただ、所謂普通の web application のような後ろに DB があって、それをコールするような処理をするのであれば、 JIT最適化で稼げる速度よりも IO の時間のほうが問題になるので、これまで callback で書いていたような所は Promise で置き換えても問題ないと思います。

Web Application Framework のミドルウェアのレイヤできちんと例外を補足する

koa v2 からは async-await ベースでミドルウェアが書けるようになり、 async 関数は Promise でラップされる、という事なので、 koa v2 を使えば微妙な try-catchの書き忘れで全体の process が落ちなきゃいけなくなるというようなことは避けることが可能です。

一方で express とか hapi だと、ミドルウェアは Promise ベースじゃないので try-catchの書き忘れにより、上記に上げたような uncaughtExceptionが起きる可能性はまだまだ高いと言えるでしょう。

ただ express に関しては StrongLoop が出している記事で、babel + async-awaitを含めた今後のエラーハンドリングの方法が書いてあるので紹介します。

StrongLoop | Asynchronous Error Handling in Express with Promises, Generators and ES7

// promiseのエラーをキャッチして next に渡す関数を用意しておくlet wrap = fn => (...args) => fn(...args).catch(args[2])

// async 関数で middleware を書く、つまり middleware の中は Promise になる
app.get('/', wrap(async function (req, res) {let data = await queryDb()
  // handle datalet csv = await makeCsv(data)
  // handle csv}))

// もしもエラーが発生して、 try-catch を書き忘れたりしたとしても、 エラーハンドラにエラーが渡るので express のエラーになり、プロセスが落ちなくて済む// エラーハンドラ
app.use(function(err, req, res, next) {
  res.status(500);
  res.send(err);
});

実際の所 try-catchでmiddleware内部を囲んでいるのとそこまで変わりません。本来であれば、try-catch を入れてエラーをその単位でちゃんとハンドリングするのが良いですが、万が一忘れたとしてもwrap関数でerrorがunhandledRejectionにならないようになってちゃんと 500 エラーになるように担保してくれます。(ほぼ koa に近づくような話ですね。)

ちなみに Stream と一緒に使うときは下記のようにします。

app.get('/', wrap(async (req, res, next) => {let company = await getCompanyById(req.query.id)
  let stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res) // on error でフックしたエラーを next に渡す}))

もしミドルウェアの外でエラーが起きても unhandledRejection を使ってエラーを補足する

さらに unhandledRejectionを使って ミドルウェアの外で Promise のエラーが発生しても気がつけるようにしましょう。

callback スタイルの時は callback の第一引数に渡したエラーがハンドリングされるかどうかを ESLint なりの Linter でチェックできましたが、 Promise になると、エラーを catch しているかどうかを見るのは静的解析だけでは厳しいので、 unhandledRejectionを入れてチェックするのをオススメします。特に開発期間中は変なtypoだったり、Nullpointer exception 的な問題で気づかないのは問題なので、 開発中は気付きやすいようにエラーログに入れたり、敢えて落とすようにしてチェックしやすくするのをオススメします。本番になったら unhandledRejection レイヤーでは基本的には何もしなくても問題のないケースがほとんどかと、強いてやるなら一応ログに書き出すくらいでしょうか。

if (process.env.NODE_ENV === 'development') {
  process.on('unhandledRejection', (err, p) => {// 開発中はログに出力する
    console.error('Error : ', err);
    console.error('Promise : ', p);
    // もしも気づくのを早めたかったら落とすとか// throw err;});
}

process.on('unhandledRejection', (err, p) => {// 本番では何もしない// もしも何かしたければ、せめてログに書く等// logger.error(err, p);});

process.on('uncaughtException', (err) => {
  console.error(err);
  process.abort(); // uncaughtException の時は落ちる});

まとめ

  • Promise を使うとエラーを画一的に処理できる
  • unhandledRejection と uncaughtException の違い
  • checked exception と unchecked exception
  • Node.js では Promise の unhandledRejection が起きても何も言わない
  • domain はもう使わない
  • 現時点で Web アプリを作る場合はどうするべきかという私見

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

$
0
0

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 の話も面白かったのでいつか書きます

Chrome Dev Summitに参加しました!

$
0
0

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 もあるのでまた行きたいですね!


Yearly Node.js 2018

$
0
0

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 にしてみました)。

再入可能なロックの話

$
0
0

突然のロックの話

いきなりロックの話をしましたが、10月に(なぜか)一緒に働いてるメンバーとの中で大盛り上がりした話題です。もともとはリクルートテクノロジーズで行われている、柴田芳樹さんのプログラミングGo勉強会で話題になった話です。

yshibata.blog.so-net.ne.jp

ここにも書いてあるのですが、 Golangでは sync.Mutexを使ったロックでは再入可能ではありません。 一方 Javaのロックは再入可能です。

で、この設計に関しては合理的な解説が Russ Cox さんからされています。

groups.google.com

意訳すると以下のような感じですね。

再入可能なロックはbad ideaだ。

mutex を利用する主な理由は mutex が不変式を保護するためだ。
不変式というのは、 例えば 円環(linkedlistのようなもの) の 全要素 p に対して p.Prev.Next == p が成立するといった内部の不変式であったり、
自分のローカル変数 x は p.Prev と等しい、といった外部の不変式を指している。

mutexのロックを取るというのは、「私は不変式を維持する必要がある」というのと、「これらの不変式を一時的に壊す」という二点の表明である。
また、 mutexのロックを開放するというのは、「不変式にはもう依存していない」というのと、「一時的に壊した不変式は元に戻っている」という表明である。

再入可能なロックを取るというのは、いったん mutex のロックを確保した状態で再び mutex のロックを取るが、この状態では不変式を一時的に壊している可能性がある。再入可能なロックは不変式を保護しない。

再入可能なロックというのは単なる間違いであり、バグの温床になる可能性がある。

この話には納得できます。ロックの再入可能可否についてはできないようにする方が良いというのは納得できます。余談ですが、プログラミング言語Go以外の本にもEffective Javaにも同様の話がありますし、詳解Unixプログラミングでもこの手の話はあります。

じゃあなぜJavaは再入可能なのか

Javaは既存のAPI資産がスレッドセーフになっているものがあり、それらの中にはメソッドの中でロックを取っているものもあります。この状況では再入可能なロックを認めざるを得なかったため、今ではバッドプラクティスとして残っています。例を挙げると、HashTableやVectorといった既存のコレクションクラスがスレッドセーフですね。

ちなみに 詳解UNIXプログラミングの中に出てくる pthread も同じく再入可能にするオプションがあります。これも既存のAPIでスレッドセーフなものが多いから渋々追加している機能です。

というわけで、そもそも再入可能なロックを提供すること自体はGoでもJavaでもUnixプログラミングでも望まれていない訳ですね。

社内で盛り上がった内容

さて、再入可能なロックを提供しないという方針については基本的に全員同意しており、これまでの話に対しては反対意見は無いですが、よくよく考えると少し腑に落ちない点があります。それは、「不変式を守るのは、ロックの有無に限らず守らないといけないのではないか?」という点です。

この不変式を守るという話でmutexの再入可能なロックを取らなくしただけではなく、本来的には壊さないように全員が気をつけるべきであり、この問題について取り組んだのが、有名な「契約による設計 (Design By Contract)」の話です。

契約プログラミング - Wikipedia

EiffelやD言語ではこのDesign By Contract を言語仕様に取り入れているというのは有名な話ですね。

Golangでは、不変式を守るという話についてここまで説明があるにもかかわらず、Design By Contractを言語仕様には取り入れていませんし、assertすらありません。

assertを提供していない件についてはちゃんとFAQがありますが、契約による設計を取り入れてない理由も含めてGo言語がそういう取捨選択をしている理由はなんなのか、というので、話が盛り上がり、何度か柴田さんやkoichikさん、和田さんを含めて議論しました。

議論ポイントを整理すると、「ロックを再入可能にしない」という設計については納得するものの、「そこまで不変式を守ると言うなら契約による設計やassertについては入れないという選択をしたのはどうしてか」といった部分ですね。

ただどちらの主張も実は衝突するような話ではなく、「ロックを再入可能にしないのは不変式を壊したくないから、ただし、不変式を壊したくないからと行ってDesign By Contractまでのゴツい仕様は入れたくなかった」という話だろうと、古川は推測していますし、この理由である程度納得しています。

並列並行処理

色んなプログラミング言語を知っていると正解は一つではないというか多様な正解があるという事がわかります。Goの考え方はUnix哲学的なものであり、シンプルな解答を用意しつつも、バグの温床になるような言語仕様は避けようとする考え方が見られます。

一方で、この手のmutexの問題は基本的に「並列プログラミングが難しい」という問題に根ざしたものであり、これに対して何度も挑戦していないと議論ができるようなポイントに到達できないな、と改めて感じ、年末年始はこの本でも読もうかなと思いました。

www.oreilly.co.jp

JavaScript が読み込まれる前でもWeb Applicationを動かす

$
0
0

今回は最近取り組んでいる、 JavaScriptが読み込まれる前であっても「ちゃんと」 Web Application が動作するように作る話をします。

Server Side Rendering における注意点と対策

BFFを使ってServer Side Rendering をすることに数年前から取り組んでいます。

まずはSSRをやる上での注意点と対策について紹介します。

SSRをすることはSEOのためだと思われがちですが、個人的にはSEOのためにしているわけではなく、 First Viewを向上するため(特に First Meaningful Paintを向上するため)にやっています。

f:id:yosuke_furukawa:20190210220602p:plain
First View

SEOSSRに関しては Googleが最近出したこの記事SEO Considerations節が詳しいです。ここでは説明しません。

SSRをしない、Client Side Renderingのみの場合、この First Meaningful PaintJavaScriptがダウンロードされてからになるので、遅れてしまいます。

ユーザーの環境は様々で、潤沢なwifi環境が揃っていて、最新のマシンが使えて高速という環境ばかりではありません。古いマシンを使って、インターネット環境が整備されてない状況ではJavaScriptをダウンロードする時間もJavaScriptをダウンロードしてから実行する時間も遅くなります。

色々なケースも想定し、SSRを使って First Meaningful Paintを向上することで、表示されるまでの時間の短縮を行っています。

ただし、SSRFirst Meaningful Paintを高速化する一方で、表示が速すぎると、操作するまでの時間、 Time To Interactまでに乖離が発生します。

f:id:yosuke_furukawa:20190210221642p:plain
Gap between FMP and TTI

乖離が長ければ長いほど「見えてるのに操作できない時間」が長くなり、ユーザーのストレスになります

これを改善するためにはページあたりに読み込まれるJavaScriptの量を減らすことで、読み込まれる時間を改善する、 Code Splitting と呼ばれる前処理をする必要があります。また、Time To Interact の時間までインジケーターを出してユーザーに処理中であることを表示するのも有効です。

ただ、Code Splitting をしたとしても分割しすぎるとページ遷移のたびに差分のJavaScriptが必要になったり、そもそも React DOM などの巨大なライブラリに依存していると全体で読み込まれるJavaScriptgzip後で数100kbを超えることも珍しくなく、限界があります。

逆にSSRをやめて、Client Side Rendering のみにしてしまうと、「見えるまでの時間」と「操作するまでの時間」のギャップはなくなります。しかしこの場合、人間としては「見えて(認知して)から操作する」ので、ユーザーにとって最適な時間にはなりません。

JavaScriptが読み込まれる前でも操作できるようにする

そこで、今取り組んでるのが、いくつかのページではJavaScriptが読み込まれる前でも普通にウェブアプリケーションとして操作できるようにしています。

HTML と Server だけでもちゃんと動くように作る

こうなると、 HTML と Server だけでもちゃんと動くように作らなければいけません。JavaScriptを disabled にした状態でどこまでちゃんと動作するかを確認しながら作る必要があります。

リンクの場合

JavaScriptのみで動作させる場合、リンクには clickイベントをフックするハンドラを用意して、そこで pushStateなどのURL変更 APIを使って遷移させることが多いです。

SSRレンダリングした上で、リンクなどの処理は aタグで書きつつ、 遷移先のURLを定義します。 clickハンドラはそのままにしておけば、JavaScriptが読み込まれる前に実行してもただの aタグでの遷移として動作します。

<a href="/foo" id="js-link" />

// JavaScript
document.getElementById("js-link").addEventListener("click", (e) => {
  e.preventDefault();
  history.pushState({}, "", e.target.href);
})

この辺りはSSRを作る上ではライブラリに頼ることも多いと思うので、ライブラリが提供してくれる機能でも動作します。 react-router などのライブラリも同様の機能を提供します。

form の場合

form の場合は複雑です。formの場合はSSRとは違って、 method が POSTのケースも多いので、 POSTを受け付けられるエンドポイントを用意する必要があります。

また、 methodactionなどの form の属性もきちんと指定する必要があります。 form の methodを指定しないために、全部 GETリクエストになってしまったり、ボタンをsubmitにしてなかったために、きちんと送信されないケースも散見されます。

また、 POSTなどの更新系の操作をする場合、厄介なのは CSRF対策もしてあげる必要があることです。 hiddenの input に対して csrf tokenと呼ばれるセッションに紐づくランダムな値を設定しないといけません。XHRとは違ってカスタムヘッダを渡すことも、Cross Origin のときにpreflightチェックのリクエストが飛ぶこともないので、準備が必要です。

// form に対してmethodを指定する。<form onSubmit={handleSubmit} method="POST">
      <div>
        <input type="email" name="username" component={RenderInput} />
        <input type="password" name="password" component={RenderInput} pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"/>
      </div>
      {/* csrf 対策を入れる */}<input type="hidden" name="_csrf" value={csrf} />
      <div>
        <button type="submit" disabled={submitting}>
          Login
        </button>
        <button type="button" disabled={submitting} onClick={reset}>
          Clear
        </button>
      </div>
</form>

ログインのような典型的なフォームの場合はユーザーが何をするべきかが表示された段階ではわかっている事が多いため、操作するまでの時間を短縮することで、効果も大きくなります。

また、検索画面のようなフォームも入力項目が少ないため、ユーザーは表示された瞬間に迷わずにクエリーを打ち込みに行きます。このときにクエリーパラメータでURLが変わったとしても動くように作ってあげる必要があります。

このアプローチのメリットとデメリット

アプローチの内容はわかったところで、メリットとデメリットについて紹介します。

メリット

まずは性能的な面でメリットがあります。先程、操作できるようになるまでの時間と表示するまでの時間に乖離が大きいとユーザーはストレスを感じる、という説明をしましたが、JavaScriptをダウンロードするまで待つ必要はなく、「操作できるようになる時間が表示された時間とほとんど同じ」になります。

ちょうど数ヶ月前に Netflix React の CSR をやめて、SSRでのみReactを採用した、という記事がありましたが、そこでもパフォーマンスの改善事例として紹介されていました。

アプローチとしてはほぼ一緒です。Time to Interact にフォーカスするのであれば、 JavaScriptをダウンロードされてなくても動作させる方が効果的です。

次に accessiblity としても効果があります。JavaScriptに頼らずに普通に作ると verified な HTMLを書く必要に迫られます。form の method や action もそうですが、input の name や type といった属性もきちんと書かないといけません。こうすることで、矯正ギブスのような役割を果たしてもらえます。

デメリット

一言で言うと、「実装するのが大変」というところです。一度すべてのページを JavaScriptをオンにして動かせるようにしたあとで JavaScriptがオフでも動くようなアプリケーションにすることは実装の観点から見るとやってやれなくはないものの、大変です。

また、完璧に JavaScriptが動作するアプリと同じUXを提供することは不可能です。先程のformの例でいうと、 validationなどは input タグの pattern属性にマッチしてるかどうかしか出すことができません。 JavaScriptを使った validationでは、もう少し細かく入力値をチェックできます。例えば「パスワードは英字、数字、記号の3種類が必ず入ってて、8文字以上」などの条件の validationpattern属性で正規表現で書いたとしても、「パターンにマッチしたかどうか」だけしかチェックされません。 JavaScriptでは、「記号が抜けてる」、「8文字以下」という細かくメッセージで何が満たせてないかを表示させることで入力を補助させることができます。

リンクならともかく、ボタンの場合や、モーダルウィンドウで確認ダイアログを出したい場合など、大体においてJavaScriptが必要なケースは多く、すべてを JS が disabled にした状態で動作させるようにするのはやはり難しいと言わざるを得ません。

個人的には、ログインやトップページの検索画面など、ユーザーがある程度最初の方に触る(JavaScriptがダウンロードされる前に触りそうな)ページを JS disabled でも動作するように作り、残りは noscriptタグで JavaScriptがオンじゃないと動かない旨を出してあげるくらいでしょうか。

また、最後にどうしようもないところですが、解析系のタグが動作する前に動いてしまうので、ユーザーのトラッキングはできない可能性があります。この手の解析系がちゃんと動かないとNGのところも多いので、注意してください。

まとめ

JavaScriptが読み込まれる前に SSRと HTML だけで動作するアプリを作ることに関しての紹介をしました。 最近の開発では、この方法を基本としており、SSRの弱点の一つである、 First Meaningful PaintTime to Interactまでの差を JavaScriptが読み込まれる前でも動作させることで解消しています。

ただし、すべてのページでできているわけではなく、トップページに近いような場所でのみ、部分的に動作させています。こうすることで以下のような効果を狙っています。

  1. JavaScriptがダウンロードされてなくてもある程度は動作することで、パフォーマンス上のメリットを狙う
  2. HTMLでも verified な状態にすることで、 accessibility の観点や JS がオフのユーザーであってもある程度は利用できるようにする
  3. すべてのUXをJSオフの状態で提供することは不可能なので、必要なページではJSがダウンロードされるまで動作しないように作る

こうすることで、SPAのUXも提供しつつ、潤沢なインターネット環境を持っていないユーザーであってもある程度高速に表示され、動作するように作っています。

npm, yarn による zero install 戦略

$
0
0

jsconf.eu 2019に行ってきました。 特に npm や yarn の今後の話とそもそも Registry をどうしていくか、の話があったのでお知らせします。 そもそも Registry をどうしていくかについては次のエントリで話します。

tink: A Next Generation Package Manager

npm の次のコマンドラインツールである tink が紹介されていました。

github.com

presentation: github.com video:

www.youtube.com

そもそも npm の仕組み

  1. ローカル依存ファイルを読む (package.json, package-lock.json, shrinkwrap.json)
  2. 存在しないパッケージのメタデータをfetchする
  3. 木構造を計算して、実行する(npm v3 以降だとflattenする)
  4. 実際に存在しないパッケージをダウンロードする
  5. インストールスクリプトを実行する

今のどこがダメなのか

npm は tar ballを fetch して、そこから gunzipして、最後にできあがったファイルをnode_modules以下にコピーするという非常にピーキーな処理をしています。最初のfetchは network IOに影響し、 gunzipCPUに影響し、ファイルコピーは file IOに影響します。

これを簡単にするためにキャッシュしたり、展開済みのファイルをハードリンクさせたりとチューニングをしています。 これ自身は必要な処理なのですが、実体のファイルとして作る関係上、どうしても node_modules は巨大になります。

https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosuke_furukawa/20180604/20180604215229.png

tink sh

tinknode本体の代替として起動します。

$ node foo.js

で起動するのではなく、

$ tink sh foo.js

で起動します。これで何がやりたいかというと、node_modulesを物理的なフォルダにするのではなく、仮想上のフォルダにすることを目指してます。

仮想上のフォルダになることのメリット

いくつか利点がありますが、まず実際にファイルをコピーする必要がない分、 File IOへの影響は緩和されます。

また、 tink自身はローカル内にハッシュ値を基にしたキャッシュを作るので、ハッシュ値が一致したパッケージに関してはキャッシュが使われます。  Network IOへの影響も緩和されます。

また、最大のメリットとして、 tink shで実行した際にランタイムで依存モジュールを解決するため、 npm installのコマンド実行が不要になります

つまり、 git clonegit pullしてから依存ファイルがなかったとしても tink sh <cmd>で起動すれば実行時に依存モジュールを解決し、起動することができます。

virtual node_modules
virtual node_modules

この試みのことを zero installと呼びます。

prepare && unwind

本番環境で Docker 等を使って毎回ゼロからイメージを作ってる場合はイメージ作成後のキャッシュがないため、毎回起動時にパッケージのダウンロードが開始されます。

これを解決するために事前にFetchしてくるコマンド(prepare)と事前に node_modules を作るコマンド(unwind)の2つが提供されています。

$ tink prepare # 事前にfetchしてくる
$ tink unwind # node_modulesの実体を作る

load map

tinknpm v8npmコマンドに統合される予定とのことでした。

f:id:yosuke_furukawa:20190610102508p:plain

yarn v2: berry

presentation:

www.youtube.com

yarn v2tinkと同様の戦略です。zero installを実現しています。 .pnp.jsというファイルを内部に作成し、そこに依存モジュールを解決できるようにしています。

開発ツールとしては tinkyarn v2も同じ戦略を取っていました。細かな機能の違いはあるのでいくつか紹介します。

yarn contraints

package.jsonと実行時の依存関係に矛盾があった場合に、警告した上でpackage.jsonを修正してくれるサブコマンドです。

一番多いユースケースは、実行時に依存してる package が package.jsonに書かれてなかったとか、 workspace と一緒に使った際にmonorepoのサブパッケージ間で異なるバージョンのライブラリに依存してた等ですね。

いわば、 package.jsonの linter みたいなものですね。

どうやって実現しているのか

tinkyarn v2も実態は nodeのプロセスである以上、 node側の標準モジュールでは node_modules以下にあるファイルをロードしに行く必要があります。 これを解決するために、tinkyarn v2では nodeの標準APImoduleにパッチを当てています(!)

moduleのファイルロードを行う箇所のメソッドを拡張し、 node_modulesがなくてもファイルをロードできるようにしています。

標準のモジュールロードからは逸脱しているため、今のところ使うのは危険ではあります。将来的に読み込み方が変わる可能性もありますし、標準の node の読み込み方を変更する可能性もあるので、まだ experimentalと言ったところでしょうか。

ただし、 electronも同様の手法でモジュールを読み込めるように拡張しているので、そこまで変更がドラスティックに加えにくい箇所でもありますね。

実際に、 tinkの解説では、 electronも同じことやってる(ので大丈夫)というニュアンスで発表していました。

f:id:yosuke_furukawa:20190610101630p:plain

まとめ

モジュール管理は zero install時代になっていくと思います。事前の処理を不要にすることで開発効率を上げて、 git clonegit pullから何もしなくても起動できるようになっていくと思われます。ただ一方で実際に本番で zero installにしてしまうと、起動してから実際に動作するまでのパフォーマンスは落ちる可能性もあるため、起動方法は変更になるかと思われます。

これらを解決するための本番環境用のコマンドとして、 tink preparetink unwindも公開されているので、それらを使って行くのではないかと思っています。

また一方で、 tinkyarn v2もほとんど同じことをしているので、どちらにも有意差があるようには見えず、状況は変わらないまま今後に突入していくのではないかと思われます。

これらを踏まえて、 Registry についての話を書きます。また次回!

JavaScript Registryの今後

$
0
0

さて、前回は tinkyarn v2における CLI戦略の話でした。次は JavaScript Registry についてです。

ちなみにこの内容が今回 JSConf.EU 2019で一番盛り上がったトピックです。

JavaScript Registry とは

JavaScript Package をバックエンドで管理しているサービスです。 npmが管理しているものがいちばん有名です。他にも GitHubが管理する Registryが公開される予定です。

The economics of Package Management

f:id:yosuke_furukawa:20190611135113p:plain
the economics of package management

slide:

github.com

video:

www.youtube.com

「Package Managementの経済」というタイトルです。聴講者からすると、何話すのか不明でしたが、惹きつけられるものがありました。終わったあとはこれぞ「Tech Talk」という発表でした。是非時間があれば全ての動画を見てみるとよいかと思います。

筆者はここで語られていた内容を基に entropicについて紹介し、最後にこの Registry で何がやりたいのかを話します。

Successful JavaScript Registry is hard

npmのパッケージは大きくなりました。いまや100万パッケージを超えるほどです。

一方で、 npm incは営利を目的にした団体です。これだけ多くのpackageがあると管理コストも高くなります。 npm incの経営状況がどうこうは色々な話がありますが、実際に公開されてる情報があるわけではないので、特に何かをここで言うつもりはありません。

ただ、 Registry の管理をビジネス的に成功させるのは難しい、ということです。

npm incの Registry は基本的に無料で使えます。無料で使う場合、すべてのライブラリは publicとして公開されます。 privateenterpriseでグループを作るなど、特別なアクセスコントロールをするライブラリを作る時には有料版を契約するというビジネスモデルです。

npmpublicOSSのライブラリが増えれば増えるほど、 運用コストがかかります。その運用コストを privateライブラリや enterprise Registry の売上で賄います。小さいサイズの Registry は運用コストがあっても寄付金で調整するのはそこまで難しくありませんが、大きなサイズになってくると寄付金だけで運用するのはやはり難しくなってきます。これをなんとかするために、 npm incは Venture Capital からも出資を受けています。 

Venture Capital の意向が変わったら方針も変わりますし、結果としてコミュニティにとって最適な運営が行われるとは限らないわけです。

この話は npmに限りません。 RubyGemsでも Perl CPANでも運営コストの話はあります。ただ、npmが管理している packageの数は膨大で、他の Registry よりも遥かに難しい状況になっています。

f:id:yosuke_furukawa:20190617182135p:plain
module counts

http://www.modulecounts.com/

じゃあどうするか

Registry を分散して管理できるようにしていこう、というのがこの発表の内容でした。

一つの団体が一つの Registry を集中管理している状況をやめて、分散管理するシステムを作れるようにして、みんなでちょっとずつ運用コストを分散できるようにしていこうという話です。

これの構想の基になっているのが、JSConf EUトークの中にある、 Entropicです。

github.com

Entropic: a federated package registry for anything but mostly JavaScript

Entropic は 新しい Package Registry です。 npmyarnではなく、新しい CLIである dsも持っています。

entropic/cli at master · entropic-dev/entropic · GitHub

dsentropicregistryにつながることはもちろん、npmの Registry も read only でつながります。read onlyなので、publishはできませんが、 npmに登録されている package もダウンロードできるようになります。

ちなみにまだ experimentalなので変更される可能性も大いにあります。

どうやってpackageを指定するか

dsdomain , namespace , package nameすべてを指定して、ダウンロードします。

namespace@example.com/pkg-name

こういう指定方法で Package.tomlに記述があると、 package がダウンロードされます。例えば、 ds本体をdownload したい場合は chris@entropic.dev/dsと指定します。

ドメインを指定するので、別サーバで別DomainのRegistryであっても package の指定ができます。 (逆に言うと Domain を失効したらダウンロードができなくなるということですね。)

もしも npmに接続したい場合は、 legacy@entropic.dev/<pkgname>legacyネームスペースを指定し、 entropic.devドメインpkgnameにアクセス先の package を指定すれば npmからダウンロードできるようになります。

(おそらく entropic.devlegacyネームスペースは npmへの alias になってるんでしょう)

Docker の実行環境さえあれば、 Registryを構築することは簡単にできます。

https://github.com/entropic-dev/entropic/tree/master/services/registry#running-your-own-registry

Entropic は自分たちの手元で起動し、対象となる package ダウンロード先のhostを指定してダウンロードします。その hostに自分たちが使う packageのキャッシュが構築され、初回は Package.tomlに記述したドメインのホストからダウンロードされますが、 2回目以降は自ホストのキャッシュを利用します。

f:id:yosuke_furukawa:20190617185414p:plain
entropic download chart

アクセス権

すべての package は publicとなり、誰からでも install し、誰からでも閲覧できる形になります。 privateのアクセスをしたい場合は GitHubの Registry を併用する形になると思います。

また、一緒にライブラリを編集したい場合は publish権限を他のユーザーに付与することも可能です。

https://github.com/entropic-dev/entropic#overview

Entropic のゴール

ここからまた少しエモい話ですが、 Entropic全ての JavaScript package を Entropic上で扱おうとしてはいません。つまり、これで npm を倒すという話ではありません。

じゃあ何がゴールになるかというと、

1. 「何もしないでnpmが倒れるか、有効策が出るまで待つ」以外の選択肢を用意したい

2. 「分散管理するRegistryの運用知見」を広めたい

3. 「中央集権から分散管理」に振り子を戻したい

という話です。 npm を倒すのではなく、 npm 以外の選択肢を用意し、その運用知見を全体に広げ、分散管理していくという話は非常に分かる話で、特別な専門知識がなくても管理できるように 共通理解をみんなで深めていこうという深い話でした。

f:id:yosuke_furukawa:20190617184648p:plain
take back the commons

まとめ

1年前、 JSConf EU 2018 で、 Ryan Dahl は module や package について、深く色々な反省をしていました。

yosuke-furukawa.hatenablog.com

その中でもあったのが、「Nodeのモジュールの管理運営自体を private controlled にしてしまったこと」があげられていました。

1年後、 JSConf EU 2019 は節目の年です。コミュニティを代表するようなカンファレンスで Entropicのような発表があったことは一つの epoch making な話だったと思います。来年は一旦 JSConf EUはありませんが、数年後にどうなっているか確認する、というバトンを渡されたような思いでした。

今年 JSConf JP をやる予定ですが、この辺の話もできれば幸いです。

ISUCON 9 予選に isucon_friends として参加し、予選総合3位でした。

$
0
0

久しぶりの本戦出場

ISUCON3 以来なので 6 大会ぶりですね。。。思えばずっと予選で負け続けてきたものです。

yosuke-furukawa.hatenablog.com

結果どうだったか

予選3位通過でした(棄権含む)。最終スコアは 27,470 ですね。 isucon.net

1位 nil 1 [1] 52,440
2位 にがり 1 [1] 36,270
3位 isucon_friends 3 [0] 27,470 ★
4位 いんふらえんじにあー as Code 3 [0] 26,460
5位 ようするにメガネが大好きです 3 [0] 25,200

僕らよりもスコアが跳ねている、にもかかわらず、学生が1名でやっている、nilにがりは本当にすごいと思います。

何をやったかまとめ

言語は Go でした。(Node.js ではありません。)

  • Index貼ったり、OR検索をUNIONに変えるなどでチューニング 2100点 => 2800点
  • キャンペーン還元率を 0 => 1 に。 この時点では3300点前後になるも、ボトルネックがログインに移った。
  • 3台構成にして、 nginx, app, DB にした。 この時点で 8800点程度、ただこの時点ではボトルネックはログインのまま。
  • bcrypt 剥がして SHA256 に rehash することに。ベンチマークを何度も回してその都度 rehash された password の結果をdumpし、パスワードハッシュ部分だけ書き換えるように。ベンチマーク流すたびに 10000点から少しずつ伸びていくので楽しかった。この時点で14000点
  • キャンペーン還元率を 1 => 2, 3, 4 と一気に変えていった。16000点に。
  • /buy 内でAPI呼び出ししてる所を並列処理化、また同時に /users/transactions.json内で API呼び出ししてるところを並列処理化。25000点に一気に伸びた。
  • もう後は Lock wait 待ちが多くなったので、 N + 1 を改善しようとするも断念。Transaction Levelを REPEATABLE READ から READ UNCOMMITTED に変えてみたりして、悪あがき。 26000点になった。 (fail されないかビクビクしてた)
  • 最後にすべてのログをオフにしてレギュレーション確認しながらベンチマークを実行、 27420点 が出たところで終了

細かく効いたかは定かではないがやったこと

  • nginxのエンドポイントを HTTP/2 にした
  • Accept-Encoding が gzipだったら gzip返すようにした (JSONのサイズが劇的に下がってた)
  • キャンペーンテーブルをインメモリに持った
  • 外部 APIへのリクエストをする際に HTTP Agent の MaxIdleConns と MaxIdleConnsPerHost を拡張、1 ホスト 3000 コネクションまで持てるようにした。

やってみようと思いつつもできなかったこと

  • N+1 の解消
  • APIのレスポンスが done status ならキャッシュする

この辺が思いついてはいたものの、残り時間で実装しきれず、断念した所です。 N+1 を IN 句に変えるだけでもやってもよかったかも。 この辺をやれていればもう少し伸びたかもしれません。

ISUCON9の感想

まずは予選突破できて本当に良かったです。若手や同僚にパフォーマンスの考え方を教えている身でありながら、本戦突破がそもそも1度しかできてなかったことを歯がゆく思ってました。

突破できて安心しました。

余談ですが、社内でR-ISUCONというISUCONを自分たち向けにカスタマイズした問題を作って会社全体で合宿しながら半年に一度のペースで会社全体で競っています。

今回、予選を突破している同僚が毎回社内ISUCONで一緒にしのぎを削ってるチームで、とても嬉しく思いました

・theorem 3 [0] 12,360
・ふんばり温泉チーム 3 [0] 12,060

R-ISUCONという社内ISUCONをやっていた結果、自分たち含めて3チームも予選を突破した事を本当に嬉しく思います。 このスタンプを社内でも使っていこうと思います。

f:id:yosuke_furukawa:20190908230806p:plain
本戦であいましょう

R-ISUCONについては今年の初めに 941さんと一緒にデブサミで登壇したのでその時の資料を展開しておきます。

logmi.jp

JSConf.JP を開催しました。 / We have held JSConf.JP !

$
0
0

Acknowledgement

集合写真
JSConf Japan Photo

色々と終わって来たので鉄は熱い内にと思ってブログを書いてます。ホント大変なこともたくさんありましたが、やりきれて本当によかったです。

We have done the rest of tasks, so I am writing a blog. I had lots of troubles, problems, issues but I am so glad that I have completed to run JSConf.JP.

これまで JSConf.JP を開催したいという声はたくさんありました。ただしこれまで開催はできませんでした。

There are lots of voice that " I would like to hold JSConf.JP ". However noone hold the event.

理由はいくつかありますが、 JSConf を開催するのに 2つレギュレーションが必要だというのが大きな理由だと思います。

There are a few reasons, but the main reason is 2 rules to hold JSConf.JP.

1つは、 JSConf に参加したことがあること、もう一つは JSConf の別なオーガナイザーからメンタリングを受けていること。

First rule is to join JSConf as attendee, 2nd rule is to have mentoring by other JSConf organizer.

1つめのルールは簡単にクリアできましたが、2つめのルールをクリアするには割と勇気がいりますね。

First rule is easy to clear, 2nd rule needs to have courage to solve the rule.

まずは JSConf Japan をやりたいと言った時に相談に乗ってくれた JSConf Colombia の Juanに感謝を言いたいと思います。彼が僕のメンターになってくれたおかげで、 JSConf.JP が開催できました。

I would like to say thank you to Juan, JSConf Colombia organizer. He gave me some advice about how to hold JSConf Japan. I could hold JSConf.JP thanks to my mentor Juan.

他にもプロジェクトマネジメントのできない僕に代わり、自分で何をするべきかを考え、色々行動してくれたスタッフ、色々と不手際がありながらも参加してくれた参加者、スポンサーの皆様にも感謝を言いたいと思います。

And I would like to say thank you all staff that they think what to do and take some actions instead of me and all attendees, and all sponsors.

JSConf Panel Talk

ユニークな試みの1つとして、 JSConf Panel Talkというパネルディスカッション形式でのトークを私がやりました。

We have an unique contents, JSConf Panel Talk. I have done panel discussion styled talk.

このセッションでは、 JSConf EUのオーガナイザーの Jan, BrooklynJS, Google DevRel の Kosamari さん, Automatic の Lena Morita さんをお呼びして、日本なりの国際カンファレンスの在り方を話し合いました。

This session is discussed about Japanese owned International Conference with Jan, JSConf EU organizer and Kosamari, Google DevRel and Lena Morita, Automatic .

トークの内容は、例えば JSConf JP にライブトランスレーションがあったほうがいいか、 JSConf JP は他の JSConf と比べてチケットが安すぎではないか?といった内容を参加者から集めて語り合いました。

For example, talked contents are JSConf.JP would be better to set up live translation? Why is JSConf JP ticket so cheap compare to other JSConf? We have collected these opinions from attendees.

どのトピックも盛り上がりました。30分では足りないくらい。

Every topic are heated up. We need more time to discuss about that.

JSConf JP Best Prices
JSConf JP Best Prices

面白かったのは、完全に海外と日本ではカンファレンスのチケットに対する考え方に差があるところです。

I have an interest in the difference of ticket thinkings between Japan and international.

海外カンファレンスのオーガナイザーは基本的に企業が従業員にチケットを買っていくもの、という考え方です。対して、日本では、個人で買うもの、という考え方です。

International Conference organizers think company buys tickets for employees. However Japanese conference organizers think an individual buys tickets for themselves.

だから数万円以上の金額になることも珍しくありません。

So their ticket price become over 100USD. This price is not so rare.

日本でもそれを念頭に置いたカンファレンスもありますが、基本的に無料だったり、安い値段になります。

some Japanese conference have the ticket price over 100USD. However basically the ticket price is free or cheap.

海外カンファレンスは高くする代わりにより設備にお金をかけます。

International conference takes more cost for equipment, facility etc.

日本だとスポンサーを増やす必要があります。

Japanese conference needs to get more sponsors if same equipment needed.

どっちがいいというわけではないですが、いま時点で日本の JSConf.JP では、例えば全セッションをライブトランスレーションを用意するなどの対応をすると、予算が足りません。

I don't know which is better, but now our JSConf.JP does not have enough budget to setup live translation in full sessions.

また、海外では hallway track を意識するとのことでした。 hallway track は廊下でのセッションで、所謂井戸端会議のようなものですね。

And International conference keeps in mind "hallway track". hallway track is a session in hallway, in Japanese Idobata-Kaigi.

つまりコミュニケーションを意識する、もっと参加者同士のつながりを持ってもらえたら、次に来るときも「あの人に会いに行きたい」という意識から来てもらえるようになる、ということでした。

They keep in mind "communication", if attendees have connections with each others, they would come in next year, they have a feelings that "I want to see that person".

Next JSConf Japan

次もやります。次は 2020 年の 9月末頃を予定しています。

We will hold JSConf.JP in next year. We are planning to hold in September 2020.

あと TC39 が 東京で9月にミーティングがあるとのことです。 TC39 のメンバーともコラボレーションしたいと思っています。

And TC39 will hold their meeting in September at Tokyo. We would like to collaborate with TC39 members.

f:id:yosuke_furukawa:20191209033045p:plain

またぜひ来てください :)

We are looking forward to seeing you in next year :)


Dual Package Hazard

$
0
0

この記事は Node.js Advent Calendar 2019 の 11 日目の記事です。

qiita.com

今回は全 Node.js で ES Modules を利用するユーザーが知っておくべき Dual Package Hazardについて紹介します。

ESModules がフラグ無しでサポートに。

これまでは ES Modules は --experimental-modulesフラグが無いと使えませんでしたが、 フラグ無しで Node.js v13.2.0 から使えるようになりました。ES Modules については CodeGrid の記事で詳しく書いているのでそれを一読していただけると理解がスムーズになると思います。逆に読んだ方は Conditional Exports / Dual Package Hazard の節まで飛ばしてもらって構いません。

www.codegrid.net

Node.js における ES Modules のおさらい

ES Modules と従来の CommonJS ベースの Script とは別なものです。 ES Modules は何も宣言しなくても Strict Mode になり、予約語も global に定義されている変数も異なります。Node.js ではこれらの従来の CommonJS と ES Modules を分けるためにいくつかの方法を提供しています。

  1. ファイル拡張子が .mjsになっていること
  2. ファイル拡張子が .jsになっている、もしくは拡張子がない場合、最も直近の親モジュールの package.jsonに記載されている typeフィールドが moduleになっていること
  3. --eval--printもしくは Node の標準入力から渡されるようなケースでは --input-type=moduleをつけること

特にこれまでと大きく異なるのは、 ファイル拡張子です。 これまで (Node v10まで) はデフォルトで 1. のファイル拡張子が .mjsしか認めていませんでしたが、これからは .jsでも package.jsonのフィールドに type: “module”の記述があれば ES Module として読み込まれます。

また、 ES Modules として宣言するのと同様の宣言が CommonJS でも可能です。.cjs拡張子をつける、 typeフィールドに commonjsと入れるなどの対応をすれば CommonJS として宣言することが可能です。

以下にフローチャートを記述します。

f:id:yosuke_furukawa:20191209230938p:plain
ES Modulesかどうか

ES Modules の相互運用性に関して

現在の Node.js には ES Modules と CommonJS の2種類の module があります。これらの module が混在する場合はどうなるでしょうか。 Node.js はどちらであっても透過的に扱えるように互換性を取っています。この「透過的に扱えるように互換性を取ること」を “Interoperability(相互運用性)” と呼びます。

この相互運用性を保つため、 ES Modules からでも CommonJS をimportできるようになっています。CommonJS から ES Modules を呼ぶことも可能ですが、詳細は後述します。

import するとき

以下のサンプルは ES Modules から module を import する時の方法です。

// このファイルは foo.mjs として定義され、拡張子が `.mjs` になっている。// すべての Node.js のモジュールは ES Modules としても import できるようになっている。import fs from “fs”;

// 名前付きで import する場合は以下のようになる。import{ readFile } from “fs”;

// ES Modules はもちろん import できる。`.js` であっても `.mjs` であっても、特別な記述がなければ import 時は ES Modules としてロードされる。import foo from “./foo.js”;

// npm module などのモジュールもロードできるが、type フィールドがmodules じゃない場合は commonjs としてロードされる。import _ from “lodash”;

// 無理矢理呼んだ場合であっても、 package.json の type フィールドが `module` ではないため、 commonjs になる。import _ from “./node_moduels/lodash/index.js”;

// commonjs としてロードしてほしい場合は拡張子を .cjs にする。import bar from “bar.cjs”;

CommonJS から ES Modules を呼びたい場合は、Dynamic Import 構文( import())を使います。

// このファイルは CommonJSとして定義されている。const _ = requrie(“lodash”);

// CommonJS から ES Modules を使う時は Dynamic Import で構文として使う。import(“./foo.js”).then((foo) => {// foo});

相互運用性をサポートしているため、CommonJS から ES Modules を呼ぶことも、 ES Modules から CommonJS を呼び出すことも可能です

ES Modules と CommonJS 両方に対応した package を作る

ES Modules 形式と CommonJS 形式の両方に対応した package を作る場合は以下の処理が必要です。両方に対応すると、 importrequireの両方でロードするモジュールを作ることが可能です。

  • 標準のロード形式を ES Modules 形式にするか CommonJS 形式にするか選択する。ES Modules 形式にする場合は、 package.jsontypeフィールドを moduleにする。 CommonJS 形式にする場合は typeフィールドを commonjsにする。
// ./node_modules/es-module-package/package.json{"type": "module", // ES Modules 形式"main": "./src/index.js"}
// ./node_modules/es-module-package/package.json{"type": "commonjs", // CommonJS 形式"main": "./src/index.js"}
  • 標準のロード形式を CommonJS 形式にした場合、ES Modules から読み込むエントリポイントを作り、そこから読み込むようにドキュメント等で記載が必要です。 ES Modules 形式にしたとしても同様で、 CommonJS から読み込むエントリポイントを作る必要があります。
// ES Modulesで読み込む用の endpoint から読み込んでもらうimport foo from “foo/esm.js”;
  • もしも import時の endpointをカスタマイズして使わせたい場合は package exports の機能を使います。package exports はファイルパスに対して別名をつける機能です。
// ./node_modules/es-module-package/package.json{"exports": {"./submodule": "./src/submodule.js"}}
// package.json に書いてある exports をパスとして使える。import submodule from 'es-module-package/submodule';

これらの機能をうまく使えば、見た目上は同じく使えます。 import する際は import submodule from “es-module-package/submodule”で、 require する際は const submodule = require(“es-module-package”);で使えます。

ただしこれだと読み込み先のモジュールの名前をドキュメントなりで教える必要があります。そこで v13.2.0 からの experimental な機能として追加された Conditional Export という機能を使うとこの状況を改善できます。

Conditional Export

読み込み先のモジュールのエントリーポイントを用意しておき、条件に応じて切り分ける機能です。

{"name": "test",
  "type": "module",
  "main": "./main.mjs",
  "exports": {".": {"require": "./main.cjs",
      "default": "./main.mjs"}}}

こうしておくと、 require("test")で呼ばれたときは main.cjsが呼ばれ、 import "test"で呼ばれたときは main.mjsが呼ばれます。 まだ実験的な機能なので使うためには --experimental-conditional-exportsフラグ付きで呼び出す必要があります。

$ node --experimental-conditional-exports foo.js

このように ES Modules でも CommonJS でも呼ばれるようにする package のことを Dual Package と呼びます。

Dual Package Hazard

前置き長かったですが、やっと本編です。

Dual Package には特定のバグのリスクが伴います。このリスクのことを Dual Package Hazardと呼んでいます。きちんと理解して Dual Package 対応を進めていきましょう。

Dual Package はES Modules と CommonJS で一見同じものをロードしているようで、実はファイルが違います。この『ファイルが違う』、という事が下手をするとインスタンスの違いを引き起こし、結果として予期しない問題になり得ます。

Dual Package Hazard
Dual Package Hazard

例を挙げて解説します。 testという Dual Package を用意します。これは importから呼ばれたときは main.mjsを呼び出し、 requireから呼ばれた時は main.cjsを呼び出します。

// node_modules/test/package.json{"name": "test",
  "type": "module",
  "main": "./main.mjs",
  "exports": {".": {"require": "./main.cjs",
      "default": "./main.mjs"}}}

package の 規定となる class を定義しておきます。これを core.cjsとして定義します。

// node_modules/test/core.cjs

module.exports = class A{};

ESModules で定義されてる main.mjscore.cjsをロードし、 instance 化した後で exportで公開します。

// node_modules/test/main.mjsimport A from "./core.cjs";
exportdefaultnew A();

CommonJS で定義されてる main.cjsの方も同様に class A を instance 化した後で、 module.exportsで設定します。

// node_modules/test/main.cjsconst A = require("./core.cjs");
module.exports = new A();

同じようなものを export してますが実体が異なります。これを同じノリで使おうとするとハマる可能性があります。

// foo.mjs// node --experimental-conditional-exports foo.mjs
async function main() {const t = require("test");
  t.core = false;
  const t2 = await import("test");
  console.log(t); // A { core: false }
  console.log(t2.default); // A { } !! 変更が反映されていない !!}

main();

この小さなパッケージでは間違えることは少なそうですが、例えば plugin 形式で context を引き継ぐような webpack, gulp, grunt のようなツールでは、ES Modules と CommonJS が同じインスタンスを指していない事で plugin 同士で context が引き継がれず思いも寄らない不具合を引き起こしかねません。

このように同じものをロードしているようで、実は実体が違うものがロードされた結果、思いも寄らない不具合を引き起こすリスクを Dual Package Hazard と呼んでいます。 Node.js の package は特に pluggable な作りのものが多いので、気をつけて使う必要があります。

Babel や esm などの transpile するやり方は CommonJS に変換して使っているものが多いため、実際にはこの問題は起きません。 Node.js が両方で読み込めるように相互運用性を高めた結果、気をつけて利用しないと、発生しうるのがこの不具合です。

Dual Package Hazard を局所化する

これに対応するのは実はそこまで難しくありません。同じインスタンスを指すようにしてしまえばよいのです。この例だけで言えば、 core.cjsからロードしたものを instance にして返却するのではなく、 instance 化済みの状態で export し、 singleton にするという手があります。 

// node_modules/test/core.cjsclass A{};
module.exports = new A(); // singleton にして公開する

このようになるべく状態を作るところを一箇所にまとめ、エントリーポイントごとに状態を共通化させる事が重要です。

もしくは、 Node.js からは CommonJS 形式でのロードに集中し、 browser などの実行環境で ES Modules 形式を使うという消極的な戦略にしてしまうことも可能です。

// node_modules/test/package.json{"name": "test",
  "type": "commonjs",
  "main": "./main.cjs",
  "exports": {".": {"require": "./main.cjs",
      "browser": "./main.mjs", // browserっていう実行環境でのみ main.mjs を読み込ませる(何らかのツールでこの情報を参照してもらう)"default": "./main.cjs"// その他の状況では commonjsにする}}}

この場合は、Nodeとbrowserで同じスタイルで読み込まれますが、そもそも実行環境がブラウザとNodeで異なるため、 CommonJS と ES Modules が同じ環境からロードされることがありません。※ browser propertyはデフォルトではブラウザが見てくれるわけではないのでなんらかのツールのアシストが必要になります。

Dual Package 自体は便利ですが、このような混乱があることを忘れずに。色々な回避策はもちろんありますが、まだベストプラクティスと呼べるほどのものが無いのが現状です。

まとめ

  • Node.js ES Modules はv13.2.0からフラグ無しで使えるようになりました。
  • ES Modules からも CommonJS からも両方から使えるように相互運用性をサポートしています。
  • ただし、 ES Modules と CommonJS の両方に対応した Dual Package を作る時は注意が必要です。
  • お互いの instance が違うことで ES Modules と CommonJS のどちらかだけでは起き得なかった不具合、 Dual Package Hazard が発生します。
  • これを直そうとするといくつかのトレードオフが発生します(singletonにする、環境ごとで切り分けるなど)
  • まだベストプラクティスはない状況です。積極的に使ってフィードバックするもよし、消極的にまだ運用を控えるのも良いでしょう。

参考資料

nodejs.org

github.com

Universal Data Fetch ライブラリ、 Specter の紹介

$
0
0

この記事は Recruit Engineers Advent Calendar 2019 の 16日目の記事です。

adventar.org

最近僕が作っている OSSである Specter の話をします。

github.com

Specter とは

Client から Backend と BFF から Backend への Universal なデータフェッチを提供してくれるためのツールです。以下の特徴を持ちます。

  • 軽量
  • TypeScript Freindly な型付け機能
  • 投機的先読み

2年ほど前に報告されたCPUの投機的実行に基づく脆弱性である、 Spectreから来ています。と言っても、脆弱性の名前ではありません。投機的先読みを機能として持っているため、この名前をつけています(不吉な名前ではありますが・・・)。

まずは、 GraphQL でも grpc-web でもなく、なぜこいつが産まれたのか、その背景から説明します。

背景

リクルートでも GraphQL を使うケースは徐々に見え始めていますが、まだまだ少数派です。よくあるケースは REST-ish な APIサーバがすでに Backend に存在し、それを基に BFF レイヤから API Aggregation をするようなケースです。

f:id:yosuke_furukawa:20191216002138p:plain
BFF が Backend APIを Aggregation するケース

Specter 以前はこの部分に Fetchr と呼ばれる OSSを使っていました。しかしながら、 Fetchr は TypeScript などの昨今のフロントエンドのトレンドにマッチしておらず、 非同期呼び出しも callback APIを中心として Promise 化されたラッパーが存在する程度でした(そこに以前は 不具合があり、一撃 DoS を直したりしてました)。

次の候補として、 Client と BFF 間の通信で、 GraphQL を使うという話もありましたが、最近は AMP で amp-list を使うなど、 Response の戻り値の型が決まっているようなケースも有り、諸々のユースケースに対応するため、独自で開発するに至ったという経緯です。今回は特徴を解説しつつ、なぜこのような設計なのかの Whyについて解説します。 最後に Howについても少しだけ解説します。

軽量

Specter 自身のファイルサイズは BundlePhobia によると、 minify, gzip 付きで 2.2kB 程度です。

bundlephobia.com

ちなみに他のよくある data fetch ライブラリと比較すると以下のとおりです。他のライブラリとは機能が違うので単純比較は難しいですが、同等の機能を提供している Fetchr よりも10kB以上削減されています

f:id:yosuke_furukawa:20191216010313p:plain
各種 bundle size まとめ lower is better

Specter は内部的に fetchを使っています。これまでの data fetch ライブラリは基本的に XHRをラップする形で Promise を提供しています。IE11等、 fetchをサポートしていないブラウザ用にはこのアプローチが必要なものの、最近では fetchをサポートしていないブラウザのが珍しいので、こうしています。

一方で、 IE11 用に unfetchと呼ばれる軽量な polyfill を用意しています。これにより、 IE11 のような fetch未サポートの環境でも Specter は動作します。

TypeScript friendly

Specter は TypeScript で型をつけられるようになっています。一方で、 TypeScript じゃなくても普通の JavaScriptでも呼べるようにAPIは作られています。 TypeScript にどっぷり浸かるような TypeScript specificなライブラリではなく、あくまで TypeScript friendlyとしているのは、そのためです。

// TypeScript で使えば普通に型をはめて使える(ただし、TypeScript specific ではないので、あくまで型を自称しているのみ。GraphQLなどのアプローチとは異なる)const request =new Request<RequestHeader, RequestQuery, RequestBody>("counter",{
    headers: {},
    query: {},
    body: {
      count: (count + 1),}});const data =await client.update<CounterResponse>(request);
// Generics をやめてしまえば、 JS から普通に使えるconst request = new Request("counter", {
    headers: {},
    query: {},
    body: {
      count: (count + 1),
    }});
const data = await client.update(request);

既存のシステムがまだまだ JavaScriptで動いているものも多い状況で派手に migration をするのではなく、段階的に移行できるようにするためにこうしています。特に Fetchr で動いているものを徐々に TypeScript にしているような状況では、 Specter のように段階的な移行ができると移行の計画が立てやすいという考えでこうしています。

投機的先読み (Prefetching)

Prefetch の方法はいくつかあります。Prefetch もパフォーマンスチューニング分野ではよく研究されているものの1つです。いくつか Prefetch の方法を紹介します。

Prefetchで、一般的なのは dev.toでも実装されている、マウスオーバーしたら prefetchするという方法でしょうか。この方法では ユーザーが押そうとしたリンクを先読みするという効果があります。ただし、この方法はマウスを使わない端末、つまりスマートフォンタブレットでは使えません。

また、 Next.js や Nuxt.js には Link 先を 投機的に prefetch してくれる機能が存在します。この機能は Prefetching Pagesと呼ばれており、静的なリンクでは viewport内に Link が入ったら先読みします。 ただし、この方法は若干富豪的です。 さらにあまりにもリンクが多い場合は通信容量も増えてしまいます(所謂ギガが減る

https://nextjs.org/docs#prefetching-pages

最近はもう少しスマートな方法として、Predictive Prefetchingと呼ばれる方法が登場しています。これは Google Analyticsなどで計測している統計データに基づいて Prefetchする方法です。 GA の情報から次に行く可能性の高いリンクを予め Prefetchする方法です。

web.dev

Specter はまだ GA と組み込んで実装する機能は未実装ですが、ヒューリスティックに先読みする機能 (経験則に基づいて先読みする機能) 自体は存在します。例えば、検索実行時に、経験則に基づいて、検索結果の上位5件を予め Prefetchするなどのアプローチをすることが可能です。

// HackerNews の top story 一覧を取得するconst res =await fetch("https://hacker-news.firebaseio.com/v0/topstories.json",{
  method: "GET",
  headers: {"Content-Type": "application/json; charset=utf-8"}});const data: Array<number>=await res.json();// 上位10件に関しては先読みするためのヒントとなる "次のリクエスト候補" に入れることができる// もしもギガが気になるなら取らなくても可能const nextReqs = data.slice(0,10).map((id)=>new ClientRequest<{},{ id: number},null>("hnitem",{ 
  headers: {}, 
  query: { id: id }, 
  body: null,}));const resp =new Response({},
  data,);
resp.setNextReqs(...nextReqs);return resp;

先読みする時は下記の要領で実行します。

const data = await client.read<HackerNewsListResponse>(request);
setTimeout(() => {// ここで先読みする、先読みしたデータは cache として残るため、次に取りに行く時には cache から fetch できるconst reqs = data.getNextReqs();
  reqs?.forEach((req) => client.read(req));
}, 100);

実際HackerNews APIを基に先読みするケースと、先読みしないケースの両方を検証した所、先読みを行わないときに比べて時間が500msec以上減っています(先読み結果をcacheしているため)。

先読みしないとき: avg. 566ms
先読みしたとき: avg. 15ms

また、 Universal なライブラリとして作っているため、 Prefetchのタイミングが柔軟に変更できることが強みの1つです。SPAなどのリッチなアプリケーション の場合は Service Worker で行えば main thread を阻害しないで 先読みが可能です。また、そこまでしなくても requestIdleCallback などのブラウザが busy じゃないタイミングで Prefetch させることも可能です。逆に AMP 等、 クライアントで柔軟に Prefetch できない場合はサーバサイド (Node.js) で Prefetchした結果を Cache に入れておく事も可能です。

詳しくは HackersNews の example を確認してください。

github.com

使い方など

Next.js や React, Redux で利用する時の方法については詳しくは GitHubの examples を参考にしてください。

Next.js で使うとき: specter/examples/nextjs at master · recruit-tech/specter · GitHub React, Redux で使うとき: specter/examples/counter at master · recruit-tech/specter · GitHub

その他、何かあれば GitHubで気軽に issue 報告をお願いします。日本語でも問題ありません。

github.com

展望

まずはリクルート社内で実践をしながら、より細かなニーズに対応していこうと思っています。 @agreed からクライアントを自動生成する、などのニーズはたくさんあります。さらに、弊社では Google Analyticsだけではなく、 Adobe Analytics や独自分析基盤などがあるため、それらともインテグレートしていく必要があります。

まだまだ実験的なデータフェッチライブラリなので、事業ニーズと一緒に考える必要は数多くありますが、ある程度まで動くものができた段階で徐々に広げていく予定です。

まとめ

Specter について解説しました。 Specter 自身は軽量で、 TypeScript friendly で、 投機的先読みが可能なデータフェッチライブラリです。それぞれの機能についての背景と利用した際にどれだけの利点があるかについても加えて解説しました。

実験的なアプローチですが、リクルート社内にはアプリケーションが多数あるので、それぞれのユースケースに対応していこうと思っています。今後は以下のようなロードマップを描いています。

  • example や 事例を増やす
  • ドキュメント拡充
  • Predictive Prefetch 実装
  • Analytics 基盤との連携

よかったら使ってみてください :) Feedback をお待ちしています。

2020年の Node.js, 2025年の Node.js (Web Standard編)

$
0
0

この記事は Node.js Advent Calendar の 25 日目の記事です。

qiita.com

Node.js の 2020 年はどうなるのか 2025 年にはどうなっているのかを予想していこうと思います。 ちなみに、あくまで筆者の予想にすぎないです。こうなるという与太話みたいなものだと思っていてください。

Node.js のこれまでと今後

Node.js は進化を続けていますが、 2018 年に語った通り、その進化の方向は以下のような方向に流れています。

  • Web Standard
  • Performance
  • Security
  • Stability

speakerdeck.com

今回は主に Web Standard の部分に限定して、これまでとこれからと更にその先を予測してみようと思います。

Web Standard 2020 / 2025

Node.jsは Web Standard に追従するという旨の発信はずっとしています。全ての Web Standard を follow するといっても実際サーバサイドには不要なものもあります。取捨選択をしながらなるべく追従をしていくという形になるでしょう。

ES Modules

ES Modules が Node.js v13.2.0 より、 ES Modules が experimental フラグが取れ experimental オプション無しで動きます(ただし、API status は experimental のまま)。相互運用性の方法もほぼ確定になりました。 Node の ES Modules 対応は 2020 年から徐々にその割合を伸ばし、 2025 年には利用されているモジュールが ES Modules : CommonJS が 3 : 7位の比率になると予想します。 徐々に .mjsesmに対応してほしいみたいな issue 増えてきましたね。

github.com

github.com

github.com

100% のモジュールが ES Modules になることは考えにくいですが、新しいモジュールや使われてるモジュールを始めとして、徐々に対応するような段階的にアップデートが行われるのではないかと思っています。詳しくは以下のエントリに書いたので詳細をご確認ください。

yosuke-furukawa.hatenablog.com

QUIC

QUIC の initial implementation の PR が作られました。

github.com

まだ ngtcp2を持ってきて binding を作った段階で、 http3 の実装などは現時点ではまだです。 Node.js の QUIC 及び、 HTTP3 の対応は 2020 年で初期実装が完遂し、experimental付きでリリース、 2025 年には quic, http3 ともに experimental flagが取れると予測します。

一方で http2 はどうなるんでしょうね。クライアントとしては生き残る気がしつつも、毎回クライアント側で http1.1, http2, http3 を呼びわけたくないので、統一的なAPIからコールされるようにならないと厳しいですね。

fetch

http1.1, http2, http3 のクライアント側の話で言うと、それらを統一的に呼び分けるために fetchを入れようという話もあります。今だと require('http')require('http2')のようにロードするモジュールから分ける必要があります。

github.com

このPRはただの既存の npm にある node-fetchを core に入れてみようとしただけの議論用のPRですが、 ES Modules のような大物が終わった後は fetch の実装を進めるのかなと予測しています。

2020年には、、、議論は進んで fetch working group ができる、 2025年には http3 とともに fetch も入ると予測します。

Streams

fetchとくると、気になるのは Streamですよね。 WHATWG Stream と既存 Stream は違うものなので別物として実装はされると思いますが、今の所進捗が見えません。リポジトリはあります。ただ議論が止まってますね。

github.com

新しくモチベーションがある人が引き継ぐ必要がありそうな気もします。 fetchの議論を進めていく上で一緒に進んでほしいと思います。やる気があったら実装してみます。

ただこうなると、Joyee が書いたこのスライドにもありますが、 fetchの完璧に compatible な実装を用意は難しいですね。また、 Browser APIと完璧に同じにする必要も無いので、 Node.js なりに互換はあるが微妙に別物な fetch になりそうですね。

2020年はわかりません、おそらく今の感じだと進捗はないでしょう fetch working groupができたらそこで議論再開ではないでしょうか。2025年には一旦 WHATWG Stream 無しで実装が進んでリリースされてそうだと予測します。

ちなみに Stream ですが、 新しく BoB Stream という提案がされています。これは今までの Push と Pull 型の混在した Stream ではなく、完全に新しい APIで再検討された Pull 型の Stream です。

github.com

WHATWG Stream のものとはAPIも概念も異なります。こっちのが議論は進んでいるので入るのは WHATWG Stream よりは早そうですね。 2025 年には決着付いてるでしょう。

Web Assembly

Web Assembly 対応は v8 中心に進んでいる上に、 WASI 対応も v13.3.0 で experimental flag 付きでリリースされました。

github.com

つまり、 Web Assembly 対応したモジュールを読み込むことができるのはもちろん、 WASI に対応した形でFile, Network IOを行うモジュールも使えるようになります。

ただまだまだ大本の WASM, WASI の仕様が unstable なのでどう転ぶかはわかりません。筆者がそこまで追いきれてない領域です。 2020年には徐々に今の native module が wasm/wasi を使って書かれるようになってくると予測します。ただ、大本の団体がまだ安定版を作れていないので、2025年でもまだ experimentalなままな気がしてます。

先日公開された TensorFlow.js が wasm を backend にするというニュースが流れてきて、徐々にモジュールのレイヤでは使われ始めてるという実感を感じます。

github.com

その他 Web Platform APIはどうなるのか

crypto は Web Crypto を Node.js APIに採用する議論は進められています。

github.com

cryptoWeb Cryptoも別物です。 Promise をベースとした Web Crypto でこちらはユーザーの APIの利便性を考えられているものの、既存の crypto は性能をベースに考えられているので趣きが若干異なりますね。今の tlsモジュールが Web Crypto をベースに変わるとかはちょっと想像ができません。こちらももう少し議論が進めば入る可能性はあります。 2020 年には決着がつきそうですね。入るかどうかは判断が難しいですが、2020年に experimental リリースされそうな気はします。

一方で、 WebTransport, Web Codec等の対応はまだ議論すらされていません。ちなみにWebSocketsも core に入れるかどうかは忘れられてる気もします。。。

Web Platform の APIを入れるかどうかは、「ユースケースが Node.js にとって妥当かどうか」、「ユーザーランドで実装が困難かどうか」、「既存の APIとの整合性」で決まるので、それぞれの判断をする議論が無い限り話は進みません。

まとめ

  • ES Modules は 2020 年には増え続けて、 2025年には ESM : CJS で 3 : 7 程度の比率になる
  • QUIC は来年 experimental でリリース (v14) HTTP3 はそこから議論が始まって RFCがでたら2025年にはリリースされてる
  • fetch は来年一旦 working group ができる、2025年にはリリースされてる
  • stream は多分来年も何も進まないが、 fetch working group で一緒に議論はされる、 2025 年には一旦 whatwg stream 無しでリリースされる
  • WASM は既に初期リリースはされているので後は仕様側が安定したら徐々に進むはず、ただまだ仕様が unstable なままだと experimental flag は取れない。2025年にもまだ experimental な気がする。
  • その他: Web Crypto が入りそう、ほかはまだまだ、ユースケースから募集中。

書いてて思いましたが、 2025 年って v24 とかですね。。。こう書くと全然予測ができない気もしてきました。やっぱり予測するものではなく、自分たちで予測を超えていく動きをしたいですね。

来年も Node.js に JSConf にがんばります。

実践 Off the main thread

$
0
0

実践 Off the main thread

実際に Off the main thread をやりつつ、パフォーマンスチューニングをする際にどこに気をつけるべきかを今やっているので、それについて話します。

Off the main thread とは

JavaScriptの処理は基本的にメインスレッドで実施します。JavaScriptの実行処理以外にも記述された内容を解釈するためのパース処理やGC処理もメインスレッドをブロックします。メインスレッドの処理が多いとUI jankと呼ばれるガタツキ、チラツキ、画面の固まりの原因になります。

UI jankが発生していると、ユーザーがクリックしたり、text入力をしようとしてから反応するまでの時間(Input Latency)が即時ではなくなります

このUI jankを無くすために、なるべくメインスレッドを阻害する要因を減らすことが Off the main thread と呼ばれるトピックです。

Input Latency

ユーザが入力してから反応するまでの時間を指しています。 Off the main thread は前述の通り、このInput Latencyを少なくするための試みです。 メインスレッドでLong Taskを実行している間、特にレンダリングなどの重たい処理を実行している間は入力があっても即座に反応しません。

f:id:yosuke_furukawa:20190319025141p:plain
Long Task実行中に入力処理があった場合

この場合、inputの処理はキューイングされ、後回しになります。

f:id:yosuke_furukawa:20190319025356p:plain
入力されてから実際に発火して反応があるまでの時間

このレイテンシを最小限にしつつ、スムーズに動作しているように見せないと、固まっているかのように見えます。

「どれくらいで固まっているように見えるか」ですが、Googleが出している、RAIL と呼ばれる原則では、100ms 以内に反応がないと、ユーザーは遅れているように見えるという指標があり、少なくともそれを満たす必要があります。 ただし、 lighthouse では 50ms 以内にしないと警告が出ます(アプリが表示し切るまでにさらに 50ms かかると推定されており、トータルで 100ms が Input に対しての Response になるため)。つまり、 Input Latency は表示し始めてから 50ms 、 入力が始まってから 100ms 以内に反応する必要があります。

developers.google.com

developers.google.com

何が Input Latency の遅延原因になっているか

Input Latency の遅延原因となっているのは主に JavaScriptの実行だというレポート結果があります。

tdresser.github.io

f:id:yosuke_furukawa:20190319030621p:plain
input latencyの中で処理の内訳

2300以上のサイトを調査し、最初の30秒間に入力処理をした際の処理の内訳を精査したところ、V8.Execute という JavaScript実行中の処理で25% ~ 70%の時間が発生しています。

これを Web Workerなど、 Worker Thread で行えるようにし、 Input Latency を改善するというのが Chromeでの topic の1つです。ただし、実際にやってみると、それ以前の問題が多く、実際にやればやるほど「Worker 以前に処理するべき問題」のが多く見つかります。

Worker 以前に処理するべき問題を放置して Worker Thread にしても Input Latency は改善できても処理そのものが重たいので、結局もっさりした動きになります。

実践 Off the main thread

今回の話はこの input latency を改善するため、 UI Jank をどうやって見つけるかとそれを改善するときのやり方、また実際にReact などの SPA を改善する時に見つけたありがちな問題について解説します。

UI Jank の見つけ方

Chromeだと DevTools の Performance タブから比較的簡単に見つけられます。 Performance タブで赤くなっている箇所では、 Jank が起きています。

UI Jank
UI Jank

この Jank を見つけたら、そこから中で何をやっているか見つけに行きます。この JSConf.JP のサイトでは hydration 処理と呼ばれる SSRCSRの状態を同期する処理で Jank が起きています。

Hydration処理
Hydration処理

このケースで言うと、 Hydration と呼ばれる SSRCSRの同期処理が走った後、React の props が変わり、 React の render 処理が中で走っています。結果として hydration 後に render が走ることで React の state が SSR時のものと同期されることになります。一方で、 Input Latency は落ちる事になります。

ただし、『 UI Jank がある == 不具合』ではないので、これを必ずしも直さないといけないわけではありません。現時点ではどうしようもない処理もあります。 Hydration はその典型です。

よくある Jank のパターン

この hydration 以外にも発生するケーススタディがありますので紹介しておきます。

スクロールごとに重たい計算をしてしまうパターン

スクロールするたびにガタつくケースはだいたいコレですね。よくあるのは、スクロールしたタイミングで要素を変更する lazyload や 無限スクロールのような処理があるケースで、実装がまずいパターンです。

実装がまずい、と一口に書きましたが、「重たいオブジェクトを毎回生成する」、「scrollのたびに現在のviewportに要素が入っているかを毎回計算する」といった処理を指しています。

前者はオブジェクトのキャッシュかインスタンス生成を減らす事を検討し、後者は Intersection Observerなどの APIで再実装が求められます。 また、 scroll イベントを毎回発生させる必要がないなら、 throttle, debounce といったイベントを間引くことで処理そのものが発生する回数を減らすことができます。

ちなみにスクロールに限らず、『頻度高く発生するイベントをトリガーに重たい処理をしている』事がそもそも問題になります。

他のケースとしては、 momentのような時刻計算をするためのオブジェクトを表示されるたびに計算し、その計算のたびにインスタンスを作ってから時間の差分を計算しているケースもありました。

不要な props を渡してしまうパターン

ここからは主に React の話ですが、React に限らず、 SPA の view libraryではよく発生すると思います。

<Foo {...props} />

のように prop を全て展開して渡しているパターンや、 実際には使わないけど渡しているパターンですね。Reactの SPA の場合、UI Jank の8割方はこれです。この不要な props を渡しているところのバリエーションが多いです。

不要に Reconciliation といった差分検出処理が走ったり、 render が走ってしまい、 Jank が発生しやすくなります。

shouldComponentUpdate を書いて除外するか、きちんと渡す時に精査してから渡してあげれば発生しません。また、型を真面目に書いていればある程度防げたりもするでしょう。

関数をそのまま handler に渡してしまうパターン

こちらもよく見ます。以下のような状況ですね。

<Foo onClick={(e) => { ... }} />

これも、ある種の不要な props なのですが、趣が若干異なります。アロー関数に限らず、関数をその場で定義した場合、propsを渡す際に毎回 function オブジェクトが生成されて React に渡されます。こうなると関数オブジェクトそのものが毎回変わっているため、差分検出処理時に必ず差分がある事になります。

useCallback 等で callback をmemo化した状態で渡すか、 class componentにするなら、関数定義を constructor で定義するか static 関数にするかして、関数を再定義しない方法で handler に渡す必要があります。

どうにもならないときの処理として Worker を使う

処理をダイエットしたけど、どうしても減らない、とにかく重い計算処理を走らせる必要がある、という時に Worker を使いましょう。 Worker は Comlink 経由で使うと利用しやすいです。

実際に利用したときのシーンは AirSHIFT のブログに掲載されています。

web.dev

// Cost計算する処理、 worker を comlink で promisified している。import React from 'react';
import{ proxy } from 'comlink';
 
// import the workerlized calc function with comlinkconst WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
exportdefaultfunction Cost({ userInfo }) {// execute the calculation in the workerconst instance = await new WorkerlizedCostCalc();
  const cost = await instance.calc(userInfo);
  return<p>{cost}</p>;
}

ただ Worker にしただけで速くなるわけではありません。 Worker にすることで Main Thread を逼迫することは減りますが、そもそも無駄な処理がないかを地道に特定し、逼迫する時間を減らすことが一番重要です。

まとめ

Off the main thread だからといって、なんでも worker でやれば OK という話ではありません。自分のアプリ、サイトが遅い要因を特定し、ある程度性能を改善してからどうしようもない時に Worker を使いましょう。そうすると Input Latency だけではなく、処理全体が軽くなります。

Worker は速くなると言っても、 Main Thread の逼迫を軽減するためのものであり、使ったからといって手放しで高速になるわけではありません。一方で、使いこなせれば非常に強力な武器になります。 Comlink などのツールは使いこなせると良いでしょう。

また、仕様側でもよりよい API設計を考えている話が去年から出ています。この辺も追っておくと良いでしょう。

nhiroki.jp

また、 『UI Jank』の原因は JavaScriptのメインスレッド逼迫だけではありません。 CSSや Layout 計算などの要因でも数多く発生します。

Jank の原因をまとめたサイトやチェックリスト等もあるので参考にしてください。

calendar.perfplanet.com

docs.google.com

jankfree.org

2019年振り返り

$
0
0

はじめに

なんかよく見たら 2年前の自分がちゃんと書いてるくせに 1年前の自分は書いてなくて、進化してるのか退化してるのかわからなかったので、今年からはちゃんと書こうと思いました。

yosuke-furukawa.hatenablog.com

会社

マネジメント

マネージャーも3年目ですね。なんだかんだ、メンバーにも恵まれてるなと思うことが多いです。 メンバーがアウトプットを率先してやってるので、エンジニアコミュニティとして大きく形成されているなと思います。エキスパート同士が互いを教え合い、また競い合いながら目的を達成していくのは見てて気持ちが良いです。

2019年振り返り.md · GitHub

engineer.recruit-lifestyle.co.jp

qiita.com

recruit-tech.co.jp

qiita.com

recruit-tech.co.jp

背中を黙って預けておけるメンバーなので、特にマネジメントの仕事と言ってもキャリアの相談や仕事の相談に乗って一緒に考えることはしてますが、やってることはその程度といえばその程度で、細かい事は任せてます。「『お前はどうしたいんだ』ってよく言うのか」、と聞かれますが、「メンバーがどうなりたいか」と「メンバーにどうなっててほしいか」というwill と期待の話はよくします。逆に言うと日々のマネジメントはそれくらいしかしてないです。

R-ISUCON で 1 位に

エンジニアコミュニティといえば社内でやってるイベントの R-ISUCON で 1位取ったりしました。

recruit-tech.co.jp

嬉しかったのは R-ISUCON やってたら ISUCON の予選を3位で通過したことです。自分が通過したことももちろん嬉しかったのですが、 R-ISUCON をやっていた他のグループも通過していてレベルが上ってることを感じました。

yosuke-furukawa.hatenablog.com

関連会社の技術顧問になった

ニジボックスという弊社の関連会社の技術顧問も新規でやることにしました。

会社の技術的な質を上げるには、技術部分の底上げをすることとトップレベルを引き上げることの2つが重要だと思っています。

トップレベルを引き上げることは自組織のマネジメントで行っていますが、縁あって底上げをする役目も一緒に行うことになりました。といってもまだ10月からなったばかりなので、 JavaScriptの研修を教えたり、そもそもコードを書く時に意識することを教えたりといった基礎的な所と意識的な所を教えています。

qiita.com

Chrome Advisory Board になった

Chromeにアドバイスする諮問委員会、 Chrome Advisory Board のメンバーになりました(正確には2018年から)。現時点では日本に 3 人の諮問委員会メンバーがいて、リクルートの他だと Yahoo!Cyber Agentのメンバーがいます。活動としてはいち早く新しい Chromeの仕様を教えてもらって試して、使った結果をフィードバックするというのが主な仕事です。

AMP をやったり、 Next.js 使ったりしてるのはその辺があります。 Next.js に最近送った PR も AMP 系ですね。

github.com

そんな使い方があるのかーという、仕様の勉強にもなります。 GoogleのAMPの中の人達にこの手の活動を通じて気軽に質問できるようになったのは大きいですね。

この他にも Paul Irish に気軽に lighthouse の中身を聞いたり、実際にサービスを見てアドバイスもらったりできて良かったです。

Japan Node.js Association の活動

会社でやったこと以外にも自分が持ってる法人の活動も書いておきます。

JSConf.JP 開催

今年はなんといっても JSConf.JP 開催できたのが大きいです。

yosuke-furukawa.hatenablog.com

今年の前半からずっと JSConf をやってたし、日本で JSConf が開催できたのは日本の JavaScriptコミュニティ全体にとって良かったと思います。トークの内容もJSコミュニティを代表するにふさわしい内容だったと自負しています。

ただこれまで JSConf EUとかで登壇依頼してたのですが、来年はやらないので新しい場所に行く必要があるかも。。

あと、来年はなんと言ってもオリンピックがあるので、前回みたいな外部の会場を抑えるのは難しいかもしれませんね。。。 今7, 8, 9月にやる予定だったイベントが軒並み9月後半、10月、11月にずれてきてるという話なので、会場の検索中です。

ただ間違いなく2020年も JSConf はやります。

個人活動

会社、自分の法人以外の活動も書いておきます。

登壇系

技術的な登壇はもとより、マネージャーや組織づくり系の登壇、エンジニアキャリア的な登壇に呼ばれましたね。

この辺に呼ばれた(自分が話した)感じですね。自分の中で新しいなと思ったのは Global CFP Day と EOF 2019 ですね。

Global CFP Day は「海外登壇にチャレンジするにはどうしたらいいか」、というテーマでめっちゃ面白いカンファレンスでした。

f:id:yosuke_furukawa:20191231193941p:plain
Global CFP Day

このカンファレンスで紹介された「Present! A Techie's Guide to Public Speaking」という本は 海外カンファレンスのテックトークとはどういうものなのかを紹介した本です。かなり面白いのでマジでおすすめです。

www.amazon.com

EOF 2019 は自分がマネジメントでやってたことをちゃんと言語化した形になりました。

あまりちゃんとした形で表明してなったけど、自分は勉強会と同じ考え方でマネジメントしてるんだなという発見になりました。

執筆系

以下の場所に記事を寄稿しました。

gihyo.jp

マイクロサービスのつなげ方の章で BFF の話を解説させていただきました。 また、 CodeGrid は Node.js v12 - v13 で変わることの話をさせていただきました。

www.codegrid.net

www.codegrid.net

後半のマジで忙しい時に2連続で執筆依頼が来たのですが、これを断っていたら今回執筆ゼロだったので受けてよかったです。

OSS

最近ちゃんとコードも書いていこうという精神で、 Write Code Everyday に挑戦しています。まだ4ヶ月が終わろうとしてるところですね。マネージャーになったけど、コード書いていないっていうのは嫌だったので、コードも書くし、マネジメントもやるし、っていう精神でやっています。

f:id:yosuke_furukawa:20191231195047p:plain
Write Code Everyday 挑戦中

OSSで言うとリクルートのライブラリで、 Specter を作ったりしていました。

github.com

yosuke-furukawa.hatenablog.com

結構中身がわかってきたし、不具合も見えてきたので、 Next.js へのコントリビューションをもう少し本格的にやろうと思います。

github.com

Node.js core contributor としては活動が少し落ち着いてしまったので、もう少し復活させようと思います。

Podcast

fukabori.fm に出させていただきました。結構この活動がきっかけで周辺会社とも絡む機会を増やせると良いなと思っています。内容は社内ISUCONですが、かなりエンジニアコミュニティづくりに寄った話が多かった気がします。

fukabori.fm

Blog

この辺が盛り上がった気がしますね。もっと書きます。。。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

来年に向けて

明らかにブログが減ってしまっているのでまずはペースを2017年に戻したいのと、2017年は海外登壇も多くやっていたので、それも復活したいです。 Node.js は QUIC や ES Modules を起点にまたコントリビューションできる所見つけてやっていきます。

そろそろ新しい言語をまた覚え直すとかやりたいし、その一方で他の人達が新しいチャレンジしてるのも見てて面白そうだなと思うので、好奇心を原動力に2020年もまたがんばります。

Viewing all 105 articles
Browse latest View live