BEAR.Sunday内部のベンチマーク

helloworldベンチマークというフレームワークのミニマムブートストラップを計測するベンチマークがあります。この記事ではBEAR.Sundayでのブートスラップコストをより細かく分割して、それぞれ内部のアクションにどれくらいのいのコストがかかっているかを見ます。

アプリケーションはユーザーのプロジェクトフォルダの`bootstrap/bootstrap.php`で、リクエストの最初から最後まで実行されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3940/de06ace3-8285-bba1-23a9-51f7a5b1e24a.png)



|action| 意味|
|-----------------|------------------|
| load | composerのautoload初期化 |
| $app | ルートオブジェクトグラフの取得(`bootstrap.php`のみで使用） |
| route| webリクエストとオブジェクトリクエストの変換 |
| request| リソースリクエスト |
| transfer | リソース状態を表現に変換してクライアントへの転送 |

インストール直後のスケルトンアプリケーションにベンチマーク用の関数`t()`とその結果を表示する`result()`を追加して、それぞれの時間を計測しました。

```
git clone https://github.com/koriym/BEAR.HelloworldBenchmark.git
cd BEAR.HelloworldBenchmark
composer install --no-dev
composer dump-autoload --no-dev
php -S 127.0.0.1:8080/ bootstrap/bench.php
```

```bootstrap.php
<?php
use BEAR\Package\Bootstrap;
use BEAR\Resource\ResourceObject;

$context = 'prod-app';

apcu_add('i', 0);
apcu_store('i', apcu_fetch('i') + 1);

require dirname(__DIR__) . '/autoload.php';
t('load');

/* @global string $context */
$app = (new Bootstrap)->getApp('MyVendor\MyProject', $context, dirname(__DIR__));
t('app');

$request = $app->router->match($GLOBALS, $_SERVER);
t('route');

try {
    $page = $app->resource->{$request->method}->uri($request->path)($request->query);
    t('request');

    /* @var $page ResourceObject */
    $page->transfer($app->responder, $_SERVER);
    t('transfer');
    if (isset($_GET['result'])) {
        result();
    }

    exit(0);
} catch (\Exception $e) {
    $app->error->handle($e, $request)->transfer();
    exit(1);
}

function t(string $key)
{
    static $i;

    apcu_add($key, 0);
    $i = $i ?: apcu_fetch('i');
    if ($i === 1) {
        return; // first call to create cache
    }
    $time = apcu_fetch($key) + microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
    apcu_store($key, $time);
}

function result()
{
    $i = apcu_fetch('i');
    $averageRequestTime = apcu_fetch('transfer') / $i;
    $lastTimer = 0;
    foreach (['load', 'app', 'route', 'request', 'transfer'] as $action) {
        $actionTime = apcu_fetch($action);
        $elapsedTime = number_format($actionTime / $i * 1000, 3);
        $periodTime = number_format(($actionTime - $lastTimer) / $i * 1000, 3);
        $lastTimer = $actionTime;
        $proportion = number_format($periodTime / $averageRequestTime / 1000 * 100, 1);
        printf("| %s | %s | %s | %s%% |\n", $action, $elapsedTime, $periodTime, $proportion);
    }
}
```

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 1.058 | 1.058 | 12.1% |
| app | 7.502 | 6.443 | 73.5% |
| route | 7.564 | 0.062 | 0.7% |
| request | 8.725 | 1.161 | 13.2% |
| transfer | 8.764 | 0.039 | 0.4% |

composerのautoloadの初期化12%。`route`と`transfer`は無視できるほど小さく、`$app`オブジェクトのアンシリアライズが約3/4とほとんどを占めています。


次に最適化オプションをつけてautoloadをダンプします。

```
composer dump-autoload --no-dev -o
```

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 1.998 | 1.998 | 21.6% |
| app | 8.068 | 6.070 | 65.6% |
| route | 8.125 | 0.057 | 0.6% |
| request | 9.212 | 1.087 | 11.8% |
| transfer | 9.251 | 0.038 | 0.4% |

autoloadの初期化に倍の時間がかかるようになってしまいました。不使用かもしれないクラスマップを読み込んでるためです。初期化コストを払うからわりにランタイムでのオートロードの時のファイルパスを探すのが早くなります。`app`と`request`に注目してください。僅かに早くなってるのはランタイムでのオートロードコストが低いためと考えられます。`$app`をアンシリアライズする課程で大量のクラスファイルをロードしないといけないからです。

このベンチマークのようなランタイム中にオートロードの必要の少ないプログラムでは逆に遅くなることになります。その差はこのhello worldが最大と考えていいでしょう。約5%程度です。

```
composer dump-autoload --no-dev
```
# Polidog.Todo

## HTML

次にHTMLアプリケーションを検討します。`Twig`と`Aura.Sql`を使ったアプリケーションで実際にデータベースもアクセスしていますす。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/3940/99b07307-f4af-d677-40b5-58dcf8fd2271.png)

`-o`なし

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 1.221 | 1.221 | 3.3% |
| app | 9.841 | 8.620 | 23.2% |
| route | 9.937 | 0.096 | 0.3% |
| request | 21.521 | 11.584 | 31.1% |
| transfer | 37.230 | 15.710 | 42.2% |

`-o`あり


|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 3.036 | 3.036 | 8.2% |
| app | 11.019 | 7.984 | 21.6% |
| route | 11.105 | 0.085 | 0.2% |
| request | 21.973 | 10.868 | 29.4% |
| transfer | 36.921 | 14.948 | 40.5% |

最適化オプションのあるなし(`-o`)での違いを見るとやはりhello worldアプリと同じ傾向がみられます。`load`の初期化コストと`app`+`request`がトレードオフの関係にあります。

`transfer`では毎回Twigのテンプレートをレンダリングしているのでこのようなコストになりました。

## API

最後にAPIです。先ほどのTodoアプリと違ってテンプレートのレンダリングはないがDBアクセスを行ってJSONを出力しています。

API -oなし

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 1.247 | 1.247 | 7.2% |
| app | 10.085 | 8.838 | 50.8% |
| route | 10.183 | 0.098 | 0.6% |
| request | 17.341 | 7.158 | 41.2% |
| transfer | 17.391 | 0.051 | 0.3% |

API -oあり

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 5.596 | 5.596 | 27.5% |
| app | 13.567 | 7.971 | 39.1% |
| route | 13.654 | 0.088 | 0.4% |
| request | 20.329 | 6.675 | 32.7% |
| transfer | 20.383 | 0.054 | 0.3% |

helloworldとHTMLの中間ぐらいの数字です。`-o`オプションをつけないでautolodaerをダンプした方が有利な結果になりました。

# BEAR.Sundayのbootstrapの特徴

オブジェクトシリアライズとPHPファクトリーコードの生成により、DIコンテナや各機能コンポーネントの初期化コストが原理的にほぼないことです。モジュール追加で機能追加してもbootstrapコストはほとんど変わりません。

「フレームワークの機能が多いから遅い／少なから速い」ということがありません。

# アプリケーションタイプによるコストインパクト

フレームワークのbootstrapコスト(hello wolrdベンチ）のインパクトはアプリケーションや用途によって大きく変わります。

データベースアクセスを用いてHTMLを返すWebアプリケーションでは相対的に低いです。単純なTodoアプリの場合で初期化に27%、リクエスト（データの作成）に32%、TwigでHTMLにするのに42%かかっています。（この場合Helloworldベンチを倍の性能にしても13.5%しか速度向上しません）

DBなどに単純なアクセスをしてJSONを返すようなAPIの場合にはアプリケーションコードよりフレームワークコストのbootstrapコストの方が高くなる場合もあります。（ちょっと納得できかねますが）

# dump-autoload -o オプション

実験の結果からは`-o`オプションはBEAR.Sundayアプリケーションではつけない方が良さそうですが、念のために実際のアプリケーションでベンチをとってみたら良いと思います。

# autoload.phpコンパイル

2018/05/19にリリースした[BEAR.Package v1.7.11](https://github.com/bearsunday/BEAR.Package/releases/tag/1.7.11)のautoloadコンパイルのベンチマークも取ってみます。この[autoload.php](https://gist.github.com/koriym/1f7125e5650f0447dae5b44a1a8ae8eb)のようなファイルを削減してautoloadの時のクラスファイルの読み込みコストを大幅に低減します。

## コンパイルなし

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 3.015 | 3.015 | 39.7% |
| app | 6.856 | 3.841 | 50.6% |
| route | 6.917 | 0.061 | 0.8% |
| request | 7.582 | 0.665 | 8.8% |
| transfer | 7.594 | 0.012 | 0.2% |

Requests per second:    123.11 [#/sec] mean
Time per request:       81.230 [ms] mean
Time per request:       8.123 [ms] mean, across all concurrent requests

## コンパイルあり

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 6.490 | 6.490 | 91.3% |
| app | 6.728 | 0.239 | 3.4% |
| route | 6.791 | 0.062 | 0.9% |
| request | 7.097 | 0.306 | 4.3% |
| transfer | 7.109 | 0.011 | 0.2% |

Requests per second:    135.79 [#/sec] mean
Time per request:       73.642 [ms] (mean
Time per request:       7.364 [ms] (mean, across all concurrent requests

`app`や`request`などの各処理の所用時間が減っているのはその時に必要なオートロードがほぼ無しになったためです。`$app`をアンシリアライズする時間も1/10以下になっています。

大きなオブジェクトのアンシリアライズが遅いのではなく、実はその際に発生するオートロードに時間がかかっていたということが分かりました。（=バイナリシリアライズオプションとか導入しても効果は限定的と思われます）

改めてはっきりしたのはBEAR.Sundayのミニマムブートストラップのうちの90%以上は`require`実行のコストということです。一方、他の処理が十分に最適化されているということも言えるかと思います。