Разработка
Одна наша ошибка в модуляризации iOS-приложения, которая замедлила процесс сборки в Xcode
Прежде чем запрашивать у менеджера новый чипсет M2 Pro, лучше сначала проверить процесс сборки кодовой базы.
Об этом никогда не упоминается в обычных статьях «Как заставить Xcode собирать быстрее«, а ведь это может происходить и с вашей кодовой базой.
Полгода назад я обновил свой рабочий ноутбук с обычного Macbook Pro с чипом M1 на Macbook Pro с чипом M1 Pro. Почувствовав себя вдохновленным, первое, что я сделал после настройки — скомпилировал свою рабочую кодовую базу, ожидая определенного скачка производительности в более быстром создании приложений. Однако этого не случилось… Я был шокирован тем, что разница оказалась не такой большой, как я ожидал. 🤔
Состояние приложения
Почти год работая в Stockbit, я поставил перед собой задачу сделать кодовую базу iOS масштабируемой и повысить удобство работы разработчиков. Поэтому оптимизация времени сборки в Xcode — это всегда актуальная задача. Мы провели множество исследований и сделали почти все то, о чем говорилось в статьях «Как сделать сборку Xcode быстрее». Мы были уверены, что в нашем конвейере сборки не было никаких серьезных или фатальных ошибок, замедляющих время компиляции Xcode. Большинство вещей, которые, как мы обнаружили, замедляли нашу работу, особенно в части быстрого времени компиляции, уже были оптимизированы в прошлом… По крайней мере, мы так думали.
В поисках виновника
Недовольный и разочарованный работой моего компьютера, я первым делом проверил монитор активности MacOS и посмотрел загрузку процессора.
Что происходит? Поэтому мой новый компьютер не показывает ожидаемых отличий? Это не очень хороший знак. Падение нагрузки на процессор в середине сборки говорит о том, что мое новое оборудование работает не так интенсивно, как могло бы, что очень печально.
Погуглив, я обнаружил, что Apple недавно выпустила новый встроенный в Xcode 14 инструмент визуализации временной шкалы сборки. Воспользовавшись им, я сделал еще более шокирующее открытие.
Доступ к этому инструменту можно получить через Report Navigator > Local > и выделить один из процессов сборки, щелкнуть правой кнопкой мыши и «Show in Timeline».
О чем говорит эта визуализация временной шкалы?
Современные процессы сборки Xcode используют модульность и параллелизм в процессе сборки. Горизонтальная ось слева направо показывает временную шкалу, а вертикальная ось — стек параллелизма, который показывает, какой процесс обрабатывается каждым потоком процессора.
Таким образом, красная линия с большим разрывом посередине показывает, что большой единичный процесс занял 1/3 времени сборки и затормозил другие дальнейшие процессы. И это… компиляция ассетов в нашем UI модуле. Этот единичный процесс не занимает все время процессора, но при этом нет других процессов, которые могли бы обрабатываться одновременно. И что самое неприятное, этот процесс настолько экстремален, что он самый большой единичный процесс, примерно в 3 раза дольше, чем второй по величине процесс во всей сборке.
Почему это происходит?
С самого начала нашего пути по модуляризации кодовой базы было принято решение поместить все, что связано с пользовательским интерфейсом, в отдельный модуль, а не во фреймворк каждого бизнес-юнита. Это было связано с тем, что многие пользовательские интерфейсы и ассеты являются общими для разных бизнес-фреймворков, и для удобства их использования мы поместили их в модуль более низкого уровня (UI Framework). Но, к сожалению, наш UI Framework находится в более высокой иерархии, чем другие низкоуровневые модули, такие как Utility, Networking или Core, поскольку U IFramework фактически все равно нуждается во всех этих модулях в качестве зависимостей.
Вот упрощенная диаграмма расположения UI-фреймворка в иерархии зависимостей.
Другими словами, все наши низкоуровневые модули не имеют горизонтальных связей и UIFramework является единственным узким местом. В сочетании с «удобством» простого перебрасывания ассетов (большинство из которых на самом деле принадлежат другим бизнес-модулям) в этот конкретный модуль UIFramework. В итоге это негативно сказывается на процессе оптимизации времени сборки.
Как это доказать?
Итак, у нас есть самые главные подозреваемые. Мы можем набросать кучу теорий и обвинять UIFramework в качестве «паршивой овцы» хоть целый день. Но можем ли мы доказать это?
Любой, кто работал над модульной кодовой базой в iOS, должен знать, что управление ассетами — одна из самых сложных задач в модульной iOS, необходима вспомогательная функция для загрузки UIImages из кросс-модулей, особенно с учетом ограничений XIB, который не может загружать наборы изображений и наборы цветов из кросс-модулей и т.д. Хотя все это пока еще только новые находки, возвращение всех наборов изображений на свои места в бизнес-модулях более высокого уровня всегда требует больших усилий, связанных с риском сбоя или отказа в загрузке активов. Поэтому, прежде чем что-то предпринимать, мы должны быть уверены, что аудит и перемещение всех активов стоит того, чтобы их выполнять.
Таким образом, мы пришли к набору экспериментов.
#1 Удалим весь .xcassets в UIFramework и соберем заново
Наш первый свидетель — результат мониторинга загрузки процессора через тот же Activity Monitor. После удаления всего xcassets в модуле UIFramework загрузка процессора моей машины оставалась полной весь процесс сборки с самого начала и до его завершения.
Затем вернемся к визуализации времени сборки. На этот раз мы не видим большой линии разрыва посередине. Все процессы параллельны и достаточно плотные, чтобы быть показанными на временной диаграмме.
Ну, поскольку мы откровенно удалили активы, очевидно, что время сборки будет намного быстрее, поскольку, по сути, мы просто удалили много процессов, которые нужно сделать.
Тем не менее, мы видим, что процессор по монитору активности и визуализации временной шкалы, используется полностью, в отличие от исходного состояния.
#2 Случайное перемещение 70% ассетов из UIFramework в модуль BusinessA
После того как эксперимент №1 показал многообещающие улучшения, мы провели аудит всего набора изображений, который показал, что около 70% активов, размещенных в UIFrameworks, могут быть перемещены в различные вышестоящие бизнес-модули. Поэтому в этот раз, вместо того чтобы удалять активы внаглую, я попытался случайным образом выбрать около 70% активов для перемещения в один более высокий бизнес-модуль фреймворка.
Получилось примерно следующее:
На временной шкале все еще есть разрыв, что свидетельствует о наличии узкого места в процессе. Но это гораздо лучше, и даже при том же количестве ассетов (ничего не удалено) время сборки сократилось примерно на 20-25%! Это еще раз доказывает, почему этим нужно и стоит заниматься.
На временной шкале появилась новая большая зеленая линия, в которую переместились наши прежние 70% активов из UIFramework. При этом остальные процессы сборки по-прежнему идут целостно и плотно. Загрузка процессора также не показывает снижения в этот период, хотя чрезвычайно длинный процесс все еще работает внутри. Почему? Из-за горизонтальной связи между всеми вышестоящими модулями! Мы уже решили предыдущую проблему с бизнес-модулями, поэтому все они независимы друг от друга, даже если есть один конкретный процесс, который занимает много времени, как показано на предыдущей визуализации, он не будет мешать всем остальным процессам и не будет создавать узких мест!
Наш план решения
С учетом всех приведенных доказательств становится ясно, что это проблема, которую стоит решить для улучшения нашей кодовой базы. В данный момент мы пытаемся отсеять все лишние ресурсы, размещенные в UIFramework, и проверить, можно ли их перенести или, что более правильно, разместить в функциональном модуле более высокого уровня.
Очевидно, что с кодовой базой, которая уже наверняка достаточно велика, чтобы иметь проблемы с узкими местами при компиляции, перемещение активов должно быть тщательно проверено и протестировано. В настоящее время мы не торопимся решать эту проблему настолько тщательно, насколько это возможно, и планируем сделать это в несколько отдельных шагов в течение многих итераций. Если в вашей кодовой базе тоже есть такая проблема, мы призываем вас сделать то же самое.
Более быстрая сборка и устранение проблемы зависимостей — это прекрасно, но отсутствие недостающих изображений, которые могут вызвать неприятные ощущения у пользователей, недопустимо и не подлежит обсуждению.
Заключение
- Иногда обновление имеющейся машины до более современной, новой и дорогой просто нецелесообразно. Поэтому, прежде чем запрашивать у менеджера новый чипсет M2 Pro, лучше сначала проверить процесс сборки кодовой базы.
- Всегда проверяйте реальный процесс сборки. Точка. В новом Xcode 14 появилась новая встроенная функция визуализации, которую вы, возможно, захотите проверить. Кто знает, может быть, и для вас это окажется сюрпризом.
- Возможно, вы уже сделали многое из того, что перечислено во многих других статьях на тему «Как ускорить процесс сборки в Xcode», но иногда проблема более специфична для кодовой базы вашей организации или даже архитектурного уровня, как это произошло в данном случае.
- Всегда старайтесь делать модульные слои как можно более горизонтальными на каждом уровне. Я не могу не подчеркнуть этого. Иногда, когда есть неизбежный единичный процесс, который занимает много времени, он не будет затягивать все остальные процессы сборки, расположенные на том же уровне.