SlideShare a Scribd company logo
"Hidden difficulties of debugger implementation for .NET WASM apps", Andrii Rublov
About me
- Software Developer at JetBrains
- Mainly working on Rider’s
- .NET WASM debugging infrastructure
- EF / EF Core tooling
- Run / Debug configurations
- Recent open-source projects
- wasmer-dotnet: .NET bindings for Wasmer WebAssembly Runtime
- rider-efcore: EF Core plugin for Rider
- rider-monogame: MonoGame plugin for Rider
- GitHub: @seclerp
- Twitter: @seclerp
- Blog: blog.seclerp.me
Prologue: About .NET WebAssembly
.NET WebAssembly Family
- Blazor WebAssembly
- DevServer-hosted
- ASP.NET Core-hosted
- wasi-experimental workload
- browser-wasi RID
- Wasi.Sdk
- NativeAOT-LLVM WASM/WASI
- wasm-experimental workload
- browser-wasm RID
- console-wasm RID
Chapter 1: .NET WebAssembly App’s Anatomy
.NET WebAssembly App’s Anatomy: Blazor
> cat wwwroot/index.html
<!DOCTYPE html>
//.
<body>
<div id="app">
//.
//div>
//.
<script
src="_framework/blazor.webassembly.js"></script>
//body>
//html>
> dotnet new blazorwasm
/-name BlazorApp1
> cd BlazorApp1/BlazorApp1
.NET WebAssembly App’s Anatomy: Blazor
> dotnet build
> cd
bin/Debug/net7.0/wwwroot/_framework
> ls
blazor.boot.json <- 1
blazor.webassembly.js <- 2
BlazorApp1.dll <- 3
BlazorApp1.pdb
dotnet.wasm <- 4
mscorlib.dll
//.
> cat blazor.boot.json
{
//.
"entryAssembly": "BlazorApp1",
"resources": {
"runtime": {
"dotnet.wasm":
"sha256-6u4NhRISP<<.",
},
"runtimeAssets": {
"dotnet.wasm": {
"behavior": "dotnetwasm",
"hash": "sha256-6u4NhRISP//."
}
//.
}
}
.NET WebAssembly App’s Anatomy: Blazor
> cd /.//.//.//./
> dotnet run
Process tree:
dotnet: run
└── dotnet:
"~.nugetpackagesmicrosoft.aspnetcore.components.webassembly.devserver7.0.5/
tools/blazor-devserver.dll" <-applicationpath
"//.BlazorApp1binDebugnet7.0BlazorApp1.dll"
.NET WebAssembly App’s Anatomy: wasm-experimental
> cat index.html
<!DOCTYPE html>
<html>
<head>
//.
<script type='module' src="./main.js"></script>
//head>
<body>
<span id="out">//span>
//body>
//html>
> dotnet workload install
wasm-tools
wasm-experimental
> dotnet new wasmbrowser
/-name WasmApp1
> cd WasmApp1/WasmApp1
.NET WebAssembly App’s Anatomy: wasm-experimental
> cat main.js
import { dotnet } from './dotnet.js'
//.
await dotnet.run();
.NET WebAssembly App’s Anatomy: wasm-experimental
> dotnet build
> cd bin/Debug/net7.0/AppBundle
> ls
managed/ <- 3
dotnet.js <- 2
dotnet.js.symbols
dotnet.wasm /- 4
index.html
main.js
mono-config.json <- 1
WasmApp1.runtimeconfig.json <- 1
//.
> cat mono-config.json
{
"mainAssemblyName": "WasmApp1.dll",
"assemblyRootFolder": "managed",
"debugLevel": -1,
"remoteSources": [],
//.
"assetsHash": "sha256-zTDnY1om//."
}
.NET WebAssembly App’s Anatomy: wasm-experimental
> cat WasmApp1.runtimeconfig.json
{
"runtimeOptions": {
"tfm": "net8.0",
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
],
"runtimeArgs": [],
"mainAssembly": "WasmApp1.dll"
},
}
}
.NET WebAssembly App’s Anatomy: wasm-experimental
> cd /.//.//.//./
> dotnet run
WasmAppHost /-runtime-config
binDebugnet8.0browser-wasmAppBundleWasmApp1.runtimeconfig.json
App url: //.
Process tree:
dotnet: run
└── dotnet: exec "C:Program
FilesdotnetpacksMicrosoft.NET.Runtime.WebAssembly.Sdk8.0.0-preview.4.23259.
5WasmAppHostWasmAppHost.dll" /-runtime-config
"D:PlaygroundWasmApp1WasmApp1binDebugnet8.0browser-wasmAppBundleWasmAp
p1.runtimeconfig.json"
Quick intermediate summary
.NET WebAssembly app:
- Runs on Mono runtime*
- Has some JS glue code between the app and runtime
- Has different hosting models and toolchains:
- DevServer
- WasmAppHost
- ASP.NET Core
- How debugger communicates with the runtime? 🤔
- How the runtime communicates with the browser and vice-versa? 🤔
* Except NativeAOT-LLVM WASM, but it’s out-of-scope for this talk, it’s very experimental right now
Debugging of regular .NET Apps
Debugging of .NET WASM Apps
Chapter 2: The Debug Proxy
Meet Mono Proxy aka Debug Proxy
Process tree:
Rider.Backend.exe: …
└── winpty-agent.exe: …
└── dotnet:
~/.nuget/packages/microsoft.aspnetcore.components.webassembly.devserver/7.0.5/
tools/blazor-devserver.dll /-applicationpath binDebugnet7.0BlazorApp1.dll
└── dotnet: exec
"~.nugetpackagesmicrosoft.aspnetcore.components.webassembly.devserver7.0.5
toolsBlazorDebugProxyBrowserDebugHost.dll" /-OwnerPid 16152 /-DevToolsUrl
http://127.0.0.1:64069
Meet Mono Proxy aka Debug Proxy
* for simplicity we will call it just “Debug Proxy”
Meet Mono Proxy aka Debug Proxy
- Debugger Client doesn’t have a direct connection to a browser page
- Debug Proxy acts a mediator role in communication flow
- Debug Proxy proxifies JS app-related events from a browser page to a debugger client
and JS related function calls from the debugger client to a browser page
- Debug Proxy listens for Mono JS events from Mono runtime and controls it’s behavior by
calls via dotnet.js API
- Debug Proxy API is not documented and it’s quite unstable
Meet Mono Proxy aka Debug Proxy
How communication between there 3 parts is organized? Which protocol is used? 🤔
Chapter 3: The Protocol
A Handshake Process
1. Debugger: starts a WasmApp1 process (DevServer or
ASP.NET Core, depending on the hosting option), which
also starts Debug Proxy as a child process
Debugger
(Client)
Browser
Debug
Proxy
> chrome
“about:blank?…”
> dotnet run
GET
/_framework/
debug/ws-proxy?…
2. Debugger: starts a compatible browser (Chrome/Edge),
with a special placeholder URL (about:blank?realUrl=//.)
3. Browser: dumps it’s debugging port and path to a special
file in user’s profile folder.
4. Debugger: constructs debugging endpoint like that:
ws://127.0.0.1:{port}/{path}
5. Debugger: sends a debugging endpoint to a special
private URL called inspect url:
GET http://localhost:5170/_framework/debug/ws-proxy?
browser=ws://127.0.0.1:{port}/{path}
runtimeReady
A Handshake Process
6. Debug Proxy: initializes a WebSocket connection to
the browser by given browser debugging endpoint,
returns a new proxy debugging endpoint (with similar
shape but with a new port) as a 302 Redirect status code
Debugger
(Client)
Browser
navigate … navigate …
Debug
Proxy
302 Found
> chrome
“about:blank?…”
> dotnet run
Establishes a WS
connection
GET
/_framework/
debug/ws-proxy?…
Establishes a WS
connection
setBreakpoints…
7. Debugger: initializes a WebSocket connection to the
debug proxy by a received proxy debugging endpoint
8. Debugger: sends breakpoint requests to be resolved
by Mono runtime once ready
9. Debugger: “resolves” a real URL from a placeholder
URL (about:blank?…) and navigates a page to it
10. Debug Proxy: sends special event indicating that
Mono runtime is ready to accept .NET specific requests
Quick intermediate summary
- Handshake process is quite complex because of a lot of moving parts
- A hack with placeholder URLs exists because we need some controlled time between Debug Proxy
initialization and Mono runtime initialization to make preparations (like setting breakpoints before
they will be reached)
- It’s impossible to run more than 1 instance of Chromium browser under the same user profile folder
(because in such a case they will have conflict because of use of identical port)
For which communication process WebSocket connections are established?
🤔
CDP aka Chrome DevTools Protocol: Definition
- Defined by a set of modules, “domains”
- Each domain may contain:
- Types: transferred data records
- Methods: client-issued calls to the
CDP server (browser, Mono Proxy,
etc.)
Events: server-issued notifications to
the client
Examples
CDP aka Chrome DevTools Protocol: Explore
API Explorer
- Official:
chromedevtools.github.io
/devtools-protocol
- Alternative:
vanilla.aslushnikov.com
- Debug Proxy specific:
mono-cdp.seclerp.me
CDP aka Chrome DevTools Protocol: Transport
- Works over WebSocket
- Based on JSON-RPC 2.0*
- Messages are strictly ordered
- Supports buffering
- Supports 3 types of messages:
- Requests (client /> server, ordered)
- Responses (client /> server, ordered)
- Events (server /> client, unordered)
- Supports sessions (in our case, session per
browser tab)
Examples
- Request:
{"id":10, "method": "Page.navigate",
"params":{"url":"http://localhost:5170/"
}}
- Response:
{"id":10, “result”:
{"frameId":"…","loaderId":"…"}}
- Event:
{"method":
"Network.requestServedFromCache",
"params":{"requestId":"98279.21"}}
CDP aka Chrome DevTools Protocol: Targets
- Target defines anything that debugger
could be attached to. Examples:
- A browser
- A page
- A service worker
- A background page
- …
- One target session could be created from
another (e.g. page target session from
browser target session)
Chapter 4: Implementation
CDP Client
// Creating connection
var connection = new DefaultProtocolClient(new Uri("ws://localhost:5151"), logger);
await connection.ConnectAsync(cancellationToken);
// Sending commands
var response = await connection.SendCommandAsync(
Domains.DotnetDebugger.SetDebuggerProperty(
JustMyCodeStepping: true
)
);
// Firing commands (when we're not interested in response)
await connection.FireCommandAsync(Domains.Debugger.StepOut());
CDP Client
// Listening for events
pageClient.ListenEvent<Domains.Debugger.BreakpointResolved>(async e />
{
ResolveBreakpoint(e.BreakpointId.Value);
});
// Creating scoped clients (clients for specific sessions)
var scopedClient = connection.CreateScoped(sessionId);
Sample: Page connection initialization
var result = await connection.SendCommandAsync(
Domains.Target.AttachToTarget(
TargetId: placeholderTarget.TargetId,
// Non-flatten mode will be deprecated in the future
Flatten: true));
var pageConnection = connection.CreateScoped(result.SessionId.Value);
logger.LogInformation("Initializing debugger-related domains//.");
await Task.WhenAll(
pageConnection.SendCommandAsync(Domains.Debugger.Enable()),
pageConnection.SendCommandAsync(Domains.Log.Enable()),
pageConnection.SendCommandAsync(Domains.Runtime.Enable()),
pageConnection.SendCommandAsync(Domains.Page.Enable()),
pageConnection.SendCommandAsync(Domains.Network.Enable())
);
Sending Messages
private readonly BlockingCollection<ProtocolRequest<ICommand/> _outgoingMessages = …
public async Task<TResponse> SendCommandAsync<TResponse>(ICommand<TResponse> command,
string? sessionId = null,
CancellationToken? token = default) where TResponse : IType
{
var id = Interlocked.Increment(ref _currentId);
var resolver = new TaskCompletionSource<JObject>();
if (_responseResolvers.TryAdd(id, resolver))
{
await FireInternalAsync(id, GetMethodName(command.GetType()), command, sessionId);
var responseRaw = await resolver.Task;
var response = responseRaw.ToObject//.
return response;
}
throw new Exception("Unable to enqueue message to send");
}
private async Task FireInternalAsync(int id, string methodName, ICommand command, string? sessionId)
{
var request = new ProtocolRequest<ICommand>(id, methodName, command, sessionId);
if (!_outgoingMessages.TryAdd(request)) throw new Exception("Can't schedule outgoing message for sending.");
}
private async Task
StartOutgoingWorker(CancellationToken token)
{
_logger.LogInformation("Starting outgoing
messages pump//.");
while (!token.IsCancellationRequested)
{
var message = _outgoingMessages.Take();
await ProcessOutgoingRequest(message);
}
}
Retrieving Messages
private Task ProcessIncoming(string message) />
DeserializeMessage(message) switch
{
ProtocolResponse<JObject> response /> ProcessIncomingResponse(response),
ProtocolEvent<JObject> @event /> ProcessIncomingEvent(@event),
_ /> Task.CompletedTask
};
private async Task ProcessIncomingEvent(ProtocolEvent<JObject> @event)
{
OnEventReceived?.Invoke(this, @event);
if (_eventHandlers.TryGetValue(@event.Method, out var handler))
await handler.Invoke(@event);
}
private async Task ProcessIncomingResponse(ProtocolResponse<JObject> response)
{
OnResponseReceived?.Invoke(this, response);
_responseResolvers.TryRemove(response.Id, out var resolver);
if (response.Error is { } error) resolver?.SetException(new ProtocolErrorException(error));
if (response.Result is { } result) resolver?.SetResult(result);
}
Sample: Set, Remove & Resolve Breakpoints
pageClient.ListenEvent<Domains.Debugger.BreakpointResolved>(async e />
{
ResolveBreakpoint(e.BreakpointId.Value);
});
private void ResolveBreakpoint(string breakpointId)
{
if (_breakpointsStorage.TryGetBreakEventInfo(breakpointId, out var info))
{
var bindingBreakEvent = new WasmBindingBreakEvent(info.BreakEvent, WasmModule.Instance);
if (!info.AddBindingBreakEvent(bindingBreakEvent))
{
_logger.LogInformation($"{bindingBreakEvent} is not added to {info}");
info.SetStatus(BreakEventStatus.NotBound, $"Could not insert breakpoint {info.BreakEvent}");
}
else
info.SetStatus(BreakEventStatus.Bound, null);
}
}
Sample: Set, Remove & Resolve Breakpoints
async Task HandleAddBreakpointRequest(BreakEventInfo<WasmModule> info)
{
// //.
// Map 'C:Foobar' to 'file:///C:/Foo/bar'
var fileUrl = new Uri(url.Path).ToString();
var (protocolLine, protocolColumn) =
WasmProxyLocationMapper.ToMonoProxyUnits(line, column);
var (breakpointId, locations) = await pageClient.SendCommandAsync(
Domains.Debugger.SetBreakpointByUrl(
LineNumber: protocolLine, ColumnNumber: protocolColumn, Url: fileUrl
));
if (!_breakpointsStorage.TryAdd(breakpointId.Value, info))
_logger.LogWarning("Can't add breakpoint '{Info}' with ID '{BreakpointId}'", info, breakpointId.Value);
// Check maybe we already know script with resolved script ID
foreach (var location in locations)
if (_scriptsStorage.IsLoaded(location.ScriptId.Value))
ResolveBreakpoint(breakpointId.Value);
}
Bonus: Few words about Hot-Reload
Hot-Reload
- Without debugging: dotnet watch and related infrastructure
- With debugging (EnC): Available in Debug Proxy since .NET SDK 7
(not yet in Rider 🥵)
- EnC follows the following algorithm:
1. User pauses execution for some reason (breakpoint, manually, …)
2. User changes things in code…
3. User hits Continue or Apply changes
4. Delta is computed (IL delta, metadata delta and PDB delta)
5. Debugger sends delta to the runtime (via Debug Proxy or other mechanism)
6. Runtime applies deltas
7. Debugger resets breakpoints (as lines in code have possibly been changed)
8. Debugger resumes execution
Thanks for your attention!
Links
- WebAssembly
- WebAssembly System Interface: wasi.dev
- Chrome DevTools Protocol
- API Explorer: chromedevtools.github.io/devtools-protocol
- API Explorer (alternative): vanilla.aslushnikov.com
- Mono Extension Explorer: mono-cdp.seclerp.me
- Blog posts
- The Future of .NET with WASM by Khalid Abuhakmeh
- Videos
- Blazor United prototype by Steven Sanderson
- Experiments with the new WASI workload in .NET 8 Preview 4 by Steven Sanderson

More Related Content

PPTX
Deploying windows containers with kubernetes
PDF
Deploying configurable frontend web application containers
PPTX
Rails Engine | Modular application
PPTX
dotNetConf2019
PPTX
Websockets
PDF
Drone CI/CD 自動化測試及部署
PPTX
Reactive application using meteor
PPTX
Phonegap android angualr material design
Deploying windows containers with kubernetes
Deploying configurable frontend web application containers
Rails Engine | Modular application
dotNetConf2019
Websockets
Drone CI/CD 自動化測試及部署
Reactive application using meteor
Phonegap android angualr material design

Similar to "Hidden difficulties of debugger implementation for .NET WASM apps", Andrii Rublov (20)

PDF
PVS-Studio: analyzing pull requests in Azure DevOps using self-hosted agents
DOCX
unit 2 of Full stack web development subject
PDF
Porting Rails Apps to High Availability Systems
PDF
Building websites with Node.ACS
PDF
Building websites with Node.ACS
PDF
PDF
Making a small QA system with Docker
PDF
Reactive Application Using METEOR
PDF
Node.js on microsoft azure april 2014
DOCX
58615764 net-and-j2 ee-web-services
PDF
DevOPS training - Day 2/2
PDF
Selenium Full Material( apprendre Selenium).pdf
PDF
Love at first Vue
PPTX
ДМИТРО БУДИМ «Mobile Automation Infrastructure from scratch» Online QADay 202...
PPTX
Docker Enterprise Workshop - Technical
PDF
Server(less) Swift at SwiftCloudWorkshop 3
PPTX
Scaling Docker Containers using Kubernetes and Azure Container Service
PPT
nodejs tutorial foor free download from academia
PVS-Studio: analyzing pull requests in Azure DevOps using self-hosted agents
unit 2 of Full stack web development subject
Porting Rails Apps to High Availability Systems
Building websites with Node.ACS
Building websites with Node.ACS
Making a small QA system with Docker
Reactive Application Using METEOR
Node.js on microsoft azure april 2014
58615764 net-and-j2 ee-web-services
DevOPS training - Day 2/2
Selenium Full Material( apprendre Selenium).pdf
Love at first Vue
ДМИТРО БУДИМ «Mobile Automation Infrastructure from scratch» Online QADay 202...
Docker Enterprise Workshop - Technical
Server(less) Swift at SwiftCloudWorkshop 3
Scaling Docker Containers using Kubernetes and Azure Container Service
nodejs tutorial foor free download from academia
Ad

More from Fwdays (20)

PDF
"Mastering UI Complexity: State Machines and Reactive Patterns at Grammarly",...
PDF
"Effect, Fiber & Schema: tactical and technical characteristics of Effect.ts"...
PPTX
"Computer Use Agents: From SFT to Classic RL", Maksym Shamrai
PPTX
"Як ми переписали Сільпо на Angular", Євген Русаков
PDF
"AI Transformation: Directions and Challenges", Pavlo Shaternik
PDF
"Validation and Observability of AI Agents", Oleksandr Denisyuk
PPTX
"Autonomy of LLM Agents: Current State and Future Prospects", Oles` Petriv
PDF
"Beyond English: Navigating the Challenges of Building a Ukrainian-language R...
PPTX
"Co-Authoring with a Machine: What I Learned from Writing a Book on Generativ...
PPTX
"Human-AI Collaboration Models for Better Decisions, Faster Workflows, and Cr...
PDF
"AI is already here. What will happen to your team (and your role) tomorrow?"...
PPTX
"Is it worth investing in AI in 2025?", Alexander Sharko
PDF
''Taming Explosive Growth: Building Resilience in a Hyper-Scaled Financial Pl...
PDF
"Scaling in space and time with Temporal", Andriy Lupa.pdf
PDF
"Database isolation: how we deal with hundreds of direct connections to the d...
PDF
"Scaling in space and time with Temporal", Andriy Lupa .pdf
PPTX
"Provisioning via DOT-Chain: from catering to drone marketplaces", Volodymyr ...
PPTX
" Observability with Elasticsearch: Best Practices for High-Load Platform", A...
PPTX
"How to survive Black Friday: preparing e-commerce for a peak season", Yurii ...
PPTX
"Istio Ambient Mesh in production: our way from Sidecar to Sidecar-less",Hlib...
"Mastering UI Complexity: State Machines and Reactive Patterns at Grammarly",...
"Effect, Fiber & Schema: tactical and technical characteristics of Effect.ts"...
"Computer Use Agents: From SFT to Classic RL", Maksym Shamrai
"Як ми переписали Сільпо на Angular", Євген Русаков
"AI Transformation: Directions and Challenges", Pavlo Shaternik
"Validation and Observability of AI Agents", Oleksandr Denisyuk
"Autonomy of LLM Agents: Current State and Future Prospects", Oles` Petriv
"Beyond English: Navigating the Challenges of Building a Ukrainian-language R...
"Co-Authoring with a Machine: What I Learned from Writing a Book on Generativ...
"Human-AI Collaboration Models for Better Decisions, Faster Workflows, and Cr...
"AI is already here. What will happen to your team (and your role) tomorrow?"...
"Is it worth investing in AI in 2025?", Alexander Sharko
''Taming Explosive Growth: Building Resilience in a Hyper-Scaled Financial Pl...
"Scaling in space and time with Temporal", Andriy Lupa.pdf
"Database isolation: how we deal with hundreds of direct connections to the d...
"Scaling in space and time with Temporal", Andriy Lupa .pdf
"Provisioning via DOT-Chain: from catering to drone marketplaces", Volodymyr ...
" Observability with Elasticsearch: Best Practices for High-Load Platform", A...
"How to survive Black Friday: preparing e-commerce for a peak season", Yurii ...
"Istio Ambient Mesh in production: our way from Sidecar to Sidecar-less",Hlib...
Ad

Recently uploaded (20)

PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PDF
Review of recent advances in non-invasive hemoglobin estimation
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PPT
Teaching material agriculture food technology
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PPTX
MYSQL Presentation for SQL database connectivity
PDF
Diabetes mellitus diagnosis method based random forest with bat algorithm
PPTX
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
PDF
Encapsulation theory and applications.pdf
PDF
MIND Revenue Release Quarter 2 2025 Press Release
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PDF
Electronic commerce courselecture one. Pdf
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
NewMind AI Weekly Chronicles - August'25 Week I
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
20250228 LYD VKU AI Blended-Learning.pptx
Review of recent advances in non-invasive hemoglobin estimation
Chapter 3 Spatial Domain Image Processing.pdf
Advanced methodologies resolving dimensionality complications for autism neur...
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
Building Integrated photovoltaic BIPV_UPV.pdf
Teaching material agriculture food technology
Reach Out and Touch Someone: Haptics and Empathic Computing
MYSQL Presentation for SQL database connectivity
Diabetes mellitus diagnosis method based random forest with bat algorithm
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
Encapsulation theory and applications.pdf
MIND Revenue Release Quarter 2 2025 Press Release
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
Electronic commerce courselecture one. Pdf
Agricultural_Statistics_at_a_Glance_2022_0.pdf
NewMind AI Weekly Chronicles - August'25 Week I
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...

"Hidden difficulties of debugger implementation for .NET WASM apps", Andrii Rublov

  • 2. About me - Software Developer at JetBrains - Mainly working on Rider’s - .NET WASM debugging infrastructure - EF / EF Core tooling - Run / Debug configurations - Recent open-source projects - wasmer-dotnet: .NET bindings for Wasmer WebAssembly Runtime - rider-efcore: EF Core plugin for Rider - rider-monogame: MonoGame plugin for Rider - GitHub: @seclerp - Twitter: @seclerp - Blog: blog.seclerp.me
  • 3. Prologue: About .NET WebAssembly
  • 4. .NET WebAssembly Family - Blazor WebAssembly - DevServer-hosted - ASP.NET Core-hosted - wasi-experimental workload - browser-wasi RID - Wasi.Sdk - NativeAOT-LLVM WASM/WASI - wasm-experimental workload - browser-wasm RID - console-wasm RID
  • 5. Chapter 1: .NET WebAssembly App’s Anatomy
  • 6. .NET WebAssembly App’s Anatomy: Blazor > cat wwwroot/index.html <!DOCTYPE html> //. <body> <div id="app"> //. //div> //. <script src="_framework/blazor.webassembly.js"></script> //body> //html> > dotnet new blazorwasm /-name BlazorApp1 > cd BlazorApp1/BlazorApp1
  • 7. .NET WebAssembly App’s Anatomy: Blazor > dotnet build > cd bin/Debug/net7.0/wwwroot/_framework > ls blazor.boot.json <- 1 blazor.webassembly.js <- 2 BlazorApp1.dll <- 3 BlazorApp1.pdb dotnet.wasm <- 4 mscorlib.dll //. > cat blazor.boot.json { //. "entryAssembly": "BlazorApp1", "resources": { "runtime": { "dotnet.wasm": "sha256-6u4NhRISP<<.", }, "runtimeAssets": { "dotnet.wasm": { "behavior": "dotnetwasm", "hash": "sha256-6u4NhRISP//." } //. } }
  • 8. .NET WebAssembly App’s Anatomy: Blazor > cd /.//.//.//./ > dotnet run Process tree: dotnet: run └── dotnet: "~.nugetpackagesmicrosoft.aspnetcore.components.webassembly.devserver7.0.5/ tools/blazor-devserver.dll" <-applicationpath "//.BlazorApp1binDebugnet7.0BlazorApp1.dll"
  • 9. .NET WebAssembly App’s Anatomy: wasm-experimental > cat index.html <!DOCTYPE html> <html> <head> //. <script type='module' src="./main.js"></script> //head> <body> <span id="out">//span> //body> //html> > dotnet workload install wasm-tools wasm-experimental > dotnet new wasmbrowser /-name WasmApp1 > cd WasmApp1/WasmApp1
  • 10. .NET WebAssembly App’s Anatomy: wasm-experimental > cat main.js import { dotnet } from './dotnet.js' //. await dotnet.run();
  • 11. .NET WebAssembly App’s Anatomy: wasm-experimental > dotnet build > cd bin/Debug/net7.0/AppBundle > ls managed/ <- 3 dotnet.js <- 2 dotnet.js.symbols dotnet.wasm /- 4 index.html main.js mono-config.json <- 1 WasmApp1.runtimeconfig.json <- 1 //. > cat mono-config.json { "mainAssemblyName": "WasmApp1.dll", "assemblyRootFolder": "managed", "debugLevel": -1, "remoteSources": [], //. "assetsHash": "sha256-zTDnY1om//." }
  • 12. .NET WebAssembly App’s Anatomy: wasm-experimental > cat WasmApp1.runtimeconfig.json { "runtimeOptions": { "tfm": "net8.0", "wasmHostProperties": { "perHostConfig": [ { "name": "browser", "html-path": "index.html", "Host": "browser" } ], "runtimeArgs": [], "mainAssembly": "WasmApp1.dll" }, } }
  • 13. .NET WebAssembly App’s Anatomy: wasm-experimental > cd /.//.//.//./ > dotnet run WasmAppHost /-runtime-config binDebugnet8.0browser-wasmAppBundleWasmApp1.runtimeconfig.json App url: //. Process tree: dotnet: run └── dotnet: exec "C:Program FilesdotnetpacksMicrosoft.NET.Runtime.WebAssembly.Sdk8.0.0-preview.4.23259. 5WasmAppHostWasmAppHost.dll" /-runtime-config "D:PlaygroundWasmApp1WasmApp1binDebugnet8.0browser-wasmAppBundleWasmAp p1.runtimeconfig.json"
  • 14. Quick intermediate summary .NET WebAssembly app: - Runs on Mono runtime* - Has some JS glue code between the app and runtime - Has different hosting models and toolchains: - DevServer - WasmAppHost - ASP.NET Core - How debugger communicates with the runtime? 🤔 - How the runtime communicates with the browser and vice-versa? 🤔 * Except NativeAOT-LLVM WASM, but it’s out-of-scope for this talk, it’s very experimental right now
  • 15. Debugging of regular .NET Apps
  • 16. Debugging of .NET WASM Apps
  • 17. Chapter 2: The Debug Proxy
  • 18. Meet Mono Proxy aka Debug Proxy Process tree: Rider.Backend.exe: … └── winpty-agent.exe: … └── dotnet: ~/.nuget/packages/microsoft.aspnetcore.components.webassembly.devserver/7.0.5/ tools/blazor-devserver.dll /-applicationpath binDebugnet7.0BlazorApp1.dll └── dotnet: exec "~.nugetpackagesmicrosoft.aspnetcore.components.webassembly.devserver7.0.5 toolsBlazorDebugProxyBrowserDebugHost.dll" /-OwnerPid 16152 /-DevToolsUrl http://127.0.0.1:64069
  • 19. Meet Mono Proxy aka Debug Proxy * for simplicity we will call it just “Debug Proxy”
  • 20. Meet Mono Proxy aka Debug Proxy - Debugger Client doesn’t have a direct connection to a browser page - Debug Proxy acts a mediator role in communication flow - Debug Proxy proxifies JS app-related events from a browser page to a debugger client and JS related function calls from the debugger client to a browser page - Debug Proxy listens for Mono JS events from Mono runtime and controls it’s behavior by calls via dotnet.js API - Debug Proxy API is not documented and it’s quite unstable
  • 21. Meet Mono Proxy aka Debug Proxy How communication between there 3 parts is organized? Which protocol is used? 🤔
  • 22. Chapter 3: The Protocol
  • 23. A Handshake Process 1. Debugger: starts a WasmApp1 process (DevServer or ASP.NET Core, depending on the hosting option), which also starts Debug Proxy as a child process Debugger (Client) Browser Debug Proxy > chrome “about:blank?…” > dotnet run GET /_framework/ debug/ws-proxy?… 2. Debugger: starts a compatible browser (Chrome/Edge), with a special placeholder URL (about:blank?realUrl=//.) 3. Browser: dumps it’s debugging port and path to a special file in user’s profile folder. 4. Debugger: constructs debugging endpoint like that: ws://127.0.0.1:{port}/{path} 5. Debugger: sends a debugging endpoint to a special private URL called inspect url: GET http://localhost:5170/_framework/debug/ws-proxy? browser=ws://127.0.0.1:{port}/{path}
  • 24. runtimeReady A Handshake Process 6. Debug Proxy: initializes a WebSocket connection to the browser by given browser debugging endpoint, returns a new proxy debugging endpoint (with similar shape but with a new port) as a 302 Redirect status code Debugger (Client) Browser navigate … navigate … Debug Proxy 302 Found > chrome “about:blank?…” > dotnet run Establishes a WS connection GET /_framework/ debug/ws-proxy?… Establishes a WS connection setBreakpoints… 7. Debugger: initializes a WebSocket connection to the debug proxy by a received proxy debugging endpoint 8. Debugger: sends breakpoint requests to be resolved by Mono runtime once ready 9. Debugger: “resolves” a real URL from a placeholder URL (about:blank?…) and navigates a page to it 10. Debug Proxy: sends special event indicating that Mono runtime is ready to accept .NET specific requests
  • 25. Quick intermediate summary - Handshake process is quite complex because of a lot of moving parts - A hack with placeholder URLs exists because we need some controlled time between Debug Proxy initialization and Mono runtime initialization to make preparations (like setting breakpoints before they will be reached) - It’s impossible to run more than 1 instance of Chromium browser under the same user profile folder (because in such a case they will have conflict because of use of identical port) For which communication process WebSocket connections are established? 🤔
  • 26. CDP aka Chrome DevTools Protocol: Definition - Defined by a set of modules, “domains” - Each domain may contain: - Types: transferred data records - Methods: client-issued calls to the CDP server (browser, Mono Proxy, etc.) Events: server-issued notifications to the client Examples
  • 27. CDP aka Chrome DevTools Protocol: Explore API Explorer - Official: chromedevtools.github.io /devtools-protocol - Alternative: vanilla.aslushnikov.com - Debug Proxy specific: mono-cdp.seclerp.me
  • 28. CDP aka Chrome DevTools Protocol: Transport - Works over WebSocket - Based on JSON-RPC 2.0* - Messages are strictly ordered - Supports buffering - Supports 3 types of messages: - Requests (client /> server, ordered) - Responses (client /> server, ordered) - Events (server /> client, unordered) - Supports sessions (in our case, session per browser tab) Examples - Request: {"id":10, "method": "Page.navigate", "params":{"url":"http://localhost:5170/" }} - Response: {"id":10, “result”: {"frameId":"…","loaderId":"…"}} - Event: {"method": "Network.requestServedFromCache", "params":{"requestId":"98279.21"}}
  • 29. CDP aka Chrome DevTools Protocol: Targets - Target defines anything that debugger could be attached to. Examples: - A browser - A page - A service worker - A background page - … - One target session could be created from another (e.g. page target session from browser target session)
  • 31. CDP Client // Creating connection var connection = new DefaultProtocolClient(new Uri("ws://localhost:5151"), logger); await connection.ConnectAsync(cancellationToken); // Sending commands var response = await connection.SendCommandAsync( Domains.DotnetDebugger.SetDebuggerProperty( JustMyCodeStepping: true ) ); // Firing commands (when we're not interested in response) await connection.FireCommandAsync(Domains.Debugger.StepOut());
  • 32. CDP Client // Listening for events pageClient.ListenEvent<Domains.Debugger.BreakpointResolved>(async e /> { ResolveBreakpoint(e.BreakpointId.Value); }); // Creating scoped clients (clients for specific sessions) var scopedClient = connection.CreateScoped(sessionId);
  • 33. Sample: Page connection initialization var result = await connection.SendCommandAsync( Domains.Target.AttachToTarget( TargetId: placeholderTarget.TargetId, // Non-flatten mode will be deprecated in the future Flatten: true)); var pageConnection = connection.CreateScoped(result.SessionId.Value); logger.LogInformation("Initializing debugger-related domains//."); await Task.WhenAll( pageConnection.SendCommandAsync(Domains.Debugger.Enable()), pageConnection.SendCommandAsync(Domains.Log.Enable()), pageConnection.SendCommandAsync(Domains.Runtime.Enable()), pageConnection.SendCommandAsync(Domains.Page.Enable()), pageConnection.SendCommandAsync(Domains.Network.Enable()) );
  • 34. Sending Messages private readonly BlockingCollection<ProtocolRequest<ICommand/> _outgoingMessages = … public async Task<TResponse> SendCommandAsync<TResponse>(ICommand<TResponse> command, string? sessionId = null, CancellationToken? token = default) where TResponse : IType { var id = Interlocked.Increment(ref _currentId); var resolver = new TaskCompletionSource<JObject>(); if (_responseResolvers.TryAdd(id, resolver)) { await FireInternalAsync(id, GetMethodName(command.GetType()), command, sessionId); var responseRaw = await resolver.Task; var response = responseRaw.ToObject//. return response; } throw new Exception("Unable to enqueue message to send"); } private async Task FireInternalAsync(int id, string methodName, ICommand command, string? sessionId) { var request = new ProtocolRequest<ICommand>(id, methodName, command, sessionId); if (!_outgoingMessages.TryAdd(request)) throw new Exception("Can't schedule outgoing message for sending."); } private async Task StartOutgoingWorker(CancellationToken token) { _logger.LogInformation("Starting outgoing messages pump//."); while (!token.IsCancellationRequested) { var message = _outgoingMessages.Take(); await ProcessOutgoingRequest(message); } }
  • 35. Retrieving Messages private Task ProcessIncoming(string message) /> DeserializeMessage(message) switch { ProtocolResponse<JObject> response /> ProcessIncomingResponse(response), ProtocolEvent<JObject> @event /> ProcessIncomingEvent(@event), _ /> Task.CompletedTask }; private async Task ProcessIncomingEvent(ProtocolEvent<JObject> @event) { OnEventReceived?.Invoke(this, @event); if (_eventHandlers.TryGetValue(@event.Method, out var handler)) await handler.Invoke(@event); } private async Task ProcessIncomingResponse(ProtocolResponse<JObject> response) { OnResponseReceived?.Invoke(this, response); _responseResolvers.TryRemove(response.Id, out var resolver); if (response.Error is { } error) resolver?.SetException(new ProtocolErrorException(error)); if (response.Result is { } result) resolver?.SetResult(result); }
  • 36. Sample: Set, Remove & Resolve Breakpoints pageClient.ListenEvent<Domains.Debugger.BreakpointResolved>(async e /> { ResolveBreakpoint(e.BreakpointId.Value); }); private void ResolveBreakpoint(string breakpointId) { if (_breakpointsStorage.TryGetBreakEventInfo(breakpointId, out var info)) { var bindingBreakEvent = new WasmBindingBreakEvent(info.BreakEvent, WasmModule.Instance); if (!info.AddBindingBreakEvent(bindingBreakEvent)) { _logger.LogInformation($"{bindingBreakEvent} is not added to {info}"); info.SetStatus(BreakEventStatus.NotBound, $"Could not insert breakpoint {info.BreakEvent}"); } else info.SetStatus(BreakEventStatus.Bound, null); } }
  • 37. Sample: Set, Remove & Resolve Breakpoints async Task HandleAddBreakpointRequest(BreakEventInfo<WasmModule> info) { // //. // Map 'C:Foobar' to 'file:///C:/Foo/bar' var fileUrl = new Uri(url.Path).ToString(); var (protocolLine, protocolColumn) = WasmProxyLocationMapper.ToMonoProxyUnits(line, column); var (breakpointId, locations) = await pageClient.SendCommandAsync( Domains.Debugger.SetBreakpointByUrl( LineNumber: protocolLine, ColumnNumber: protocolColumn, Url: fileUrl )); if (!_breakpointsStorage.TryAdd(breakpointId.Value, info)) _logger.LogWarning("Can't add breakpoint '{Info}' with ID '{BreakpointId}'", info, breakpointId.Value); // Check maybe we already know script with resolved script ID foreach (var location in locations) if (_scriptsStorage.IsLoaded(location.ScriptId.Value)) ResolveBreakpoint(breakpointId.Value); }
  • 38. Bonus: Few words about Hot-Reload
  • 39. Hot-Reload - Without debugging: dotnet watch and related infrastructure - With debugging (EnC): Available in Debug Proxy since .NET SDK 7 (not yet in Rider 🥵) - EnC follows the following algorithm: 1. User pauses execution for some reason (breakpoint, manually, …) 2. User changes things in code… 3. User hits Continue or Apply changes 4. Delta is computed (IL delta, metadata delta and PDB delta) 5. Debugger sends delta to the runtime (via Debug Proxy or other mechanism) 6. Runtime applies deltas 7. Debugger resets breakpoints (as lines in code have possibly been changed) 8. Debugger resumes execution
  • 40. Thanks for your attention!
  • 41. Links - WebAssembly - WebAssembly System Interface: wasi.dev - Chrome DevTools Protocol - API Explorer: chromedevtools.github.io/devtools-protocol - API Explorer (alternative): vanilla.aslushnikov.com - Mono Extension Explorer: mono-cdp.seclerp.me - Blog posts - The Future of .NET with WASM by Khalid Abuhakmeh - Videos - Blazor United prototype by Steven Sanderson - Experiments with the new WASI workload in .NET 8 Preview 4 by Steven Sanderson