YAPC::Tokyo 2019 にカヤックのエンジニアが3名登壇します! #yapcjapan

技術部の小池です。

いよいよ今週末に迫りました YAPC::Tokyo 2019、弊社からも3名登壇予定なのでご紹介させていただきます!

yapcjapan.org

トーク内容をプロポーザルの抜粋にてご紹介します。いずれもカヤックの仕事から生まれたノウハウが詰まった内容となっています。

Perl5の静的解析入門 機械と人間双方の歩み寄りによる平和編

by macopy

Room1 で 14:00 からです。

https://yapcjapan.org/2019tokyo/talks.html#/detail/2

"Only perl can parse Perl" という言葉にもあるように、長年Perlスクリプトをperlコマンド以外が解析し、利用することは難しいことであると思われてきました。もちろんPPI.pmやperlapiを利用するソリューションもいくつかは ありましたが、Perlを静的解析するアプローチはこれまであまり活用されて来なかったように思えます(筆者の視野内の感想です)。

このトークでは、主にDamian Conway氏の作ったPerlスクリプトを正規表現で解析するPPR.pmを用いて、Perlスクリプトの静的解析を行う話をします。具体例を挙げると、昨今のモダンな言語のコンパイラやlinterが備えているような機能を実装します。 また、人間が自由気ままにPerlスクリプトを書くと、機械が解析するのにも限界があります。他のプログラミング言語では人間側に型を書くことを強制させたり、その場で関数名を文字列で組み立てて呼び出しするのを禁止するなど、静的解析がしやすい言語デザインになっています。 Perlを書くときも安全安心なコードにするために静的解析を用いたい、そのためには人間側にも歩み寄りが必要です。このトークでは具体的な「静的解析がしやすいPerlスクリプトの書き方」についても提案します。

こんな人に聞いてもらいたい

  • Perlスクリプトを静的解析できないと思っている方
  • "Use of uninitialized value" に対抗できる魔術がないか探している方
  • 静的解析という言葉はわからないが、なにか惹かれるものを感じる方
  • 静的解析がしにくい例を通じていかにPerlが人間中心のプログラミング言語であるかを感じたい方

Perlでも分散トレーシングしたい! - AWS::XRayによる解析とその実装

by fujiwara

ホール で 15:40 からです。

https://yapcjapan.org/2019tokyo/talks.html#/detail/3

皆さんマイクロサービスしてますか?

昨今のWebアプリケーションは、内部ネットワークにある DB や KVS へのクエリだけではなく、多数の外部サービスと通信する必要が増えてきています。外部への通信は往々にして不安定で、レイテンシやエラーの発生率も高いものです。

障害が発生した場合にその原因を追及したり、パフォーマンスに問題があるのでチューニングを行いたい場合には、これらの外部呼び出しの状況を可視化することが大変強力な手段になります。

AWS(Amazon Web Services) X-Ray は、外部リソースへの通信をモニタリングし、解析するためのマネージドサービスです。AWS 外で動作しているアプリケーションに対しても使用できます。残念ながら Perl 用の公式 SDK は提供されていないため、AWS::XRay 等のライブラリを実装し、CPAN で公開しました。

このトークでは、分散トレーシングの概要、AWS::XRay, Plack::Middleware::XRay による実際のアプリケーション(例として ISUCON8 本選の Perl 実装)を解析する手法、どのように AWS::XRay が実装されているかの解説を行います。

聞いてもらいたいかた

  • マイクロサービスやるぞ!というかた
  • Webアプリケーション(特に既存のPerl製のもの)のパフォーマンス解析がしたいかた
  • グラフを眺めてニヤニヤするのが好きなかた
  • Perl 用の SDK は提供されないことが多いけど頑張っていこう、というかた

AWS::XRay についてはこちらのブログにも記事があるので興味のある方はご覧ください。

sfujiwara.hatenablog.com

自前運用のZabbixからマネージド監視サービスMackerelへ - ソーシャルゲームタイトルのサーバ監視の移行事例

by suzuki

Room0 で 14:50 からです。

https://yapcjapan.org/2019tokyo/talks.html#/detail/39

弊社ではZabbixでサーバ監視を行ってきましたが、自前運用のZabbixサーバにかかる運用コストが問題になっていました。

この問題をマネージドなサーバ監視サービス Mackerel の導入によって解決しようとしています。

本トークではそれらの取組のうち、自社ソーシャルゲームタイトル「僕らの甲子園ポケット(以下ぼくポケ)」におけるZabbixからMackerelへのサーバ監視の移行事例について話をさせていただければと思います。

具体的には以下のような内容となる予定です

  • 監視・メトリクスの移植
  • アラートではないが通知はほしいケースへの対処方法
  • Mackerelならではの便利な機能
  • オートスケールやプロビジョニングとmackerel-agentをうまく付き合わせるための工夫
  • 本家のrepoへPRを送って積極的に改善していく
  • github.com/ryotarai/waker と連携したアラートエスカレーション
  • 自作プラグイン・ツールを利用した柔軟な監視
  • サーバ以外の監視: アラート通知の変更を通知して誤操作に気づけるようにする
  • mackerelをservice discoveryとして活用して深刻な障害時に緊急メンテを入れる

チケットのご案内

あ〜これ読んだら聞いてみたくなったわ〜という方、なんとまだチケットを購入することができます。みなさまのご参加をお待ちしております!前夜祭および懇親会のチケットは別売りなのでご注意ください。

passmarket.yahoo.co.jp

おわりに

今回登壇する3名以外にも弊社の社員が会場にいる予定です。発表内容やカヤックに興味を持っていただけた方は懇親会等でお気軽にお声がけくださいませ〜

カヤックでは焼酎を飲むエンジニアも募集しています!

関連記事

techblog.kayac.com

UnityEngine.UI.Imageが透けてる所を塗るのが許せない

f:id:hirasho0:20190116110517j:plainf:id:hirasho0:20190116110451p:plain

こんにちは。技術部平山です。

この記事では、UnityEngine.UI.Image、つまり「普通のImage」 だと余計な所まで塗ってしまってGPU負荷が大きいので、 塗る面積を削ってみた、というお話をいたします。

一枚目の画面写真で、右端が普通のImageで、左と中央が今回作ったものです。 左はSpriteに入っている頂点を利用したもので、頂点は増えますが面積が減ります。 中央は、黒い枠を重ねて顔のところだけを切り出したもので、 手動で頂点を編集することでマスクなしでの切り出しをしています。 二枚目の画面写真はoverdraw表示で、塗る面積が減っていることがわかります。

また、画像をSpriteAtlasにまとめている場合、パッキングをtightにしたり回転を許したりすると、 普通のImageでは正常に表示できなくなりますが、今回はそこにも対応しました。 より小さな容量に詰めることができます。

以下は実行時の画面写真です。

f:id:hirasho0:20190116110456j:plain

普通のImageではゴミが映り込んでしまいます。 また、アトラス内で画像が回転していることに対応できず下の人の絵が天地逆転しています。 アトラスは以下のようなものです。

f:id:hirasho0:20190116110417p:plain

tight設定と回転の許可により、より小さな容量にテクスチャをまとめることができます。

サンプルコードはgithub に実行可能な形で置いてありますので、よろしければ持っていってください。 弊社東京プリズン用に作ったものを、この記事のために整理、汎用化、高機能化したものですので、 実地の使用にも耐えるかとは思いますが、その過程でバグを入れている可能性もあります。 あくまでもサンプルということでよろしくおねがいいたします。

塗り面積問題

塗る面積によるGPU負荷は、結構馬鹿になりません。

据置き機では4K(3840x2160)なんて話になってますし、 持ち歩くスマホやタブレットでも1920x1080を超える解像度の機械が 山ほど出てきました。 価格の高い高級機が高い解像度を持つのはかまわないのですが、 問題は安物です。2万円3万円クラスの機械のGPUはだいぶ性能が劣るわけですが、 それでも1920x1080の解像度を持っていたりします。 まるで性能が足りません。とりわけチップがSnapdragon 4XXの奴とかは要注意です。 きっとどこの会社でもテスト機種のラインナップには そういう機械を入れておられることでしょう。

これに対する妥当な対策は、 起動時にScreen.width/heightをいじって「必要十分」な解像度に落としておく ことかと思います。弊社東京プリズンでは標準の解像度を1136x640としています。 しかし、諸事情あってイラストの画質を落とせないこともあるでしょう。 そういう場合に塗り面積を減らす選択肢としては、二つほどあるかと思います。

  • SpriteRendererを使う
  • UnityEngine.UIでどうにかする

テラシュールブログの記事 にあるように、SpriteRendererを使えば、何もしなくても透明な領域を削って描画してくれます。 これが素直な手です。頂点の数も調整できます。 しかし、「UnityEngine.UIで作っているのでSpriteRendererを混ぜたくない」 という場合には使いにくいかと思います。東京プリズンもそうでした。

二つ目の選択肢は、UnityEngine.UIでどうにかする方法です。 UnityEngine.UI.MaskableGraphic を継承して自分で頂点を詰めれば、 普通のImageやTextと共存できるコンポーネントが作れます。

Spriteの頂点を使う

SpriteRendererで使われる頂点配列は、Spriteが持っています。 verticesuvtrianglesの3つのプロパティを使って、 UnityEngine.UI.Graphic.OnPopulateMesh を実装します。これを使った最小のコンポーネントのコードは以下のようになります。

using UnityEngine;
using UnityEngine.UI;

public class MinimumCutoutImage : MaskableGraphic
{
    [SerializeField]
    Sprite _sprite;
    public override Texture mainTexture { get { return (_sprite != null) ? _sprite.texture : null; } }

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();
        if (_sprite == null){ return; }
        for (int i = 0; i < _sprite.vertices.Length; i++)
        {
            vh.AddVert(_sprite.vertices[i] * _sprite.pixelsPerUnit, this.color, _sprite.uv[i]);
        }

        for (int i = 0; i < _sprite.triangles.Length; i += 3)
        {
            vh.AddTriangle(_sprite.triangles[i + 0], _sprite.triangles[i + 1], _sprite.triangles[i + 2]);
        }
    }
}

これもGithubに置いてあります。 普通のImageと同様にInspectorでSpriteを設定し、 OnPopulateMeshで、もらったVertexHelperに頂点を設定します。 UnityEngine.UIは大抵ピクセル単位でサイズや位置を設定しますので、 Sprite.pixelsPerUnit を頂点座標(vertices)に乗算する必要があります。

普通のImageと互換性を持たせたい

上に示したMinimumCutoutImageは実は結構不便です。 おそらくは普通のImageから後で置き換える、という用途でしょうから、 widthとheightが同じなら同じ大きさになってほしいのですが、 そうなっていません。現状RectTransform.sizeDeltaを見ていないので、 widthやheightに何を入れてもサイズが変わらないのです。 SetNativeSizeボタンがないのも同様に不便ですね。 そこで、さらに改造を加える必要があります。

まずSetNativeSize

まずはSetNativeSizeボタンを足しましょう。 RectTransform.sizeDeltaにSpriteの解像度を入れる機能を用意し、 それをエディタ拡張のボタンとして用意します。

public class CutoutImage : MaskableGraphic
{
    /// ...中略...
    public override void SetNativeSize()
    {
        if (_sprite == null)
        {
            return;
        }
        var rect = _sprite.rect;
        rectTransform.sizeDelta = new Vector2(
            rect.width,
            rect.height);
    }
#if UNITY_EDITOR
    [CustomEditor(typeof(CutoutImage), true)]
    public class Inspector : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            var self = (CutoutImage)target;
            if (GUILayout.Button("Set Native Size"))
            {
                self.SetNativeSize();
            }
        }
    }
#endif
}

Spriteの解像度は、 Sprite.rect で取れます。SpriteAtlas化した場合でも元の解像度を返します。 このwidthとheightを rectTransform.sizeDelta につっこむだけです。 そして、エディタ拡張のボタンは上のようなコードで足せます。 privateメンバに楽にアクセスできるので、エディタ拡張のクラス(上で言うInspector)は クラスの中に作るのが楽だと思います。

かくして、SetNativeSizeボタンがつき、押すとwidthとheightが自動設定できるようになりました。 この段階のコードはこちらです

サイズ調整

さて、ボタンはつけたものの、押しても何も起こりません。 何もしなければwidthとheightは100ですが、その状態でボタンを押して 例えば256x256にしても表示は何も変わらないのです。 そこで、普通のImageのように widthやheightに応じて表示サイズが変わるようにしましょう。

元々Sprite.verticesに入っている頂点座標は、ピクセル単位ではありません。 すでに見たように、Sprite.pixelsPerUnit を乗算して初めてピクセル単位になります。加えて 普通のImageのようにwidthやheightをいじって拡大縮小したい場合には、 widthやheightを持っているRectTransform.sizeDeltaの値を頂点座標に反映させる必要があります。

例えば、元解像度が128x256で、width=64、height=64で表示したい場合、 xは0.5倍、yは0.25倍する必要があります。 つまり、元解像度とwidth及びheightの比を出して、それを乗算すればいいわけです。

protected override void OnPopulateMesh(VertexHelper vh)
{
    vh.Clear();
    if (_sprite == null){ return; }
    var spriteRect = _sprite.rect;
    var sizeDelta = rectTransform.sizeDelta;
    var scale = new Vector2(
        _sprite.pixelsPerUnit * sizeDelta.x / spriteRect.width,
        _sprite.pixelsPerUnit * sizeDelta.y / spriteRect.height);
    for (int i = 0; i < _sprite.vertices.Length; i++)
    {
        var v = new Vector2(
            _sprite.vertices[i].x * scale.x,
            _sprite.vertices[i].y * scale.y);
        vh.AddVert(v, this.color, _sprite.uv[i]);
    }
    /// ...略...

Sprite.rectとRectTransform.sizeDeltaの比を各頂点座標に乗算するだけです。 これで、サイズが正しくなります。 この段階のコードはこちらです

位置がずれてる...

実はまだ対応しないといけないことがあります。 RectTransform.pivotをいじると位置がズレてしまうのです。

f:id:hirasho0:20190116110459p:plainf:id:hirasho0:20190116110503p:plain

上の図ではRectTransformのpivotが(0.5, 0.5)で、白い背景と人の絵が合っていますが、 下の図ではpivotが(0, 0)で、背景とズレています。 普通のImageであれば、pivotをいじった時のPosX,PosYの自動調整で 同じ位置に留まるのですが、何故かそうなっていません。

つまり、普通のImageでpivotをいじった時にUnityが中でやっている処理を、 自作しないといけない、ということです。 上のコードを見てみましょう。どこにもRectTransform.pivotを見ている場所がありません。 pivotを見ていないのにPosX,PosY、つまりRectTransform.anchoredPosition をいじれば、位置がずれるのは当然です。

というわけで、RectTransform.pivotを見るようにコードを直します。 RectTransform.pivotは0から1の範囲で、widthとheightに対する比率を表しています。 ですから、頂点座標にその分だけ加えてやればいいわけです。 例えば元々pivotが(0.5, 0.5)の時に中央であったとします。 pivotを(0, 0)に変えると、PosXとPosYが自動で-128に変わりますから、 左下にずれます。これを補正すればいいので、 「pivotが減ったら、減った分だけ座標を増やす」となります。 結果こんな感じです。

protected override void OnPopulateMesh(VertexHelper vh)
{
    vh.Clear();
    if (_sprite == null){ return; }
    var spriteRect = _sprite.rect;
    var sizeDelta = rectTransform.sizeDelta;
    var scale = new Vector2(
        _sprite.pixelsPerUnit * sizeDelta.x / spriteRect.width,
        _sprite.pixelsPerUnit * sizeDelta.y / spriteRect.height);
    var offset = new Vector2(
        sizeDelta.x * (0.5f - rectTransform.pivot.x),
        sizeDelta.y * (0.5f - rectTransform.pivot.y));
    for (int i = 0; i < _sprite.vertices.Length; i++)
    {
        var v = new Vector2(
            _sprite.vertices[i].x * scale.x,
            _sprite.vertices[i].y * scale.y);
        v += offset;
        vh.AddVert(v, this.color, _sprite.uv[i]);
    }
    /// ...略...

offsetというVector2を用意して、全頂点に足します。 pivotが減るほど大きな値を足すので、0.5f - rectTransform.pivot.x という引き算です。これにwidth(=sizeDelta.x)とheight(=sizeDelta.y)を掛けてピクセル単位に直します。

f:id:hirasho0:20190116110506p:plain

めでたく合うようになりました。 この段階のコードはこちらです

実はまだまだ続く

と、ここでちょっと完成形の CutoutImageのコード をご覧ください。

だいぶ長いですね。実のところ、このように一段づつ改造の詳細を説明するには長くなりすぎるくらいの改造が、 CutoutImageには入っています。 というわけで、この記事ではあと何をやれば完成形になるのかの概要だけを示すことにします。

Sprite側のpivotを見ていない

実は、Spriteもpivotを持っています。 先程のコードで0.5fというそのまんまな数字が出てきたことに気持ち悪さを感じた方はいらっしゃいますか? 実はあれは、Sprite側のpivotなのです。

f:id:hirasho0:20190116110509p:plain

たまたまデフォルト値が0.5だから良かったものの、もし変えていればそのままでは位置がズレてしまいます。 なので、Sprite.pivotも見るようなコードにしないと本当には正しく動きません。

手動で頂点を設定する機能

Spriteが持っている頂点をそのまま使う場合は比較的簡単なのですが、 そのまま使いたくない、というケースもあります。 と言っても、単に「自動で生成された頂点が多すぎる/少なすぎる」という場合には、 マニュアル にあるように調整できるので問題はありません。

では手動で頂点をいじりたいケースが何かと言えば、マスクが欲しくなるような状況です。 これをご覧ください。

f:id:hirasho0:20190116110446j:plain

顔の所だけが切り取られていて、黒い枠がついています。これをマスクでやることもできますが、 コンポーネントを足すのは手間ですし、ステンシルバッファの操作が必要になってGPUも重いですし、 シェーダが切り変わるのでDrawCallが増えてCPU負荷も上がります。 いいことがありません。 やろうと思えば上述の「手動スプライト頂点調整」でもできますが、 枠の形は使う場所によって異なるのが普通でしょう。 東京プリズンの場合、この例のように綺麗な長方形ではなく、 場所によって形が違うため、なおさらスプライト頂点側でやるのは面倒です。 そのコンポーネントの設定としてやれた方がいいでしょう。

そこで、完成版では手動で頂点を編集する機能を入れてあります。

f:id:hirasho0:20190116110511j:plain

pivot同様に、左端を0、右端を1、下端を0、上端を1、 とする座標で頂点を手動設定できます。 VertexOverrideEnabledを有効にした上で、 OverrideVerticesを編集します。頂点は時計周りに並んでいるものと解釈されます。 また、Inspectorの「GUI VertexEditing」トグルを有効にすると、 Sceneビュー側の各頂点に丸が出てきて、 この丸をマウスでドラッグして頂点を調整できます。

さて、この機能の実装が結構面倒でして、 CutoutImageの行数のかなりの部分がこれに割かれています。

一番面倒なのがuv、つまりテクスチャ座標です。 頂点を自分で作るとなるとSprite.uvが使えないので、自力で生成せねばなりません。 Sprite.verticesとSprite.uvの間は何らかの線形変換で変換できますから、 3つづつverticesとuvを持ってきて連立方程式を立て、 逆行列を計算して変換を2x3行列の形で持っておきます。 頂点はマウス等々でどんどん変更されますので、その度にOnPopulateMeshが呼ばれ、 その中でこの行列で変換してuvを求めます

もし、SpriteAtlasの生成時に回転を許可しておらず、かつ、 tightパッキングでなければ、アトラスの中の位置は、 Sprite.textureRectSprite.textureRectOffset で求まるのでズラすだけで済みます(これらのプロパティは回転があったりtightだったりすると例外を吐きます)。

実は東京プリズンに使っている実装はtightパッキングや回転を許しておらず、 もっと簡単なコードでした。せっかくなので今回対応してみたのですが、なかなか面倒です。 Unityが標準で持っている4x4行列のライブラリでは使いにくいので、 2x3行列、3x3行列の必要な機能も自力で実装してあります。完全に趣味です。

なお、マウスでの頂点編集のために、エディタ拡張の方で OnSceneGUIを実装して、そこで、 Handles.matrixHandleUtility.GetHandleSizeHandles.FreeMoveHandleあたりを使って マウスで動かせる丸を描いています。

gizmoへの対応

MonoBehaviour.OnDrawGizmosSelected を実装することで、エディタでオブジェクトが選択された時にいろいろなことができます。 ここでは、頂点がどうなっているのかがわかるように、ワイヤーフレームの描画をしてみました。 だいたいこのあたりのコードです

f:id:hirasho0:20190116110514p:plain

Gizmos.matrixTransform.localToWorldMatrix を設定し、 Gizmos.color に好みの色を入れ、 Gizmos.DrawLine に頂点の座標を渡して描画しています。 Gizmos.matrixにローカル座標からワールド座標に変換する行列を 渡してあるので、頂点はローカル座標のままでかまいません。 RectTransformのローカル座標、というのは、つまり ピクセル単位の馴染みの座標系ですので、 OnPopulateMesh()でVertexHelperに渡したものをそのまま渡せばいいわけです。 この実装では再計算を避けるために配列に覚えておいています。

終わりに

今回は「塗り面積を減らす」という課題から入って、 Spriteの仕様、UnityEngine.UI.Graphicを継承した独自クラスの生成、頂点の手動生成、 gizmoの実装方法、といったところに触れてみました。

こういったものを書くには、「座標変換」がミソになります。 回転や拡大縮小、移動を含めた変換を行列で表現することや、 連立方程式と行列の関係が手に馴染んでいると、何かと楽になりますので、おすすめです。

なお、元々gizmoを使っていろいろ表示したり、マウスで頂点を編集できるようにしたのは、 この記事を書いた しはんです。 彼がこれを実装してくれるまで、gizmoを自分で表示しようなんて思いもしませんでしたし、 「面倒くさいなー」と思いながら数字をいじって頂点座標を編集していました。 タスクとバグに追われる毎日だと、ちょっとしたことを調べる余裕もなくなってしまって良くないですね。 短期と長期をバランスするだけの余裕を常に持ちたいものです。