Unityのデバグ作業をSlackで効率化する

f:id:hirasho0:20190215190609p:plain

この記事では弊社東京プリズンでの Slackを使ったデバグ支援について、技術部平山が紹介いたします。

上の画像は今回のために作った 仮のワークスペースのスクリーンショットです。 以下のようなものがUnity側から投稿されていることがわかります。

  • 「スニペットテスト」とあるのは、スニペット扱いで投稿したもので、長めのテキストに適します。もちろん単なるメッセージも投稿できます。
  • 次がスクリーンショットです。
  • 「またね」と書いてある画像は、任意のテクスチャを投稿する機能によります。主にRenderTarget描画の確認に用いました。
  • 最後は任意のファイルを投稿する機能です。ここではUnityのログを記録したものを投稿しています。

Slack側の登録が済んでいれば動くコードを、 サンプルプロジェクトごとgithubに置いておきました。 東プリで用いたものを整理し、一部削ったもので、そのままではありません。 このままでもある程度は使えると思いますが、 製品ごとの事情に合わせていじった方が、より効率の良い開発体制が作れるでしょう。

なお、これら機能の多くは先輩方によって書かれたものであり、 平山はこの紹介記事を担当しただけです。 便利な機能を作ってくださった先輩方に感謝いたします。

動機

チームで開発していると、何らかの不具合が起きた時に、 その状況をチームで共有したくなります。 カヤックではコミュニケーションに主としてSlackが用いられていますので、 不具合に関する情報もSlackに集約するのが自然です。

不具合に関する情報と言えば、

  • スクリーンショット
  • 起きた端末の情報
  • 時刻
  • それまでに溜まっているログ
  • 現在開いているシーン

といったものでしょう。これらを、 UnityEditorからに限らず、実機たるスマホ端末からであっても ボタン一つで送信して集積したい、と思うのは自然なことです。 そうすれば、多数のスマホ実機でテストプレイをする際に 出た不具合情報も集積できます。

幸い、そのために必要な実装はさほど大きくありません。 ただし、運用においては少々気をつけるべきこともあります。

Slack側の準備

まずSlackのアカウントやAppの準備が必要です。 これに関しては他に良い記事がたくさんあります。 例えばこちらが参考になります(私もこれを見てやりました)。 概要を示せば、

  • Workspaceを作る(製品の開発ならもうありますよね)
  • Appを作って権限を付与し、トークン(token)を得る

となります。

なお、スクリーンショットなどのファイルをアップロードしたい場合、 権限には"Send messages as ユーザ名(chat:write:bot)"だけでなく、 "Upload and modify files as user(files:write:user)"も必要です。

f:id:hirasho0:20190215190606p:plain

なお、このトークンは秘密にしないといけません。 漏らしてしまうと、わけのわからないところから書きこみが飛んでくる恐れがあります。 とはいえ、厳重にやればやるほど面倒くさくなりますので、 そこは匙加減でしょう。さしあたり、

  • gitには入れない
  • 会社で一個作って使い回し、でなく、製品ごとにする

といったあたりは守っておいて良いのではないかと思います。

今回のサンプルでは、.gitignoreに書いてgitに入らなくした テキストファイルをStreamingAssetsの下に置き、 そこにトークンを書いてあります。 ビルドに入ってしまいますので、 不特定多数に配布する場合は、ファイルを削除してからビルドし、 Slack関連機能も無効化する必要があります。

東京プリズンでは一段進めて、トークンを暗号化しています。

実装

ではUnity側です。これに関しても良い記事は多数あります。

最後は弊社の記事でして、東京プリズンで最後まで一緒に過ごした趙さん のものです。

とはいえ概要はこの記事だけでわかった方が便利でしょう。以下にて粗く説明いたします。

概要と単純投稿

要するに特定の作法でhttpを叩けばいいわけで、手順を粗く示せば、

となります。単純な投稿の場合をコードで書けば、

IEnumerator CoPost(string channel, string message)
{
    var form = new WWWForm();
    form.AddField("token", _apiToken); // どうにかしてトークンを渡す
    form.AddField("channel", channel); // 送りたいチャネル名。channelsでなくchannel。単数形。
    form.AddField("text", message);
    var req = UnityWebRequest.Post("https://slack.com/api/chat.postMessage", form);
    var operation = www.SendWebRequest();
    while (!operation.isDone)
    {
        yield return null;
    }
}

という具合です。これをStartCoroutineに渡すか、UpdateでMoveNext()するなりすれば、 そのうち送信が終わります (自力でMoveNext()する選択肢を残すために、isDoneを見ながらyield return null;しています)。

スニペット投稿

少し長くなると、チャネルにベタでテキストが貼られると邪魔なことがあります。 こういう時はスニペット投稿です。 普通に使う時は、書き込み欄の左にある+マークを押すと出てきます。 これと同じことをUnityから行います。

f:id:hirasho0:20190215190613p:plain

単純なメッセージ投稿と違うのは以下です。

  • 送り先がchat.postMessageでなく、files.upload。
  • textでなくcontentに中身を入れる。
  • filenameを指定することもできる。しなくてもいい。
  • channelでなくchannels。単数形じゃなくて複数形。

コードで書けば、

var form = new WWWForm();
form.AddField("token", _apiToken); // どうにかしてトークンを渡す
form.AddField("channels", channel); // 複数形だよー
form.AddField("content", message);
form.AddField("filename", filename);
var req = UnityWebRequest.Post("https://slack.com/api/files.upload", form);
...(以下省略)...

こういう具合になります。スニペットはファイルのアップロードという扱いなわけです。 スクリーンショットを送るのも、ログを送るのも同様です。

任意ファイル投稿

今度は任意のファイルを投稿しましょう。 スニペットもつまりはファイルのアップロードなので同じなのですが、 渡すものがバイト列(byte[])になるので、データの詰め方が変わります。

  • 中身はAddBynaryData()でfileに入れる。
  • メッセージはinitial_commentに入れる。
var form = new WWWForm();
form.AddField("token", _apiToken); // どうにかしてトークンを渡す
form.AddField("channels", channel); // 複数形だよー
form.AddBinaryData("file", binary, filename);
form.AddField("initial_comment", message);
var req = UnityWebRequest.Post("https://slack.com/api/files.upload", form);
...(以下省略)...

AddBinaryDatabyte[]とファイル名を渡し, メッセージはinitial_commentに入れます。 これで好きなファイルを投稿できます。

スクリーンショット

さていよいよスクリーンショットです。手順は、

  • 関数をコルーチン(IEnumeratorを返す関数)として用意する。
  • yield return new WaitForEndOfFrame();で描画終了時に以後が実行されるようにする。
  • 受け取り側Texture2Dを生成。
  • Teture2D.ReadPixelsで描画結果をもらう。
  • ImageConversion.EncodeToPNGでPNG形式のファイルデータを得る。
  • さっきの「任意ファイル投稿」で送る。

コードはこんな感じです。

public IEnumerator CoPostScreenshot()
{
    yield return new WaitForEndOfFrame();
    var width = Screen.width;
    var height = Screen.height;
    var tex = new Texture2D(width, height, TextureFormat.RGB24, false);
    tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    var pngBytes = tex.EncodeToPNG();
    yield return CoPostBinary(pngBytes); // 任意ファイル送信関数で送る
}

WaitForEndOfFrameを使っているので、StartCoroutineで呼ぶ必要があります。 自前でMoveNextしても正常に動かないでしょう。

多くの記事ではReadPixelsの後でTexture2D.Apply を呼んでいますし、東京プリズンでも書いてあったのですが、必要なのかどうかがよくわかりません。 ReadPixelsのマニュアルを見た感じ ミップマップへの反映が不要ならいらない気がしますし、 実際エディタとStandaloneビルドでは動いています (ApplyってGPUへの転送やミップマップ生成を予約するだけですよね?)。 とはいえ今回実機では試してないので、もしかしたら書かないと動かないのかもしれません。 その時は教えてください。

運用

さて、機能自体はできたわけですが、問題はこれをどう開発に使うかです。 今回のサンプルでは、これらのSlackへの投稿機能を DebugSlackクラスとして用意しました。 少しコードを眺めていただけるとわかるのですが、それほど小さくはありません。 以下では運用のために行った工夫について紹介します。

クラス設計

クラスはMonoBehaviourではない普通のクラスです。newして使います。

public DebugSlack(string apiToken, string defaultChannel)

サンプルではこのnewを行うコードはMain クラスにあり、

Kayac.DebugSlack _slack;

void Start()
{
    var tokenFilePath = Application.streamingAssetsPath + "/slackToken.txt";
    var tokenFile = new System.IO.StreamReader(tokenFilePath); // 暗号化しといた方がいいよ!
    var token = tokenFile.ReadToEnd();
    tokenFile.Close();
    _slack = new Kayac.DebugSlack(token, _defaultChannel);
}

といった感じです。

余談: なぜシングルトンでないか

さてこのDebugSlackはどこからでも使いたい機能であり、インスタンスは一個で足ります。 となるとシングルトンにしたくなりますが、ここではしていません。 私は「オブジェクトの所有関係が木になっていること」に喜びを感じるタイプなので、 木から外れるシングルトンが嫌いなのです。

それに、シングルトンは後からinterfaceを用いて抽象化したくなってもできない、 という理由もあります。

例えば、DebugSlackクラスを使っているクラスなりシーンなりを 切り出して単体テストしたくなったとします。 DebugSlackがシングルトンなら、DebugSlackクラスなしではテストができません。 しかし、シングルトンでなければIDebugSlackみたいなinterfaceを作って 空の実装を用意すれば本物のDebugSlackなしでテストができます。

とは言え、シングルトンの「参照がなくても使える」という便利さは魅力的です。 そこで、 「世界の根本に一つシングルトンクラスがあって、それが全てをインスタンス変数として持つ」 という妥協が生まれます。

東京プリズンの場合、起動時にロードされて永遠に破棄されないBaseSceneというシーンがあり、 そこには同名のBaseSceneというMonoBehaviourクラスのインスタンスがあります。 これをシングルトンとし、 「なんとかマネージャー」の多くはそのインスタンス変数としています。例えば、

BaseScene.instance.soundManager.PlayBgm("hoge");
BaseScene.instance.debugUi.AddButton("ボタン");

といった具合にアクセスできます。 このままでは切り出せないので台無しなんですが、利便性優先です。 実際問題、切り出してテストすることなんてあまりなく、 テスト時にBaseSceneもロードしちゃう方が楽だったりします。

しかし、シングルトンなのがBaseSceneだけであれば、 本当に切り出したくなった時に選択肢が残ります。 BaseSceneを見ないように直すだけで済むからです。例えば、

ISoundManager _soundManager;
IDebugUi _debugUi;
public void Init(ISoundManager soundManager, IDebugUi debugUi)
{
    _soundManager = soundManager;
    _debugUi = debugUi;
}

//使うところ
_soundManager.PlayBgm("hoge");
_debugUi.AddButton("ボタン");

みたいな感じに変形すれば本物のSoundManagerやDebugUiがなくても、 空の偽実装を用意すれば単体テストできます。 何もかもがシングルトンだったら、もうどうにもなりません。

まとめると、

  • オブジェクトの所有関係を木に保つため
  • いつか切り出してテストしたくなるかもしれないから

の2点が、私がシングルトンをあまり使わない理由です。

スクリーンショット

スクリーンショットの関数は以下のような引数構成を持っています。

public IEnumerator CoPostScreenshot(
    string message = null,
    System.Action onImageCaptured = null,
    OnComplete onComplete = null,
    string channel = null,
    int waitFrameCount = 0)

メッセージが省略されれば、自動で機種とOS種別をつけます。 SystemInfo.deviceModelとSystemInfo.operatingSystemです。

onImageCapturedは、画面データの取得が完了した時に呼ばれます。 例えば、スクリーンショット撮影前にデバグUIを消してある場合、 これを復活させる処理などが考えられます。 送信には時間がかかりますので、送信完了のonCompleteとは 別に用意してあります。

そしてonCompleteは送信完了時の処理です。ポップアップを出したいならここに指定するのが便利でしょう。

最後のwaitFrameCountは、画面データを取る前に何フレーム待つかを指定します。 例えばデバグUIを消す場合、Updateの順序や内部処理によっては、 何フレームか待たないとデバグUIを消したことが画面に反映されないかもしれません。 テキトーに3くらいを指定しておけば、おそらくどこでも動くのではないかと思いますが、 そのせいで決定的瞬間を逃す場合もあるかと思いますので、実際の環境で決めてみてください。

なおファイル名は時刻から自動生成しています。

テクスチャ投稿

任意のテクスチャを送信する機能です。 といっても、前もって用意したテクスチャに関して使いたくなることは稀でして、 主眼はRenderTextureにあります。 東京プリズンでは結構いろんなところでRenderTextureを使っているのです。

例えばキャラクターはいろんな服に着替えるのですが、 パーツごとにテクスチャがバラバラだとDrawCallが増えまくって遅いので、 その時の服の組み合わせを1枚のテクスチャに集めてしまうのです。

他にも、複雑な画面では、負荷を下げるために一旦RenderTextureに描画し、 次のフレームからはそれを使う、ということをしていたりもします。

このRenderTextureがまともに動いているか、というのは結構デバグしにくくて、 とりわけスマホ実機でしかバグらない場合は非常に厄介です。 であればSlackに投稿してしまうのが良いでしょう。

   var tex2d = texture as Texture2D;
#if UNITY_2018_1_OR_NEWER
    if ((tex2d == null) || !tex2d.isReadable) // 2Dでない場合及び、そのまま読めない場合
#else
    if (true) // 2018より前にはisReadableがなく判定できないため、常にRenderTextureを経由する
#endif
    {
        RenderTexture renderTexture = texture as RenderTexture;
        // 来たのがRenderTextureでないならRenderTexture生成してそこにコピー
        if (renderTexture == null)
        {
            renderTexture = new RenderTexture(texture.width, texture.height, 0);
            Graphics.Blit(texture, renderTexture);
        }

        // 読み出し用テクスチャを生成して差し換え
        tex2d = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
        RenderTexture.active = renderTexture;
        tex2d.ReadPixels(new Rect(0, 0, tex2d.width, tex2d.height), 0, 0);
    }
    var pnbBytes = tex2d.EncodeToPNG();

textureが引数でもらったテクスチャです。 2018以降であればTexture2D.isReadable を使って、直接EncodeToPNGできるかどうかがわかるのですが、 以前のバージョンではわかりません。 そこで、まずそれがRenderTextureかどうか判定します。 RenderTextureであれば、これをRenderTexture.activeに設定してしまえば、 「画面に今それが映っている」かのようにReadPixelsでデータが取れます。

RenderTextureでない場合、一旦RenderTexture を生成し、そこにGraphics.Blit でコピーし、ここから受け取り用のTexture2DにReadPixelsする、という順序でデータを取得します。

あとは任意ファイル送信と同じです。

ログ送信

Debug.LogやDebug.LogWarningはデバグにおいて欠かせないツールですが、 実機においてはこの助けを得にくくなります。 実機の画面に表示させる、というアプローチもあり、 東京プリズンもそうしているのですが、 なにせスマホは狭いので、大した量は表示できません。 スクロール可能にすればそこの問題はなくなりますが、 あまり蓄積すればメモリを圧迫します。

そこで、東京プリズンではPersistentDataにログファイルを生成して、 そこに書きこむようにしました。 ログは複数配置でき、「0番はシーン遷移関連、1番は戦闘演出関連...」 のような使い分けがあります。

しかし、これを端末から取り出すのは面倒ですから、当然Slackに送りたくなるわけです。 複数のログをzipしてSlackに送る機能を用意していました。

ただ、これを丸ごとサンプルに入れるとzip関連まで入って大きくなりますし、 多くの開発では標準のDebug.Logを基本にしているでしょう。 今回のサンプルでは、「Debug.Logの出力を捕捉してメモリに蓄積し、 それをSlackに送る」というところまでとしました。 MemoryLogHandler をご覧ください。

Unityのログを捕捉する方法

Debug.Logなど、 エディタのコンソールに出てくるものを捕捉するには、 Application.logMessageReceivedThreaded にコールバック関数を追加します。 Application.logMessageReceivedでもかまいませんが、 別のスレッドでDebug.Logしても来なくなるので、実用性は落ちます。

Debug.unityLogger が持つlogHandler を差し換える方法もありますが、今の用途では適しません。 ILogHandler.LogException はnull死などの例外では呼ばれず、例外だけ別経路を通す必要がありますし、 標準のlogHandlerに渡し直す処理も必要で(でないとコンソールに出てこなくなる)、 文字列フォーマットの負荷も二重にかかります。

public MemoryLogHandler(int lineCapacity)
{
    ...(省略)...
    Application.logMessageReceivedThreaded += HandleLog;
}

void HandleLog(string logString, string stackTrace, LogType type)
{
    var message = DateTime.Now.ToString("MM/dd HH:mm:ss.fff") + " : " + type.ToString() + " : " + logString;
    // コールスタックはError系でだけ吐くことにする。設定可能にしても良いかもしれない。
    if ((type == LogType.Exception) || (type == LogType.Error) || (type == LogType.Assert))
    {
        message += "\n" + stackTrace;
    }
    Add(message);
}

MemoryLogHandlerも普通のクラスで、newして使います。 シングルトンにすれば間違って複数作ってしまって メモリとCPUを無駄にする危険をなくせますが、 その価値はないように感じます。お好みでどうぞ。

ただし、もしunityLogger.logHandlerをいじる実装をする場合は、 二度呼ぶことが致命傷になるかもしれませんので、 何らかの方法でインスタンスが複数作られないようにしておく方が良いかと思います。 シングルトンはその方法の一つです。 今回の実装はApplication.logMessageReceivedThreadedを使っており、 イベントへの+=を用いているので、複数追加しても誤作動は起こりません。

さて処理の本体はHandleLog()です。時刻とLog,Warning,Assertといった種別を追加し、 重要度が高いものではコールスタックも追加した上で、 Add()で内部メモリに蓄積しています。

Add()の中身は見ていただければわかりますが、string[]を循環バッファとして用意し、 そこにどんどん記録しているだけです。 ただし、Application.logMessageReceivedThreadedは 別のスレッドからも飛んできまので、中はスレッドセーフにする必要があります。 単にlock (何か){ ... }でくくるだけです。

必要な時にはTail()で必要な行数だけ取り出したり、GetBytes()で丸ごとbyte[] に変換したりできます。これを任意ファイル投稿機能でSlackに投げればいいわけです。

なお、BuildSettingsにてDevelopment Buildを有効にしないと、 ビルドでコールスタック情報が取れませんので、ご注意ください。

バグ報告

さて、スクリーンショットとログ送信があるならば、それを一緒にして 1行で不具合報告できる方がいいですよね。 実機にはそれを駆動するデバグボタンを用意しておけばいいですし、 Exceptionが出た時には自動で駆動する、という考えもあるでしょう (おすすめしません。バグり方によっては例外が毎フレーム出て投稿量がえらいことになります)。

今回はSlack系の機能とログの機能は別で作っており、 それを束ねるクラスは用意していませんが、 製品においては「バグ報告を統括するクラス」は用意すべきでしょう。 今回のサンプルでは、 Main.csReportError という関数を用意していまして、 画面上の「バグ報告ボタン」を押すと呼ばれるようになっています。

void ReportError()
{
    _onGuiDisabled = true;
    StartCoroutine(_slack.CoPostScreenshot(
        () => _onGuiDisabled = false,
        "エラー報告",
        null,
     channel: null,
     waitFrameCount: 1)); // 次のフレームでOnGUIで何もしない状態にしてから撮影
    var log = _logHandler.GetString();
    var sb = new System.Text.StringBuilder();
    var now = System.Datetime.Now;
    sb.Append("----SystemInfo----\n");
    sb.AppendFormat("[ErrorLog] {0}\n", now.ToString("yyyy/MM/dd HH:mm:ss.fff"));
    sb.AppendFormat("device: {0} {1} {2} Memory:{3}\n", SystemInfo.deviceModel, SystemInfo.deviceName, SystemInfo.deviceType, SystemInfo.systemMemorySize);
    ...(省略)...
    sb.Append("----SceneInfo----\n");
    for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
    {
        sb.AppendFormat("{0}\n", UnityEngine.SceneManagement.SceneManager.GetSceneAt(i).name);
    }
    sb.Append("----Log----\n");
    var bytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString() + log);
    StartCoroutine(_slack.CoPostBinary(
        bytes,
        "errorLog" + now.ToString("yyyy_MM_dd__HH_mm_ss_fff") + ".txt"));
}

ここではまずデバグ用のボタン類を無効化しています。 製品においても、デバグUIが邪魔なことは多いでしょう。 フラグをtrueにするとOnGUIの先頭で抜けるようにしてありますので、 1フレーム待ってからスクリーンショットを撮影します(CoPostScreenshot)。 ただし、ログに出ない重要情報がデバグUIに出ている場合など、 デバグUI込みの方が良いこともあるとは思います。選択式にするか、両方吐くのが良いでしょう。

次に、ログを用意します。MemoryLogHandler.GetString()で 全量もらい、そこにSystemInfo から有用な情報を抜いて付加します。

  • deiceModel,deviceName,operatingSystemは誰のどの端末かを特定するのに便利です。
  • graphicsDeviceNameは特定のGPUだけ絵がおかしい、という時に便利です。
  • graphicsDeviceTypeはOpenGLなのかMetalなのか、の切り分けに使えます。
  • graphicsDeviceVersionは、例えば「想定外にOpenGL3に対応してない機械がいる」ことを知るのに使えます。
  • graphicsMemorySizeやsystemMeorySizeは、「これがこの数字だとここで落ちる」というようなことがわかりますから、メモリの使用量の限界を定めるのに使えます。
  • batteryLevelは、変に処理落ちしている時にバッテリーのせいかを疑うのに使えます。一定以上の処理落ちはログに吐くようにしておくと良いでしょう。
  • maxTextureSize,npotSupportあたりは、絵が変だった時に原因を調べるのに使えます。特にnpotSupportは「テクスチャをリピートにしてるはずなのに伸びた変な絵が出てる」という時には疑いましょう。「2のべき乗でない限りリピートできない機械」はまだあります。

さらに、現在ロードされているシーンの一覧も書きこみます。 東京プリズンの場合は、独自のシーン管理機構があり、 より詳しい情報を吐くことができましたし、 サーバ名、ユーザ名、なども追加していました。

あとはログを連結して、UTF8にエンコードし、任意ファイル投稿機能で投げます。

余談: ログに記録する時刻について

実時刻でなく、サーバの設定時刻をログに記録したいことがあります。 「8月からのイベントのテストを7月にする」「21時からマルチプレイなのでそのテストをする」 といった具合です。この場合はサーバを偽の時刻に設定してテストをします。 この場合、サーバ側のログと照合するためにも、 Unityから吐くログの時刻はサーバ時刻にしておく方が便利なのです。 そういった機能も用意する必要があるでしょう。選択式にしても良いかと思います。

もちろん、ログ自体につける時刻は実時刻が良いでしょう。

おわりに

テストプレイ情報をどう集め、どう蓄積するか、というのは決定的に大事です。 そこの効率が悪いと、開発終盤のバグつぶしでかかる時間が予想外に伸び、 発売を遅らせる危険が増します。

Slackを上手に利用することで、データの収集と蓄積がかなり楽になります。 バグ情報蓄積サーバ、みたいなものを持っている所もあると思いますが、 案外Slackでも行けます。日々Slackをお使いであれば、 そこに統合されて便利です。ただ、どうしても流れてしまいますので、 そこは工夫が必要でしょう。

あと重要なのはログの品質です。 「ログに何が出ているか」がデバッグの効率を大きく左右します。 個人的には、Debug.Logがあまり大量に出ると速度的にもキツいし、 エディタ実行時に邪魔になるので、 「ファイルにだけ書かれる」という選択肢を別途設けることをおすすめします。 Debug.LogやDebug.LogWarningは当然ファイルにも書かれるのですが、 ファイルにしか出てこないログ関数も作る、ということです。 それなら、かなりの量吐いても速度に影響が出ず、 かなり詳細な情報を吐き出す気になります。

東京プリズンの開発中は、試合中のサーバとの同期バグを取るために、 1試合あたり数百KBの巨大なログを吐いており、 それを一晩自動で回していました。 無事朝まで動いていれば、だいたい150MBくらいになります。 異常を検出するために大量のassertが埋め込まれており、 全てがログに吐かれていました。 そして毎朝assert経由のメッセージをログから検索し、 見つけたら必死で辿ってバグを直すのです。 これを何週間か続けました。 Slackで投げられるようになってからは、複数台でテストをすることも苦でなくなり、 Amazon EC2で仮想マシンを何台も借りて、自動テストを回すこともできました。

このように、作業の手間が減れば、それだけ強力なデバッグをすることができ、 より早く楽に品質を上げることができます。 Slackはそのための一つの道具として有効かと思います。