У чому насправді проблема, коли твій проєкт повільно компілюється на сучасному «залізі»

Всім привіт! Мене звати Павло, я розробник, і маю щастя спіпрацювати з компанією MacPaw. Я в більшості маю справу з проектом CleanMyMac X, який є доволі великим macOS-застосунком, написаним, в основному, на Objective-C та Swift. Це складний, достатньо зрілий проєкт з багатьма залежностями.

Як і будь-який інший великий проєкт, він має свої власні проблеми, такі, наприклад, як повільна швидкість компіляції. Свого часу ми в команді реалізували різні рішення для прискорення процесу компіляції: інкрементальні збірки, попередньо зібрані фреймворки, оптимізацію налаштувань збірки та структури проєкту, максимізацію паралельної компіляції тощо. Мушу сказати, що це дозволило скоротити час збірки до задовільного рівня.

Однак, в проєкті була дуже цікава проблема, яка достатньо довго турбувала мене. Компіляція декількох конкретних файлів у проєкті займала невиправдано довгий час (так, розумію, все відносно). Я витратив досить багато часу на пошук причини. І ось, що я знайшов.

Ця історія почалася в наприкінці 2019 року...

Проблема

CleanMyMac X розбитий на декілька модулів, відповідно до функціональності. У нас є утиліти, прошарок рівня моделей, а також функціональні модулі, які побудовані на основі базових. На самому верхньому рівні ми маємо декілька застосунків, у яких власний UI та бізнес-логіка.

Ось тут трошки спрощена діаграма структури проєкту:

Так ось, коли збирався проєкт, більшість таргетів з нижніх рівнів збиралися дуже швидко, як і має бути, але на на рівнях повище, десь в районі модулів функціонального рівня (Functional Module X), з’являлися декілька файлів, які компілювалися занадто довго.

Ну, скажіть, хто очікує, що один .m файл буде компілюватися 40(!) секунд на сучасних (на той момент) лептопах? Особливо, коли в файлі менше 10 рядків коду, і половина — коментарі. В той самий час, коли інші модулі компілюються 10-20 файлів за секунду або навіть швидше.

Швидкість компіляції деяких файлів з Functional Module X

Мабуть, єдине, що в цій ситуації було позитивного — це те, що це траплялося виключно на чистих збірках. Інкрементальні збірки були швидкими, і таких проблем не виникало. Але кожен розробник Xcode знає, що дуже багато речей можна виправити, виключно почистивши DerivedData.

Розслідування, частина 1. Спляча Красуня

З того, що мені було відомо, — clang (компілятор, що використовує Xcode для Objective-C коду) для компіляції кожного .m файлу створює окремий процес. Тому я вирішив подивитися, що саме відбувається під час збірки з цими процесами.

І от те, що я побачив, було дуже неочікуваним. 16 процесів clang з гордістю висіли в менеджері задач і виконували належну роботу. Якщо бути точнішим, то вони спали. Всі, крім одного. Той самий, «обраний», виконував якусь роботу і навантажував лище одне ядро CPU на ~66%.

Власне, тут і 64 ядер не допомогло б, з таким-то розподіленням ресурсів

Нутрощі Clang

Я зробив sample clang-процесів і перевірив стек викликів. Зі стеку було зрозуміло (ха-ха), що більшість процесів на щось чекають і, відповідно, поки нема що робити — спокійно сплять.

Було досить складно зрозуміти, що саме відбувається, оскільки в стеку викликів не було імен функій та методів — лише їх адреси. А, зважаючи на те, що Xcode — це closed source, то файли з дебаг-інформацією було годі й шукати. Тому через деякий час, після довгого гугління, я вирішив, що мені потрібно мати свій власний clang з БлекДжеком та дебаг-логами.

Я знав, що clang — це open source проєкт, тому, знову після гугління, я склонував репозиторій з GitHub і почав пробувати його зібрати.

Після декількох днів гри з документацію, запитів на StackOverflow та обговорень в приватних повідомленнях, мені нарешті вдалося зібрати свою власну версію clang. Ох, це був той ще досвід.

Найскладнішим з усього було зрозуміти, які параметри треба «згодувати» cmake, щоб зібрати проєкт. LLVM — це дуже великий проєкт, який підтримує дуже багато різних систем збірки, архітектур та платформ. Тому моїм основним завданням було прибрати все непотрібне і залишити тільки важливі для мене речі (власне clang).

Десь в глибинах мого попереднього досвіду я пам’ятав, що для того, щоб підмінити компілятор для проєкту в Xcode, достатньо змінити дві змінні в `xcconfig` файлі:

CC=~/Projects/llvm/install/bin/clang
CCX=~/Projects/llvm/install/bin/clang++

Звичайно, з першого разу нічого не спрацювало ¯\_(ツ)_/¯. Мене завалило десятками помилок на кожен файл, до того ж помилки не були достатньо інформативними (з першого погляду, звичайно). Компілятор лаявся на те, що він не розуміє якихось прапорців, що йому підсовує Xcode. Щось завертілося в моїй голові, і я зрозумів, що «Це не ̶ті̶ ̶д̶р̶о̶ї̶д̶и̶ той, clang який я шукаю».

Я згадав, що у Apple є власна версія clang. І, чесно кажучи, в той момент ця думка мене дещо засмутила, оскільки Apple часто не ділиться своїми кодом. Але, на щастя, версія clang, яку використовує Xcode, була з відкритим кодом, тому я просто повторив все з новим репозиторієм.

Через ще декілька годин мені нарешті вдалося зробити так, щоб проєкт успішно збирався з моєю версією clang. Настав час перевіряти, хто там і чому вирішив товкти воду в ступі.

Блокування, блокування, блокування

З власним компілятором справи пішли трошки швидше. Тепер я міг бачити назви функцій в стеку викликів процесів. Швидко стало зрозуміло, що всі процеси чекають на одне й те саме блокування (lock).

Трошки детальніше, про те, що відбувалося. Коли Xcode починав збирати Module X, він запускав багато clang процесів — по одному для кожного .m файла в модулі. Щоб зібрати Module X, спочатку треба було зібрати Module1 та Module2.

Загалом, мої очікування були такими, що коли Module X почне збиратися, всі його залежності будуть вже зібрані, і не треба буде їх збирати знову. Але щось було не так.

Коли clang бачить залежність файлу, який він компілює, наприклад @import Module1, то він спочатку перевіряє, чи вже є необхідна інформація про інтерфейс Module1. Якщо така інформація є, то clang одразу її використовує. Якщо ж такої інформації немає, то доводиться чекати, поки хтось її збере, або збирати самому. Щоб не збирати цю інформаціюю багато разів, clang використовує блокування на рівні файлів.

Cхема блокування при доступі до загального ресурсу була зрозуміла, але в реалізації було декілька нюансів. Кожен раз, коли процес не міг отримати доступ до залежності через блокування, він спав. І час цього сну збільшувався з кожною невдалою перевіркою.

Ось декілька рядків з коду LockFileManager.cpp:

/// For a shared lock, wait until the owner releases the lock.
/// Total timeout for the file to appear is ~1.5 minutes.
/// \param MaxSeconds the maximum total wait time in seconds.
WaitForUnlockResult waitForUnlock(const unsigned MaxSeconds = 90);

WaitMultiplier *= 2;
if (WaitMultiplier > MaxWaitMultiplier) {
    WaitMultiplier = MaxWaitMultiplier;
}

Це призводило до того, що навіть коли залежність була розблокована, процес міг продовжувати спати ще декілька секунд. А через те, що таких залежностей було багато, і вони могли мати дуже велику вкладеність, це призводило до того, що час очікування збільшувався, а за ним і час на сон.

Мені тоді, чомусь, ця ситуація нагадала ситуацію з фільму Braveheart.

Рішення

Що ж, єдине рішення, яке мені прийшло того часу на думку, — створити Pull Request до репозиторію llvm. Я, звичайно, очікував, що буде достатньо просто створити PR на Github і перевірити, а потім змержити. Але `llvm` має досить складний процес внесення змін: треба створити патч і надіслати його на поштову розсилку, потім знайти того, хто цей патч подивиться, потім чекати на ревью, і потім ще чекати, поки хтось його, можливо, змеджить.

Не зважаючи на це, я все ж таки вирішив спробувати. Я подумав, як можна зменшити час сну, при очікуванні. Через те що в llvm використовувалися блокування на рівнів файлів, я подумав, що має бути спосіб підписатися на системні повідомлення при видаленні файлу блокування.

Я знайшов бібліотеку kqueue, яка, власне, і реалізовувала цю функціональність. Вона дозволяла підписатися на повідомлення при змінах у файловій системі. І, що не менш важливо, ця бібліотека була доступна на macOS, тому не треба було тягнути додаткові залежності.

Мені вдалося створити патч до репозиторію llvm. В ньому, замість того, щоб спати при очікуванні на розблокування, clang підписувався на системні повідомлення про файл блокування. Як тільки файл блокування зникав, clang відразу ж продовжував роботу.

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

  • llvm написаний на C++, а я от зовсім, ніяк не C++ розробник. Ніколи й ніде не вивчав С++;
  • llvm — це велетенський проєкт, тому пошуки потрібних місць в коді були досить складними;
  • я це робив у свій вільний час, і це накладало деякі обмеження;
  • процес внесення змін до репозиторію був для мене новим і, маю сказати, не зовсім прозорим і зрозумілим.

Хоч би там як, я дотиснув себе і залив патч на llvm reviews site. Якимось чином я знайшов потрібних людей, і навіть зробив пару підходів з оптимізацією і рефакторингом.

Стан на сьогодні

На жаль, мої змінні не були втягнуті до проєкту. Виявилося, що був інший PR, який також вирішував проблему, але трохи інакше. Один з розробників, Ladd, запропонував просто обмежити максимальний час, коли компілятор буде спати, поки очікує на розблокування.

// Since we don't yet have an event-based method to wait for the lock file,
// implement randomized exponential backoff, similar to Ethernet collision
// algorithm. This improves performance on machines with high core counts
// when the file lock is heavily contended by multiple clang processes
const unsigned long MinWaitDurationMS = 10;
const unsigned long MaxWaitMultiplier = 50; // 500ms max wait
unsigned long WaitMultiplier = 1;
unsigned long ElapsedTimeSeconds = 0;

Ну, що ж, маю сказати, що це — гарне рішення. Просте, зрозуміле і не вимагає використання складної логіки з використанням системних файлових повідомлень. На поточний момент саме це рішення і використовується в llvm.

Що стосується мого патчу, то він все ще відкритий. І ви спокійно можете його переглянути, а може, якщо буде бажання, пропушити 🙂. Я найближчим часом лізти до llvm-коду не збираюся.

Висновки

Впродовж цієї історії я отримав багато досвіду, багато що побачив і потицяв паличкою. Можливо, навіть чомусь навчився. Коли справді хочеться докопатися до суті, тебе не зупиняють складнощі на кшталт незнайомої мови програмування, великого проєкту, якихось невідомих раніше складних процесів. А відчуття, коли насправді вдається докопатися до суті (хє-хє) — складно з чимось порівняти.

Тоді я продовжив використовувати свою версію clang для себе. Я запропонував її використовувати в нашій команді, але підтримка такого рішення була досить складною, тому ми вирішили просто почекати, поки Apple не оновить clang в новій версії Xcode. Profit.

Кінець...?

Розслідування, частина 2. Це має бути швидко, правда?

Швидко переносимося в 2023-й. Змінилося багато речей. росія напала на Україну, а ми продовжували комітити, тільки тепер не з офісів і дому, а з бункерів.

Проєкт ріс, більшість частин переписані, Apple випустила нову систему збірки. Я тепер мав M1 Macbook Pro, який просто неймовірно швидкий. Очікувано, що компіляція проєкту мала б теж стати швидше, правда?

Нове «залізо», старі проблеми

Зараз XXI століття. Скільки часу ви готові витратити на очікування компіляції проєкту на топовому «залізі»? 5 хвилин? Може, 10 хвилин? Чи годину? Звичайно, все залежить від розміру проєкту, але є якась верхня межа, досягаючи якої ви розумієте, що треба щось міняти.

В цілому ми, як розробники, завжди хочемо мати швидкий feedback loop. Чим швидше ми можемо побачити результати своїх змін, тим швидше ми можемо піти пити каву. Я, наприклад, коли бачу, що цей feedback loop занадто довгий — лізу розбиратися.

Пам’ятаєте, скільки часу clang витрачав на компіляцію деяких файлів в проєкті в далекому 2019? Так ось, ця проблема все ще залишалася. Звичайно, розмір проблеми зменшився, за рахунок нового «заліза», але проблема була.

В CleanMyMac X є багато таргетів, і більшість з них компілювалася швидко, але деякі з них пригальмовували, і досить сильно. При компіляції деяких файлів компілятор наче підвисав і витрачав багато часу (ніколи такого не було, і ось — на тобі). Це було дивно, тому що більшість файлів в схожих таргетах компілювалися майже моментально, як 20 файлів за секунду, але ці — компілювалися декілька десятків секунд.

Десятки секунд на один файл? На Apple M1 в 2023? Серйозно?

Час компіляції деяких файлів на M1 Pro

Вузьке місце

Через те, що я був знайомий з проблемою і вже знав, куди дивитися, я відновив проєкт і почав розслідування. Не витративши багато зусиль, я відносно швидко знайшов метод, який був відповідальним за проблему з підвисанням.

compileModuleAndReadAST(
    CompilerInstance &ImportingInstance,
    SourceLocation ImportLoc,
    SourceLocation ModuleNameLoc,
    Module *Module, StringRef ModuleFileName) {

Тепер я знав, що компілятор намагався зібрати модуль. Але що таке модуль і чому його збірка займає так багато часу?

Модулі в clang

Офіційна документація досить гарно пояснює, що є модуль, але хто ж її читає. Якщо коротко, то модуль — це спосіб організації вашого інтерфейсу бібліотеки. Бібліотека або фреймворк можуть надавати модуль. Якщо це велика бібліотека, Вона надає деклька модулів. Якщо дуже спростити, то модуль для clang — це структурована інформація про інтерфейс бібліотеки.

Коли файл залежить від якогось фреймоврка і ви пишете @import ModuleX, то компілятор шукає відповідний модуль (інтерфейс фреймворка) і не просто препроцесить його, як було раніше у випадку з файлами .h, а прекомпілює його у спеціальний формат, який потім використовується як є, без додаткових перетворень, при генерації коду вашого файлу, який компілюється.

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

.build/DerivedData/ModuleCache.noindex/3GVC60OTD7R0S/Foundation-1XMN11U6GYNC8.pcm
.build/DerivedData/ModuleCache.noindex/3GVC60OTD7R0S/AppKit-1XMN11U6GYNC8.pcm

Чому ж так довго

На той момент, коли збираються таргети верхнього рівня, наприклад, CleanMyMac X, всі модулі залежностей вже мали б бути зібрані і лежати в кеші. Що ж тоді збирає компілятор, і чому це займає так багато часу? І чому це відбувається не в усіх таргетах, а тільки в деяких?

В мене було декілька основних припущень, які я вирішив перевірити:

  • Якимось чином кеш інвалідується при збірці таргетів верхнього рівня. Наприклад, через якісь дивні рейс-кондішини. Через це доводиться робити повну або часткову перезбірку модулів.
  • Деякі таргети верхніх рівней не перевикористовують кеш.

Інвалідація кешу

Спочатку я поліз розбиратися, що може стати причиною інвалдації PCM-кешу, і чому він не може (а мав би) бути перевикористаним. Я поліз в код і побачив, що при генерації PCM-файлу формується унікальний підпис, який використовується для інвалідації кешу. Цей підпис по факту генерується на основі кожного біту інтерфейсу модуля. Тобто навіть найменша зміна в модулі може призвести до інвалідації кешу.

Копнувши глибше, я зрозумів, що якщо я почну лізти в алгоритм формування бінарного коду модуля, то мені доведеться осягнути величезну частинку кодової бази. Тому я спробував підійти до проблеми з іншого боку. Я розмірковував так: можна просто дивитися на PCM-файли під час збірки і якщо вони поміняються, то проблема в інвалідації кешу.

Різниця в PCM files

Дивитися різницю двох бінарних PCM-файлів мені здалося не найкращою ідеєю, і тому, покопавшись в документації, я знайшов команду, яка дозволяла вивести інформацію про PCM-файл в читабельному форматі.

clang -module-file-info <path to PCM file>

Ось приклад gist виводу для AppKit модуля.

Я написав (так-то запитав у ChatGPT) скріпт, який перевіряв, чи змінюється PCM-файл або інформація про нього при збірці. Перезібрав проєкт декілька раз і побачив, що PCM-файли не змінюються. Як тільки вони були зібрані, більше вони не мінялися.

Однак, я знайшов, що різні таргети використовували PCM-файли з різних директорій. Назви файлів в цих директоріях були однакові, але от вміст файлів дещо різнився.

./.build/DerivedData/ModuleCache.noindex/  25WGJ3ZHJ6332  /Foundation-1XMN11U6GYNC8.pcm
./.build/DerivedData/ModuleCache.noindex/  3UU6V472V783N  /Foundation-1XMN11U6GYNC8.pcm
./.build/DerivedData/ModuleCache.noindex/  H6DQB240ZJHB   /Foundation-1XMN11U6GYNC8.pcm
./.build/DerivedData/ModuleCache.noindex/  3B85C81QBGQUK  /Foundation-1XMN11U6GYNC8.pcm
./.build/DerivedData/ModuleCache.noindex/  3MW80CL1WV63C  /Foundation-1XMN11U6GYNC8.pcm
./.build/DerivedData/ModuleCache.noindex/  3GVC60OTD7R0S  /Foundation-1XMN11U6GYNC8.pcm

Порівнюючи файли, побачив наступне:

<       -DNDEBUG=1
<       -DUSE_DEBUG_LOGGING=1
---
>       -DNDEBUG=0
>       -DUSE_DEBUG_LOGGING=0

Там були й інші зміни, але вони здебільшого просто включали в себе шляхи до директорій, де ці файли знаходилися. В цей момент мій мозок мені спробував натякнути, що можливо, я колись чув про PCM і прапорці компіляції в одному реченні.

Я пішов читати документацію і знайшов, що шукав. Нарешті.

Нотатка стосовно особливостей коміляції модулів

Знідно з документацією, PCM-файли будуть використовуватися як кеш, якщо прапорці препроцесора не змінюються. В нашому ж випадку, майже кожен таргет використовував свої прапорці препроцесора. Ми їх використовували для деяких констант, для перемикання функціоналу, для деяких інших речей. В результаті, у нас було дуже багато різних кешів — під кожен унікальний набір прапорців препроцесора.

Пояснення

Що ж, в результаті вібувалося? Більшість таргетів використовували один і той же набір прапорців препроцесора. І, відповідно, всі залежності цих таргетів попадали в один і той самий кеш. Але коли процесор доходив до таргетів, які використовували інший набір прапорців, то доводилося перебудовувати весь кеш PCM для всіх залежностей, починаючи з системних фреймворків, і закінчуючи нашими модулями.

Директорії в ModuleCache

Вміст однієї з директорій (всі залежності)

Рішення

Рішення було досить простим. Довелося «просто» перевірити всі таргети і їх прапорці препроцесора, і зробити їх максимально ідентичними для всіх. Додатково виявилося, що в Xcode для цього є спеціальний прапорець GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS. Цей прапорець працює так само, як і GCC_PREPROCESSOR_DEFINITIONS, але його значення не впливають на те, який PCM кеш використовувати. Якщо копнути ще, глибше, то можна побачити, що Xcode додає прапорець fmodules-ignore-macro= для кожного прапорця препроцесора, який вказаний в GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS.

Результати

Вирішив перевірити, чи це дійсно допомогло. Я запустив бенчмарк на всьому проєкті CleanMyMac X за допомогою інструменту hyperfine.

Чесно кажучи, я був дуже приємно здивований результатами. В 2 рази швидше в Debug конфігурації, і майже на 25% швидше в Release!

Приблизно так виклядає графік з часом компіляції файлів в Xcode, до і після змін (однакові модулі підсвічені однаковим кольором).

Графік з часом компіляції

Висновки

Це була дуже цікава подорож. Вона почалася з простого запитання 4 роки тому і закінчилася (сподіваюся) цією статтею. Сподіваюся, вам сподобалося, і ви, може, знайшли для себе щось нове, а може навіть зарядилися для пошуку причин багів на ваших проєктах.

Сам баг був цікавий тим, що не мав жодних очевидних симптомів. Проєкт якось збирався, а ми звикли до часу збірки, нас вона влаштовувала. Ну, майже всіх :)

Сучасне «залізо» душе швидке. Тож якщо ваш проєкт компілюється за 10 хвилин, тому що «завжди було так», то ви точно повинні спробувати перевірити, чи це дійсно так. Навіть якщо це вимагатиме від вас дебагу компілятора.

Маю подякувати пану Олексію Денисову за всю допомогу і підтримку. Навряд чи ця стаття була б написана без його допомоги.

Якщо у вас є питаннячка, пишіть в Twitter @PaulTaykalo.

Цікаві посилання

* Clang Modules — тут документація.
* Hyperfine — тут інструмент для бенчмарку.
* MacPaw — тут компанія, яку я люблю, і яка розробляє CleanMyMac X.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось29
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

WaitMultiplier *= 2;

Це стандарт для Internet у випадках, коли точна причина і строк усунення переповнення невідомі, але це якийсь обмежений ресурс (звичайно, або в байтах, або в пакетах). І да, відомі ті ж проблеми — наприклад, 1 хвилину не було звʼязку, TCP зʼєднання ще «сплять» дещо більше 1 хвилини, поки не прокинуться знову.

Ідеальне рішення тут взагалі не робити такі паузи за часом, а підписатись на нотифікації «умовної змінної». Навіть якщо такої нема, це можна зробити, наприклад, через named pipe, файл-флаг чи ще декількома методами — головне, що це буде нотифікація, яку побачить хоча б один бажаючий.

Переглянув код і чесно був трохи здивований реалізацією. Inotify ще досить капризний для використання (залежить від ОС, файлової системи, тощо..) Вибираючи з двух зол краще мабуть не подвоювати періодичнисть опитування, а залишити її сталою величиною. Автору респект, що докопався до кореня проблеми!

теж чекав на зміни в першій частині через системні івенти (типу inotifywait), але так, по аналогії з c# - Thread.Sleep(ms) живіше всіх живих))

Коли розробляєш операційні системи, дуже боляче читати, як хтось збирає свій проект за 4.5 хвилини, і це довго, тому зоптимізував і тепер збирає його за 3.5 хвилини :)
У нас — з ранку поставив зборку (якщо з нуля) і займаєшся іншими справами. Наприкінці дня можеш подивитись результат :-D

Робив би все можливе, щоб зменшити Feedback Loop. Не можу без цього :)

Коли треба зібрати порядка 3000-4000 пакетів, починаючи з gcc, clang та glibc, тут не дуже пооптимізуєш ;-) Нє, звісно, коли вже один раз зібрано, то перезібрати із незначними змінами в парі пакетів займає всього декілька хвилин. Але коли міняється «версія» ОС — це попа ;-)

ccache, sccache, bazel? Таке точно не повинно збиратись на одній машині.

Вітаю, Дмитро! Радий бачити :)

Ми ж стартап: у нас, звісно, є фабрика для збирання, але вона все ж більше для наших клієнтів, тому, для активного девелопмента краще тримати локальну пребілжену версію.
Ми вже потихеньку це оптимізуємо, але це не в приоритеті — багато інших більш актуальних задач. А так, у нас є публічний sstate-cache, на черзі — хеш-сервер, щоб не перезбирати локально різні пакети із різними хешами, якщо результат їх збирання бінарно ідентичний.

Якщо перезбирати той же самий реліз, що вже є в паблік-кеші, то буває, що більше 90% пакетів метчиться.

З іншого боку, ми ж активно саме свій дістр розвиваємо (docs.foundries.io/...​e-manual/linux/linux.html) і часто влізаємо в base-класи Yocto/OE, що, буває, приводить до перезбирання майже всього ;-)

І ні, це не просто локальні хакі, це все масово апстрімиться в meta-yocto, open-embedded, meta-freescale, я уже не кажу про u-boot, op-tee, actualizr-lite чи кернел :)
(github.com/foundriesio)

Робив би все можливе, щоб зменшити Feedback Loop. Не можу без цього :)

divide et impera (к) (тм)

Android із нуля збирається за ~4 години.
Android incremental build до 10 хв.
Там більше 700 репозиторіїв включаючи Linux.

На Yocto/OE форс інкрементал займає 30 секунд :-)
А так, дуже радий за вас ;-)

Дякую за статтю! Мені як фулстек веб девелоперу, було цікаво почитати про нюанси роботи з бінарними десктопними програмами :) Згадав давні уроки С++

Імхо такого матеріалу потрібно більше на сайті — бо це корисний, аутентичний і профільний матеріал. Таке зазвичай лише на Хабрі можна знаходити — тому приймати участь в створенні аналогічних матеріали для україномовного сегменту вебу, це дуже добра річ.

Довелося «просто» перевірити всі таргети і їх прапорці препроцесора, і зробити їх максимально ідентичними для всіх.

Ще один варінат запобігти цій проблемі — .xcconfig
help.apple.com/...​ac/current/#/dev745c5c974

В цілому — це допомагає трошки швидше їх шукати. У нас є .xcconfig на проекті. Вони дуже гарно себе зарекомендували у випадках, коли .xcodeproj не комітиться в репозиторій, а генерується на стороні розробника (xcodegen/tuist). В таких випадках флаги повністю визначаються тим, що є в .xcconfig. А ось якщо .xcodeproj в git’і, то .xcconfig — yet another level. тому що ніхто не заважає «випадково» змінити щось в .xcodeproj.

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1

Або ось такий варіант (один з реальних кейсів), коли .xcconfig просто додає додаткові флаги, і все одно треба йти дивитися. а що там по факту є в .xcodeproj.
Мені вдалося створити патч до репозиторію llvm. В ньому, замість того, щоб спати при очікуванні на розблокування, clang підписувався на системні повідомлення про файл блокування. Як тільки файл блокування зникав, clang відразу ж продовжував роботу.

Тут проблема, якщо декілька потоків чекають на один ресурс. Вони одночасно прокинуться, будуть конкурувати, і усі крім одного мають знову заснути. Себто, в реальному житті підхід може призвести до витрат зайвих системних ресурсів на перемикання контекстів та контеншн на мютексах.

Так, мені тоді в email повідписували за таке рішення. Пропонували щось комбіноване писати з рандомними таймаутами після unlock event’у. І виходило що (system events + timers) vs (timers). Рішення з timer only має в цьому плані багато переваг через меншу складність і простоту розуміння.

Альтернативно, можна подивитись граф викликів для клангу — в нього є ключ -ftime-trace (releases.llvm.org/...​s.html#new-compiler-flags ), який продукує .json, який можна згодувати хрому в (chrome://tracing/) та подивитись граф
(aras-p.info/...​chart-profiler-for-Clang)

Тут запитали, як подивитися, чи «в мене є така проблема на проекті». Ну, по-перше, треба Objective-C (відчуваю себе динозавром). Можна зробити Clean Derived Data, і зібрати проект. І після цього подивитися на кількість директорій в ModulesCache. Їх там має бути ±8. Якщо у вас там пару десятків — то скоріш за все, ви можете трошки підтюнити білди.

Зазвичай цю проблему вирішують задопомогою Bazel / Buck... й в тому більше сенсу ніж намагатись контрібьютити в llvm. Swift iOS проекти та нативний Android пришвидшують там в 5-6х разів зазвичай тим же bazel’ем, але бракує розробників що готові з ним працювати (бо ліниві й безвідповідальні). Принаймні community adoption rate зараз чудовий, хоч у bazel’я трохи правила страждають... й з виходом buck2 думаю конкуренції більше буде.

Так само є розподілена збірка під llb buildkit, типу denzp/cargo-wharf.
Краще було б додати підтримку розподіленої збірки в відповідні інструментації та фронтенди мов програмування, СLI обгортки. Тобто для swift то було б розширенням типової swift build команди, та могло б стати самостійним проектом білдера. Принаймні воно могло б жити як самостійний проект.

Так само на llb можна написати розподілене тестування задопомогою табличного з графом залежностей, та синхронізацією стану... в результуючому контейнері був би звіт тестування та покриття.

epoll/kqueue поллери подій, то вже було ... й зазвичай обирають найбільш портабельне рішення — максимум прикрутили в 2019ому костиль на таймерах. В LLVM ніколи не запихнуть там якейсь libev / libuv суто з політичних причин... а от в кастомних білдерах воно нікому точно не заважатиме.

A ще існує ccache/icecream та sccache( firefox-source-docs.mozilla.org/...​dsystem/sccache-dist.html ) - варіація ccache, написаний мозілою під себе — дозволяє до купи ще й кешувати результати роподіленої збірки на спільному мережевому стораджі, аби люди не збирали ті самі файли, а мали одразу обʼєктніки з стораджу, прозоро під час збірки.

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

:wow: Так багато всього і в одному коменті. Пішов перевіряти, що змінилося в Bazel/Buck. Колись пробував, і не раз завести, і кількість зусиль на підтримку цього всього і ознайомлення команди займало занадто багато часу. Треба щоб хтось прийшов і показав, що і де і як. Пішов дивитися доки і відоси.

Цікаво, як саме Ви впевнилися, що той снепшот що ви використали — саме він є версією клангу, що використовується у Вашій версіі х-коду?
Мій досвід пошуку таких версій кожного разу був досить болючий, звідсіля й цікавість, особливо не маючи повного набору ключів для конфігурації для його cmake конфігу

А не треба було мати відповідність 1-в-1. Взяв останню версію релізну, що була в репозиторії. Було достатньо.

Цікава історія, я працював з xcode тільки в контексті гібридної розробки мобільних додатків. Але навіть я зрозумів основну ідею :)

Неймовірне дослідження! Добре, що на цей раз не довелось читати документацію Інтела про регістри :)

Треба буде перенести з Ґабру сюди

По-перше хороша технічна стаття, прочитав за один раз бо написана живо, а по-друге зрозуміла навіть фахівцям без досвіду з Swift, C++.

Підписатись на коментарі