こもろぐ @tenkoma

What We Find Changes Who We Become -- Peter Morville著『アンビエント・ファインダビリティ 』

開発環境でOpcacheを有効にする

Docker Desktop for Mac の環境で動かしていたPHPアプリケーションが遅い(1リクエスト1.2〜1.4秒ほどかかっていた)。 ずっと、Docker Desktop for Macの Mount 機能は速度が出ないからだ、と思いこんでいて、最近知った Mutagenを試したけど、200ミリ秒ほどしか改善しなかった。そこでもしかしたら遅い原因は他にあるんじゃないかと思い、調査したところ、以下の2つを変更したらかなりレスポンスタイムが短くなった(300〜450ミリ秒になった)

  • Xdebugを無効にする(xdebug.so を読み込まない)
  • OPcache を有効にする

Xdebug については、リモートデバッグやプロファイラなど遅くなりそうなオプションを全部Offにしても300ミリ秒くらいは増えるので、拡張自体が重いのかも、と思っている。

開発環境でOpcacheは無効化されていたけど、開発環境向けにファイルを変更したら即時反映するよう設定出来るんじゃないかと思ってやってみたら、できそうだったので記事としてまとめる。

開発環境向けOpcache 設定

PHPスクリプトへの変更が即時反映されるための設定は以下の通り。

; Opcache を有効にする
opcache.enable=1
; PHPスクリプトのタイムスタンプをチェックして、更新されていたらオペコードキャッシュを作り直す
opcache.validate_timestamps=1
; タイムスタンプの再チェックを行う時間(秒)。0なら実行毎にタイムスタンプチェックする
opcache.revalidate_freq=0

Laravel 7.2 Hello, world アプリで測ってみた

フレームワークを使ったPHPアプリケーションでどのくらい効果があるかみるために、以下のようなサンプルアプリケーションを作ってみた。

composer create-project してHello, world ページを作っただけのLaravelアプリケーションがあるので、

git clone git@github.com:tenkoma/opcache-config-for-dev-sample.git
cd opcache-config-for-dev-sample
cp .env.example .env
docker-compose run composer composer install
docker-compose up -d
docker-compose exec php-fpm php artisan key:generate

して、http://localhost:8000/hello にアクセスしてテストできる。 初回のアクセスはキャッシュを作るので2回目以降のタイムが速くなる。 Hello, Worldのコードは以下の通り。

<?php
// routes/web.php
Route::get('hello', function () {
    return 'Hello World';
});

テスト結果は以下の通り(10回ほどリクエスト後に10回計測)

  • 1) Volumeオプション:デフォルト(consistent), opcache.enable=0: 平均 0.96s (min 0.84s, max 1.09s)
  • 2) Volumeオプション: cached, opcache.enable=0: 平均 0.44s (min 0.38s, max 0.48s)
  • 3) Volumeオプション: cached, opcache.enable=1 他上記設定を有効: 平均 0.03s (min 0.03s, max 0.04s)

(2) → (3) で 0.35s〜0.44s くらい速くなった。

おわり。

⛳ PHPer Code Golf by pixiv(PHPerKaigi 2020) 上級編の回答について解説

PHPerKaigi 2020 、お疲れ様でした!! PHPer Code Golf by pixiv で賞を頂いたtenkomaです。

イベントについてのエントリーは別途書くとして、Code Golf上級編で書いたコードの解説をします。 1問目はHello, World, 2問目はFizzBuzzなので詳しい解説はしません。

個人的にはCode Golfガチ勢では全く無いのですが、賞をもらってしまったので、コードを晒す義務が発生した気がします。 ちなみに会場のネットワークの混雑等のトラブルもあり、第3ホールの開催時間は1時間20分ほどだったようです。

出題概要

PHPerKaigi 2020参加者向けに用意されたこのサイトで、問題を解く形式です。サイトは停止するかもしれないので出題文を転載します。

配列を展開せよ

あるサイトにのAPIにリクエストを送ると、結果がとても変な形式のJSONで帰ってきてしまいます。

{"foo": "1", "bar[0]": "A", "bar[1]": "B", "buz[0][0]": "00", "buz[0][1]": "01"}

あなたの仕事はこの独特なフォーマットのレスポンスを常識的に綺麗な形状の配列に直すことです。

{"foo": "1", "bar": ["A", "B"], "buz": [["00", "01"]]}

結果の配列は json_encode() で変換して出力してください。

コード入力

<?php

declare(strict_types=1);

$input = json_decode(stream_get_contents(STDIN), true);

$converter = function (array $in) {
    // ...
};

// 最終的にこの変換結果が出力されるようにしてください。
// コード内の好きな位置に関数やクラスを定義しても構いません。
echo json_encode($converter($input));

問題文は以上です。

問題についての注意点

出力後のjson は所々スペースが入ってますが、json_encode()の出力ではスペースを入れることができなかったので、レギュレーションについてゴルフ場デベロッパーさん (@tadsan) / Twitterに問い合わせして、スペースを含まないjsonで正解することができるようになりました。

とりあえず正解にたどり着く

最初に正解にたどり着いたときのコードは以下のような感じでした。

<?php

foreach (json_decode(stream_get_contents(STDIN), true) as $k => $v) {
    $a[] = "{$k}={$v}";
}
parse_str(implode('&', $a), $a);
echo json_encode($a);

この答えにたどり着くまでに可変変数や eval() などを試していたのですが、うまくパスできませんでした。 そのころから、JSONハッシュのキーがURLのクエリのキー形式として使えそうだったので、URLクエリ形式に変換して parse_url() で配列化する方法をためして、正解することができました。 $a 配列をimplode() でまとめた文字列が

foo=1&bar[0]=A&bar[1]=B&buz[0][0]=00&buz[0][1]=01

になるので、それを parse_url() で求める連想配列にできる、という感じです。

最適化1 標準入力の受け取りが長い

stream_get_contents(STDIN)

ここですね。いかにも冗長な感じがします。検索したところ、

fgets(STDIN)

で受け取れることが分かったので、短くなりました。 なお、今回のレギュレーションだと、単に文字数が短いというのが高得点のコツではないそうですが、スコアは良くなりました。

最適化2 ループをなくす

次に見たのがここです。

foreach (json_decode(fgets(STDIN), true) as $k => $v) {
    $a[] = "{$k}={$v}";
}

json_decode して foreach するとか、コード書きすぎですね。。。 ループをなくして、文字列変換ですませることができれば、かなりコードをシンプルにできそうです。

{"foo": "1", "bar[0]": "A", "bar[1]": "B", "buz[0][0]": "00", "buz[0][1]": "01"}

この入力を見ると、jsonのキーと値は ":", 各要素は "," で区切られていることが分かるので、それぞれを =,&に変換してから、周りの{}" を除去できれば、期待するURLクエリに変換できそうです。 そこで最適化したコードが以下です。

<?php
parse_str(strtr(trim(fgets(STDIN), '{}"'), ['":"' => '=', '","' => '&']), $a);
echo json_encode($a);

標準入力を trim(fgets(STDIN), '{}"') で、余計な文字列を削除して以下にします。

foo": "1", "bar[0]": "A", "bar[1]": "B", "buz[0][0]": "00", "buz[0][1]": "01

あとは strtr(..., ['":"' => '=', '","' => '&'])でURLクエリになるように変換します。 (入力のjsonにもスペースがなかったようです) 時間制限最後の回答は以下だったかと思います。

<?php
parse_str(strtr(trim(fgets(STDIN), '{}"'), ['":"' => '=', '","' => '&']), $a);
echo json_encode($a);

今思えば、値に :, が入ってると変換できないですね。

最適化3 (時間切れですが、少し最適化)

LTの時間にやっていたのですが、 preg_replace() を使って {}" をすべて削除してから、 strtr() で1文字毎に変換(:=, ,&)するコードだと抽象構文木としてはシンプルになるので試したところ、スコアが良くなりました。

<?php
parse_str(strtr(preg_replace('/[{}"]/','',fgets(STDIN)),':,', '=&'), $a);
echo json_encode($a);

そのときのスコアは以下の通り。

  • 項目A(低いほど高評価) 105 (最適化2では107)
  • 項目B(低いほど高評価) 4 (最適化2では4)
  • 項目C(低いほど高評価) 39 (最適化2では46)
  • 項目D(低いほど高評価) 105 (最適化2では107)
  • 項目E(高いほど高評価) 8 (最適化2では10)

いまのところ、僕の最高スコアはこちらになります。

おまけ: FizzBuzz について

FizzBuzz については昔作ったプログラムを少し最適化して

<?for(;$i++<100;)echo(($i%3?'':Fizz).($i%5?'':Buzz)?:$i).'
';

として解いてました。しかし、このゴルフ場はPHP7.4、7.4の最新の文法を使ったら、もしかしたらPHPerチャレンジのボーナストークン的なものがもらえるかも!?とおもい、アロー関数を使って

<?for(;$i++<100;)echo(($f=fn($d,$w)=>$i%$d?'':$w)(3,Fizz).$f(5,Buzz)?:$i)."
";

と書いて正解になりましたが、別のトークンはもらえませんでした!

終わり。

Chrome 80 クッキーのSameSite属性対応のややこしい注意点2つ

2020年2月4日に Chrome 80 がリリースされました。 このバージョンでは既存のWebアプリケーションの振る舞いに大きな影響を与える変更として、クッキーのSameSite属性の既定値が変わるというのがあります。

ただし、振る舞いの変更は、2月18日以降に予定されています。(正確には「2月17日の週でアメリカ合衆国大統領の日を除く」)

Enforcement rollout for Chrome 80 Stable: The SameSite-by-default and SameSite=None-requires-Secure behaviors will begin rolling out to Chrome 80 Stable for an initial limited population starting the week of February 17, 2020, excluding the US President’s Day holiday on Monday. We will be closely monitoring and evaluating ecosystem impact from this initial limited phase through gradually increasing rollouts. SameSite Updates - The Chromium Projects

この仕様変更についてはChrome 80が密かに呼び寄せる地獄 ~ SameSite属性のデフォルト変更を調べてみた - Qiitaが詳しいので、この記事では説明しません。 しかし、概要を把握した上で、以下を読んでおくことをお勧めします。

1) 2分間は、SameSite=Lax のクッキーがPOST時に送信される仕様

Chrome 80 以降の振る舞いをテストするには、chrome://flags で以下の項目をEnabledにする必要があります。

  • SameSite by default cookies
  • Cookies without SameSite must be secure

さらに、仕様変更の影響を緩和させる措置として、SameSite=Lax のクッキーはトップレベルのクロスドメインPOST 時に発行から2分間は送信されるChromeの仕様に注意が必要です。(最終リクエストから2分間ではなく、クッキー発行から2分間)

トップレベルのクロスドメインリクエストとは、iframe 内のPOSTやXHRではない、通常のフォームでの画面遷移を伴うPOSTのことです。

つまり既存のアプリケーションの振る舞いに影響しないかをChromeを使って調査するときは、クッキー発行後2分待つ必要があるということです。

この一時的な介入は将来的に削除される予定ですが、アプリケーションの検証時には、無効化したいこともあると思います。その場合、以下のようにChrome起動時にオプションを指定して無効化できます。(macOSの場合) (Chrome 79では使えなかったのでChrome Canaryを使う必要がありましたが、Chrome 80では使えるようになっています。(おそらく80 Beta版でも使えたのではないでしょうか))

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --enable-features=SameSiteDefaultChecksMethodRigorously

Starting in Canary version 80.0.3975.0, the Lax+POST temporary mitigation can be disabled for testing purposes using the new flag --enable-features=SameSiteDefaultChecksMethodRigorously to allow testing of sites and services in the eventual end state of the feature where the mitigation has been removed.

2) SameSite=None が一部の古いブラウザで機能しない問題

SameSite=Lax だと動作しなくなる場合、SameSite=None; Secure に変更することで振る舞いを維持する、という解決策があります。(SameSite=None; Secure を送出する場合、CSRFのリスクを考慮する必要があります。)

Google Developers Japan: 新しい Cookie 設定 SameSite=None; Secure の準備を始めましょう

特定のバージョンの Chrome、Safari、UC Browser など、一部のブラウザは None 値を意図しない方法で処理する可能性があります。その場合、デベロッパーはそのようなクライアント向けに例外処理をコーディングする必要があります。これには、古いバージョンの Chrome が提供している Android の WebView も含まれます。既知の互換性のないクライアントの一覧はこちらです。

と書かれていて、SameSite=None のクッキーを削除したり、SameSite=Strict とするブラウザがあるようです。そのようなブラウザを判定するためのコードがSameSite=None: Known Incompatible Clients - The Chromium Projectsで紹介されています。それによれば

  • Chrome 51〜66
  • Android版UC Browser バージョン12.13.2以前
  • MacOS 10.14 のSafari とSafari組み込みブラウザ、iOS 12のすべてのブラウザ

に問題があるとのことです。macOS 10.14 と iOS 12 の影響が大きそうです。

まとめ

最初にSameSiteの話を詳しく調査したとき、2分間はSameSite=Lax+POSTでクッキー送信されるというのを見逃していて後日知って焦りました。共有するために記事を書きました。

CSRF対策として既定がSameSite=Laxに変わることは歓迎しますが、既存アプリをサポートする立場だと注意点がありますね。

CHROMEのAVAIL BACKPACKを買った

https://www.chromeindustries.jp/smartphone/detail.html?id=000000000632

2014年にcote&ciel のリュックを買って使ってましたが、底が破れたり、肩のベルトがどんどんずれて背負い心地が悪くかったので買い換えようと。 以前CHROMEのメッセンジャーバッグを使っててよかったのでまたCHROMEに。

肩紐が全くズレないし、背中が暑くならないのでウォーキングが快適です。 ショップでは15インチノートPCが入るかは書いてありませんでしたが、 Macbook Pro 15インチがちょうど入る大きさでした。 電車で立つ時に、前に抱えるのも楽。 良い買い物をしたと思います。

PHPカンファレンス福岡2019 マネクラからの挑戦状 Webサイトコードゴルフ スコア60230(非公認)の解説

PHPカンファレンス福岡2019内企画 マネクラからの挑戦状、開催時間内にはほとんどやってませんでしたが、後日やってみて、スコア60230を出せたので解説してみます。開催時間外なので非公認スコアです。

(記事公開後、 @m3m0r7)さんと相談しながら縮めた結果、60300→60230へとさらに70短縮できました。

PHPカンファレンス福岡2019 マネクラからの挑戦状 Webサイトコードゴルフ

スコア60230(PHPファイル(480文字)+画像ファイル)

まずは入念にレギュレーションをチェック

プログラム一式をマネージドクラウドインスタンスにデプロイ後、オンライン上に用意されたDiffツールを使って、ブラウザレンダリング結果が一致しているかチェックします。合格したら、make check コマンドでスコアを計測する、という流れです。また以下のレギュレーションが存在します。

  • PHPで作成されたサイト https://phpconfuk-codegolf-php.lolipop.io/ (正解サイト)のソースコードをお渡しします(このリポジトリです)。
  • サイトの 表示を一切変えずに Webサイトを構成する全コード全ファイルの合計ファイルサイズを小さくして、あなたのロリポップ!マネージドクラウドPHPプロジェクト にデプロイしてください。
  • 最も小さいファイルサイズになった人が優勝です
  • コンテナ内の /var/www/html 内にWebサイトを表示するのに必要な全てのコード、およびファイルを設置してください
    • /var/www 等への設置は禁止です
  • Makefile内の check タスク内のコマンドの変更は禁止です

レギュレーション

プログラムがやっていること

エントリーポイントとなる index.php を見てみましょう。

コアとなるロジックは、

  • クエリストリングのキーと値の組み合わせを tokens テーブルに保存。
  • 値降順(第一ソート順), キー降順(第二ソート順)でとりだし、3番目の値の文字列を取得し逆さにする
  • HTMLテンプレートに文字列を埋め込んで表示

という感じでした。

vendors ディレクトリ、データベースアクセスを取り除く

初期のスコアは400万を超えていますが、その大部分が vendors ディレクトリであることがわかります。 プログラムを把握した上であらためてレギュレーションを確認してみると、データベースに関してはなんら記述がないので、データベースへ保存するコードを除去できます。 よって、受け取ったクエリストリング配列を処理して、レンダリングに必要な文字列を返すコードと、その他のHTMLを全てindex.php に入れれば、大分スコアが減らせそうです。そうして出来たコードにインデント・スペースを補完したコードが以下。

<?
    $a=$_GET;
    krsort($a);
    arsort($a);
    echo strrev(array_values($a)[2])
?>

最初のforeach で配列を整形してる部分をなくせないかな、と思いましたが思いつかなかったです。 ユーザー定義のソート関数ではキーと値のソートルールが同じだったので、$i = $a[1] != $b[1];$itrueなら値、false ならキーでソートされます。

コードゴルフなのに、そこそこ読みやすいコードになってしまいましたね。

short open tag

short open tagは将来的に廃止されそう(PHP: rfc:deprecate_php_short_tags)ですが、PHP 7.3.2ではまだ動作しますし、マネクラの環境でもデフォルトで使えます。タグとforeach キーワードの間にスペースが無くても動きます。

不要なファイルを削除する

これで、 vendors も削除できますし、 model/token.php も不要になりました。表示に必要なファイルは、 index.php, mc.png の2つになったので、改変不可な make check ファイルを含む Makefile を入れて、3ファイル以外の全てのファイルを削除しましょう。 .env, .htaccess が無くてもDiffチェッカーはパスします。

HTMLドキュメントを短くする

残るはHTMLを短くするのみです。PNGファイルは、名前を変える以外の変更はしていません。

無駄なCSSを取り除く。

lolipop-mc-codegolf-challenge/home.liquid at master · pepabo/lolipop-mc-codegolf-challenge · GitHub

CSS定義でファイルサイズがかなり大きくなってます。 まず、HTMLにはクラス属性が1個も定義されてなかったので、セレクタにクラスを含むものは安全に削除できます。 使ってないタグを含むセレクタの定義も削れます。 結果以下が残りました。

body {
    height: 100vh;
    font-family: Helvetica;
    letter-spacing: .04em
}
table {
    height: 100%
}
* {
    border: none;
    padding: 0;
    margin: 0;
    font-weight: 400;
    color: #465560;
    text-align: center
}
td {
    height: 50%;
    width: 5%
}

font-family は、レンダリングに使われている物だけをのこしました。画面に表示されている文字列要素が h2 だけだったので、 * に統合することができました。

表示に影響しないタグを削除していく

<html><header><meta><body> などを削ってもチェッカーにパスしました。

表示に影響しない閉じタグを削除していく

一箇所 </table> が残りましたが、それ以外は削除できます。 </style> については、ドキュメントの最後に移動すると、削除できます。 ところどころにある &nbsp; も不要です。

属性値を"で囲まない

はい。業務で真似してはいけない。

DOCTYPE宣言

<!DOCTYPEhtml>

宣言内のスペース不要でした。

pngファイル名をmc.pngm

5文字縮まりましたね。拡張子なんていらなかった。

まとめ

これで解説は終わりです。 全般的に業務で役に立たない知識です。

没テクニック

テーブルレイアウトをやめてdivにする

挑戦しましたが、Diffが出てしまうので断念。

PNGファイルサイズを縮める

PNGファイルからアルファチャンネルを削除すれば、スコアを縮められるかと思ってGIMPでやってみましたが、Diffチェッカーに引っかかって失敗しました。サイズは8,000ほど縮まりそうだったので残念です。

レギュレーションを守った上で最高のスコアは

なんと0になります。スコアチェッカーのコマンドは以下の通りですが、 *.git/*, Makefile, .env が除外されているため、.git/ ディレクトリ以下に index.php, mc.png を移動して、シンボリックリンクを作れば、レギュレーションを完全に守った上で0が達成できます。 チェッカーで除外するのではなく、デプロイ時に除外すればこの穴はできないので、運営側で意図した穴なのかな、と思います。

@ssh -p ${SSH_PORT} ${SSH_USER}@${SSH_HOST} 'find /var/www/html -type f -not -iwholename "*/.git/*" -not -name "Makefile" -not -name ".env" | xargs cat | wc -c'

55型有機EL TVとサウンドバーの感想

55型有機EL TV (OLED55C8PJA)

ヨドバシ.com - LGエレクトロニクス OLED55C8PJA [有機ELテレビ 55V型 4K対応 OLED55C8P]

1年前発売・有機EL 55型で一番安いLG製を購入。10年前に買ったSHARP製32型液晶からの買い換え。 NetflixとAmazon Prime, Youtube, Nintendo Switch用に。

良かったと思うところ

  • 黒が黒い
  • いままでPC接続でEIZO FlexScan 27.0インチ ディスプレイモニター (4K UHD/IPSパネル/ノングレア/ブラック/USB Type-C搭載/5年間保証&無輝点保証) EV2785-BKでNetflixを見てたけど、Safariで見ないと高画質にならないらしく不便でした。あとPCを操作しないといけないので、早く見たいとかながら見とかしにくい。そのあたりの障害が取り除かれました
  • Nintendo Switchをスリープ解除したらTVも起動してHDMIに自動切り替えしてくれるので楽
  • リモコンが変な形だけど、直立できるのは便利かもしれない
  • サウンドバーとHDMI 接続してる。両方がHDMI-ARCという仕様に対応してて、別途線を付けなくてもいいし、TVのリモコンでサウンドバーの音量を操作できる

いままであった32型液晶は過去7年くらい、地上波を見なくなって、ほぼ使ってなかったけど、これはNetflix, Amazon Prime, YouTube用に長く使えそうな気がします。 4Kはコンテンツがまだ少ないと感じる。Amazon Primeの4Kコンテンツは綺麗ですが、まだまだ数が少ない。

微妙なところ

  • 電気代。55型だし仕方ない…
  • フレーム補完機能。既定でオンですが、オフにしてます

サウンドバー

ヨドバシ.com - ヤマハ YAMAHA YAS-408(B) [MusicCast BAR 400 フロントサラウンドシステム]

薄型テレビはスピーカーが弱いらしい、ということで。あとSpotify ConnectとBlueToothスピーカー機能目当て。 いままで使っているスピーカーはGX-D90(Y) (色違いのGX-D90(B)はまだ新品を売ってる)を、大学のときから15年くらいPCスピーカーとして使っていて、壊れるまでこれでいいや、と思ってますが、TV用に別途欲しくなり。

良かったところ

  • TVの電源に連動して自動ON/スリープしてくれる
  • Spotify Connect は期待通り。PCでもスマホでも操作できて、スピーカーは同一のものが使える
  • iPhone/MacのiTunes/ミュージックアプリからの再生でも自動電源ON/スリープしてくれる
  • サブウーファー(いままで使ったことない)、いままで聞いてた曲を再生しても、聞こえなかった低音が出るので良かった。

微妙なところ

  • どうしようもないことかもしれないけど、Bluetooth 経由の再生開始に1秒強の時間がかかるのが気になる

ドラム式洗濯機 Cuble (NA-VG2300L-X) 感想

ヨドバシ.com - パナソニック PANASONIC NA-VG2300L-X [ななめドラム式洗濯機 Cuble(キューブル) 洗濯・脱水10kg/乾燥5kg 左開き プレミアムステンレス]

同僚に「ドラム式は人権だ」、と言われたので、高いものを買いました! ヨドバシカメラ秋葉原で28万円、ポイント差し引いて5年保証を付けて実質24万ちょいで購入。 洗濯物を入れてボタン2つ(電源、スタート)を押すと3時間後に乾燥して出てくる魔法の箱を手に入れた、という感じです。 10年ほど、ずっと部屋干ししていたので、以下の手間が無くなりました。

  • 洗剤を洗濯前に入れる手間
  • 洗濯後に干す手間
  • 乾燥後に洗濯物を取り込む手間

いままで使っていた縦型の洗濯機(8年前購入)にも乾燥機能は付いていましたが、シワだらけになったので、2, 3回使ってやめました。 ドラム式だからかシワも気にならなくて、毎回乾燥させてます。

電気代が増えたかどうかについては、まだよく分からないです。 同時期にテレビも買ったので、そちらでは増えてる気がします。