как собрать, отладить и не взорвать офис / Блог компании Group / Хабр

как собрать, отладить и не взорвать офис / Блог компании Mail.ru Group / Хабр

У нас был небольшой бюджет и большие проблемы с рутинным тестированием в match3-игре, у которой накопилось более 1500 уровней. А вот чего у нас не было, так это идеально подходящего коробочного решения, работающего на лету и без пересборок. Поэтому мы нагородили собственную ферму с высаженной грядкой из десятка Xiaomi, отправкой статистики, отчетами в Slack, блекджеком и коровой.

Я Павел Щеваев, CTO студии BIT.GAMES, которая является частью международного игрового бренда MY.GAMES. Вы можете знать нас по RPG «Гильдия Героев», а ваши мамы — по «Домовятам» в Одноклассниках. Да, это были мы. 🙂 Но сегодня речь пойдет о нашем новом проекте Storyngton Hall. Это головоломка «три в ряд» с сюжетом, по которому красивые леди разгадывают загадки, декорируют комнаты, примеряют платья, устраивают балы, и, в конце концов, выходят замуж.

Игра реализована на С# и Unity, доступна на iOS и Android. На сегодняшний день у неё 4 млн установок. Какие проблемы назрели?

Большие объемы. В игре более 1500 уровней, каждые две недели добавляется еще несколько десятков. И вот на 934 уровне ломается туториал.

Ребятам из QA практически нереально оперативно добраться до 934 уровня, до того как приложение окажется у игроков.

Бюджеты памяти. Не единожды происходило неконтролируемое потребление памяти, которое мы обнаруживали уже в консоли Google Play. Вот, например, график падений после того, как художник запушил несжатую текстуру; игра перестала помещаться в бюджет и начала чаще падать.

Что мы придумали

Нам захотелось получить автоматизированное решение, которое помогало бы ловить проблему ещё до того, как она ушла в билд. Идея была такова:

  • берем весь наш массив уровней;
  • делим их на батчи с наборами уровней, поделенные по количеству устройств;
  • прогоняем уровни на реальных устройствах.

Помимо ловли ошибок мы хотели дать возможность разработчикам больше пространства для смелых экспериментов, организовав так называемую «сетку безопасности».

Готовые решения на рынке

Изобретать что-то самим не особо хотелось, и мы начали смотреть, что предлагает рынок.

Требования были такие:

  • Надёжность: ферма должна чётко и стабильно выполнять тесты каждую ночь.
  • Простота: ферма должна использовать инструментарий, который нами изучен и понятен.
  • Масштабируемость: ферма должна автоматически определять новые устройства и распределять по ним уровни.
  • Скриптуемость: тесты должны быть написаны на скриптовом языке, чтобы была возможность запускать их без пересборки приложения, т.к. сборка приложения занимает много времени.

К сожалению, ни Device Farmer (бывший STF), ни Selenium, ни прочие варианты не подошли. Пришлось городить ферму самим.

Android Test Farm (ATF)

Ферма представляет собой хаб из десятка устройств, подключенного к Mac Mini. В этом видео я инициирую сессию тестирования со своей машины — загружаются тестовые скрипты, затем запускаю приложение:

Скрипты полностью эмулируют сессию игрока: открывают окошки, проходят туториалы, играют в уровни и проч. Все тапы и свайпы эмулируются в ускоренном режиме.

Что по железкам?

Аппаратный состав фермы:

  • старенький Mac Mini;
  • 10-портовый USB-хаб, найденный на Ozon (есть и на 30);
  • бесперебойник;
  • жаропрочный короб;
  • 10 одинаковых смартфонов Xiaomi 9A.

Почему 10?

Сначала смартфонов было 4, но оказалось, что для текущего объёма тестирования оптимально использовать 10. Если потребуется ускорить выполнение тестов, то добавим ещё.

Почему одинаковые?

Чтобы точно ориентироваться в просадках по памяти, нам нужны одинаковые устройства. Если смартфоны будут разные, то показатели потребления памяти будут сильно разниться из-за различий железа. Однажды попробовав использовать 5 разных устройств для этих целей, мы осознали бесполезность затеи.

Почему Xiaomi?

Мы старались выбирать такие устройства, которые соответствуют среднему уровню смартфонов игроков: не супертоповые, но и не самые дешёвые. И если игра работает на них хорошо, то этот опыт можно экстраполировать на схожие и более дорогие устройства. Практика показала, что Xiaomi 9A — это относительно надежное устройство из средней ценовой категории, которое хорошо работает через ADB и подходит нам в полной мере.

Делать iOS-ферму не планировали изначально, потому что Apple не предоставляет открытых средств для низкоуровневого доступа к устройствам. К тому же игра написана на Unity и в ней минимум платформозависимого кода, поэтому всю массу уровней можно спокойно тестировать на Android-устройствах. Кроме того, эти десять смартфонов Xiaomi укладываются в стоимость одного приличного iPhone. А если нет разницы, зачем платить больше?

Стек технологий

Здесь мы довольно консервативны и используем то, что проверено временем: PHP, BHL, ADB, SSH, Slack, ClickHouse, Redash.

Из всего перечисленного вам точно не знаком

BHL

— это интерпретируемый, скриптуемый, строго типизируемый язык программирования, наша собственная разработка. В нём есть встроенные примитивы для удобного псевдораспараллеливания кода, поддерживается hot reload на лету. Благодаря этому мы можем компилировать скрипт в бинарный код, отправить на устройство и запустить без пересборки приложения.

Как устроены наши тестовые скрипты

Рассмотрим простейший пример.

Функция

TestWaitUI

является корутиной и выполняется в неблокирующем режиме на протяжении нескольких кадров. И пока она выполняется, не блокируется основной геймлей. В этом коде ожидается, что появится UI окно —

UILogin

. Как только оно появлятся, бот нажимает кнопку «close_btn», используя функцию

TestTapUIButton

, таким образом пытаясь его закрыть.

Еще пример реального кода с фермы:

Самое интересное происходит внутри конструкции

рaral

. Всё, что находится в ней — а именно несколько секций

forever

, — выполняется параллельно друг другу. Тестовый скрипт ожидает появления разных UI, и в зависимости от их типа выполняет те или иные действия. В основном всё сводится к тому, что мы ждём появления какой-нибудь кнопки вроде

continue

/

close

и нажимаем её. В свою очередь секция paral выполняется в цикле, пока не появится UI главного окна «UIMainScreen».

Полный цикл тестирования

Предположим у нас есть вышеупомянутый тестовый сценарий:

Давайте загрузим его на устройства через инструмент ATF, который предоставляет API для работы с фермой:

Инструмент ATF через SSH связывается с хостом, к которому подключен USB-хаб. Он же заводит тред в Slack…

… и впоследствии синхронизирует все необходимые данные с массивом устройств через интерфейс ADB:

В процессе тестирование устройства сами сообщают о прогрессе и обо всём, что на них происходит, через внешний файл.

С помощью механизма ADB мы получаем данные с устройств, важные события отсылаем в Slack и записываем статистику в ClickHouse. Позже по накопленной статистике можно построить различные графики и отчеты с помощью Redash.

Работа с устройствами через ADB

ADB — это низкоуровневый USB-интерфейс, с помощью которого можно подключаться к устройствам и делать с ними практически всё, что угодно. При помощи ADB мы:

  • записываем на устройства внешний файл с тестовым скриптом BHL;
  • туда же пушим специальную версию приложения, в которую добавлена тонкая надстройка, позволяющая выполнять тестовые скрипты BHL;
  • периодически считываем с устройств файл со статусом выполнения тестов.

Интеграция со Slack

Когда у нас начинается тестовая сессия, мы записываем её название с уникальным идентификатором и запускаем тестовые планы — это заданные разработчиком логические группы тестов, и их может быть сколько угодно. В данном случае у нас два тестовых плана:

  1. Прогон туториальных уровней. План выполняется первым, потому что проверке туториалов отдан наивысший приоритет.
  2. Прогон обычных уровней.

В каждый тестовый план включена такая информация:

  • версия сборки;
  • количество устройств;
  • статистика, какие устройства работают, а какие отключились;
  • счётчики ошибок, вылетов, зависаний;
  • прогресс выполнения.

Есть еще цветовая идентификация:

  • зелёный — всё хорошо;
  • оранжевый — что-то было, но не критичное;
  • красный — критичные ошибки.

Перейдя в тред тестового плана можно посмотреть подробности и понять, какие неприятности произошли.

О чём мы сообщаем в случае ошибки

  • Последний скриншот. В случае фатальной ошибки на устройстве игра останавливается, и мы можем увидеть, в каком состоянии была игра.
  • Стектрейс.
  • Код реплея — это текстовая строка, в которой закодированы все действия игрока.
ljPaAC9AbTMvbGV2ZWxzL2V4cGVyaW1lbnQvY3J5c3RhbC9sdmxfMTFfOV9zcXVhcmVfMs0Pc56VAZIFA5IFAkwAlQGSBQeSBQjMoQCVAZIGBJIGBczQAJUBkgUGkgUHzPoAlQKSBgeSBgfNARwAlQGSCAOSCATNAVUAlQGSBAGSBALNCqoAlQGSBACSBAHNCzwAlQGSBAKSAwLNC2AAlQGSAwKSAgLNDKUAlQGSAwWSAwTNDUQAlQGSAgKSAwLNDm4AlQGSAwOSAgPNDsgAlQGSAwSSAgTNDw8AAJA=

В нашей игре движок Match3 детерминирован, и, имея все инпуты от игрока, мы можем воспроизводить пользовательскую сессию в редакторе. Это позволяет понять, что человек делал.

Реплей загружаем в редактор, перематываем на последний ход, стартуем симуляцию — и бац! — вот она, ошибка. Мы видим её саму и действия, которые к ней привели.

Отчёты в Redash

В Redash мы выгружаем статистику:

  • потребления памяти;
  • падений;
  • предупреждений по перерасходу памяти.

Когда приложение начинает вести себя нехорошо, то Android и iOS шлют предупреждения, чтобы разработчики могли предпринять действия. Именно это мы и записываем в Redash.

Случай из практики

В отчёте из Redash вы можете видеть динамику потребления памяти. Здесь мы журналируем максимальное потребление (синий), среднее (красный) и медианное (зелёный). График идёт в обратном порядке времени: слева свежие данные, правее — старые. Особое внимание мы обращаем на максимальное потребление. Если у игроков превышен уровень потребление памяти, то с большой вероятностью это приведёт к падению.

На крайнем правом столбце видно сначала повышенное потребление памяти — 808 Мб, а потом оно снижается до 650. Это результат того, что наш ведущий программист покудесничал с текстурами, добавил сжатие и это отразилось на показателях.

Когда в продакшене было повышенное потребление, билд себя чувствовал не очень, и это можно заметить по отчету о падениях в консоли Google Play. После того, как мы залили сборку с более оптимальными текстурами, количество падений пошло вниз. Ферма подсказала нам, что дела стали намного лучше ещё до того, как мы стали что-то выпускать в продакшн.

Итог

На всё про всё ушло полгода разработки разной степени интенсивности. Ферма в рамках CI каждую ночь прогоняют 1500+ уровней примерно за 5 часов. По мере необходимости размер фермы будем увеличивать.

Маленькие рецепты для тех, кто захочет обзавестись собственной фермой

Как эмулировать пользовательский ввод

  1. Использовать новую подсистему Input System для Unity (в последней версии). Возможно, она даже неплохо работает, но нам не подошла, потому что проект Storyngton Hall разработан под те версии Unity, где подсистема недоступна. Переезжать на новую версию Unity нам из-за этого было бы неразумно. Тем не менее, рекомендую посмотреть в ту сторону — это самое гибкое решение.
  2. $ adb shell input tap x y — через низкоуровневые механизмы ADB тоже можно эмулировать ввод. Всё замечательно, но дико медленно: эмуляция каждого тапа занимает несколько секунд. Если бы мы пошли таким путём, то ферма проходила бы весь цикл тестов не за 5, а за 15 часов.
  3. Связка Java + Android Activity оказалась нашим спасением. Мы эмулируем ввод на уровне Android Activity, обращаясь к ней из кода приложения. В C# это выглядит примерно так:
public void Tap(Vector3 p) {
    var pt = World2Display(p);
#if UNITY_ANDROID
    const string bridge_name = "com.goplaytoday.utils.atf.ATFUtils";
    using(var bridge = new AndroidJavaClass(bridge_name))
    {
      bridge.CallStatic("EmulateTap", (int)pt.x, (int)pt.y);
    }
#endif
  }

Как эмулировать тапы

public static void EmulateTap(Activity activity, int x, int y) {
    PointerProperties[] properties = new PointerProperties[1];
    PointerProperties prop = new PointerProperties();
    prop.id = 0;
    prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
    properties[0] = prop;
    PointerCoords[] coords = new PointerCoords[1];
    PointerCoords coord = new PointerCoords();
    coord.x = x;
    coord.y = y;
    coords[0] = coord;
    int source = InputDevice.SOURCE_TOUCHSCREEN;
    int deviceId = 0;
    final long ms = SystemClock.uptimeMillis();
    activity.dispatchTouchEvent(
      MotionEvent.obtain(ms, ms, MotionEvent.ACTION_DOWN, 1, properties, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
    activity.dispatchTouchEvent(
      MotionEvent.obtain(ms, ms+200, MotionEvent.ACTION_UP, 1, properties, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
}

Привожу работающий пример на Java для Unity — бился над ним целый день. Все берут примеры со Stack Overflow, которые должны работать, но они, естественно, не работают. Пришлось довольно долго итерировать и прорабатывать, пока наконец не получилось то, что нужно.

Как эмулировать свайпы

Свайпы отняли у меня еще полдня. Привожу пример работающего кода:

public static void EmulateSwipe(Activity activity, int x1, int y1, int x2, int y2, int durationMs) {
  PointerProperties[] props = new PointerProperties[1];
  PointerProperties prop = new PointerProperties();
  prop.id = 0; prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
  props[0] = prop;
  PointerCoords[] coords = new PointerCoords[1]; PointerCoords coord = new PointerCoords();
  int source = InputDevice.SOURCE_TOUCHSCREEN; int deviceId = 0; int maxMoveSteps = 10;
  int sleepTime = durationMs / maxMoveSteps;
  activity.runOnUiThread(new Runnable() {
    public void run() {
      final long ms = SystemClock.uptimeMillis();
       coord.x = x1; coord.y = y1; coords[0] = coord;
       activity.dispatchTouchEvent(
         MotionEvent.obtain(ms, ms, MotionEvent.ACTION_DOWN, 1, props, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
       for(int i=0;i

Как измерить потребление памяти

Существует много различного инструментария, тот же Unity Profiler, Android Studio и прочее. Но зачастую они не отражают объективного положения дел, особенно Unity Profiler. Вы думаете, что у вас потребляется одно количество памяти, а на деле — в разы больше. Мы столкнулись с тем, что механизм ADB даёт наиболее точные результаты. Команда

$ adb shell dumpsys meminfo <бандл приложения>

помогает отследить, сколько сейчас реально занимает ваша игра в памяти.

Как ускорить тесты

Мы ускоряем тесты в несколько раз там, где это уместно. Благодаря тому, что у нас симуляция match-3 изолирована от представления, мы смело можем ускорить тесты в четыре раза. В Unity есть довольно удобный механизм, который позволяет изменить масштаб времени: Time.timeScale. За подробностями обратитесь к документации.

Устройство может перестает отвечать в любой момент

В мире реальных и не самых топовых железок может произойти всё, что угодно: устройства могут зависнуть, проложения вдруг начнут вылетать и ещё по всякому сходить с ума. Если будете делать ферму, то готовьтесь к тому, что всё может пойти в разнос. Эту проблему мы решаем довольно просто:

  • Наши тестовые скрипты периодически записывают в файл особую строчку PING.
  • ATF считывает этот файл и анализирует, как давно не было пингов с устройств.
  • Если пингов не было давно, то устройство считается зависшим, мы его перегружаем и заново запускаем набор тестов.

Команда ADB может зависнуть

Команда ADB, конечно, замечательная и надёжная, но и она может войти в ступор. Мы решили проблему кардинально. Все наши shell-команды обёртываются в специальный таймаут-скрипт, у которого есть жёсткие ограничения по времени выполнения. В примере выше, если команда не выполняется за 10 секунд, то мы считаем это ошибкой и сигнализируем о ней в Slack.

Устройство может взорваться

Один наш смартфон вспучился, задняя крышка стала отходить — вздулся аккумулятор. Мы отложили его, и где-то через пару недель он реально взорвался. Именно поэтому необходимо нормальное охлаждение и жаропрочный корпус.

Резюмирую

Достоинства Android Test Farm

  • Оправдано для больших, долгоиграющих проектов от 1 года, которые находятся в активной разработке или на поддержке.
  • Снимает много рутины и облегчает жизнь тестерам.
  • Формирует сетку безопасности и придаёт разработчикам смелости для экспериментов.

Недостатки Android Test Farm

  • Нет смысла внедрять для маленьких проектов.
  • Решение не из коробки, придётся попотеть самим.
  • Требует постоянной поддержки и человека, который возьмёт ферму на контроль. Разработчики постоянно добавляют новый функционал, поэтому тестовые скрипты время от времени ломаются.
  • Не полностью защищает от ошибок, скорее это ещё один эшелон защиты, который добавляет уверенности, что всё работает более-менее нормально.
  • Ферма не покажет визуальные глюки, когда какой-то спрайтик съехал, или что-то неправильно отображается.
  • Иногда требуется физический доступ к устройствам, с удалёнки не поуправляешь.

Если решитесь делать такую ферму, то реализуйте её как можно раньше, еще до релиза проекта. Так вы сможете замерить потребление памяти, увидеть регрессии и принять нужное решение ещё до того, как опубликуетесь в сторах. Мы внедрили ферму не сразу и упустили момент с потреблением памяти, хотя могли отловить это перед тем, как уже нагнали рекламы и привлекли много пользователей. Так что если планируется серьёзный проект на много денег — внедряйте как можно раньше.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *