Site icon AppTractor

Измерение задержки старта iOS-приложений в масштабе Uber

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

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

Производительность приложений

Эта статья — первая в серии статей о том, как мы в Uber измеряем производительность и с какими проблемами сталкивается Uber в плане масштабируемости при измерении различных показателей производительности. Сегодня мы сосредоточимся на измерении производительности запуска приложений на iOS, но в следующих статьях будут рассмотрены другие показатели производительности и другие мобильные платформы.

В Uber мы отслеживаем множество критически важных показателей, начиная от задержки потока пользовательского интерфейса и заканчивая использованием памяти, от пропускной способности до замусоривания пользовательского интерфейса. В этой статье мы поговорим о критически важной метрике, являющейся стандартом отрасли — времени запуска приложения. Будучи одним из основных показателей производительности, который непосредственно влияет на клиентский опыт, мы обнаружили, что конечные пользователи имеют ограниченную терпимость к медленному приложению, особенно когда они хотят путешествовать и им нужно быстро добраться до места назначения.

Для запуска приложения мы специально измеряем холодную продолжительность запуска приложения, которая состоит из первого создания процесса приложения, инициализации нашего кода main.swift, различных сетевых вызовов для получения контента в реальном времени и первого прохода рендеринга, необходимого для отображения экрана. Холодный старт может произойти после новой установки, обновления приложения, первого запуска приложения после перезагрузки устройства, или если приложение было убито или иным образом удалено из памяти в предыдущей сессии. Это отличается от «горячего» запуска, когда приложение уже инициализировано, находится в памяти и просто выводится на передний план из фона.

В дополнение к указанию этих ключевых потоков внутри приложения для получения метрик после холодного старта, мы также создали конвейер данных для получения метрик частоты зависаний от Apple, что позволяет получить более глубокое представление о том, что происходит на уровне ОС.  Ниже мы расскажем о том, как мы измеряем эти показатели производительности и какие инструменты и процессы мы создали для того, чтобы не допустить регресса.

Задержка при запуске

Чтобы определить задержку запуска приложения, мы измеряем время, которое проходит с момента нажатия пользователем иконки до момента, когда пользователю становится доступно первое представление. В данном контексте первое представление — это первое View, которое принимает пользовательский ввод.

С введением функции предварительного прогрева в iOS 15, сама ОС может принимать решение о запуске процесса приложения в памяти на основе внешних условий, которые предвидят запуск приложения пользователем. Это сокращает время, которое пользователь должен ждать, пока приложение станет доступным при следующем его тапе на иконку. Но это нововведение усложнило процесс измерения задержки холодного старта для iOS, так как теперь уже не представлялось возможным указывать время от создания процесса до первого просмотра. Это привело к пересмотру процесса измерения задержки холодного запуска в Uber.

Измерение времени запуска до iOS 15

До внедрения предварительного прогрева мы использовали простой метод измерения задержки холодного запуска, при котором мы регистрировали время между определенными событиями («указателями») во время запуска и использовали это время для определения полной продолжительности задержки холодного старта.

Для этого мы разделили последовательность запуска на два основных интервала измерений, сумма которых и составила нашу задержку холодного запуска:

  1. Pre-main: Измеряется как время до вызова функции main() в приложении после создания процесса. Мы используем вызов ядра Mach, как показано ниже, чтобы получить время создания процесса в нашей точке входа main.swift.
  2. Post-main: Это время между завершением функции main() и первым интерактивным экраном.

Эти промежутки «pre-main» и «post-main» были далее разделены на другие подпространства, основанные на фазах задержки холодного запуска, которые мы хотели измерить. Вот как выглядело бы наше старое измерение запуска (без поддиапазонов):

Рисунок 1: Иллюстрация диапазонов измерения холодного старта до предварительного прогрева приложения

С введением предварительного прогрева в iOS 15 точка входа main() для приложения может быть вызвана до того, как пользователь коснется значка приложения, инициализируя последовательность запуска. Но из-за этого возникает пауза перед вызовом UIApplicationMain. Это приводило к завышенным значениям pre-main, поскольку наши диапазоны не учитывали эту паузу в последовательности запуска. В результате мы столкнулись со 130% увеличением общего времени запуска и ненадежным показателям.

Измерение времени запуска с предварительным прогревом

С предварительным прогревом наши «указатели» и показатели задержки запуска перестали быть надежными, что привело нас к необходимости поиска более детерминированных средств измерения задержки запуска нашего приложения.

В конечном итоге мы решили использовать MXAppLaunchMetric из набора MetricKit от Apple, но у него было несколько проблем, которые мы должны были решить, прежде чем использовать его в нашем случае:

  1. MXAppLaunchMetric измеряет время между инициализацией процесса приложения и вызовом didFinishLaunch() нашего приложения. Хотя это может быть подходящим косвенным показателем для измерения задержки запуска, это был бы регресс по сравнению с нашим предыдущим измерением, где мы включали время, необходимое для начальной визуализации пользовательского интерфейса приложения.
  2. Данные MetricKit измеряются для каждого пользователя на ежедневной агрегированной основе (окно последних 24 часов). Наши предыдущие измерения имели гранулярность измерения для каждой сессии.

Чтобы решить обе эти проблемы, мы создали новый конвейер метрик, который собирал данные MetricKit на уровне пользователя и объединял их с нашими пользовательскими измерениями на уровне сессии, чтобы создать новую метрику для задержки запуска.

Поскольку концепция pre-main больше не соответствовала нашим измерениям, мы придумали новые определения для фаз задержки запуска приложения:

  1. Pre-launch: Вместо нашего собственного измерения времени pre-main мы теперь используем MXAppLaunchMetric для получения времени от создания процесса до didFinishLaunch().
  2. Post-launch: Поскольку мы хотели измерить весь процесс запуска, мы все еще полагались на кастомное отслеживание времени от didFinishLaunch() до того момента в приложении, когда мы определили, что пользовательский интерфейс полностью отрисован.

Рисунок 2: Иллюстрация интервалов измерения холодного запуска с учетом предварительного прогрева приложения

Как и в предыдущих случаях, мы дополнительно разделили интервалы до и после запуска на основе последовательности запуска приложения. Например, чтобы добавить поддержку для приложений Uber, которые обрабатывали переходы состояния сцены через делегатов, мы разделили измерение диапазона после запуска на поддиапазоны «PostLaunchBeforeWindow» и «PostLaunchAfterWindow».

Рисунок 3: Иллюстрация диапазонов измерения холодного запуска с учетом предварительного разогрева приложения с помощью делегатов сцены

Поскольку мы полагаемся на MetricKit для получения метрик до запуска, мы можем получать их только на уровне пользователя каждые 24 часа. Однако, поскольку мы сами измеряем метрики после запуска, у нас есть гибкость в измерении задержки после запуска для каждой пользовательской сессии. В следующих нескольких разделах мы объясним, как эта гибкость помогла нам получить более полную картину показателей запуска нашего приложения.

Объединение метрик на основе пользователя и сеанса

Мы рано приняли решение не обрабатывать данные MetricKit на клиенте, а отправлять их в полуструктурированном формате JSON на бэкэнд. Для MXAppLaunchMetric данные представлены в виде гистограммы в форме MXHistogram, где каждый bin/bucket указывает количество раз, когда метрика запуска приложения попадала в определенный диапазон значений в течение 24-часового отчетного периода.

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

  1. Используя корзины, предоставленные в MXHistogram, мы можем анализировать, сколько раз у пользователя было высокое время запуска (т.е. количество времени до запуска, превышающее 5 секунд для одного пользователя).
  2. Это также позволило нам снять нагрузку с вычислений на устройствах пользователей и отделить изменения доступных данных MetricKit от наших регулярных еженедельных выпусков приложений.

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

Рисунок 4: Образец данных гистограммы, представляющей время запуска приложений MetricKit

Рисунок 5: Визуальное представление собранных данных гистограммы, представляющих время запуска приложения из MetricKit

В этих данных bucketStart — это начальное значение интервала для корзины, bucketEnd — конечное значение интервала, а bucketCount — количество наблюдаемых образцов, которые попадают в эту корзину.

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

Рисунок 6: Уравнение, используемое для преобразования гистограммированных данных MetricKit в скалярное значение

Используя это уравнение, мы смогли получить скалярное значение, которое представляет собой среднее значение для любой гистограммированной метрики, предоставляемой MetricKit.

Мы создали конвейер данных, который объединил следующие две метрики на уровне пользователя:

  1. Входящие данные из Metrickit, которые представляют среднюю задержку запуска пользователя, т.е. данные pre-launch за 24 часа, которые мы преобразовали в скалярное значение.
  2. Входящие данные из наших пользовательских данных post-launch, которые собираются для каждой сессии.

Объединение этих двух показателей на уровне пользователя дало нам более полную метрику задержки запуска. Мы также храним 50-й, 75-й, 90-й и 95-й процентили этих данных в отдельной базе данных, что дает нам более целостное представление о задержке запуска пользователя с течением времени.

Обработка данных MetricKit

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

В процессе обработки мы также преобразуем значения гистограмм в скалярные величины, такие как задержка запуска приложения и частота зависаний. Эти предварительно обработанные данные хранятся в промежуточном наборе данных, где мы переводим каждый JSON-ключ из данных MetricKit в более структурированный столбец реляционной базы данных.

Рисунок 7: Пример меппинга выходных метрик MetricKit JSON в столбцы

С помощью этих агрегированных данных мы можем ответить на некоторые вопросы, на которые MetricKit не может ответить сам по себе, например:

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

Заключение

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

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

В следующей статье этой серии мы планируем рассказать о том, как мы измеряем и решаем другие проблемы надежности, такие как утечки памяти, вызывающие проблемы, связанные с переполнением памяти (OOM), и отзывчивость мобильных приложений Uber.

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

Источник

Exit mobile version