Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

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

リフレクション使うと 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');
}
$ 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 ファイルはものすごいサイズのデータになる。

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

2MBやばい。

対処方法

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

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

@MirrorUsed を使う。

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

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] も試してようやくできた。

これをビルドすると:

$ 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 と激減している:

$ 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 プロパティ固定で監視する仕様にしてみる:

--- 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 が参考になった。

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

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

まとめ

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

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

k_ui
ねこほしい
http://k-ui.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away