Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WASI] timers based on wasi:clocks #105879

Merged
merged 14 commits into from
Aug 8, 2024
Merged

Conversation

pavelsavara
Copy link
Member

@pavelsavara pavelsavara commented Aug 2, 2024

  • TimerQueue on top of wasi:clocks
  • WasmTestRunner for wasi now calls the event loop Poll
  • fixed GC finalization of Pollable in WasiHttpHandler
  • fixed leaking of abandoned Pollable/Task registration in WasiEventLoop
  • itemized ILLinkDescriptorsLibraryBuildXml and use it to protect WasiEventLoop members. Related issue

Fixes #105810
Contributes to #102894

cc @dicej

@pavelsavara pavelsavara added arch-wasm WebAssembly architecture area-VM-threading-mono os-wasi Related to WASI variant of arch-wasm labels Aug 2, 2024
@pavelsavara pavelsavara added this to the 9.0.0 milestone Aug 2, 2024
@pavelsavara pavelsavara self-assigned this Aug 2, 2024
- generate with 0.2.1
@pavelsavara pavelsavara marked this pull request as ready for review August 6, 2024 15:45
{
WasiEventLoop.DispatchWasiEventLoop();
while (!mainTask.IsCompleted)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably too naive solution.
There could be other pending tasks scheduled to C# thread pool.
But not in the await chain leading to this entrypoint.

Such code/tasks/continuations could have (expected) externally observable side effects (after we exit the entry point).

But I don't know how to do that at this point. So I guess it will be some next PR.

cc @dicej

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the browser we schedule browser callback any time the new job triggers ThreadPool.RequestWorkerThread(), but we could not do it here.

Maybe we need to run ThreadPoolWorkQueue.Dispatch(); until there was no call to ThreadPool.RequestWorkerThread() before we return from the entry point.

cc @SingleAccretion , any thoughts ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that full event loop emulation requires what is effectively:

bool workItemsLeft;
do
{
    workItemsLeft = ThreadPoolWorkQueue.Dispatch();
}
while (workItemsLeft)

I think it's ok to wait for the top-level task only here. It matches "normal" platform's behavior:

ThreadPool.QueueUserWorkItem(x => { Thread.Sleep(100); Console.WriteLine(); });
> dotnet run
; Nothing printed because thread pool threads are background threads.

So it seems unlikely our tests would depend on it.

It does not match Browser's behavior, of course. It is a point to debate whether we'd want the eventual async main experience to match browser or not-browser.

@vitek-karas
Copy link
Member

/cc @sbomer for the library descriptor changes as an FYI mostly.

@pavelsavara
Copy link
Member Author

pavelsavara commented Aug 8, 2024

thanks @SingleAccretion

It does not match Browser's behavior, of course. It is a point to debate whether we'd want the eventual async main experience to match browser or not-browser.

I think parity with desktop/server dotnet is more interesting than parity with browser dotnet.

I also tested following on desktop dotnet and it doesn't print either.

// no await
Task.Delay(10000).ContinueWith(_=>{
    Console.WriteLine("Later");
});

In the browser, the dotnet/mono application keeps running, because we execute the main entrypoint as any other export.
We also have variation of API which exit emscripten and prevent further calls from JS, after the promise of the entrypoint resolves. This is confusing for app developers, if they use it, but useful in unit testing the runtime.

Nodejs (as another example of single threaded engine with promises) is not exiting the process until promises settle.
Same for pending setTimeout. I experienced few situations where this was preventing the process to gracefully stop.
For example https://github.com/rollup/rollup/pull/5195/files

In library mode, if the call to such UCO/export scheduled un-awaited tasks/jobs/continuations, they will execute on next call to any export/UCO. I wonder what problems that will cause.

For this PR, I think we could keep it simple (as is for now) and we will see later if there is any need to change this before or after WASIp3.

@pavelsavara
Copy link
Member Author

pavelsavara commented Aug 8, 2024

Another interesting place would be the WASIp2 in library mode.

We will need to add same or similar top level event loop to each export/UCO (so that HTTP client would work).

We can do that by calling this new Thread.PollWasiEventLoopUntilResolved via [UnsafeAccessor] from the wit-bindgen generated code of the export.

But even if/after that becomes public C# API, I don't like that UCO pattern.
It feels to me that spinning event loop is responsibility of the runtime.
The generated code is user code and there are also backward compatibility problems with such generated library code being inside nuget across dotnet releases.

I would prefer this to be part of the UCO wrapper code in C together with the initialization of the runtime.
Binding for wasi:io/poll could be generated for C.

I'll probably try to prototype it.

@pavelsavara pavelsavara merged commit 7c0364e into dotnet:main Aug 8, 2024
156 of 158 checks passed
@pavelsavara pavelsavara deleted the wasi_timer branch August 8, 2024 08:59
@SingleAccretion
Copy link
Contributor

But even if/after that becomes public C# API, I don't like that UCO pattern.
It feels to me that spinning event loop is responsibility of the runtime. The generated code is user code and there are also backward compatibility problems with such generated library code being inside nuget across dotnet releases.

I agree it is a problematic pattern w.r.t. versioning. My hope is that the eventual P3 with host-side event loop will allow us to get rid of it.

I view spinning the event loop as equivalent to Task marshalling in JS interop. As with task marhshalling, without it, you can't run anything async.

The major difference with JS interop is that the generator is not part of the runtime, and there is no public API that it can use to spin the loop, so we're adding a "backdoor" for it. I suspect that if we had a "spin the thread pool" API in .NET FW 1.0 BCL, we wouldn't be that upset about using it in the generator.

I would prefer this to be part of the UCO wrapper code in C together with the initialization of the runtime.

I will note that adding any marshalling to UCOs is a non-starter in RyuJit because there is no wrapper - UCO inlines the RPI transitions. And, more generally, it would go against the design of UCO.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
arch-wasm WebAssembly architecture area-VM-threading-mono os-wasi Related to WASI variant of arch-wasm
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[WASI] make Task/Pollable registration weak ref
7 participants