Как мы отмечали 256 день года и рисовали пиксели через API
13 сентября в Контуре отмечали День программиста. В самом большом офисе разработки играли в Pac-Man и пытались съесть 280 коробок с пиццей. Одновременно полторы тысячи человек рисовали пиксели в онлайне. В этом посте четыре разработчика рассказывают, как делали праздник.
Часть 1. Рассказывает Игорь green_hippo, который стырил идею на Reddit
День программиста у нас отмечает вся компания, а не только разработчики. Поэтому была нужна идея для онлайновой игры, в которой могут участвовать все желающие. Я вспомнил, что в апреле прошёл Reddit Place — социальный эксперимент по коллективному рисованию на холсте 1000×1000 пикселей, в котором участвовал миллион человек.
Я решил, что надо сделать свой Place, с таймлапсом и API.
На Reddit миллион человек рисовал на холсте размером один мегапиксель. Каждый мог закрасить не больше одного пикселя раз в 5–20 минут. Если сделать праздничный холст 256×256 пикселей (в 15 раз меньше) и учесть, что у нас не миллион сотрудников (а в 200 раз меньше), то задержку между пикселями тоже должна быть примерно в 10 раз меньше.
Поэтому для нашего поля 256×256 пикселей я выбрал задержку от 2:56 до 0:32. А после этого рассказал об идее коллегам, которые согласились помочь.
Часть 2. Рассказывает Вероника aminopyrodin, которая поборола себя и тормозной canvas
Я сразу поняла, что на фронте будет нужен холст, палитра и зум. Но дизайнеры (Владимир dzekh и Юлия krasilnikovayu) оказались хитрее и придумали ещё перемотку, статистику, лидерборд и скриншоты.
Кстати, сначала в палитре было меньше цветов, но потом ребята добавили коричневый, чтобы не ограничивать ничьи творческие порывы.
Тем временем я, как современный фронтендер, рефлекторно начала думать о том, чтобы настроить Webpack, Babel и Autoprefixer. А когда очнулась, узнала, что бэкенд-разработчик уже всё сделал. И оно даже работало. Криво-косо, но работало: точки на canvas ставились, зум зумился. Я отпилила от прекрасного дизайна все ненужное и красивенько сверстала.
Остались две проблемы: Edge и Safari.
В Safari и правда все тормозило со страшной силой. Сначала обнаружила, что canvas не вынесен в отдельный композитный слой. Поэтому браузер при каждом обновлении холста перерисовывал весь документ. Добавила канвасу transition: translateZ(0)
, и все стало тормозить быстрее. Потом отрефакторила остальной бакендерский код, избавилась ещё от десятка перерисовок. Интерфейс полетел на первой космической.
Об IE я сразу не заботилась, потому что знала, что игроки будут пользоваться нормальными браузерами. Беда пришла от старшего брата. Если просишь Edge нарисовать квадрат, он категорически отказывается. Говорит: «Но плавные переходы лучше!» — и размывает весь рисунок.
Такая же проблема была у ребят из Reddit. Сначала я решила её с помощью CSS-свойства image-rendering
и флага CanvasRenderingContext2D.imageSmoothingEnabled
. Но перед запуском оказалось, что Edge косячит при общении с сервером через вебсокеты. Поэтому я и его объявила ненормальным браузером.
Горжусь, что трижды пыталась принести в код React, Webpack, Babel, LESS и Autoprefixer, но смогла победить себя. В итоге всё написано на чистом ES6+ и CSS, но с модными гридами, вебсокетами и fetch-ем.
Часть 3. Рассказывает Иван vansel, который попробовал новую классную библиотеку и не рад этому
Я не хотел писать всё с нуля, поэтому поискал готовое. Оригинальный Place лежит на Github, но там слишком много кода. Я взял простой клон под NodeJS и прошёлся по нему напильником. Именно поэтому, когда за дело взялась Вероника, интерфейс уже как-то работал. Вообще, есть уйма клонов, выбирайте для себя любой.
В коде выбранного клона пришлось пофиксить уязвимости и добавить недостающее: таймер, расширенную палитру, перемотку в онлайне, сбор статистики, рисование через вебсокеты вместо REST-запросов, вход через Паспорт (наш внутренний аутентификатор).
Архитектура была такая: пользователь ставил пиксель в браузере, браузер отправлял сообщение через вебсокет на сервер, сервер отправлял сообщение об изменении холста в очередь (Apache Kafka). Потом серверы забирали данные из очереди и отправляли всем клиентам. Выше оригинальная схема от автора клона, на которой клиенты ещё общаются с сервером с помощью REST-запросов.
Мы планировали сделать перемотку, поэтому я решил кэшировать снэпшоты холста с версиями. При старте сервер должен был взять последний снэпшот из кэша, а не строить его с нуля. Тот же снэпшот получал клиент при подключении к серверу, отчего загрузка холста была почти мгновенной. Можно было отправить клиенту снэпшот любой версии и таким образом организовать перемотку. Новый снэпшот сохранялся после изменения каждых ста пикселей.
Примерно через сутки после начала игры случился инцидент. Я исправил баг и перезапустил сервер. А пользователи увидели, что часть нарисованных точек пропала.
Дело было в том, что при подключении к серверу клиент запрашивал конкретную версию холста. Поэтому после старта сервер взял из кэша не последний снэпшот, а какую-то старую версию, которую запросил один из клиентов. Пользователи не увидели свежие пиксели и начали рисовать по-новой, а сервер продолжил складывать данные в очередь.
Когда я понял, в чём дело, я почистил кэш. Сервер заново загрузил данные из очереди и сгенерировал актуальную версию холста. Получился забавный эффект: новые рисунки пользователей наложились на предыдущие, так как вернулось всё потерянное и добавились новые изменения. Фикс был быстрый, поэтому ничего не испортилось:
В общем, не зря говорят, что инвалидация кэшей — это одна из двух сложнейших задач в компьютерных науках.
Забавно, что я, пока чинил кэш, случайно выключил аутентификацию. Тут же нашёлся коллега-кулхакер, который закрасил скриптом несколько тысяч пикселей за пару минут. Я выкатил фикс, но зелёная полоса осталась:
Ещё я хотел после окончания игры сделать хороший таймлапс и посчитать статистику. Для этого я решил завести базу данных и сохранять в ней для каждой нарисованной точки координаты, цвет, дату и время, а также идентификатор пользователя.
Сервер был под NodeJS, поэтому я выбрал LokiJS. Эту базу хвалили за простоту и скорость работы, потому что все данные хранятся в памяти и автоматически записываются на диск через заданные интервалы времени. Для моей задачи подходило.
Я настроил сохранение раз в 1 минуту. Протестировал локально, в том числе под нагрузкой — всё работало как часы. А на боевой площадке происходило что-то паранормальное. Данные сохранялись на диск не по расписанию, а по собственному желанию. Например, в течение нескольких часов не сохранялись ни разу. За три дня я так и не нашёл причины этого поведения. В итоге, много статистики потерялось при перезапусках сервера.
Однако я был готов к этому с самого начала, потому что таймлапс уж очень был нужен. Каждую минуту я сохранял холст в виде картинки в файл. Получилось несколько гигабайт картинок, зато видео с таймлапсом было записано и озвучено парой команд:
$ ffmpeg -pattern_type glob \
-i "*.png" \
-c:v libx264 \
-vf format=yuv420p \
timelapse.mp4
$ ffmpeg -i timelapse.mp4 \
-i sci-fi.mp3 \
256.mp4
Часть 4. Рассказывает Павел xoposhiy, который загнул радугу и запустил ракету через API
После начала игры все быстро поняли, что один в поле не воин. Началась самоорганизация в Стаффе, нашей внутренней соцсети:
Я тоже в этом поучаствовал:
Но даже с командой было неинтересно рисовать масштабную картинку. Я понял это после первых семи пикселей радуги. Не радовало даже, что коллеги сделали кучу полезных инструментов:
Я ждал от Дня программиста большего. И дождался — на второй день Игорь опубликовал в Стаффе такой фрагмент кода и стал раздавать желающим API-ключи:
Это было уже что-то!
Для разминки я написал бота, который рисовал заданную картинку. Это было не очень весело. Потом придумал создавать картинку алгоритмически. Получился бот, который загнул идеально круглую радугу. Но это тоже было скучно.
Я понял, что нескучный бот должен не просто рисовать пиксели, а взаимодействовать с окружающим миром и чужим творчеством. Но нужно было избежать вандализма, потому что бот — это сила, а с силой должна идти ответственность.
Можно было нарисовать часы с текущим временем. Или движущуюся картинку, которая ползёт по холсту и затирает чужие рисунки… чтобы потом их восстановить. И тут я придумал сюжет, который объединил эти две идеи.
Часы стали таймером обратного отсчёта, движущаяся картинка — взлетающей ракетой. К тому же, ракету очень удобно рисовать — сначала на пиксель удлиняешь верхнюю часть, потом на пиксель укорачиваешь нижнюю. Это не только хорошо смотрится, но и экономит пиксели, ведь задержку при рисовании через API никто не отменял.
Это должен был быть самый медленный полёт ракеты в истории человечества. С текущей задержкой за пару часов я мог сдвинуть ракету всего на несколько пикселей. Нужно было либо уменьшать ракету, либо двигать её скачками, либо смириться с тем, что лететь она будет сутки. Поделился муками выбора с Игорем, а он со словами «Твори добро!» внезапно отсыпал без малого 50 ключей для API. С таким количеством ключей ракета могла достичь скорости один пиксель в секунду!
Осталось немного: выбрать дизайн ракеты и написать весь код. Я отбросил мультяшные ракеты и выбрал ракету-носитель «Восток». Сразу стало понятно, что полёт ракеты должен заканчиваться выводом на орбиту корабля Восток-1.
Почему «Восток»? Потому что прямо сейчас куча инженеров из Контура занимается секретным проектом с кодовым названием Vostok. Я хотел, чтобы парням было приятно.
Я настроил бота, запустил таймер обратного отсчёта, позвал зрителей через Стафф. Ракета взлетела. И тут я понял, как нелепо выглядит ракета в космосе с неотделёнными разгонными блоками и первой ступенью. Чудом нашёл 10 свободных минут, чтобы добавить отделение ступени и перезапустить бота. Так что это был не только самый медленный полет ракеты в истории человечества, но и первый полёт ракеты, в середине которого поменяли её конструкцию.
Было приятно наблюдать, как коллеги стирали копию ракеты, из-за бага оставшуюся на стартовом столе. Пририсовывают однопиксельного человечка в окошко ракеты. Переделывают слово «поехали» в «понаехали». Вообще, радовало, что все вели себя культурно, несмотря на отсутствие правил. Даже когда место на холсте закончилось:
Кстати, без NSFW-контента не обошлось. Кто-то из нарисованного моим первым нескучным ботом слова TRON упорно делал слово PRON.
Ваня потом рассказал, что 13 сентября на холсте одновременно рисовало 1630 человек и десяток ботов, то есть примерно треть всех работников компании. В среднем к серверам было подключено 440 клиентов, а в дневные часы — 840.
В итоге у нас получилась такая картинка:
И такой таймлапс. Моя ракета взлетает на 27 секунде:
А вы программируете по праздникам и для праздников? Расскажите нам в комментариях.
P.S. Если интересно, о чём мы не рассказываем на Хабре, подписывайтесь на наш канал в Телеграме.