Истории успеха

Как инженеры Instagram Direct создали архитектуру интерфейса на основе ИИ с помощью Jetpack Compose и снизили стоимость токенов за сеанс агента на 33%

Время на чтение: 11 минут.
Посмотреть профиль пользователя Pavlo Stavytskyi Посмотреть профиль пользователя Rebecca Franks
Pavlo Stavytskyi и Rebecca Franks

Эта запись блога написана в сотрудничестве с командой Meta

Instagram Direct – один из основных разделов Instagram, в котором пользователи ежедневно отправляют миллиарды сообщений. За годы работы команда выжала из устаревшей системы Android View все возможные микрооптимизации. Однако поддержка и расширение сильно оптимизированного устаревшего интерфейса создает значительный технический долг и инженерные накладные расходы, особенно по мере того, как команды все чаще внедряют декларативный пользовательский интерфейс и помощь в программировании ИИ. 

Переход на Jetpack Compose для Instagram Direct был не просто обновлением интерфейса. Команда создала базу кода для интерфейса на основе ИИ, которая на 50% меньше исходной реализации. При этом время выполнения ИИ-агента сократилось на 35% , количество обменов между инженером и агентом уменьшилось на 32% , а стоимость токенов снизилась на 33% . В тесном сотрудничестве с Google команда внедрила Jetpack Compose, сохранив высокую производительность. Благодаря оптимизации производительности Meta и Google улучшили Compose не только для Instagram, но и для всей экосистемы разработчиков Android.

Модернизация базы кода в огромном масштабе

ИИ быстро стал повседневным помощником для инженеров в отрасли, и его применение к крупномасштабной базе кода, такой как Instagram, уже приносит реальный прирост производительности. Команда Instagram Direct поставила перед собой более амбициозную цель. Вместо того чтобы просто использовать ИИ-инструменты для работы с существующим кодом, команда переработала базу кода и ее архитектуру, чтобы они были изначально ориентированы на ИИ. Это позволило значительно повысить эффективность ИИ по сравнению с тем, чего можно было бы достичь за счет простой модернизации.

Команда Instagram Direct выбрала Jetpack Compose в качестве ключевого компонента для создания архитектуры пользовательского интерфейса на основе ИИ. Декларативный характер обеспечивает лаконичность, предсказуемость и структурную простоту кода, что облегчает его анализ ИИ-моделями, снижает количество побочных эффектов, уменьшает неявное состояние и делает границы компонентов более четкими.

Переход на Jetpack Compose потребовал тщательного планирования. Сотни миллионов людей ежедневно отправляют сообщения в Instagram, поэтому переход должен был быть постепенным, плавным и без каких-либо сбоев в работе, пока команда перестраивала фундамент. Чтобы проиллюстрировать масштаб задачи, приведем пример: отдельные компоненты пользовательского интерфейса могут отображаться в более чем 160 различных состояниях, а один экран чата обрабатывает более 200 различных типов сообщений.

Product Design 1.png

При переносе базы кода такого размера в Compose возникает соблазн пойти по простому пути и встроить компоненты пользовательского интерфейса Compose в существующую иерархию View. Это вполне допустимо в качестве промежуточного шага при постепенном переносе. Однако в долгосрочной перспективе интеграция Compose в базу кода на основе View представляет собой проблему. ИИ-инструменты часто идут по пути наименьшего сопротивления. Если вы смешаете декларативный и императивный код пользовательского интерфейса, ИИ, скорее всего, объединит их неправильно, что приведет к незаметным ошибкам, техническому долгу и снижению производительности. 

Создание архитектуры интерфейса на основе ИИ

При масштабе Instagram определенная степень архитектурной абстракции неизбежна, и именно она позволяет поддерживать приложение по мере его роста. Рассмотрим распространенный шаблон, в котором каждый тип элемента RecyclerView моделируется как потомок пользовательского базового класса RecyclerViewItem, который предоставляет обычные хуки жизненного цикла, такие как onBind.

Пример 1

class ChatItem(
  val features: FeatureFlagProvider
) : RecyclerViewItem<ComposeViewHolder, ChatUiState> {

  // Imperative context:
  // AI could often take the path of least resistance and generate a mutable
  // state here, dispatched outside the ChatUiState. This class survives
  // re-bindings and is shared across multiple items, ultimately leading to
  // unexpected, hard-to-reproduce bugs.
  var isPinned: Boolean = false

  override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) {
      // Imperative context
      val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")

      // Declarative context
      holder.composeView.setContent {

        // Blending imperative and declarative contexts
        if (isPinnedChatsEnabled) {
          Button(onClick = { isPinned = !isPinned }) {
            Text(if (isPinned) "Unpin" else "Pin")
          }
        }
        
        ...
      }
  }
}


В приведенном выше фрагменте есть две проблемы. Сначала флаг isPinnedChatsEnabled считывается в императивном коде, а затем захватывается внутри лямбда-выражения Compose, что представляет собой тонкую связь между парадигмами. Во-вторых, isPinned – это изменяемое поле в самом элементе, а не в ChatUiState, поэтому оно сохраняется при повторном связывании RecyclerView и повторном использовании в строках, что приводит к утечкам и ошибкам, которые трудно воспроизвести.

Даже если код очищен путем присвоения элементу специальной функции @Composable, те же проблемы остаются.

Пример 2

class ChatItem(
  val features: FeatureFlagProvider
) : ComposeRecyclerViewItem<ChatUiState> {

  // Imperative context
  val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")
  var isPinned: Boolean = false

  // Declarative context
  @Composable
  override fun Content(uiState: ChatUiState) {

 
    // Blending imperative and declarative contexts
    if (isPinnedChatsEnabled) {
      Button(onClick = { isPinned = !isPinned }) {
        Text(if (isPinned) "Unpin" else "Pin")
      }
    }

    ...
  }
}

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

Чтобы база кода была удобной для ИИ, она должна соответствовать двум практическим правилам: 

  • Минимизируйте зависимость от пользовательского контекста. Чем больше специфических знаний о базе кода требуется ИИ-агенту, чтобы внести правильное изменение, тем ниже качество его работы. Чем ближе база кода к известным рекомендациям, тем лучше результаты ИИ.
  • База кода, ориентированная на ИИ, должна сама обеспечивать соблюдение своих границ. Устранение пробелов в дизайне с помощью навыков ИИ не масштабируется, поскольку каждый навык, загруженный в контекст, стоит токенов и может снизить производительность агента. Вместо этого архитектура сама должна нести этот вес. ИИ-агенты естественным образом выбирают путь наименьшего сопротивления, поэтому дизайн должен направлять их к созданию правильного и качественного кода, усложняя и удорожая реализацию плохих проектных решений.

Элемент списка по-прежнему может быть представлен собственной абстракцией, но в этом случае весь код Compose находится в конструкторе, поэтому у него нет доступа к членам класса или состоянию, а его единственным источником аргументов является конструктор. Это делает ее эквивалентной простой функции @Composable, но при этом соответствующей существующей архитектуре.

Пример 3

class ChatItem(
  val features: FeatureFlagProvider,
  val onPin: (Boolean) -> Unit,
) : ComposeItem<ChatUiState>(

 
   // Compose UI
   content = { uiState: ChatUiState ->
    val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature")

    if (isPinnedChatsEnabled) {
      Button(onClick = { onPin(!uiState.isPinned) }) {
        Text(if (uiState.isPinned) "Unpin" else "Pin")
      }
    }
    
    ...
  },
)

Перенос базы кода такого размера – это огромная работа. Долгое время сотни компонентов, из которых состоит Direct UI, сосуществовали с устаревшими аналогами, и нам приходилось поддерживать и те, и другие. Рабочие процессы на основе ИИ помогли ускорить написание огромного количества кода и сделать возможным параллельный перенос. Такой подход позволил команде Direct выполнить перенос в рекордно короткие сроки, не мешая остальным сотрудникам, которые продолжали выпускать функции, улучшающие работу миллионов людей каждый день.

Несколько инженеров запустили собственных ИИ-агентов, используя общую базу знаний, которая содержала многократно используемые навыки и соглашения, созданные во время переноса. Это позволило синхронизировать рабочие процессы и рекомендации для всей команды, а не заставлять каждого инженера заново открывать их для себя. Для каждого сервиса перенос выполнялся в несколько этапов:

  • Напишите весь код Compose с помощью ИИ.
  • Дорабатывайте его, устраняя недочеты и повышая производительность, пока не будете готовы выпустить интерфейс для реальных пользователей в рамках публичного тестирования.

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

Результаты переноса подтвердили правильность подхода. Для перенесенных разделов Instagram Direct Jetpack Compose позволил команде сократить общий объем кода пользовательского интерфейса на 50%. Чем меньше кода нужно сгенерировать ИИ, тем выше качество результата и ниже стоимость токенов для каждой задачи. 

Quote-Pavlo-New.jpg

Внутренний анализ базы кода Android для Instagram Direct сравнил сеансы ИИ-агента, работающего над Compose UI, с теми же задачами, использующими Android Views. Повышение эффективности было очевидно по двум параметрам:

  • На символ добавленного кода: Compose потребовалось на 32% меньше обменов между инженером и агентом и на 35% меньше времени выполнения агента(время, прошедшее с момента, когда агент начинает работать над запросом инженера, до момента, когда он возвращает ответ).
  • За сеанс агента. Общая стоимость токенов снизилась на 33%  при использовании Compose по сравнению с Views.

Мы сообщаем как об эффективности вывода, так и о типичных номерах сеансов, поскольку они являются независимо полезными результатами. Обмен данными между инженером и агентом и время выполнения сравнивают использование ресурсов на единицу полученного результата, а количество токенов – общую стоимость типичного сеанса агента.

Данные также выявили стабильную разницу в том, как два фреймворка обрабатывают сложный или хрупкий код. Meta отслеживает это с помощью оценки риска изменений кода, которая оценивает общее качество кода и вероятность того, что изменение вызовет инциденты в производстве. В ходе анализа измерялась эффективность использования ресурсов агентом. Для этого использовался составной показатель, учитывающий потребление токенов, время выполнения агента и взаимодействия инженера с агентом. Чем выше оценка риска файлов, тем менее эффективно используются ресурсы в сеансах ИИ-агентов. 

Когда совокупная оценка риска файла удваивается, интерфейс, реализованный с помощью Android Views, снижает эффективность ресурсов агента на 30% (на каждый символ). В тех же условиях при использовании Jetpack Compose UI снижение составляет всего 9%.

Благодаря партнерству Google и Meta команда Instagram Direct по-новому взглянула на внедрение Compose – не просто как на переписывание пользовательского интерфейса, а как на подготовку базы кода к использованию ИИ. Эта работа показала, что Compose – отличная основа для создания баз кода и архитектур, ориентированных на ИИ, особенно в масштабе таких приложений, как Instagram.

Оптимизация эффективности 

Instagram Direct – один из самых важных разделов приложения, и пользователи ожидают, что он будет работать быстро и без задержек. Переход на Jetpack Compose означал, что интерфейс придется существенно переписать. Главной целью было сохранить высокое качество и не допустить регрессий.

За годы итераций устаревшая реализация на основе View в Instagram достигла исключительно высокого уровня производительности, и команде нужно было соответствовать этому стандарту при переходе на совершенно новый фреймворк пользовательского интерфейса.

Instagram измеряет сотни, если не тысячи, показателей эффективности. При внедрении Compose наиболее важными были следующие три:

  • Time to Interactive – время между открытием экрана и возможностью использовать его.
  • Время полной загрузки  – время между открытием экрана и полной загрузкой всего контента (например, изображений).
  • Производительность прокрутки – плавность прокрутки экрана без пропущенных кадров.

Эти показатели отслеживаются во время выполнения в рабочей среде, что позволяет проводить A/B-тестирование, сравнивая перенесенный интерфейс Compose с устаревшим интерфейсом, и оценивать влияние этих усилий на производительность.

Обычно в таких случаях начинают с малого: переносят несколько компонентов интерфейса, собирают данные и изучают их поведение. Хотя эти ранние результаты и полезны, они дают лишь частичную картину, предоставляя ложноотрицательные результаты в отношении внедрения Compose, поскольку:

  • Не репрезентативны. Один перенесенный компонент интерфейса может предоставить полезные данные о своей общей эффективности на определенном экране. Однако разные компоненты ведут себя по-разному по причинам, которые не поддаются обобщению, поэтому вы не всегда можете экстраполировать их.
  • Стоимость взаимодействия. Небольшой фрагмент Compose внутри большой базы кода View платит непредсказуемую стоимость моста между двумя системами. Эти накладные расходы искажают результаты измерений, поэтому ранние мелкомасштабные результаты не отражают того, как на самом деле будет выглядеть полная миграция.

В результате небольшие миграции, хотя и полезны, не всегда отражают полное влияние Compose. Чем больше элементов интерфейса перенесено без прерываний, тем лучше будет производительность.

Основные экраны в Instagram Direct построены на длинных списках различных типов элементов, изначально реализованных с помощью RecyclerView. Архитектура использует пользовательские абстракции для масштабируемости, но остается привязанной к жизненному циклу системы на основе View.

Diagram 1.png

Основной задачей команды был постепенный перенос нескольких сотен отдельных элементов списка в Compose в рамках существующей архитектуры на основе RecyclerView. Они выпускались в производство небольшими независимыми группами в рамках A/B-тестирования – и все это без видимых изменений в работе с сообщениями.

Самый большой недостаток такой настройки – значительная зависимость от устаревшей системы View через архитектуру ядра RecyclerView, даже после полной миграции каждого элемента списка в Compose. Следующим шагом стала замена базовой архитектуры на основе RecyclerView на альтернативу, созданную специально для Compose, – LazyColumn.

Это значит, что компоненты интерфейса Compose должны быть абстрагированы от фреймворка, в который они заключены, и при этом совместимы как с RecyclerView, так и с LazyColumn. Не менее важна возможность переключаться между ними во время выполнения с помощью флагов функций, чтобы включить A/B-тестирование.

Diagram 2.png

Новые элементы Compose совместимы с LazyColumn и могут быть подключены к непрерывному дереву композиции. Также был создан API взаимодействия, позволяющий вставлять их в RecyclerView. Это позволило запустить настройку LazyColumn в рамках A/B-тестирования параллельно с RecyclerView, повторно используя те же элементы Compose и улучшая производительность, не мешая остальной команде создавать и совершенствовать функции.

Масштаб, сложность и чувствительность Instagram даже к малейшим регрессиям создали уникальную проблему для Jetpack Compose. Для решения этих проблем потребовалось итеративное, практическое партнерство. Инженеры Google и Meta тесно сотрудничали, анализируя показатели, чтобы определить и разработать новые возможности Compose, которые соответствуют контрольным показателям на основе представлений или превосходят их. В результате этого партнерства в Jetpack Compose появились следующие дополнения: приостанавливаемая композиция с LazyLayoutCacheWindows и отслеживание видимости. 

Приостанавливаемая композиция с LazyLayoutCacheWindows

Приостанавливаемая компоновка (включена по умолчанию в Compose 1.10) позволяет постепенно компоновать ресурсоемкие элементы ленивого списка в разных кадрах, чтобы предотвратить задержки. В сочетании с LazyLayoutCacheWindow (добавлено в Compose 1.9) это значительно повышает плавность прокрутки. Недавнее внутреннее тестирование в Meta показало, что сочетание приостанавливаемой композиции с одной областью просмотра LazyLayoutCacheWindow снижает количество больших пропусков кадров в минуту примерно на 13% по сравнению с обычной Compose. Окно кеша само по себе снизило его примерно на 8% по сравнению с тем же базовым уровнем. LFDs/m – это внутренний показатель, который Meta использует для отслеживания заметных задержек при прокрутке.

Quote-Fabio.jpg


Использование LazyLayoutCacheWindow в приложении позволяет подготавливать и сохранять элементы за пределами экрана в пределах полосы на основе пикселей вокруг области просмотра, чтобы обеспечить быстрое пролистывание. Чтобы использовать LazyLayoutCacheWindows в своем приложении, вы можете установить последнюю версию Compose 1.13.0-alpha03 и настроить ее, как показано в примере ниже.

val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp)
// OR
val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f)

LazyColumn(state = state, cacheWindow = cacheWindow) {
    ...
}

Настроить окно кеша можно двумя способами. Оба описывают одно и то же: сколько контента за пределами экрана нужно сохранить, но в разных единицах.

  • Dp – фиксированная абсолютная длина. ahead = 150.dp сохраняет 150 dp контента, составленного за видимым краем, независимо от устройства.
  • Float – доля области просмотра. aheadFraction = 0.5f сохраняет половину экрана, скомпонованного заранее, поэтому абсолютное количество масштабируется в зависимости от высоты экрана, поддерживая различные форм-факторы: больше на планшете или развернутом складном устройстве, меньше на компактном телефоне. 

Команда Instagram настроила дробные значения окна кеша специально для структуры контента и размеров элементов Direct. Поскольку идеальные значения зависят от конкретных параметров интерфейса, для поиска правильного баланса требуются некоторые эксперименты.

Регистрация показов с помощью onVisibilityChanged


Ещё одним важным результатом технического партнерства между Google и Meta стал API  onVisibilityChanged , добавленный в Compose 1.9.0. Она позволяет крупным поверхностям Jetpack Compose определять, когда composable-функция действительно видна на экране, заменяя пользовательские реализации, которые использовались ранее. Только в Instagram Direct эти сигналы видимости используются в сотнях файлов для поддержки показателей качества продукта, которые зависят от того, были ли элементы пользовательского интерфейса фактически показаны людям.

Производительность при запуске

Внедрение Jetpack Compose в Instagram Direct привело к неожиданному повышению производительности в других разделах приложения. Среда выполнения Jetpack Compose требует затрат на прогрев, которые вы платите только один раз, и поскольку обмен сообщениями – это раздел с высоким трафиком, который часто посещают в начале сеанса пользователя, другие разделы Instagram, использующие Compose, продемонстрировали заметное улучшение производительности.

Производительность Compose UI в Instagram Direct была оптимизирована с помощью профилей Baseline, которые предварительно компилируют часто используемые пути выполнения кода во время установки. Это позволяет Compose быстро выполнять отрисовку с самого первого запуска.

Переход Instagram Direct на Jetpack Compose 

  • Jetpack Compose сразу же окупает вложенные средства. Чтобы воспользоваться преимуществами Compose, вам не нужно использовать расширенные рабочие процессы ИИ. Объем кода сократился примерно на 50 %, а значит, его стало проще поддерживать и в нем будет меньше ошибок.
  • Разработка архитектуры, ориентированной на ИИ, привела к значительным улучшениям, включая сокращение времени выполнения ИИ-агента на 35 %, уменьшение количества обменов между инженером и агентом на 32% и снижение стоимости токенов на 33 %.
  • Хотя существует множество API взаимодействия и поддержка объединения Views и Compose, стремитесь переносить более крупные поверхности, а не отдельные мелкие компоненты. Это позволяет сохранить пользовательский интерфейс в рамках единой непрерывной иерархии композиции и разблокировать все лучшие оптимизации производительности, присущие Compose.
  • Используйте Pausable composition вместе с LazyLayoutCacheWindow. Это дает лучшие результаты, чем использование только окон кеша. Если бы использовалось только окно кеша, тяжелый элемент все равно мог бы попытаться скомпоноваться за один проход, что могло бы привести к превышению бюджета кадра.
  • Внесите свой вклад в развитие Compose Компания Meta сотрудничает с командой Jetpack Compose, чтобы воплощать свои идеи и отзывы в Compose. Работа над набором инструментов с открытым исходным кодом означает, что мы все выигрываем, когда ошибки и улучшения производительности вносятся централизованно. Поэтому не забывайте оставлять отзывы.

Переход на Jetpack Compose позволил значительно повысить эффективность разработки с помощью ИИ и упростить повседневную работу над пользовательским интерфейсом Instagram. Декларативный подход сокращает шаблонный код, упрощает понимание состояния и повышает общую производительность разработчиков. Команда разработчиков Instagram планирует использовать Compose и в других разделах приложения. Google и Meta продолжат сотрудничать, чтобы улучшать Instagram и Jetpack Compose.

Если вы ещё не пробовали Compose, то сейчас, когда в нем есть ИИ-помощник, перейти на Jetpack Compose стало проще, чем когда-либо.

Благодарности. Благодарим Михала Зелински (Michal Zielinski) и Мэтью Ду (Matthew Du) из Meta, а также Андрея Шикова и Джорджа Маунта (George Mount) из Google за работу над повышением производительности Compose в рамках сотрудничества между Meta и Google. Также благодарим Гэри Йе (Gary Ye) из Meta за помощь в интеграции Compose в Instagram Direct и Гопала Джунеджу (Gopal Juneja) из Meta за поддержку этого проекта в области науки о данных.

Автор:
Продолжить чтение