patorashのブログ

方向性はまだない

AngularDartのコンポーネントでSassを使う

AngularDartのチュートリアルでは、cssしか書かなかったのですが、普段はSass(scss)で書いているので、こちらもそう書けるように設定しようとしました。

ライブラリの選定

DartでのSassのライブラリは、

の2つがメジャーっぽいんですが、githubリポジトリの更新頻度をみると、現時点(2017-11-16)ではsass_builderのほうがメンテナンスされているみたいなので、sass_builderを採用しました。

インストール

pubspec.yamlの更新

pubspec.yamlのdev_dependenciesにsass_builderを追加します。そして、transformerの先頭にsass_builderを追加します。

dev_dependencies:
  # 省略…
  sass_builder: ^1.0.0

transformer:
- sass_builder
- angular:
    entry_points:
    - web/js/main.dart
- dart_to_js_script_rewriter

強調するように、先頭に!と書きましたが、transformerは順番通りに処理が行われるようなので、dartファイルが解釈される前にscssファイルをcssファイルに変換しておかないと、ファイルが存在せずにエラーになります。自分はこれで数十分悩んでしまいました…。

app_component.dartで指定する

AngularDart側では、styleUrlsには、拡張子cssで指定します。コンパイル後のファイルが参照されるからです。

import 'package:angular/angular.dart';

@Component(
  selector: 'my-app',
  styleUrls: const ['app_component.css'],
  template: '''
    <h1>{{title}}</h1>
  ''',
)
class AppComponent {
  final title = 'Tour of Heroes';
}

そして、app_component.scssを作ります。試しに色と文字の大きさを変えてみました。

h1 {
  color: red;
  font-size: 11px;
}

そして、pub serveでサーバを起動して、アクセスしてみます。

結果

before

f:id:patorash:20171115021813p:plain

after

f:id:patorash:20171116015316p:plain

ちゃんと変わりました!

編集後記

transformerの順番には注意しましょう!普通に考えたらそりゃそうだろう、と思うのですが、当初はなんとなく追記してしまっていました…。とにかく、これでscssを書けるようになったのでよかったです。

AqueductアプリをHerokuにデプロイしてみた

DartのサーバサイドフレームワークのAqueductとAngularDartで開発してみようとしています。ひとまずAqueductからpub serveへのproxyも通して、grind serveでAqueductとpub serveのどちらも立ち上がるようになってかなり快適になりました。アプリは全く作っていませんが、とりあえずAngularDartが動くことが確認できたのでよしとします!これらについてはまた別の記事を書こうと思います。

Herokuにデプロイしてみる

Aqueductのドキュメントに、Herokuへのデプロイの仕方が書いてあったので、とりあえずやってみることにしました。せっかく作ってもデプロイできなかったら悲しいですからね。ドキュメントは以下になります。

aqueduct.io

とりあえずこのままやってみます。

1. Herokuにアプリを作る

Herokuにアプリを作るところはコマンドラインでやりました。アプリ名はユニークでないといけないので、なにかしらprefixをつけておくと、バッティングしづらいです。データベースを使う場合はheroku-postgresqlも入れておきましょう。

heroku apps:create [prefix-app-name]
heroku addons:create heroku-postgresql:hobby-dev

2. Aqueductアプリを作って、Herokuで動かす設定を行う

まだアプリがない場合はアプリを作ります。

aqueduct create [app-name]
cd [app-name]
git init

そして、herokuで動かすための設定を行うコマンドを実行します。もしheroku cliでログインしていなかったらログインしておいてください。

aqueduct setup --heroku=[prefix-app-name]

このコマンドを実行すると、以下のようなログが流れます。

-- Aqueduct CLI Version: 2.5.0
-- Aqueduct project version: 2.5.0
-- Setting up Heroku for patorash-project-chat
    Running heroku git:remote -a patorash-project-chat in /Users/patorash/sources/github.com/patorash/[app-name]
    Running heroku config:set DART_SDK_URL=https://storage.googleapis.com/dart-archive/channels/stable/release/latest/sdk/dartsdk-linux-x64-release.zip in /Users/patorash/sources/github.com/patorash/[app-name]
    Running heroku config:add BUILDPACK_URL=https://github.com/stablekernel/heroku-buildpack-dart.git in /Users/patorash/sources/github.com/patorash/[app-name]
    Running heroku config:set PATH=/app/bin:/usr/local/bin:/usr/bin:/bin:/app/.pub-cache/bin:/app/dart-sdk/bin in /Users/patorash/sources/github.com/patorash/[app-name]
    Running heroku config:set PUB_CACHE=/app/pub-cache in /Users/patorash/sources/github.com/patorash/[app-name]
    Removing config.yaml from .gitignore

どうやら、

  • heroku git:remoteを実行(これでgit push heroku masterなど、heroku指定できるようになる)
  • 環境変数Dart SDKのURLの設定
  • Dartを含んだビルドパックのURLの設定
  • Dartへのパスを通す
  • Dartのライブラリの位置を指定する
  • .gitignoreからconfig.yamlを外す

という処理が実行されたようです。

また、ここには書かれていませんが、Procfileが作られています。ProcfileはHerokuにデプロイしたら実行される処理が書かれています。

release: /app/dart-sdk/bin/pub global run aqueduct:aqueduct db upgrade --connect $DATABASE_URL
web: /app/dart-sdk/bin/pub global run aqueduct:aqueduct serve --port $PORT --no-monitor

リリースしたら、DBのマイグレートが自動で行われ、Aqueductアプリが起動する設定でした。

3. 設定ファイルを書く

config.yamlに、データベースへの接続設定を書く必要がありますので、以下のように書きます。

database: $DATABASE_URL
logging:
 type: console

$DATABASE_URLはheroku postgresqlを入れたら設定される環境変数です。これを指定しておきます。

4. デプロイする

まずマイグレーションファイルを作っているようですが、私はすでにマイグレーションファイルがあったのでここは飛ばしました。

aqueduct db generate

このコマンドで、すでにモデルファイルがあれば、その情報を元にマイグレーションファイルが作られます。

チュートリアル的に、準備ができたみたいなので、コミットしてデプロイしてみましょう。

git add .
git commit -m "initial commit"
git push heroku master

こうすると、ズラズラーっとログが表示されます。Dart SDKのダウンロードとビルドパックの設定、それにライブラリのダウンロードが毎回実行されるようで、デプロイの時間は結構長いです。私の環境では、3分半くらいかかりました。なお、pub buildは自動で実行されます。

そして、Dynoを起動させましょう。

heroku ps:scale web=1

これで、チュートリアルは完了。早速アクセスしましょう。以下のコマンドで、アプリのURLをブラウザで表示してくれます。

heroku open

っと思ったら、エラーが起きて落ちていました…。ログを見てみましょう。

heroku logs -t

すると、以下のようなログが。

2017-11-14T15:45:51.731620+00:00 heroku[web.1]: Starting process with command `/app/dart-sdk/bin/pub global run aqueduct:aqueduct serve --port 18743 --no-monitor`
2017-11-14T15:45:55.599716+00:00 app[web.1]: -- Aqueduct CLI Version: 2.5.0
2017-11-14T15:45:55.655924+00:00 app[web.1]: -- Aqueduct project version: 2.5.0
2017-11-14T15:45:56.832025+00:00 app[web.1]: -- Preparing...
2017-11-14T15:45:56.836338+00:00 app[web.1]: -- Starting application '[app-name]/[app-name]'
2017-11-14T15:45:56.836641+00:00 app[web.1]:     Sink Type: AppNameSink
2017-11-14T15:45:56.836841+00:00 app[web.1]:     Config: /app/config.yaml
2017-11-14T15:45:56.837397+00:00 app[web.1]:     Port: 18743
2017-11-14T15:45:58.574365+00:00 app[web.1]: ConfigurationException: AppNameConfiguration contained unexpected keys: logging
2017-11-14T15:45:58.574378+00:00 app[web.1]:  #0      ConfigurationItem.readFromMap (package:safe_config/src/configuration_item.dart:76:7)
2017-11-14T15:45:58.574379+00:00 app[web.1]: #1      new ConfigurationItem.fromString (package:safe_config/src/configuration_item.dart:19:5)
2017-11-14T15:45:58.574380+00:00 app[web.1]: #2      new ConfigurationItem.fromFile (package:safe_config/src/configuration_item.dart:26:45)
2017-11-14T15:45:58.574381+00:00 app[web.1]: #3      new ProjectChatConfiguration (package:[app-name]/[app-name]_sink.dart:194:47)
2017-11-14T15:45:58.574382+00:00 app[web.1]: #4      new ProjectChatSink (package:[app-name]/[app-name]_sink.dart:31:23)
2017-11-14T15:45:58.574384+00:00 app[web.1]: #6      _LocalClassMirror.newInstance (dart:mirrors-patch/mirrors_impl.dart:842)
2017-11-14T15:45:58.574383+00:00 app[web.1]: #5      _LocalClassMirror._invokeConstructor (dart:mirrors-patch/mirrors_impl.dart:916)
2017-11-14T15:45:58.574385+00:00 app[web.1]: #7      isolateServerEntryPoint (package:aqueduct/src/application/isolate_application_server.dart:85:8)
2017-11-14T15:45:58.574385+00:00 app[web.1]: #8      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:268)
2017-11-14T15:45:58.574386+00:00 app[web.1]: #9      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:151)
2017-11-14T15:45:58.574387+00:00 app[web.1]:
2017-11-14T15:46:52.005405+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch
2017-11-14T15:46:52.005405+00:00 heroku[web.1]: Stopping process with SIGKILL
2017-11-14T15:46:52.148959+00:00 heroku[web.1]: Process exited with status 137
2017-11-14T15:46:52.168200+00:00 heroku[web.1]: State changed from starting to crashed
2017-11-14T15:46:52.171578+00:00 heroku[web.1]: State changed from crashed to starting

config.yamlに設定されているloggingていう項目がわからないとということのようなので、config.yamlから削除しました。うーむ、サンプル通りにしたのだけれど…。とりあえず以下のようにしてコミット。

database: $DATABASE_URL

そして再push。

git push heroku master

すると、出ましたー!

f:id:patorash:20171115021813p:plain

AngularDartのチュートリアルの最初で設定する文字列が表示されました。

編集後記

DartのプログラムがHerokuで動作することが確認できましたので、安心して開発することができます!

grinderのタスクに引数を渡す

Dartのタスクランナーであるgrinderですが、タスクに引数を渡す方法がわかりませんでした。サンプルにも載っていないので、issueの中身を読み漁ったりした結果、とりあえず作れるということはわかりました。

main関数内で定義する

github.com

上記のリンクのコードを見る限り、main関数内で、add_task関数を使って定義していくのがよさそうです。

main(args) {
  addTask(new GrinderTask('foo', taskFunction: () async {
    var arg1 = context.invocation.options['arg1'];
    var arg2 = context.invocation.options['arg2'];
    // 何らかの処理を行う
  }, options: [new Option(name: 'arg1'), new Option(name: 'arg2')]
  ));

  grind(args);
}

こうすると、

grind foo --arg1=bar --arg2=baz

という形で引数を渡せるようになりました。

タスクの引数にアノテーションを使って定義できるようにする、みたいなissueがあるのですが、どうもまだ実装はされていないようです。残念。

github.com

Aqueductのチュートリアルを終えた

aqueduct.io

Dart製のフルスタックなサーバサイドフレームワークのAqueductのチュートリアルをやってみました。 2016のDart Summitでもトラックがあったみたいで、有望なフレームワークみたいです。

特徴としては、

  • テストを書ける仕組みが整っている
  • 特に何もしなくてもIsolateを使ってマルチプロセスで動く(サイトにはマルチスレッドと書いてあるが…はて?)
  • ルーティングの仕組みがわかりやすい
  • 静的型付けのORMがある。マイグレーション機能付き
  • アノテーション利用で安全なプログラミングがしやすい

というところで、チュートリアルを終えた時点ですが、個人的にはかなり惚れ込んでます!Railsと比べても、「うーん、これイケてないなー」という点が、いまのところありません。

バージョンも2.5とどんどんバージョンアップもしているし、ドキュメントも英語ですが、とてもしっかりしています。

今後はAqueductでサーバサイドのAPIを作って、Angular Dartでフロントエンドの実装をする、というふうにして何か作ってみようと思います。

Angular Dartのチュートリアルを終えた

webdev.dartlang.org

Angular Dartチュートリアルをようやく終えました。仕事の昼休憩の時間と、夜中に少しずつ進めていたので3日くらいかかったと思います。

やってみた感想

まんまなぞっていっただけなので、理解が怪しいところがたくさんありますが、感想などを上げていきます。ちなみに私はAngular未経験者です(JSでも)。Reactもやってないですし、Vueもやってないです。Knockout.jsをプロダクトで使っているので、MVVMには慣れているほうだと思います。

便利な点

コンポーネント単位でhtml, css, dartがまとめられるのは、わかりやすくていいと思います。再利用性も増すので、細かいコンポーネントを気軽に作れるのが嬉しいですね。

不便な点

Angular Dartでは、アノテーションで色々指定していくのですが、指定項目が多くて初心者的には、そこがかなり混乱します。多機能であるがゆえに、使う機能を宣言するのが面倒というか…。まぁ普段Railsやっているので、便利なgemのinclude用メソッドを呼び出すようなものだと考えたら、そんなでもないのかな?という気もしてきます。

あとはサーバサイドが絡んだチュートリアルではないので、そのあたりはどうするのが正解なのかなというのがまだよくわかっていません。Angularのrouterで使うルーティングと本当にあるURLが被ってはいけないからWeb APIだけにしてhttp://localhost:8080/api/v1/postsみたいにRESTでJSON返すのが正解なんでしょうけれども。

Streamが便利そう

Angular Dartとは関係なくてDartのほうなんですが、検索でStreamを使ったサンプルがありました。検索キーワードの入力途中で結果を返すやつで、最後の検索結果を取り出して返すので、重複リクエストが発生しても最後のリクエストの結果が反映されるというふうに書かれていました。Streamは他の言語でも耳にすることはあったのですが、実際に使ってみるという機会があんまりなく、よく理解していませんでした。正直、コードを読んでもやっていることはわかるがなんでそうなるのかがわからないという感じでした。

www.dartlang.org

このStreamの説明ページを読んで、await forで処理を行って結果を返すというのと、チェーンメソッドのあたりの説明を読んで、結構わかった気がしました。使いこなせるよう頑張ろうと思います。合わせてFuture周辺のドキュメントも読んでおきたいと思います。

とりあえずIsolateを使って並列化してパフォーマンスを見た

Dartの強みといえば、非同期処理と並列化の簡単さだと思います(本を読みかじった感じだと)。

そこで、ちょっと気になったのでIsolateでサーバを並列化して、パフォーマンスを見てみることにしました。本に書いてあるけれど、実際にやってみた感じです。まぁ環境は自分はrikulo streamを使っているので、それに置き換えてやってます。

私のマシンはMacBook Pro Late 2013のcore i5でコア数は2ですが仮想コアで考えると4になりますので、4プロセスまで実験できそうです。

wrkを入れる

まずはベンチマークツールのwrkを入れます。全然詳しくないのですが、luaで拡張したりできるとかなんとか。自分の環境はMacなので、Homebrewで入れました。

brew install wrk

main.dartの書き換え

Isolateを使った実装にあたって、以下のスライドを参考にやってみました。

rikulo streamにも、sharedオプションがあったので、それをtrueにしています。また、実験のため、ひとまずIsolateを使うところはコメントアウト

import 'dart:io';
import 'dart:async';
import "dart:convert" show JSON;
import 'dart:isolate';
import "package:stream/stream.dart";
import "package:rikulo_commons/io.dart" show getContentType;

//URI mapping
var _mapping = {
  "/": serverInfo
};

void serverInfo(HttpConnect connect) {
  final info = {"name": "Rikulo Stream", "version": connect.server.version};
  connect.response
    ..headers.contentType = getContentType("json")
    ..write(JSON.encode(info));
}

void addServer(int i) {
  print("Launch server: $i");
  new StreamServer(
    homeDir: 'client',
    uriMapping: _mapping,
  ).start(shared: true);
}

void main() {
  addServer(0);
  // Isolate.spawn(addServer, 1);
  // Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

実験のコードはrikuloのチュートリアルでやったやつでjsonを返すコードです。

1プロセスで実験

まずは1プロセスで実験です。以下のコマンドでrikulo streamを起動します。

dart web/webapp/main.dart

そして、wrkでベンチマーク

wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080

1スレッド、256コネクション、30秒で、http://127.0.0.1:8080にアクセスするという設定です。

結果はこちら

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    31.76ms   21.07ms 412.23ms   97.24%
    Req/Sec     8.57k     1.56k    9.56k    86.67%
  255918 requests in 30.03s, 73.46MB read
Requests/sec:   8521.67
Transfer/sec:      2.45MB

1秒あたり8521のリクエストを捌いているようです。

2プロセスで実験

つぎは2プロセス。コメントアウトを外します。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  // Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

で、サーバを再起動。そして結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    23.50ms   43.48ms 753.46ms   98.57%
    Req/Sec    13.33k     2.51k   15.23k    93.33%
  398198 requests in 30.06s, 114.31MB read
  Socket errors: connect 0, read 116, write 6, timeout 0
Requests/sec:  13248.59
Transfer/sec:      3.80MB

13248に増えています。1.6倍くらいでしょうか?

3プロセスで実験

つぎは3プロセス。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  Isolate.spawn(addServer, 2);
  // Isolate.spawn(addServer, 3);
}

結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    18.16ms   34.03ms 743.47ms   98.14%
    Req/Sec    17.19k     3.64k   20.26k    92.33%
  513436 requests in 30.05s, 147.38MB read
  Socket errors: connect 0, read 70, write 5, timeout 0
Requests/sec:  17088.10
Transfer/sec:      4.91MB

17088まで増えました!2倍くらいですね。

4プロセスで実験

いよいよ4プロセス。

main() {
  addServer(0);
  Isolate.spawn(addServer, 1);
  Isolate.spawn(addServer, 2);
  Isolate.spawn(addServer, 3);
}

結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    15.78ms   13.58ms 135.88ms   88.92%
    Req/Sec    18.67k     1.99k   21.86k    81.33%
  557971 requests in 30.06s, 160.17MB read
Requests/sec:  18562.11
Transfer/sec:      5.33MB

18562。思ったほど伸びませんでしたが、増えてますね!

もっとプロセス数を増やしたらどうなるのか?

どうせ実験なので、コア数以上にIsolateを増やしてみよう!と思い、やってみました。

5つの結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    24.38ms   32.29ms 537.93ms   87.20%
    Req/Sec    17.16k     4.44k   21.31k    88.26%
  509821 requests in 30.05s, 146.35MB read
  Socket errors: connect 0, read 2, write 0, timeout 0
Requests/sec:  16966.75
Transfer/sec:      4.87MB

なんと、16996に減りました。

そして、6つの結果はこちら。

⋊> ~/s/g/p/stream-sample on master ⨯ wrk -t 1 -c 256 -d 30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
  1 threads and 256 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    22.90ms   34.82ms 595.32ms   88.45%
    Req/Sec    16.56k     4.60k   21.48k    88.51%
  493331 requests in 30.08s, 141.61MB read
  Socket errors: connect 0, read 22, write 10, timeout 0
Requests/sec:  16402.62
Transfer/sec:      4.71MB

16402に減りました。

やはりCPUコア数以上に増やしてもよくなさそうですね。

チャートにしてみる

Online Chart Maker | amChartsを使って、チャートにしてみました。仮想コア数と同じ4プロセスがもっとも良い結果となりました。

Isolateを増やしても伸び悩んだ原因は、Mac上で色々動かしている状態でテストしたからかな〜と思います。wrk自体の動作でも30%程度CPUを使っていたりもしたので。Dockerとか、その他もろもろのアプリを停止してやってみればよかったですね。

自動的にCPUコア数だけIsolateを起動するコード

先のスライドでは、コア数を変数にハードコードしていましたが、英語のDart本をみたところ、Platform.numberOfProcessorsでCPUコア数が取得できるようです。そこで、以下のように書き換えてみました。

main() {
  addServer(0);
  for(int i = 1; i < Platform.numberOfProcessors; i++) {
    Isolate.spawn(addServer, i);
  }
}

これで4コア分のIsolateが動きます。本番環境の場合はこうすることでCPUコアを全て使うことができます。

編集後記

サクッとCPUコア数だけ並列化できるのはすごいなーと思います。Default Isolateとのやりとりが発生しないようにしているので、その辺りをちゃんとやろうと思ったら、もう少し複雑なコードになるとは思いますが、英語のDart本だとDefault IsorateでServerSocketを表に立たせる方式を書いてあったので、その方式でも実験してみたいと思います。

grinderを使ってpub serveとサーバサイドの起動を同時に行う

前回の記事では、サーバサイドにアクセスしたら特定の条件によってpub serveで起動しているサーバを中継してdartやjsやcssのファイルを返すようにしました。今度は、この2つのサーバを同時に立ち上げたいなと思ったのでやってみることにしました。grinderを使うと簡単にできました。grinderの関数のrunAsyncを使います。

@DefaultTask()
@Depends(compile)
serve() async {
  runAsync('pub', arguments: ['serve', 'web/client', '--port=8000']);
  Process process = await getStreamProcess();
  stdout.addStream(process.stdout);
  stderr.addStream(process.stderr);

  // rspファイルの変更を検知してコンパイルする
  var clientWatcher = new DirectoryWatcher(path.absolute('web/client'));
  clientWatcher.events.listen((event) async {
    if (event.path.endsWith('.rsp.html')) {
      runAsync('dart', arguments: ['build.dart']);
    }
  });

  var serverWatcher = new DirectoryWatcher(path.absolute('web/webapp'));
  serverWatcher.events.listen((event) async {
    if (event.path.endsWith('.dart')) {
      log("Server: file change detected.");
      // ファイルのコンパイル後にrekulo streamを再起動させる
      log("Server: kill process...");
      if (process.kill(ProcessSignal.SIGTERM)) {
        process = await getStreamProcess();
        log("Server: restart done.");
      }
    }
  });
}

自分でpub serveを行うProcessを立ち上げてもいいのですが、それを行うと標準出力・エラー出力の周辺の取り回しが面倒なので、runAsyncに任せたほうが楽そうでした。でもrunAsyncを使うと、戻り値がProcessではないので、pub serveのような終わらない処理を終了させるにはCtrl + Cしかありません。まぁ今回はそれでよかったので、このままいこうと思います。