Время запуска приложения — одна из самых важных составляющих при создании мобильных приложений. Если приложение запускается медленно, пользователи могут разочароваться и переключиться на другое приложение даже не дожидаясь, пока оно загрузится. В FullStory (платформа записи видео того, что делает пользователь в приложении) мы стремимся помочь нашим клиентам создать идеальный цифровой опыт, выявляя и количественно оценивая негативные взаимодействия с пользователями (например, «мертвые» клики, «яростные» клики, долгое время загрузки и т.д.) и визуализируя этот опыт с помощью Session Replay. Когда наши клиенты интегрируют наш SDK в свои мобильные приложения, мы хотим автоматически фиксировать как можно больше таких впечатлений с минимальным воздействием на пользователя.
Однако недавно один из клиентов указал, что, добавив наш SDK в свое приложение, он заметил увеличение времени запуска приложения на 6%, даже когда наш SDK не был включен, что показалось очень подозрительным. После того как клиент поделился отчетом от Emerge Tools, мы смогли найти быстрый способ сократить время запуска SDK на Android на 75%.
Проблема
Клиент, оценивавший наш SDK, написал нашей команде, что он заметил увеличение времени запуска приложения после добавления нашего SDK. Во многих случаях такие отчеты имеют тенденцию быть очень ненаучными, поэтому мы задаем нашим клиентам много вопросов, включая:
- Какую часть запуска они измеряют?
- Какое устройство они используют для измерения?
- Какова была методология?
- Сколько раз они запускали приложение? Было ли это сделано после свежей установки или после некоторого разогрева?
Без этих деталей и надлежащей среды тестирования производительности очень трудно определить первопричину ухудшения производительности или влияющие на нее факторы. В данном случае мы задали вышеупомянутые вопросы потенциальному заказчику, и он поделился скриншотом отчета, сгенерированного Performance Analysis от Emerge Tools. В отчете указывалось, что некоторые методы FullStory влияют на производительность запуска примерно на 6% в приложении клиента, что в тестовой среде составляет около 100 мс.
Хотя это и не было огромным числом, оно удивило нас. Наш SDK не должен так сильно влиять на время запуска. Даже отключение SDK через опции конфигурации gradle-плагина не уменьшило бы влияние, потому что нам все равно нужно запускать инициализацию SDK при запуске приложения.
Что нам показал Emerge
При запуске приложения мы стараемся делать как можно меньше работы, пока не подтвердим, что сессия началась.
- Мы загружаем наш нативный код из нашего общего ядра Rust.
- Мы читаем конфигурационный файл из нашего gradle-плагина, чтобы убедиться, что recordOnStart не является ложным.
- Если FullStory настроен на включение, мы запускаем фоновую задачу для выполнения запроса к FullStory, чтобы получить правила конфиденциальности и убедиться, что захват данных включен.
Это происходит в ContextWrapper#attachBaseContext класса Application. Если внимательно посмотреть на отчет Emerge Report, которым поделился клиент, то большой блок времени в этом методе был связан с вызовом в нашем SDK процедуры Class#getResourceAsStream.
В блоге NimbleBlog есть фантастическая статья, углубляющаяся в исходный код Android, чтобы объяснить, почему этот метод работает медленно, и я настоятельно рекомендую прочитать всю статью, чтобы получить более глубокое понимание того, как этот метод работает в Android. Для наших целей этот метод немного избыточен, учитывая то, для чего мы его использовали: чтение файла конфигурации.
Первоначальное решение — использование ассетов Android
С пониманием проблемы решение кажется довольно очевидным — не использовать ClassLoader#getResourceAsStream. С помощью некоторого творческого осмысления я смог переместить файл конфигурации в каталог ассетов Android. Как упоминалось в заметке NimbleBlog, система Resource.get*(resId) в Android позволяет избежать замедления работы getResourceAsStream, что делает доступ к активам намного быстрее.
Поскольку мы еще не были клиентами Emerge, нам пришлось вручную проводить измерения в нашем SDK, чтобы понять влияние этого изменения. Общий процесс описан ниже:
- Добавить несколько измерений в наш метод инициализации.
- Написать сценарий для запуска приложения и его завершения.
- Запустить тест на устройстве несколько раз, чтобы собрать достаточно данных (80 проб на Sony Xperia X compact под управлением Android 8).
- Собрать результаты в Google Sheet.
Мы запустили этот процесс на нашей последней ветке релиза, а также на ветке с исходной версией, и сравнили результаты средних измерений.
Первоначальное влияние: Скорость инициализации SDK улучшилась на 50% для нашего примера приложения.
Количественно цифры улучшились с ~270 мс до ~125 мс, но на сырые цифры не стоит полагаться, поскольку они могут меняться в зависимости от множества различных факторов, включая характеристики APK, характеристики устройства, версию ОС, холодный и теплый запуск и т.д. Наш образец приложения специально создан для тестирования широкого спектра сценариев работы приложений, но это не такой большой APK, как обычно бывает у пользовательских приложений, поэтому можно ожидать, что влияние на клиентские приложения будет еще более выраженным.
Вооружившись данными и измерительным комплектом и никогда не желая останавливаться на достигнутом, я начал задаваться вопросом, не скрываются ли еще какие-нибудь улучшения на виду.
Бонус: откладывать, откладывать и еще раз откладывать
При более внимательном рассмотрении кода и данных я все еще был немного удивлен тем, сколько времени потребовалось на некоторые задачи. Один из аспектов использования конфига заключается в том, что он содержит две «категории» конфигурации: конфигурация из плагина gradle и метаданные об APK, которые нам нужны, когда мы подтвердили, что должны собирать данные. Но для клиентов, которые используют нашу конфигурацию recordOnStart для запуска захвата только через вызов API, мы все еще читаем и разбираем динамические метаданные, которые нам нужны только тогда, когда наш SDK действительно начинает записывать данные. В зависимости от приложения, метаданные могли быть довольно большими, и я начал задумываться — действительно ли нам нужно разбирать всю эту информацию при запуске?
С помощью некоторого рефакторинга мне удалось разделить конфигурацию на два отдельных файла, которые можно было читать независимо друг от друга. Во время запуска мы считывали конфигурацию gradle, которая была намного меньше, а когда мы подтверждали, что должны начать захват сессии, мы считывали метаданные, неся затраты на разбор этих метаданных вне основного потока.
Чтобы измерить влияние этого изменения, мы можем повторно использовать вышеприведенную схему. Однако важно было понять и измерить, что мы также добавляем время между подтверждением сеанса и фактическим захватом первого кадра. Чтобы учесть это, я добавил несколько измерений, чтобы увидеть влияние.
Результаты
По сравнению с нашей релизной веткой, инициализация SDK улучшилась на 75% для нашего примера приложения. Однако время чтения метаданных при запуске сессии увеличилось примерно на 116%. Но это увеличение наблюдается вне основного потока, что означает, что оно не повлияет на цифровой опыт конечного пользователя и делает второй подход более предпочтительным решением.
В качестве проверки здравомыслия я хотел убедиться, что время до первого сканирования не слишком сильно влияет на пользователя, и я обнаружил, что оба подхода примерно одинаковы. Видно, что второй подход немного медленнее первого (предположительно потому, что нам приходится дважды читать/открывать ассеты).
Резюме
Без измерения данных легко не заметить, как такие простые задачи, как чтение конфигурационного файла, могут оказать неожиданное влияние на производительность. Благодаря тому, что клиент поделился данными, мы смогли определить и устранить узкое место в производительности, на которое в противном случае не обратили бы пристального внимания. Мы гордимся тем, что помогаем нашим клиентам измерять то, что важно, и Emerge помогает нам измерять то, что важно для нашего SDK.