Как мы ускоряли работу отладчика Swift

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет! Меня зовут Александр Скворцов, я работаю в команде Яндекс.Браузера для iOS. Это очень большой проект, который насчитывает около тысячи clang-модулей и примерно 600 Swift-модулей. Наверное, из-за таких масштабов мы чаще других наталкиваемся на проблемы инструментов разработки, например, находим критические ошибки в компиляторе, неработающую подсветку и автодополнение. Это бывает неприятно, но жить можно. 

Самая серьёзная проблема возникла с отладкой. В худшем случае с момента запуска до остановки в отладчике на точке входа в приложение проходило больше 20 минут. И это на свежем MacBook Pro 16! С таким «быстродействием» инструментов разработки невозможно эффективно развивать проект, поэтому мы решили разобраться в причинах и поискать возможные решения. В результате получилось не только снять остроту проблемы у себя, но и внести правки в код отладчика Swift — со временем описанные в статье неприятности перестанут беспокоить всех пользователей Xcode. А теперь расскажу подробнее, как это было.

История уходит корнями в появление замечательного языка Swift, задуманного как замена Objective-C для разработки в экосистеме Apple. Много воды утекло с момента выхода первой версии: значительно изменился синтаксис, расширились возможности стандартной библиотеки, добавлено много синтаксического сахара, облегчающего жизнь, появился стабильный ABI. Но кое-что (надёжность инструментов разработки) сохранилось практически в первозданном виде…

Механизмы интеграции Swift с кодом на других языках

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

Bridging Header

Тривиальный способ, он не требует предусловий от импортируемого кода. Bridging Header — привычный для разработчиков на Objective-C заголовочный файл, в котором с помощью директивы #import перечисляется список заголовков, содержимое которых нужно использовать в коде на Swift. Однако, у этого способа есть существенный недостаток: Swift-модуль, в котором используется Bridging Header, нельзя импортировать в другой Swift-модуль (на самом деле можно, если вы компилируете без Xcode, но цена этого — сложнорешаемые проблемы со сборкой и отладкой). Поэтому он подходит только для модулей, содержащих точку входа в приложение, а мы из-за этого ограничения полностью отказались от Bridging Header-ов.

Clang-модули

Альтернативным способом интеграции с C и Objective-C являются clang-модули. Это более универсальный подход, так как, в отличие от Bridging Header-ов, clang-модули не накладывают ограничений на Swift-код. Что важно, ведь Swift уже стал основным языком разработки под iOS.

Для оптимизации по времени при компиляции Swift для clang-модулей используется аналог предварительно откомпилированных заголовков: создаётся неявно управляемый компилятором кеш из предварительно откомпилированных модулей, который позволяет не интерпретировать clang-модуль заново каждый раз при обработке директивы import в Swift-коде. Компиляция clang-модуля занимает нетривиальное количество времени (до нескольких секунд), поэтому при большом количестве модулей в проекте наполнение кеша бывает довольно долгим. Сейчас (в версии Swift 5.3) единственная настройка поведения кеша модулей — путь к нему, но уже анонсирован механизм явной сборки.

В поисках проблемы

К счастью, в сборке lldb, которая поставляется в составе Xcode, есть названия символов, поэтому процесс lldb-rpc-server можно информативно профилировать. В результате профилировки, как я уже писал выше, выяснилось, что основную часть времени при запуске занимает как раз компиляция clang-модулей:

14.15 min  100.0% 0 s   lldb-rpc-server (42247)
14.15 min   99.9% 0 s    thread_start
14.15 min   99.9% 0 s     _pthread_start
12.99 min   91.7% 0 s      threadFuncSync(void*)
12.99 min   91.7% 0 s       RunSafelyOnThread_Dispatch(void*)
12.99 min   91.7% 0 s        llvm::CrashRecoveryContext::RunSafely(llvm::function_ref<void ()>)
12.99 min   91.7% 0 s         void llvm::function_ref<void ()>::callback_fn<compileModuleImpl(clang::CompilerInstance&, clang::SourceLocation, llvm::StringRef, clang::FrontendInputFile, llvm::StringRef, llvm::StringRef, llvm::function_ref<void (clang::CompilerInstance&)>, llvm::function_ref<void (clang::CompilerInstance&)>)::$_3>(long)

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

Но нас всё ещё не устраивает время запуска отладчика, поэтому копаем дальше. Включаем логи отладчика по инструкции и смотрим на них в надежде увидеть хоть что-то необычное… А вот и результат — натыкаемся на очень интересную строчку:

Extra clang arguments        : (28814 items)

После неё идёт длинный список из аргументов, состоящий в основном из флагов -I, -F и -fmodule-map-file=, значения которых повторяются. Такая находка наводит на мысль о возможной неэффективности процесса сборки clang-модулей, который и занимает львиную долю времени запуска отладчика. Дело за малым — разобраться, насколько это предположение близко к истине.

Об особенностях работы отладчика Swift

Прежде чем приступить к проверке гипотезы, следует немного углубиться в детали работы отладчика Swift. Формат DWARF, использующийся для C, Objective-C и C++, не позволяет записать всю необходимую для вычисления выражений информацию. Поэтому частично она хранится в файлах swiftmodule, которые являются одним из артефактов сборки Swift-кода. Абсолютный путь до каждого swiftmodule записывается линковщиком в исполняемый файл, чтобы отладчик мог читать из него нужную информацию.

Интересующая нас часть отладочной информации — список аргументов clang. Он формируется на основе параметров компилятора Swift, передаваемых при сборке модуля. Найденная аномалия заключается в дублировании аргументов clang, поэтому важно убедиться, что оно происходит не по нашей вине — такое дублирование может происходить в том числе из-за неправильной работы системы сборки. Смотрим подробный отчёт вызываемых при компиляции команд и видим, что значения параметров -I, -F и -fmodule-map-file= уникальны внутри каждого вызова компилятора, поэтому переходим к изучению отладчика.

We need to go deeper

Первым делом скачиваем исходный код и находим место, в котором печатается интересующая нас строка лога:

  swift::ClangImporterOptions &clang_importer_options =
      GetClangImporterOptions();

  log->Printf("  Extra clang arguments        : (%llu items)",
              (unsigned long long)clang_importer_options.ExtraArgs.size());
  for (std::string &extra_arg : clang_importer_options.ExtraArgs) {
    log->Printf("    %s", extra_arg.c_str());
  }

Путём нехитрого анализа окружающего кода выясняем, что дублирующиеся аргументы добавляются в этом методе, который в цикле вызывается вот отсюда:

  std::function<void(ModuleSP &&)> process_one_module =
      [&](ModuleSP &&module_sp) {
        <...>

        SwiftASTContext *ast_context =
            llvm::dyn_cast_or_null<SwiftASTContext>(&*type_system_or_err);
        if (ast_context && !ast_context->HasErrors()) {
            <...>

            swift_ast_sp->AddExtraClangArgs(ast_context->GetClangArguments());
          }
        }
      };

  for (size_t mi = 0; mi != num_images; ++mi) {
    process_one_module(target.GetImages().GetModuleAtIndex(mi));
  }

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

Как видно из кода, при формировании конечного списка аргументы, прочитанные изswiftmodule, предварительно обрабатываются: 

  • отбрасывается флаг -Werror, чтобы не вызывать лишних ошибок при интерпретации выражений в консоли отладчика; 

  • относительные пути дополняются до абсолютных; 

  • и, наконец, устраняются дублирующие определения макросов

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

В этом плане есть серьёзные изъяны: 

  • во-первых, чтобы проверить гипотезу путём внесения изменений в код отладчика, нужно его скачать и скомпилировать, что займёт немало времени и места на SSD;

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

Что ж, нам ничего не остаётся, кроме как искать обходной путь.

Обходной путь

Для интерпретации выражений в консоли отладчика создаётся некий контекст компиляции Swift. Его параметры должны разрешать использование любых типов или функций из любого модуля в исполняемом файле. Как мы уже выяснили, информация о clang-модулях попадает в контекст через пути поиска, прочитанные из файлов swiftmodule, которые, в свою очередь, добавляются в исполняемый файл линковщиком. Фрагмент аргументов линковщика выглядит так:

-add_ast_path path/to/swift/module1.swiftmodule
-add_ast_path path/to/swift/module2.swiftmodule
...
-add_ast_path path/to/swift/moduleN.swiftmodule

Мы хотим избежать конкатенации путей поиска из всех N файлов swiftmodule, которая приводит к дублированию и неконтролируемому росту списка аргументов clang в контексте компиляции Swift. Для этого можно попробовать передать линковщику только один swiftmodule. Здесь нас поджидает небольшая проблема: хочется не только ускорить запуск отладчика, но и не потерять его работоспособность.

Значит, единственный swiftmodule должен предоставлять объём отладочной информации, эквивалентный тому, что даёт полный набор. Проанализировав код отладчика, обнаруживаем, что из swiftmodule берутся только аргументы clang, поэтому сформировать нужный нам «супермодуль» не составит труда — он должен состоять из одного файла, где будут перечислены директивы import каждого clang-модуля, входящего в исполняемый файл:

import ClangModule1
import ClangModule2
...
import ClangModuleN

Такое содержимое позволяет убедиться, что мы используем полный список путей поиска. В противном случае при обработке проблемной директивы import возникнет ошибка компиляции.

Как мы уже знаем, внутри отдельно взятого swiftmodule список аргументов clang уникален, поэтому заменяем наши многочисленные -add_ast_path на единственный -add_ast_path path/to/swift/supermodule.swiftmodule, и наблюдаем искомый результат:

Extra clang arguments        : (962 items)

Новая профилировка запуска отладчика показывает, что гипотеза о неэффективности компиляции clang-модулей подтвердилась:

3.99 min  100.0%  0 s   lldb-rpc-server (44423)
3.99 min   99.9%  0 s    thread_start
3.99 min   99.9%  0 s     _pthread_start
2.96 min   74.3%  0 s      threadFuncSync(void*)
2.96 min   74.3%  0 s       RunSafelyOnThread_Dispatch(void*)
2.96 min   74.3%  0 s        llvm::CrashRecoveryContext::RunSafely(llvm::function_ref<void ()>)
2.96 min   74.3%  0 s         void llvm::function_ref<void ()>::callback_fn<compileModuleImpl(clang::CompilerInstance&, clang::SourceLocation, llvm::StringRef, clang::FrontendInputFile, llvm::StringRef, llvm::StringRef, llvm::function_ref<void (clang::CompilerInstance&)>, llvm::function_ref<void (clang::CompilerInstance&)>)::$_3>(long)

При этом отладчик остаётся исправным, а время старта сокращается примерно до 4 минут! Конечно, это всё ещё невыносимо долго, но уже намного лучше, чем было раньше, поэтому создаём пул-реквест и радуем коллег.

Вместо заключения

Описанный способ невозможно реализовать, если вы используете встроенный xcodebuild для сборки проекта. К счастью, это не наш случай — размеры проекта не позволяют ограничиться стандартными инструментами. Мы работаем с нестандартным тулчейном, основанным на gn, благодаря которому удалось относительно легко провернуть манёвр с «отладочным»swiftmodule.

Решив проблему локально, мы не забыли внести правки (раз, два) в код отладчика. Со временем дублирование аргументов clang останется в прошлом для всех пользователей Xcode. Однако наша война ещё не окончена — 4 минуты на запуск отладчика (напомню, что это худший случай: после очистки DerivedData и перезапуска Xcode) нас не устраивают. А значит, возможно не менее увлекательное продолжение истории. Stay tuned!

Источник: https://habr.com/ru/company/yandex/blog/544734/


Интересные статьи

Интересные статьи

Мне было необходимо делать 2 раза в сутки бэкап сайта на «1С-Битрикс: Управление сайтом» (файлов и базы mysql) и хранить историю изменений за 90 дней. Сайт расположен на VDS под уп...
Сравнивать CRM системы – дело неблагодарное. Очень уж сильно они отличаются в целях создания, реализации, в деталях.
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...
Этот пост будет из серии, об инструментах безопасности, которые доступны в Битриксе сразу «из коробки». Перечислю их все, скажу какой инструмент в какой редакции Битрикса доступен, кратко и не очень р...
Доброго времени суток, уважаемое Хабросообщество. Год назад был точно такой же весенний день, как и сегодня. Я как обычно ехал на работу на общественном транспорте, испытывая все те прекрасны...