Lunacy
Я пришёл в Icons8 LLC в 2019 году. После работы над Pichon присоединился к команде Lunacy — графического редактора для интерфейсов, иллюстраций, макетов и другой визуальной работы. Тогда в команде было около 8 человек, в основном разработчиков, а самому продукту — примерно два года.
Продукт был ещё очень сырым: написан на WPF, работал только под Windows и был болезненно медленным. Изначально Lunacy создавался как просмотрщик файлов .sketch для Windows, а редактирование добавляли постепенно. За рендеринг отвечала Skia — тот же графический движок, который используется в Chrome.
Мои первые задачи в Lunacy
Первой задачей была интеграция иконок Icons8 прямо в Lunacy. Я сделал все наборы доступными внутри продукта, чтобы пользователи могли просматривать их и перетаскивать на холст без лишних действий.
В эту работу входила и серьёзная интеграция лицензирования и оплаты. Мы поддержали лицензии на иконки через Microsoft Store и сайт Icons8, встроив коммерческое использование контента в рабочий процесс, вместо того чтобы оставлять его отдельной внешней библиотекой.
Производительность и GPU-рендеринг
Затем я занялся производительностью и обнаружил множество серьёзных проблем. Некоторые удалось исправить быстро, другие потребовали очень много времени.
Главная проблема заключалась в том, что всё рисовалось на CPU. На больших документах приложение могло зависнуть на несколько секунд ради одного кадра. Я начал долгий и сложный переход на GPU.
Это стало одной из самых трудных инженерных задач в моей карьере. WPF работает через DirectX 9, а Skia не поддерживала нужный нам путь рендеринга напрямую. Она умела рисовать через OpenGL, но это само по себе не решало интеграцию с WPF. В итоговой архитектуре Skia рисовала через OpenGL с помощью ANGLE в текстуру DirectX 11, которую затем нужно было преобразовать в текстуру DirectX 9 для WPF. В этом участвовали SharpDX и OpenTK, и всё должно было работать без классической проблемы airspace в WPF.
Собрать из этого стабильное решение было невероятно сложно. В то время почти никто не публиковал полноценной практической реализации такой цепочки. Позже я выложил работающий пример: WpfSkiaAngleSharpDxOpenTK.
Но это была лишь часть работы. Продукт также нужно было сделать многопоточным. Раньше один поток занимался и интерфейсом, и рисованием холста. Я выделил отдельный поток рендеринга и спроектировал стабильный, точный и удобный диспетчер GL-потока, чтобы явно разделить код потока окна и потока рисования холста.
Результат оказался впечатляющим: GPU-рендеринг был до 100 раз быстрее прежнего рендеринга на CPU.
От разработчика к руководителю команды
Благодаря этим улучшениям меня повысили до руководителя команды Lunacy. Пришлось перестроить часть команды: расстаться с менее мотивированными сотрудниками, нанять более сильных и постепенно расширить команду тестирования, чтобы повысить стабильность продукта.
Я также написал около 30 внутренних документов, подробно описывающих рабочие процессы. Они оставались основой нашей работы годами и не требовали серьёзной замены. Я горжусь тем, что в команде не было разрушительных внутренних конфликтов: люди уважали друг друга, помогали коллегам и поддерживали профессиональную, дружелюбную атмосферу.
Даже став руководителем команды, Scrum-мастером и владельцем продукта, я не стал меньше заниматься технической работой. Из примерно 50 000 коммитов за 10 лет около половины были моими. Похожая картина была и в Jira: количество функций, за которые я лично отвечал, было примерно втрое больше, чем у остальных разработчиков вместе.
Я старался показывать пример: сложные задачи можно решать быстро, эффективно и качественно. Многое начинается с хорошей постановки задачи, поэтому я проводил встречи, где учил правильно оформлять задачи, разбивать работу на части и разбирать сложные вопросы реализации.
Технические стандарты и избавление от устаревшего кода
Я стремился избавлять продукт от ненужного устаревшего кода, внедрять современные технологии и контролировать технический долг. Мы часто переходили на новые версии .NET в день выпуска или даже раньше и по возможности убирали зависимости от устаревших библиотек.
Хороший пример — стек отмены действий. Когда я пришёл, он был реализован через замыкания: после сбоя терялись и состояние документа, и история отмены. Отлаживать такую систему было крайне трудно.
Я полностью переделал её, чтобы она целиком сохранялась на диск в читаемом JSON. Это дало два важных результата:
- после сбоя можно было восстановить действия пользователя
- систему отмены стало гораздо проще отлаживать и покрывать модульными тестами
Переписывание Lunacy с WPF на Avalonia
В 2021 году я принял важное стратегическое решение: переписать приложение с WPF на AvaloniaUI. На переход ушло около шести месяцев.
В результате Lunacy заработал на Linux и macOS, а не только на Windows. Мы выпустили его в Snapcraft, Flathub, а также стали первым приложением на Avalonia в App Store. Последнее оказалось особенно сложным. Ограничения Apple Sandbox были настолько строгими, что пришлось напрямую подключить сотрудника Apple. Вместе мы добились корректной работы приложения на Avalonia в рамках правил App Store.
В процессе я вносил множество исправлений в саму платформу: создал десятки pull request в Avalonia, чтобы заработали трекпады Apple, покупки в App Store, закладки доступа к файлам в sandbox и другие платформенные особенности.
Поскольку эти изменения не всегда принимали достаточно быстро, я решил поддерживать собственные форки Avalonia и связанных библиотек: AvalonEdit, Avalonia Behaviors, Avalonia SVG. Это потребовало дополнительной работы над сборками и процессами, но на практике оказалось совершенно правильным решением без большой нагрузки на сопровождение.
Позже я подробно рассказал о переходе в докладе на dotNEXT:
Локализация, типографика и редактирование текста
Ещё одной моей областью была локализация. Я хотел сделать её удобной, компактной, с мгновенным переключением и небольшим потреблением памяти. Это удалось реализовать очень эффективно. Lunacy поддерживает более 20 языков.
Для арабского и иврита интерфейс переключается в полноценный режим справа налево, RTL. Это выглядит впечатляюще и до сих пор отсутствует во многих настольных приложениях.
В какой-то момент я также взял на себя типографику: убрал старую реализацию и с нуля переписал текстовую систему на основе кода Avalonia. Благодаря этому Lunacy получил стабильный текстовый редактор с поддержкой RTL-языков, стилей и эмодзи.
Самые сложные функции продукта
Одними из самых трудных частей Lunacy были компоненты, стили, переопределения и общие библиотеки. За всё это отвечал я.
Сложность была не только в реализации: приходилось постоянно держать в голове огромное количество пограничных случаев и правил поведения. Стоило забыть важную деталь — что-нибудь сразу ломалось, и при недостаточной внимательности тестировщики обнаруживали проблему уже после выпуска.
Переключение тем и плавающий интерфейс
Я реализовал переключение тем настолько эффективно, что позже разработчики Avalonia спрашивали, как я добился переключения за 1 мс, тогда как их тестовые приложения зависали примерно на секунду. Я объяснил подход, после чего они тоже улучшили производительность.
Я также был главным сторонником плавающего интерфейса. Для его реализации пришлось доработать наш форк Avalonia, чтобы разные части интерфейса рисовались в отдельные текстуры. Тогда обновление холста под интерфейсом не заставляло перерисовывать весь интерфейс. Получилось очень быстро и эффективно.
Годы оптимизации рендеринга
Все эти годы я продолжал работать над рендерингом и попробовал десятки способов кеширования и ускорения. Наиболее эффективными оказались:
- удаление LINQ из цепочки рендеринга
- перенос повторяющейся работы каждого кадра в заранее вычисленное кешируемое состояние
- кеширование последнего кадра
- кеширование больших слоёв
- снижение выделений памяти при рисовании кадра до нуля
Рендеринг без выделений памяти существенно повысил производительность и в некоторых случаях позволил обойти конкурирующие продукты.
Совместная работа через облако
Одна из самых сложных задач — совместное редактирование через облако. Здесь очень помог стек отмены на JSON. На его основе и с использованием идей CRDT мы создали эффективную систему совместной работы.
Для знакомства с CRDT рекомендую доклад Руди Чена: Как создать текстовый редактор для совместной работы в реальном времени за 5 шагов — на английском.
Мы хотели свести стоимость серверов к минимуму, поэтому сервер почти ничего не делал, кроме пересылки сообщений WebSocket. Документы оставались открытыми на компьютерах пользователей, а входящие изменения применялись локально.
Даже недорогой сервер с 2 ядрами CPU и 2 ГБ оперативной памяти мог поддерживать сотни участников в одном документе и одновременно сотни открытых документов. Инфраструктура обходилась очень дёшево.
Для приемлемой задержки по всему миру мы со временем развернули около 10 серверов в разных регионах. Архитектура соединений позволяла работать с низким пингом и сотрудничать между регионами без ручного переключения региона в приложении.
Trimming, NativeAOT и скорость запуска
Ещё одной большой задачей было внедрение trimming и движение к NativeAOT, чтобы добиться очень быстрого запуска.
Пришлось удалить часть зависимостей, сделать форки других ради устранения рефлексии и серьёзно переработать внутренний код Lunacy, чтобы по возможности обходиться без неё. Обработку JSON тоже перевели на генерацию кода без рефлексии.
Работы было много, но она окупилась: Lunacy запускается почти мгновенно.
Lunacy в браузере
Lunacy также стал первым крупным приложением Avalonia, выпущенным на WASM. Веб-версия Lunacy потребовала огромных усилий.
Из-за ограничений веб-платформы по производительности, памяти, потокам и хранилищу пришлось переписать многие функции. Фактически у нас был один поток и не было настоящего диска, что потребовало серьёзных архитектурных изменений.
В общей сложности веб-версия разрабатывалась около 2–3 лет. Мы дошли до выпуска, преодолев сотни серьёзных проблем, многие из которых в разные моменты ставили под вопрос всё направление WASM.
Обратная разработка .fig и создание .free
В какой-то момент понадобилась поддержка файлов .fig. Это бинарный формат без публичной спецификации, и его разбор потребовал немало усилий.
Я также с нуля спроектировал собственный формат дизайн-файлов .free с открытой спецификацией. Он решал ряд проблем .sketch и .fig: получился эффективнее .sketch и гораздо понятнее .fig.
ИИ, плагины и приватность
Мы старались использовать передовые технологии. Одними из первых внедрили в Lunacy MCP, построили вокруг него API плагинов, а затем разработали ИИ-чат на современных LLM. Он генерировал качественные дизайны, в том числе макеты с уже настроенным Auto Layout.
Я также последовательно отстаивал приватность пользователей. В какой-то момент обсуждалась более агрессивная аналитика поведения, но мне удалось убедить руководство отказаться от этого направления.
Сегодня я считаю Lunacy одним из самых приватных и безопасных решений для графического дизайна. Это стало одной из отличительных черт продукта.
Работа с пользователями
Я постоянно читал обратную связь и следил, чтобы идеи пользователей и сообщения об ошибках попадали в бэклог Jira. Часто отвечал людям лично, разбирался в причинах проблем и сам занимался запросами на улучшения.
Я считаю работу с пользователями главным приоритетом владельца продукта. Мы также регулярно проводили юзабилити-интервью с дизайнерами, не знакомыми с Lunacy, и улучшали продукт, наблюдая за их реальным поведением.
Технологии
AvaloniaUI, Skia, NativeAOT, ASP.NET, WebSockets, Postgres, Docker, Entity Framework, C#, Objective-C, WPF, SignalR, OpenGL, OpenTK, SharpDX, AvalonEdit, MVVM, S3, CDN, Linux, GitLab CI, OpenCode, MCP, SVG, CRDT, WASM, GraphQL
Почему проект фактически закрыли
Почему проект фактически закрылся? За все эти годы у нас так и не появилось сильной маркетинговой стратегии. В маркетинге и продажах мы проиграли.
Сегодня недостаточно создать хороший и честный продукт — его ещё нужно уметь продавать. Это было нашим слабым местом. В какой-то момент основатель сопоставил выручку и расходы, оценил ситуацию на рынке, включая обвал акций Figma и падение акций Adobe, и решил, что у проекта нет жизнеспособного будущего.
Команду сократили до одного человека на неполной ставке, который поддерживает работоспособность продукта.
Напоследок
Хочу сказать спасибо всем, с кем работал в Icons8. Вы настоящие профессионалы, и создавать Lunacy вместе было удовольствием.