Когда в 2023 году Meta* запустила Threads, оно стало самым быстрорастущим приложением в истории, набрав 100 миллионов пользователей всего за пять дней. Сейчас число ежемесячных международных пользователей приложения превысило 300 миллионов, а команда разработчиков расширилась от небольшой группы инженеров-отщепенцев до организации с более чем сотней соавторов.
Если оглянуться на то, каким было приложение Threads для iOS год назад, то многое изменилось: мы вышли в Европу, интегрировались с Fediverse, запустили публичный API, разработали множество новых способов для людей поделиться тем, что происходит в их мире, и внедрили новые методы для поиска и чтения лучшего контента. Мы даже отпраздновали наш первый день рождения с праздничными шляпами и иконками приложения!
Чтобы приложение было простым и приятным в использовании, а также чтобы его можно было масштабировать с быстро растущей базой пользователей и командой разработчиков, оно должно быть производительным. Вот как мы думаем о производительности приложения Threads для iOS, чему мы научились за первый год работы и как мы решали некоторые из наших самых больших проблем с производительностью.
Как Threads измеряет производительность в масштабе
Быстрое и производительное приложение имеет решающее значение для обеспечения наилучшего пользовательского опыта. Мы хотим, чтобы Threads был лучшим местом для живых, креативных комментариев о том, что происходит сейчас. Это означает, что Threads также должен быть самым быстрым и отзывчивым приложением в своем классе. Если приложение не будет работать молниеносно, если оно будет зависать или разряжать батарею телефона, никто не захочет им пользоваться. Наши функции должны работать надежно и редко давать сбои, независимо от того, каким телефоном пользуется человек, сколько у него памяти, использует ли он Threads в местах с надежным сотовым покрытием или в местах, где сеть постоянно отсутствует.
Некоторые проблемы с производительностью возникают крайне редко, но все равно могут расстраивать. Поскольку в течение первого года после выхода приложения для iOS его использование быстро росло, мы хотели узнать, что является наиболее болезненными моментами для большинства людей, а также какие проблемы с производительностью испытывает небольшой процент пользователей. Мы измеряли, насколько быстро запускается приложение, сколько времени требуется для публикации фото или видео, как часто происходят сбои и сколько сообщений об ошибках было отправлено людьми.
%FIRE: Frustrating image-render experience
Помимо текстовых обновлений, которыми делятся люди, в Threads много фотографий. Если изображения загружаются медленно или не загружаются вовсе, это может заставить человека прекратить пользоваться приложением. Именно поэтому мы отслеживаем важную метрику, чтобы предупредить о регрессе в загрузке изображений для наших пользователей. Эта метрика, %FIRE, представляет собой процент людей, испытывающих проблемы с загрузкой изображений, и рассчитывается, как показано на рисунке ниже.
Ухудшать %FIRE могут самые разные вещи, как на стороне клиента, так и на стороне бэкенда, но не все ошибки рендеринга изображений подпадают под эту метрику. Например, в Threads iOS в начале этого года у нас была ошибка, когда фотографии профиля пользователя мерцали из-за того, что мы сравнивали модели представлений при их повторном использовании. Это приводило к разочарованию пользователей, но не к тому, чтобы такие пользователи вносили свой вклад в %FIRE.
Time-to-network content (TTNC)
То, как быстро запускается приложение и как быстро мы доставляем пользователю его ленту, также имеет большое значение. Мы знаем, что если кому-то придется слишком долго смотреть на экран запуска приложения, крутилку активности или мерцание загрузки, он просто закроет приложение. Все это измеряется показателем, который мы называем TTNC, или время до сетевого контента (time-to-network content). Помимо того, что приложение должно быстро запускаться, люди также хотят, чтобы мы показывали им, что происходит сейчас, поэтому TTNC измеряет, насколько быстро мы можем загрузить свежую, персонализированную ленту, а не только кэшированные, локально сохраненные сообщения.
Команда Threads iOS также улучшила время запуска приложения, сохранив небольшой размер двоичного файла приложения. Каждый раз, когда кто-то пытается закоммитить код в Threads, он получает предупреждение, если изменение кода увеличит размер двоичного файла нашего приложения выше настроенного порога. Код, нарушающий нашу политику размера, не допускается к слиянию.
Мы также работаем на опережение. Чтобы уменьшить TTNC, с момента запуска Threads мы потратили много времени на удаление ненужного кода и графических ассетов из пакета приложений, в результате чего бинарный файл стал размером в четверть меньше Instagram*. Не стоит забывать, что это также позволяет сократить время сборки нашего приложения для iOS, что делает его более интересным для разработки! Threads компилируется в два раза быстрее, чем Instagram, в наших неинкрементальных сборках.
Коэффициент успешности создания и публикации (cPSR)
Если %FIRE и TTNC измеряют, как контент представляется пользователю, то у нас есть еще одна важная метрика: cPSR, коэффициент успешности создания и публикации (Creation-publish success rate). Мы измеряем этот показатель отдельно для текстовых сообщений, фотографий и видео, опубликованных в Threads. Когда кто-то пытается опубликовать фото или видео, многие вещи могут помешать этому. Фотографии и видео локально перекодируются в форматы, которые мы хотим загрузить, что происходит асинхронно в процессе публикации. Они используют гораздо больше данных и загружаются дольше, чем текст, поэтому есть больше времени для того, чтобы что-то пошло не так. Пользователь может закрыть приложение после нажатия кнопки «Опубликовать», не дожидаясь успеха, а на iOS у нас есть всего несколько секунд, чтобы завершить загрузку до того, как нас прервет операционная система.
Позже в этом блоге мы расскажем о некоторых стратегиях, которые мы используем для улучшения cPSR.
Глубокое погружение: задержка навигации
Задержка навигации важна для пользовательского опыта, поскольку от нее зависит скорость запуска приложения и все действия пользователя после его запуска. Когда мы измеряем задержку навигации, мы хотим знать, сколько времени требуется для завершения рендеринга контента после того, как пользователь переходит в нужную часть приложения. Это может быть после старта приложения, либо при нажатии на push-уведомление от Threads, либо при простом нажатии на сообщение в вашей ленте и переходе к просмотру беседы.
В начале 2024 года команда Threads Performance поняла, что мы хотим сосредоточиться на нескольких ключевых областях, но на каких именно? Данные Instagram указывали на то, что задержка при навигации имеет большое значение, но Threads используется иначе, чем Instagram. На тот момент Threads был доступен для загрузки всего шесть месяцев, и мы знали, что для определения приоритетных областей улучшения нам сначала придется потратить некоторое время на понимание и обучение.
Обучение на основе пограничного теста
Для начала мы создали пограничный тест для измерения задержки, сосредоточившись на нескольких ключевых местах, которые люди посещают при запуске Threads или использовании приложения. Пограничный тест — это тест, в котором мы измеряем крайние точки, чтобы понять влияние недостатков. В нашем случае мы сделали небольшую задержку для небольшого процента наших пользователей при переходе к профилю пользователя, к представлению обсуждения для сообщения или к ленте активностей.
Эти задержки позволили бы нам экстраполировать эффект, если бы мы аналогичным образом улучшили способ доставки контента к этим представлениям.
У нас уже была надежная аналитическая система логирования, но у нас не было возможности различать навигацию к этим представлениям при холодном запуске приложения и изнутри приложения. Добавив это, мы инжектировали задержки в трех группах, каждая из которых слегка варьировалась в зависимости от ситуации.
Мы узнали, что пользователи iOS не терпят больших задержек. Чем больше мы добавляли задержек, тем реже они запускали приложение и тем меньше времени в нем находились. При наименьшем увеличении задержки влияние было небольшим или незначительным для некоторых View, но наибольшие инъекции оказывали негативное влияние повсеместно. Люди читали меньше сообщений, реже публиковали их и в целом меньше взаимодействовали с приложением. Помните, что мы не вводили задержку в основную ленту; только в профиль, пермалинки и активность.
Измерение задержки навигации с помощью SLATE
Задержку навигации трудно измерять неизменно. Если у вас большое приложение, которое делает много разных вещей, вы должны иметь последовательный способ «запуска» вашего таймера, измерения времени рендеринга представления на многих разных поверхностях с разными типами контента и поведения, и, наконец, «остановки» вашего таймера. Кроме того, необходимо помнить о состояниях ошибки и пустых представлениях, которые следует считать терминальными состояниями. Может существовать множество пермутаций и пользовательских реализаций на всех поверхностях приложения.
Чтобы решить эту проблему и последовательно измерять задержки навигации, мы разработали новый инструмент, который назвали SLATE: логгер «Systemic LATEncy». Он дает нам возможность наблюдать за событиями, которые запускают новую навигацию, когда создается пользовательский интерфейс (UI), когда отображаются спиннеры активности или шиммеры, когда контент отображается из сети и когда пользователь видит условие ошибки. Он реализован с помощью набора общих компонентов, которые являются основой для многих наших пользовательских интерфейсов, и системы, которая измеряет производительность путем установки «маркеров» в коде для определенных событий. Как правило, эти маркеры создаются с определенной целью. Самое замечательное в SLATE то, что он автоматически создает эти маркеры для разработчика, если он использует общие компоненты. Это делает систему очень масштабируемой и поддерживаемой в очень большой кодовой базе, такой как Threads или Instagram.
Когда наши iOS-разработчики создают новую функцию, легко проверить, влияет ли она на задержку навигации. Любой желающий может включить отладчик SLATE прямо во внутренней сборке нашего приложения, и легко создать дашборд, чтобы получить отчет о том, как работает код в продакшене.
Пример из практики: Использование SLATE для проверки внедрения GraphQL
За последний год Instagram и Threads перешли на GraphQL для сетевых запросов. Несмотря на то, что Meta* создала GraphQL еще в 2012 году, мы построили Instagram на сетевом стеке, основанном на REST, поэтому Threads для iOS и Android изначально унаследовали этот технический стек.
Когда был разработан Threads для веба, это была новая кодовая база, построенная на современном стандарте GraphQL вместо REST. Хотя это было здорово для веба, это означало, что новые функции для веба и iOS/Android приходилось писать дважды: один раз для поддержки конечных точек GraphQL и один раз для REST. Мы хотели перевести новые разработки на GraphQL, но поскольку реализация была непроверенной для Threads, нам сначала нужно было провести измерения и убедиться, что она готова к внедрению. Мы ожидали, что GraphQL приведет к уменьшению объема данных, которые нужно будет передавать по сети, но для анализа и хранения данных инфраструктура для его поддержки может внести дополнительные задержки.
Мы решили провести тест, в котором взяли одно из наших представлений и реализовали его код доставки по сети с помощью GraphQL. Затем мы могли бы запустить реализацию REST и GraphQL бок о бок и сравнить результаты. Мы решили провести тест для представления «Список пользователей», которое управляет списками Followers и Following, и определить, был ли новый код, который доставлял и анализировал ответы GraphQL, по крайней мере, таким же быстрым, как старый код REST.
Это было легко сделать с помощью Swift. Мы создали абстракцию, которая извлекала существующий API в протокол, который могли использовать как REST, так и GraphQL-код; затем, когда код вызывался, фабричный метод генерировал соответствующего провайдера.
После того как код был запущен, нам нужно было измерить влияние на сквозную задержку получения результатов из сети и вывода контента на экран. SLATE пришел на помощь! Используя маркеры производительности SLATE, мы смогли легко сравнить данные о задержках для каждой сетевой реализации пользовательских представлений.
Ниже приведен пример графика данных о задержке (p95) для случая, когда пользователь просматривает список своих подписчиков. Синяя линия сравнивает данные по задержкам REST и GraphQL, которые очень похожи. Мы увидели схожие результаты для всех различных представлений, что дало команде Threads iOS уверенность в принятии GraphQL для всех новых конечных точек.
Глубокое погружение: надежность публикации и задержка
Как уже говорилось ранее, cPSR — одна из главных метрик, которую мы пытаемся улучшить в Threads, потому что если люди не могут надежно публиковать то, что хотят, у них будет ужасный пользовательский опыт. Мы также знаем, читая сообщения об ошибках, присланные пользователями, что постинг может быть источником разочарования для людей.
Давайте рассмотрим две функции, добавленные в Threads iOS, которые подходят к улучшению работы с сообщениями совершенно по-разному: Черновики и уменьшение воспринимаемой задержки при отправке текстовых сообщений.
Черновики (Drafts)
В начале 2024 года Threads ввела базовое сохранение черновиков на iOS и Android. Помимо того, что это одна из самых востребованных пользователями функций, «Черновики» обеспечивают устойчивость к неожиданным сбоям, таким как плохое подключение к сети. Просматривая отчеты об ошибках, написанные пользователями, мы заметили, что основной проблемой была невозможность отправить сообщение. Часто пользователи не знали, почему они не могут написать сообщение. Мы знали, что проект функции поможет решить некоторые из этих проблем.
Эти сообщения об ошибках пользователей использовались для оценки успеха Drafts. Черновики не влияют напрямую на показатель cPSR, который измеряет надежность отправки сообщений за одну сессию, но мы предположили, что они могут привести либо к увеличению количества создаваемых сообщений, либо к уменьшению общего недовольства пользователей по поводу отправки сообщений. Мы выпустили Drafts для небольшой группы людей и сравнили количество последующих сообщений об ошибках, связанных с постингом, которые они отправили, с сообщениями от людей, у которых не было Drafts. Мы обнаружили, что на 26% меньше людей отправляли сообщения об ошибках, связанных с публикацией, если у них были «Черновики». Эта функция явно меняла ситуацию.
Вслед за этим мы быстро внесли небольшое, но необходимое улучшение. Раньше, если пользователь сталкивался с сетевой проблемой во время отправки сообщения, ему предлагалось повторить попытку или отменить ее, но не было возможности сохранить сообщение в черновиках. Это означало, что многие люди, которые не могли отправить сообщение, теряли его, что очень расстраивало. К сожалению, измерить влияние этой функции было сложно, потому что с ней столкнулось не так много людей.
Затем произошло удивительное событие: серьезная ошибка на короткое время вывела из строя весь Threads. Хотя это было плохо, это дало побочный эффект: мы протестировали некоторые из наших функций отказоустойчивости, включая Drafts. Мы увидели огромный всплеск использования во время короткого перерыва, что подтвердило, что людям было полезно иметь возможность сохранить свои сообщения в случае серьезной проблемы.
На рисунке ниже вы можете увидеть всплеск использования Drafts во время отключения около полудня 31 марта.
Минимизация локального хранилища для «Черновиков»
После выхода Drafts в открытый доступ мы обнаружили досадную ошибку: средний объем используемого Threads хранилища резко увеличился. Пользователи тоже заметили это и написали множество жалоб. Некоторые из них сообщали, что Threads занимает много гигабайт дискового пространства. Поддержание небольшого дискового пространства помогает производительности, и устранение этой ошибки дало возможность узнать о влиянии чрезмерного использования диска в Threads.
Виновником оказался «Черновики». В приложении для iOS мы используем PHPickerViewController, представленный в iOS 14, для работы с фото- и видеогалереей, представленной в Composer.
PHPickerViewController — это приятный компонент, который запускается не в процессе и обеспечивает пользователям конфиденциальность и безопасность, позволяя им предоставлять приложениям доступ именно к тем медиафайлам, которые они хотят. Когда выбирается фотография, приложение получает URL, указывающий на ресурс изображения на устройстве. Однако мы обнаружили, что доступ к этому изображению является временным: между сеансами работы Threads терял разрешение на чтение изображения, которое было прикреплено к черновику. Кроме того, если пользователь удалял изображение из галереи, оно исчезало и из черновика, что было не очень удобно.
Решение заключалось в копировании фотографий и видео в область в контейнере приложения, предназначенную для черновиков. К сожалению, скопированные медиафайлы не очищались полностью, что приводило к росту использования диска, иногда очень значительному, с течением времени.
Очистка чрезмерного использования диска дала значительные результаты в тех областях, которые мы не ожидали. Запуск приложений стал быстрее (-0.35%), число наших ежедневных активных пользователей выросло (+0.21%), люди разместили больше оригинального контента (+0.76%) — в общем, стало заметно лучше.
Потрясающе быстрые текстовые сообщения
Как и при тестировании границ навигационной задержки, команда разработчиков ранее измерила влияние задержки текстовых ответов и знала, что мы хотим их улучшить. В дополнение к реализации улучшений, направленных на снижение абсолютной задержки, мы решили уменьшить воспринимаемую задержку.
Новая функция в сетевом стеке Threads позволяет серверу уведомлять клиента о том, что запрос на отправку сообщения был полностью получен, но еще не обработан и не опубликован. Большинство сбоев происходит в общении между мобильным клиентом и серверами Threads, поэтому, как только запрос получен, он с большой вероятностью будет успешно выполнен.
Используя новый колбек подтверждения от сервера, клиент iOS теперь может выводить тост «Опубликовано», когда запрос на публикацию получен, но до того, как пост полностью создан в бэкенде. Это будет выглядеть так, как будто текстовые сообщения публикуются немного быстрее. В результате улучшился пользовательский опыт, и приложение стало более разговорчивым.
Переход на Swift Concurrency для повышения стабильности кода
Перенос кода Threads iOS с синхронной модели на асинхронную также выявил потенциал возникновения условий гонки. Помимо упомянутого ранее асинхронного шага транскодирования, появилось несколько новых, связанных с управлением задачами загрузки и получения метаданных медиафайлов. Мы заметили несколько загадочных вредоносных полезных нагрузок, которые лишь изредка появлялись в нашей аналитике и инструментальных панелях. Работа в огромных масштабах, как правило, приводит к появлению редких крайних случаев, которые могут негативно сказаться на показателях производительности и создать у людей неприятные впечатления.
Один из лучших моментов в работе с кодовой базой Threads — это то, что она в основном написана на Swift. Однако часть опубликованного кода была написана на Objective-C. Хотя у Objective-C есть много преимуществ, сильная защита от гонки данных и безопасность типов в Swift была бы улучшением, поэтому мы решили перенести код Threads на Swift.
iOS-команды по всей компании внедряют «полный параллелизм» (complete concurrency) Swift в рамках подготовки к переходу на Swift 6. В команде Threads мы переносим старый код на Swift и используем полный параллелизм в новых фреймворках, которые мы создаем. Переход на полный параллелизм — это, пожалуй, самое большое изменение в iOS-разработке с тех пор, как в iOS 4 появился автоматический подсчет ссылок (ARC). Когда вы переходите на полный параллелизм, Swift отлично справляется с предотвращением досадных гонок данных, например, тех, которые вызывали проблемы с нашим оптимистичным загрузчиком. Если вы перейдете на строгий параллелизм Swift и включите полный параллелизм в свой код, вы обнаружите, что ваш код стал более стабильным и менее подверженным трудноотлаживаемым проблемам, вызванным гонками данных.
Будущее производительности Threads iOS
По мере того как Threads будет расширяться в течение второго года и далее, приложение для iOS должно будет адаптироваться к новым задачам. По мере добавления новых функций продукта мы будем следить за такими проверенными временем показателями, как %FIRE, TTNC и cPSR, чтобы убедиться, что пользовательский опыт не ухудшится. Мы обновляем код, который доставляет вам сообщения, чтобы вы видели контент быстрее и видели меньше индикаторов загрузки. Мы продолжим использовать самые современные возможности языка Swift, что сделает приложение более стабильным и быстрым для разработки и загрузки в память. В то же время мы продолжаем итерации и развиваем такие инструменты, как SLATE, которые помогают нам улучшать тестирование и отлаживать регрессии.
Будучи частью сообщества Threads, вы также можете внести свой вклад в улучшение приложения. Мы уже упоминали, что сообщения об ошибках, отправленные пользователями, используются для определения областей, на которых команда разработчиков должна сосредоточиться, и для проверки того, что такие функции, как «Черновики», действительно решают проблемы пользователей. В приложениях Threads и Instagram вы можете нажать на вкладку «Главная» или встряхнуть телефон, чтобы отправить сообщение об ошибке. Мы действительно их читаем.