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 вместе было удовольствием.