リフレクション (dart:mirrors) を使ったライブラリを dart2js する

__tl;dr__ dart2js する予定ならリフレクションやめろ、もしくは覚悟しろ

リフレクション使っているライブラリをそのまま dart2js する辛さと、それを緩和する方法について。

* [今回説明に使ったプロジェクト](https://github.com/kui/dart_reflection_example) 

リフレクション使うと dart2js が大変
=======================================

例えばとあるライブラリでこんな感じでリフレクションを使っているものがあるとする:

```lib/periodical_observer.dart
library reflection_test.periodical_observer;

import 'dart:async';
import 'dart:mirrors';

/// 1秒ごとに特定オブジェクトの特定プロパティをチェックして、
/// 変化があったら console に出力するクラス。
class PeriodicalObserver {
  Object _value;

  PeriodicalObserver(Object target, String propertyName) {
    final n = new Symbol(propertyName);
    new Timer.periodic(new Duration(seconds: 1), (_) {
      var v = readProperty(target, n);
      if (v == _value) return;
      print(v);
      _value = v;
    });
  }
}

Object readProperty(Object object, Symbol name) {
  return reflect(object).getField(name).reflectee;
}
```

見ての通り `dart:mirror` を使っている。

これをそのまま使って dart2js すると:

```web/index.dart
// dart2js するやつ
import 'dart:html';
import 'package:reflection_test/periodical_observer.dart';

void main() {
  new PeriodicalObserver(querySelector('input'), 'value');
}
```

```bash
$ pub build
Loading source assets...
Building reflection_test...
[Info from Dart2JS]:
Compiling reflection_test|web/index.dart...
[Warning from Dart2JS on reflection_test|web/index.dart]:
1 hint(s) suppressed in package:reflection_test.
[Dart2JS on reflection_test|web/index.dart]:
1 warning(s) suppressed in dart:_js_mirrors.
[Warning from Dart2JS] :
web/index.dart:
7162 methods retained for use by dart:mirrors out of 8425 total methods (85%).
[Info from Dart2JS on reflection_test|web/index.dart] :
packages/reflection_test/periodical_observer.dart:4:1:
This import is not annotated with @MirrorsUsed, which may lead to unnecessarily large generated code.
Try adding '@MirrorsUsed(...)' as described at https://goo.gl/Akrrog.
import 'dart:mirrors';
^^^^^^^^^^^^^^^^^^^^^^
[Info from Dart2JS]:
Took 0:00:18.321964 to compile reflection_test|web/index.dart.
Built 5 files to "build".
```

と警告が出ている通り、出来上がった js ファイルはものすごいサイズのデータになる。

```bash
$ ls -sh build/web/index.dart.js
2.0M build/web/index.dart.js
```

2MBやばい。

対処方法
==============

ライブラリ作者か否かでだいぶ違うので分けて説明する。

あなたがライブラリ作者ではないなら
-----------------------------------

<code>[@MirrorUsed][]</code> を使う。

ライブラリを使用するコード（上記の `web/index.dart` のようなアプリケーション）に、<code>[@MirrorUsed][]</code> を使い、どのクラスのリフレクションを取得するのかを書く。

`web/index.dart` ならこんな感じで修正する:

```web/index.dart
import 'dart:html';
@MirrorsUsed(targets: const [InputElement], override: '*')
import 'dart:mirrors' show MirrorsUsed;
import 'package:reflection_test/periodical_observer.dart';

void main() {
  new PeriodicalObserver(querySelector('input'), 'value');
}
```

mirrors のインポートと、`@MirrorUsed` が増えてる。

書いてから気がついたけど、`querySelector` が何クラスを返すのかよくわかりにくかったので、あんまり良い例じゃなかった。実際こう書くまで、`const [InputElementBase]`, `const [TextInputElement]` も試してようやくできた。

これをビルドすると: 

```bash
$ pub build
Loading source assets...
Building reflection_test...
[Info from Dart2JS]:
Compiling reflection_test|web/index.dart...
[Dart2JS on reflection_test|web/index.dart]:
1 warning(s) suppressed in dart:_js_mirrors.
[Warning from Dart2JS] :
web/index.dart:
241 methods retained for use by dart:mirrors out of 971 total methods (25%).
[Info from Dart2JS] :
web/index.dart:3:1:
Import of 'dart:mirrors'.
import 'dart:mirrors' show MirrorsUsed;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Info from Dart2JS on reflection_test|web/index.dart] :
packages/reflection_test/periodical_observer.dart:4:1:
Import of 'dart:mirrors'.
import 'dart:mirrors';
^^^^^^^^^^^^^^^^^^^^^^
[Info from Dart2JS]:
Took 0:00:08.484569 to compile reflection_test|web/index.dart.
Built 5 files to "build".
```

警告らしいものは出ているけれど、問題なく動作するしファイルサイズも 2.0M → 168K と激減している:

```bash
$ ls -sh build/web/index.dart.js
168K build/web/index.dart.js
```

問題点としては、ライブラリ利用者は __予めリフレクション取得する可能性のあるすべてのクラスを列挙する必要がある__ という点。今回のライブラリの例だと、監視する可能性のあるクラスすべてを列挙する必要がある。またこれをするのに __ライブラリをきちんと読み解く必要がある。__ つらい。

あなたがライブラリ作者なら
-----------------------------

1. その仕様を見直す。
2. [smoke][] を使う。

あなたがライブラリ作者なら、冗談抜きでその仕様を見なおしたほうが良いかもしれない。

理由としては:

1. 後述する `smoke` も `@MirrorUsed` もどちらもそれなりの覚悟が必要になるから。
2. `smoke` も `@MirrorUsed` も、圧倒的に遅いから。
3. `smoke` も `@MirrorUsed` も、それでもやっぱりファイルサイズ大きいから。

例えば上記の `lib/periodical_observer.dart` の仕様で言うなら、`propertyName` を動的に指定するのを諦めて、`value` プロパティ固定で監視する仕様にしてみる:

```diff
--- a/lib/periodical_observer.dart~
+++ b/lib/periodical_observer.dart
@@ -11,7 +11,7 @@ class PeriodicalObserver {
   PeriodicalObserver(Object target, String propertyName) {
     final n = new Symbol(propertyName);
     new Timer.periodic(new Duration(seconds: 1), (_) {
-      var v = readProperty(target, n);
+      var v = (target as dynamic).value;
       if (v == _value) return;
       print(v);
       _value = v;
```

それでもやっぱり動的じゃないと、というライブラリ作者は、ライブラリ利用者の負担をなるべく減らしたい人は、[smoke][] を考えてみても良いかも。

[smoke][] は `@MirrorUsed` を書く形式をやめ、静的コード解析して `@MirrorUsed` 相当のものを自動で作るライブラリ。smoke を使った有名なライブラリは [Polymer.dart][]。使い方に関してはここで書くと大半がその内容になってしまうし、正直もきちんと分かってないので割愛。とりあえず、polymer パッケージの [script_compactor.dart](https://github.com/dart-lang/bleeding_edge/blob/master/dart/pkg/polymer/lib/src/build/script_compactor.dart) が参考になった。

ただし、[smoke][] を使うにしても、静的コード解析を行う transformer を `pubspec.yaml` に書くことが必須になってしまうので、ライブラリ利用者の負担という意味ではあまり減っていないのかもしれない。

あと、script_compactor.dart のコード見ても苦労の後が見えるあたりから察するに覚悟が必要っぽい。

まとめ
=========

リフレクション使わないようにしようと思いました。（小学生並みの感想）

[smoke と mirror の説明しているブログ記事](http://dovdev.com/smoke-and-mirrors/) も参照するとわかるが、パフォーマンスやファイルサイズの面でもリフレクション使わないに越したことはないことが分かる。

[@MirrorUsed]: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart:mirrors.MirrorsUsed
[smoke]: https://pub.dartlang.org/packages/smoke
[Polymer.dart]: https://www.dartlang.org/polymer-dart/