Hidden difficulties of debugger implementation for .NET WASM apps
Talk presentation
Debug infrastructure implementation for .NET (Blazor) WebAssembly apps is challenging due to its unique execution environment. In this talk, we will dive deep into the hidden difficulties of debugger IDE frontend implementation for .NET WASM apps.
We'll start with an overview of Blazor WASM app execution anatomy, reviewing Debug Proxy in prticular. We will then compare regular .NET debugging with Blazor debugging and introduce Rider Debugging Infrastructure. Next, we'll discuss the steps involved in debug session initialization, including how the CDP (Chrome DevTools Protocol) is used. We will cover breakpoints, evaluation and explore multiple console views orchestration. Finally, we will discuss a few words about hot-reload, how it works and how it is supported from the IDE side.
This talk is essential for .NET developers working with Blazor WASM and anyone interested in understanding the complexities of debugging .NET WASM applications.
- JetBrains, Software Developer at Rider
- He is a software engineer mainly interested in programming languages tooling creation and a little bit of game dev.
- Actively contributing to different open-source projects in his spare time. Trying to do his best in finishing existing pet projects and creating new ones.
- Creator of EF Core and MonoGame plugins for JetBrains Rider
- LinkedIn, GitHub
Talk transcription
Привіт. Мене звати Андрій Робльов. Я softer developer у компанії JetBrains, і я працюю у команді, що займається розробкою DEA-Rider. Сьогодні я хочу розповісти про реалізацію функціоналу відладки для Blazor та інших типів .NET WebAssembly додатків, а також про їх відмінності від звичайної відладки .NET WebAssembly додатків. Крім того, ми розглянемо анатомію та структуру таких додатків. Трошки про мене: у JetBrains я займаюся підтримкою та вдосконаленням відладчика для Blazor додатків, Entity Framework та Entity Framework Core Tool. Також я працюю над деякими фічами RAM-конфігурації, наприклад, конфігурацією для відладки розслідувальних Source-генераторів. Крім цього, я взявся за open-source-проекти та власні інтереси у вільний час.
Нижче ви можете знайти посилання на мої соцмережі та мій блог. Почнемо з самого початку. А саме з того, яким членом є не така вже й маленька родина .NET WebAssembly додатків. Історично, Blazor не був першим. Існували більш та менш відомі фреймворки від Microsoft та сторонніх авторів, які переносили .NET в браузер. Згадаймо, наприклад, Silverlight або Bridge.net, більш новітній, проте вже майже мертвий фреймворк. Blazor став першим fest-party framework для .NET в браузері завдяки технології WebAssembly.
Зазначу, що у цій доповіді я залишив за межами Blazor Server, Blazor United та Blazor Hybrid фреймворки, оскільки вони самі по собі є темою для окремої розмови і реалізовані технічно значно інакше. Наразі Blazor WebAssembly можна використовувати як незалежний SPI додаток, який буде працювати за допомогою DevServer, що надає комплект .NET-SDK, або, якщо у вас є власний Aspen Core бекенд. За нашими спостереженнями, це найчастіше використовуваний варіант, оскільки Blazor це перший крок до .NET FullStack, і у вас вже є .NET-бекенд. З появою .NET 7 з'явилися дві нові опції в новому SDK Workload, які дозволяють збирати не тільки Blazor, а майже будь-який .NET-код у WebAssembly. Разом з цими з'явилися два нові ідентифікатори рантайму: браузерний для виконання WebAssembly у браузері, як і раніше з Blazor, та консольний для використання в Node.js.
У .NET 8 з'явиться аналогічний Workload для WebAssembly System Interface, стандарту для WebAssembly, що надає можливість запускати веб-систему, де WebAssembly код не обмежений середовищем браузера, але може взаємодіяти із файловою системою, мережею, системними викликами тощо. WASI SDK – це прототип того, що я тільки що розповів. Він не є дуже актуальним, але він простий у використанні – просто встановіть через пакетний менеджер та збудуйте. І все буде працювати.
І останнє, менше відоме – це .NET 8 LLVM WebAssembly. Це експериментальний спосіб компіляції додатків та рантайму за допомогою спеціальних скриптів, що є частиною проєкту .NET 8 LLVM. Код буде компілюватися майже повністю в режимі "head of time” Цей підхід має багато переваг перед звичайним моно-IoT компілятором, проте наразі не є готовим для використання у реальних продакшн проєктах. Перейдемо до першого розділу, а саме подивимося на те, що представляє з собою майже будь-який .NET WebAssembly додаток. Цей розділ буде проходити у такому напівпрактичному режимі. Усі команди, які будуть надалі показуватися, ви зможете їх повторити самостійно при потребі.
Почнемо з Blazor WebAssembly. Спочатку створимо новий Blazor проект. Шаблон за замовченням. Потім перейдемо до новостроєнної директорії і подивимося, як виглядає index.html файл. Це точка входу до нашого додатку. Тут чітко видно, де Blazor підвантажується та ініціалізується. Принцип майже такий самий, як і в звичайних single-page applications на JS. Тобто ми маємо деякий div з деяким id, про який знає фреймворк, та який цим фреймворком повністю контролюється.
Тепер, якщо ми виконуємо збірку нашого проекту, перейдемо до директорії, де в нас наші build артефакти з'являються, і проаналізуємо в місці директорії, то побачимо декілька цікавих файлів. Перш за все, це Blazor Boot JSON. Якщо ми його відкриємо та подивимося, він відповідає за те, які залежності має додаток. Версії цієї залежності та інші менш цікаві файли. Серед іншого бачимо .NET VASM. Це наш .NET Runtime. Що було спеціально скомпільовано за допомогою MScript у VBSM-любінарний файл. Саме цей Runtime запускає код додатку, що ми створили.
У тій самій директорії бачимо .NET VBSM-любінарний файл, а поряд з ним і звичайний DLL з новоствореним додатком. Ще можна побачити вже відомий нам Blazor VBSM-DJS, що склеює усі інші файли, докупи та налаштовує Runtime. Тепер подивимося на дерево процесів. Пройдемо назад до основної директорії проєкту і запустимо додаток через звичайний додатковий трафік. Нас цікавить те, як саме виглядатимуть процеси, що створюють фреймворк після виконання команди запуску. Тому, використовуючи, наприклад, процес хакеру Windows чи Htop у Unix, ми можемо побудувати дерево, у якості вузлів якого тут надалі будуть демонструватися командний рядок, який було використано до .NET SDK, щоб запустити той чи інший процес. Так як ми створили проєкт за замовченням, статичні файли з Blazor FrontEnd розглянуті раніше будуть хоститися за допомогою DevService, як ви можете бачити. Що є звичайним додатком насправді? І це видно, по-перше, аргументом до .NET. А другим аргументом вже йде параметр, що передає шлях до додатку, що треба запустити. Тут все доволі прямолінійно, скажемо так.
Тепер повторимо ті ж самі, так би мовити, досліди з проєктним шаблоном, що встановлюється разом з Blazing Experimental Workloader. Для цього встановимо два необхідні додатки. Треба встановити два необхідних нам workload'и. Створимо новий проєкт з новим шаблоном, який додався разом з workload'ами. І перейдемо до теки з проєктом і знову подивимося на index.html файл. І тут ми бачимо, що ніякого Blazor тут вже немає. JavaScript код з main.js підвищується як модуль до виконання. Цей main.js – це вже наш код. І це сильно відрізняється від Blazor підходу, тому що тут ми вже маємо, що ми можемо явно бачити, де відбувається точка входу до нашого додатку і ми повністю, насправді, контролюємо весь процес. Тепер подивимося на сам main.js.
Він доволі простий і маленький у дефолтному темплейті. І бачимо, що тут вже ми маємо більше контролю. І такий контроль дає нам змогу робити точніші налаштування, передаючи параметри конфігурації, наприклад, до моно рантайму. Наприклад, налаштовувати політики завантаження файлів з мережі, їх ретраї, деякі розміри, максимальність для завантаження і таке інше. У випадку використання, наприклад, WebGL, тут можна досить просто вказати canvas, що потрібно використовувати для рендерингу. Чого-то, наприклад, замінити рівень логування логів рантайму. Це дуже важливо при складному портуванні .NET-додатку у WebFrontend.
Так само роздивимося результат збірки. На відміну від Blazor маємо директорію AppBundle, що складається з дещо інакшого місця. З'явився MonoConfig.json файл, що генерується автоматично з налаштування проєкту, який також дає більш точне розуміння того, що саме буде виконано. Одразу стає зрозуміло, що є Entry Assembly, та є деякі директорії, де зберігається Entry Assembly та деякі інші DLL файли. Це менеджер папка. А поруч з .NET WebAsm ми маємо той самий API з main.js, який є точкою входу та API для рантайму. Це .NET.js файл. Замість Boot.json маємо Runtime.config. Давайте його розглянемо. Згадаємо. В Asm.Experimental проєкти додають новий Runtime.int.fire, тому саме Runtime.config.json файл був створений. Такі файли також створюються для деяких інших .NET таргетів при пабліші. Тут можна також побачити посилання на Entry Assembly, на індекс HTML файл і інші супутні дописи.
Ну і подивимося на відмінності у дереві процесу таких додатків. Одразу бачимо, що проєкт запускається не за допомогою dev-сервера, а за допомогою внутрішнього додатку, що називається wasmap-post. Це щось на кшталт серверу, як і раніше, проте встановлюється він не за допомогою негайд-пакету, а береться з директорії wasm-experimental, на відміну від того, як це було з Blazor. Дуже ймовірно, що такий хід буде надалі використано у .NET 8 та більш пізніх версіях, як для самого Blazor у .NET SDK, бо він більш автентичний та зрозумілий з точки зору тулінгу.
І тепер чітко видно, де використовується наш runtime-config, який ми розглядали раніше. Тут, в принципі, дуже аналогічна ситуація до Blazor, але дещо видозмінений командний рядок, що виконується. Тож, передумов про мішкові підсумки. Майже будь-який .NET WebAssembly додаток виконується з допомогою Mono Runtime. Це дуже важливо розуміти, бо, незважаючи на те, що ви вказали сумісну версію мови і таргет-фреймворк, деякий функціонал C-Sharp, що ви скомпілюєте, не буде зрозумілий Runtime. От зі свіжих прикладів це, наприклад, generic types у атрибутих. Якщо ви без проблем зможете скомпілювати такий атрибут у своєму коді, проте у Runtime все воно відбувається.
Тому що він просто не знає про цю фічу і в Mono Runtime ще не вмершили необхідний функціонал для її підтримки. І я думаю, що .NET 8 це виправлять, але наразі маємо, що маємо. Ми, як розробники додатку, маємо змогу конфірмувати Runtime під необхідну поведінку. І .NET WebAssembly додаток може роздаватися, різними серверами. Це, як DevServer, як у вас MapHost і .NET Core, те, що ми якраз зараз і обговорили.
Залишається кілька не закритих питань. Перше за все, це як саме Debugger буде працювати з нашим додатком і Runtime? А як Debugger буде маніпулювати, взагалі, браузер або браузер Debugger? Це необхідно, якщо порівняти, наприклад, JS Debugger. Ніж розкрити усі карти .NET десктопу додатку. Маємо деякий Runtime. Runtime запускає код нашого додатку. І коли Runtime запускається, він чекає, поки дебагер до нього включиться, і після цього продовжує виконувати код.
У випадку локального дебагу, клієнт дебагера знаходиться майже у тому самому оточенні, що й Runtime. Тому час життя цих сутностей зрозумілий та очікуваний. А тепер перейдемо до .NET WebAsm додатків. Тут все веселіше. Перш за все, маємо Chrome-based браузер, який запускається. Браузер запускає, хостить у собі сторінки користувача у вигляді вкладки. Вкладка хостить у собі звичайний WebAsm Runtime, що може виконувати спеціальний WebAsm код. Це те, що він робить за замовченням. І у цьому Runtime є Runtime. У цьому Runtime запускається вже наш Mono Runtime, який має змогу декодувати та виконувати .NET DLL збірки. І нарешті у рамках цього Runtime вже виконується код, що нас цікавить з точки зору дебагу. І вимальовується доволі складна, цибулоподібна архітектура, через яку дебагер потрібно мати можливість контролювати процес виконання коду та отримувати івенти у зворотній бік.
І такий механізм існує. Він називається Mono-debug proxy. І у цьому розділі ми подивимося на нього більш детально. Повернемося до дерева процесу. Якщо ми запустимо його у дебаг режимі через райдер, щоб помітити різницю, то побачимо новий отчайлд-процес. Цей процес має деякі цікаві аргументи. Перш за все, це процес ID батьківського процесу. Він необхідний, щоб коректно відслідковувати зміни його стану у працівній системі та відповідно реагувати. Він нас не сильно цікавий. І також є DevTools URL, один із елементів його роботи. Але ми бачимо, що з'явився новий процес. І познаючи це, давайте повернемося до нашої схеми зі знайденим пропущеним пазлом. Тепер все на своїх місцях. Debug proxy, будемо називати його так, без спрощення, стартуючи, отримує всю необхідну йому інформацію для роботи. Якість вкладки в браузер, щоб так чи інакше впливати на неї та отримувати вентиві в браузер. Цей проміжковий рівень абстракції спрощує імплементацію Debug-арного клієнта для нас, як розробників. Бо тепер йому потрібно лише правильно реалізувати роботу з однією сутністю цього проксі, щоб мати змогу спілкуватися як з браузером, так і з рентайном. Тож давайте підсумуємо. Клієнт не працює зі вкладкою браузера напряму. Це відбувається через всю ось Debug proxy.
Debug proxy виконує роль такої собі шинної обміну між браузером, Debugger-клієнтом та Runtime . Якщо ми кажемо про взаємодію з браузером, в першу чергу мається на увазі самопередача викликів чи івентів до та з браузерної сторінки, якщо вони не мають відношення до .NET світу. Наприклад, інформація про браузер. І про те, що сторінка щось завантажила з мережі, чи необхідно змінити її URL вкладника. Те ж саме стосується і викликів та івентів .NET світу, що не потрібно віддавати браузеру.
На жаль, так як це доволі специфічна частина .NET SDK, яка використовується лише в домені IDEA-тулінгу, вона не має стійкого публічного API, а щоб витримати коректний контракт, потрібно вивчати її вихідний код. Тож, ми вже знаємо концепцію роботи цих трьох компонентів між собою, але як щодо деталей? Бо як саме взагалі відбувається хендшейк та спілкування між ними? Настав час поговорити про протокол, що використовується при обміні повідомленнями між ними, між цими трьома акторами. Почнемо з handshake. Це найскладніший і нестійкий процес у всьому механізмі, тому його треба розібрати у дрібних подробицях. Справа, ви можете бачити діаграму послідовностей, щоб не загубитися у цьому хаосі. Тут будуть з'являтися по мірі того, як ми будемо йти за цим планом.
Перш за все, Debugger-клієнт запускає процес з додатком. Якщо зробити все правильно, то .NET сам розбереться, як додаток потрібно захостити. У випадку ASP.NET Core простіше, бо просто беремо та запускаємо бекенд, як у випадку звичайного ASP.NET Core додатку. Інфраструктура для цього додається за допомогою UseBlazorDebugging методу. На етапі стартапу, я думаю, що дехто бачив цей метод і для цього він потрібен. У результаті маємо дебаг проксі як у одному з попередніх слайдів з термом процесу. Дочекавшись, що додаток запустився, ми можемо перевірити це за допомогою спеціального call-чеку по HTTP.
Debugger-клієнт стартує браузер у режимі Debug. Наразі підтримується лише сімейство Chromium, тобто Chrome.Ch у першу чергу через обваження самого DebuggerProxy. Тут дуже важливо зробити уточнення, що ми запускаємо браузер не з кінцевою сторінкою додатку, там localhost і board, а зі спеціальним URL, який ми звемо placeholder. Пізніше поговоримо, навіщо це взагалі потрібно. Браузер в момент запуску створює, зберігаю в директорії користувача, в спеціальному файлі порт та пас, які потрібні для створення Debugger з'єднання.
Після цього Debugger-клієнт вже створює Debugging endpoint. Це спеціальний WebSocket URL, що включає в себе порт і пас, що були отримані після читання. І маючи відповідний Debugging endpoint, його потрібно віддати DebugProxy, щоб він міг зробити з'єднання з браузером. Для цього ми маємо head-запит, що оброблюється з допомогою UsePloserDebuggingMiddleware. Формат посилання, що використовується у запиті, може бути знайомим деяким, бо шаблон цього запиту спрягається у Launch Settings, у властивості профілю. Тому ми можемо зробити запит, який може бути знайомим деяким, бо шаблон цього запиту спрягається у Launch Cities.
DebugProxy реагує на наш запит наступним чином. Він створює усі необхідні з'єднання з браузером та MonoRuntime, а потім вертає спеціальний статус-код 302 Redirect, а також спеціальний URL у цілій відповіді. Ми, маючи URL з Redirect, вже наш Debugger-клієнт створює з'єднання з проксі. Після цього розпочинається логіка, реалізація якої необхідна для коректного функціонування Debug. Перш за все, ми відсилаємо до протоколу усі брейкпоінти, що були додані користувачем у редакторі до моменту запуску додатку. Нагадую, ми запустили браузер не з пустою сторінкою, тому Runtime ще не був завантажений, а тому Debug-проксі запише наші запити, а реалізовувати їх буде лише тоді, коли MonoRuntime буде готовий їх приймати.
Якщо б ми робили інакше і одразу б завантажували сторінку користувача, ми б не мали змоги зробити необхідну конфігурацію вчасно. Приклад, користувач поставив брейкпоінт на самому початку методу main. Дуже велика вірогідність того, що Runtime виконав би даний рядок коду до того, як ми сповістили б Debug-проксі про те, що взагалі потрібно брейкпоінт поставити. І він би не спрацював вчасно. Далі, коли необхідні попередні запити були виконані, Debugger-клієнт нарешті, може замість placeholder відкрити справжню сторінку користувачеві. До речі, це приклад запиту, що проксується у випадку у вкладку, а не виконується в модах.
І останній крок – це Debug-проксі закінчує всі заходи щодо активації Runtime, завантаження збірок і тому потім не та відсилає клієнту сигнал, що він готовий до роботи. І після цього вже є наслідки. Тобто, ця реалізація може вважатися виконаним. Хвилинка підсумування. Такий доволі складний процес створює загрозу того, що будь-що може піти не так, не кажучи вже про різноманітні середовища, що може бути на комп'ютері користувача. І тому кожен крок ми покриваємо тестами, намагаємося багато всього. Особливо меседжі. Хак з placeholder, дозволяє нам відсрочити ініціалізацію Runtime і відповідно запуск коду додатка на стільки, на скільки нам потрібно. Існують також і інші прийоми, як, наприклад, відправка пауз-сигналу у Debugger Proxy в початку спілкування, але вони не такі надійні і можуть бути проігноровані, наприклад, у Debugger Proxy. Ми про це навіть не дізнаємося.
Є ще один нюанс, пов'язаний з браузером. Неможливо запустити два браузера одночасно. Які б мали спільні директорії користувача. Якщо ви спробуєте зробити це через командний рядок, вкладка відкриється в вже існуючому браузері. І доволі часто користувачам потрібно тестувати додатки у середовища з справжніх аккаунтів і налаштування хмар. Такий кейс треба намагатися якось коректно ідентифікувати та повідомляти юзерам про це. У нас самих це не дуже гарно зроблено і ми просимо на тим, щоб це зробити краще.
Отже, що це за протокол, і чому ми говоримо саме про вебсокет-з'єднання? Браузер реалізує так званий протокол Chrome DevTools. Ті, хто працював з Selenium або іншими інструментами автоматизації тестування фронтенду, можуть знати про нього. Цей інтерфейс дозволяє робити з браузером та сторінками майже все, що завгодно в межах користувальницьких налаштувань безпеки. На щастя для нас, цей інтерфейс добре документований і версіонований. Він складається з так званих доменів або модулів, які в свою чергу містять типи, або структури, які взаємодіють з конкретними даними. Також, тут є методи виклику, які клієнти можуть викликати для взаємодії з екземпляром Chromium, і події, які відсилає екземпляр Chromium клієнтові.
Для розгляду доменів протоколу Chrome DevTools в деталях, можна скористатися офіційним API Explorer. Ваша посилання ви можете бачити на слайді. Debug Proxy має деякі додаткові методи та події, але, на жаль, як я вже казав, офіційного стандарту для них не існує. Тому ми створили власний API Explorer, який оновлюємо при кожному додатку чого-небудь нового у вихідному коді. Посилання також на слайді.
Тепер говоримо більше саме про транспортний рівень. По-перше, це те, що вже повністю очевидно: цей протокол працює через WebSocket. WebSocket є дещо неформальним варіантом JSON-RPC 2.0, формату для опису віддалених викликів за допомогою JSON, який також використовується, наприклад, у Visual Studio Code у рамках Language Server Protocol. Повідомлення від клієнта завжди є впорядкованими, і також він підтримує буферизацію, щоб дозволити поділ досить великих запитів на дрібні, які коректно обробляються з іншого боку. Є три види повідомлень: Запити, Відповіді та Події. Події не є впорядкованими, на відміну від повідомлень, які йдуть від клієнта. Це дуже важливо, і можна створити кілька так званих сесій в межах одного з'єднання. Це необхідно, наприклад, коли ми хочемо контролювати і браузер, і вкладку браузера одночасно, не створюючи нового з'єднання. Тут ви можете побачити приклади запитів, відповідей та подій. Це, насправді, звичайні JSON-пейлоади у спеціальному форматі, які досить просто парсити.
Також існує поняття цілей для прикріплення сесій. Цілі можуть мати різні види. Перш за все, нас цікавлять сам браузер і сторінка браузера. Але є ще багато різних типів цілей, залежно від того, яка поведінка вам необхідна для використання цього протоколу. За замовчуванням, при створенні з'єднання ви одразу приєднуєтеся до цілі браузера, яка є вузловою і може мати вкладені сесії, такі як сторінка. Тепер ми можемо перейти до деяких прикладів імплементації цього протоколу, який реалізований у нас в Redi.
По-перше, це API, необхідне для створення та підтримки клієнтів для CDP-з'єднання. CDP – це протокол інструментів розробника Chrome, згаданий раніше. Ми створили досить простий API. На цьому слайді ви можете побачити створення, з'єднання та відправку команд. Команди відповідають методам термінології CDP. Їх можна відправити, очікуючи відповіді або у режимі Fire-and-Forget, коли нам не цікаво, що повертається, просто потрібно виконати щось. У цьому прикладі використовується метод SetDebuggerInProperty у домені .NET Debugger, який сам визначається у DebugProxy. Це не метод самого CDP; це його розширення. Отже, майже всі необхідні домени підтримуються розробниками у вигляді JSON-специфікацій.
Тому ми вирішили використовувати кодогенерацію для створення всіх необхідних методів, івентів і типів автоматично. Ми уникнули використання SourceGenerators через зайвий навантаження у даному випадку і те, що нам не потрібно мати відношення до C-sharp чи ROS. Бо це просто JSON-схеми. Також через те, що оновлення відбуватиметься рідко і генерувати треба багато класів, це було б зайво для DR-розробників. Оскільки кожен тип перегенерував би всі ці дані, це було б непрактично. У результаті навіть з інкрементальним генератором це було б зайво, оскільки це не часто потрібно робити. Ми отримали чисте API з мінімальною ручною втручанням, окрім відправки переданих даних.
Крім того, є можливість слухати івенти та створювати нові сесії, які ми називаємо Spokes. Ми намагаємося уникати прив'язки до термінології CDP, оскільки плануємо розширювати цей API у майбутньому для підтримки також відладчика Firefox, якщо це стане можливим, оскільки в даний час цей відладчик не підтримує Debugger proxy. Тут ви можете побачити приклади ініціалізації сеансу вже у вигляді коду, а також приклади викликів методів. Через те, що методи, які викликає користувач, повинні бути впорядковані, вони не відправляються безпосередньо, а збираються в спеціальній черзі.
У попередньому прикладі, в спеціальному long-running task, вони відправляються з призначеним порядковим номером. Момент відправки створюється, як ви можете бачити, за допомогою task completion source, ідентифікованого за тим самим ID повідомлення. Цей source необхідний нам для того, щоб користувач міг дочекатися відповіді на цей запит. Також, в окремому long-running task, відбувається прослуховування зворотних повідомлень від браузера. Вони можуть бути як прості івенти, так і відповіді на конкретне повідомлення, яке ми вже відправили. У випадку івенту, ми просто викликаємо відповідний делегат, який користувач встановив через listen event. У випадку відповіді на повідомлення, ми отримуємо відповідний task completion source, який ми створили при відправленні, знаючи його ID. Залежно від результату, чи це успішна відповідь, чи помилка, ми встановлюємо статус.
Це дуже важливо робити своєчасно, щоб користувач не бачив невалідні брейкпоінти, які виглядають сірими або некоректними. Обробка логіки відправки повідомлень про створення нових та видалення старих брейкпоінтів також виконана у формі черги, але це пов'язано з особливостями нашої інфраструктури в Rider. На слайді ви бачите знімок інфраструктури. Зараз перейдемо до сторінок API Explorer з більш детальним описом цього методу протоколу, який ми використовуємо.
Хоча ми не встигли розглянути всі теми, пов'язані з Debug, такі як обчислення виразів, стек викликів та інші, вони не є унікальними з точки зору імплементації. На завершення хотів би сказати кілька слів про hot reload. Це може бути темою окремого спілкування, але важливо відзначити основні моменти, які стосуються як WP Assembly, так і звичайних .NET додатків. По-перше, без Debug hot reload вже можливий через .NET watch, який існує досить довго. Також, використовуючи стартап-кукси Startup Assemblies у .NET SDK, можна впроваджувати нагети, не модифікуючи код представника. Один з таких нагетів - Armamento - призначений для версії .NET Watch, у центрі виконання, і непорушеної версії. Для hot reload під час дебагу наразі єдиний варіант - це Edit and Continue.
Наразі ми працюємо над новими зусиллями, щоб, нарешті, внести підтримку hot reload за допомогою Edit and Continue, але це все ще в процесі. Давайте розглянемо алгоритм дій Edit and Continue в нашому випадку по крокам. По-перше, користувач потрапляє в паузовий контекст, наприклад, рантайм стикається з брейкпоінтом, користувач натискає на паузу або іншим чином потрапляє в паузу. Користувач вносить зміни в код, завершує редагування коду, натискає "continue". Зміни порівнюються зі старим кодом, що призводить до виникнення трьох важливих компонентів: зміни в EL-коді, зміни в метадані збірки та зміни в Debug-символах.
Усі ці зміни розраховуються редактором, але важливо правильно визначити дії користувача в віртуальному робочому просторі, щоб не втратити жодної невеликої зміни. Debug відправляє ці зміни до Runtime, який їх приймає і застосовує до метаданих і символів PDB. Далі Debug повинен повторно відправити всі брейкпоінти, які були в Runtime, оскільки їх попередні позначки стали не актуальними через зміни у коді, такі як порядок і кількість рядків у файлах. Після цього Debug продовжує виконання коду з того місця, де він зупинився. І це все. Дякую вам за увагу.