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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions eng/illink.targets
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
<ILLinkTrimOutputPath>$(IntermediateOutputPath)</ILLinkTrimOutputPath>

<ILLinkDescriptorsXml Condition="'$(ILLinkDescriptorsXml)' == '' and Exists('$(ILLinkDirectory)ILLink.Descriptors.xml')">$(ILLinkDirectory)ILLink.Descriptors.xml</ILLinkDescriptorsXml>
<!-- ILLink.Descriptors.LibraryBuild.xml files are only used during building the library, not an app. They shouldn't be embedded into the assembly. -->
<ILLinkDescriptorsLibraryBuildXml Condition="'$(ILLinkDescriptorsLibraryBuildXml)' == '' and Exists('$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml')">$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml</ILLinkDescriptorsLibraryBuildXml>
<ILLinkDescriptorsXmlIntermediatePath>$(IntermediateOutputPath)ILLink.Descriptors.xml</ILLinkDescriptorsXmlIntermediatePath>

<ILLinkSubstitutionsXmlIntermediatePath>$(IntermediateOutputPath)ILLink.Substitutions.xml</ILLinkSubstitutionsXmlIntermediatePath>
Expand All @@ -41,6 +39,9 @@
</PropertyGroup>

<ItemGroup>
<!-- ILLink.Descriptors.LibraryBuild.xml files are only used during building the library, not an app. They shouldn't be embedded into the assembly. -->
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml"
Condition="Exists('$(ILLinkDirectory)ILLink.Descriptors.LibraryBuild.xml')" />
<ILLinkSuppressionsLibraryBuildXml Include="$(ILLinkSuppressionsXmlPrefix).LibraryBuild.xml"
Condition="Exists('$(ILLinkSuppressionsXmlPrefix).LibraryBuild.xml')" />

Expand Down Expand Up @@ -210,7 +211,7 @@
<ILLinkArgs Condition="'$(ILLinkRewritePDBs)' == 'true' and Exists('$(ILLinkTrimAssemblySymbols)')">$(ILLinkArgs) -b true</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkRewritePDBs)' == 'true' and Exists('$(ILLinkTrimAssemblySymbols)') and '$(DeterministicSourcePaths)' == 'true'">$(ILLinkArgs) --preserve-symbol-paths</ILLinkArgs>
<!-- pass the non-embedded descriptors xml file on the command line -->
<ILLinkArgs Condition="'$(ILLinkDescriptorsLibraryBuildXml)' != ''">$(ILLinkArgs) -x "$(ILLinkDescriptorsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkDescriptorsLibraryBuildXml)' != ''">$(ILLinkArgs) -x "@(ILLinkDescriptorsLibraryBuildXml->'%(FullPath)', '" -x "')"</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkSubstitutionsLibraryBuildXml)' != ''">$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkSuppressionsLibraryBuildXml)' != ''">$(ILLinkArgs) --link-attributes "@(ILLinkSuppressionsLibraryBuildXml->'%(FullPath)', '" --link-attributes "')"</ILLinkArgs>
<!-- suppress warnings with the following codes:
Expand Down
21 changes: 20 additions & 1 deletion src/libraries/Common/tests/WasmTestRunner/WasmTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,35 @@

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.TestRunners.Common;
using Microsoft.DotNet.XHarness.TestRunners.Xunit;
using System.Runtime.CompilerServices;

public class WasmTestRunner : WasmApplicationEntryPoint
{
protected int MaxParallelThreadsFromArg { get; set; }
protected override int? MaxParallelThreads => RunInParallel ? MaxParallelThreadsFromArg : base.MaxParallelThreads;

public static async Task<int> Main(string[] args)
#if TARGET_WASI
public static int Main(string[] args)
{
return PollWasiEventLoopUntilResolved((Thread)null!, MainAsync(args));

[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "PollWasiEventLoopUntilResolved")]
static extern int PollWasiEventLoopUntilResolved(Thread t, Task<int> mainTask);
}


#else
public static Task<int> Main(string[] args)
{
return MainAsync(args);
}
#endif

public static async Task<int> MainAsync(string[] args)
{
if (args.Length == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<TargetFrameworks>$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent)</TargetFrameworks>
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>
<!-- DesignTimeBuild requires all the TargetFramework Derived Properties to not be present in the first property group. -->
<PropertyGroup>
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'browser'">$(DefineConstants);TARGET_BROWSER</DefineConstants>
<DefineConstants Condition="'$(TargetPlatformIdentifier)' == 'wasi'">$(DefineConstants);TARGET_WASI</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="WasmTestRunner.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ ITypes.OutgoingRequest request
}
else
{
await WasiEventLoop.RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
await RegisterWasiPollable(future.Subscribe()).ConfigureAwait(false);
}
}
}
Expand Down Expand Up @@ -461,19 +461,22 @@ private static async Task SendContentAsync(HttpContent? content, Stream stream)
}
}

private static class WasiEventLoop
private static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
var handle = pollable.Handle;
pollable.Handle = 0;
return CallRegisterWasiPollable((Thread)null!, handle);
var handle = pollable.Handle;

// this will effectively neutralize Dispose() of the Pollable()
// because in the CoreLib we create another instance, which will dispose it
pollable.Handle = 0;
GC.SuppressFinalize(pollable);

return CallRegisterWasiPollableHandle((Thread)null!, handle);

[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollable")]
static extern Task CallRegisterWasiPollable(Thread t, int handle);
}
}

[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RegisterWasiPollableHandle")]
private static extern Task CallRegisterWasiPollableHandle(Thread t, int handle);

private sealed class InputStream : Stream
{
private ITypes.IncomingBody body;
Expand Down Expand Up @@ -559,8 +562,7 @@ CancellationToken cancellationToken
var buffer = result;
if (buffer.Length == 0)
{
await WasiEventLoop
.RegisterWasiPollable(stream.Subscribe())
await RegisterWasiPollable(stream.Subscribe())
.ConfigureAwait(false);
}
else
Expand Down Expand Up @@ -697,7 +699,7 @@ CancellationToken cancellationToken
var count = (int)stream.CheckWrite();
if (count == 0)
{
await WasiEventLoop.RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
await RegisterWasiPollable(stream.Subscribe()).ConfigureAwait(false);
}
else if (offset == limit)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<linker>
<assembly fullname="System.Private.CoreLib">
<!-- these methods are temporarily accessed via UnsafeAccessor from generated code until we have it in public API, probably in WASI preview3 and promises -->
<type fullname="System.Threading.Thread">
<method name="RegisterWasiPollableHandle" />
<method name="PollWasiEventLoopUntilResolved" />
</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,9 @@
<ILLinkSubstitutionsXmls Include="$(ILLinkSharedDirectory)ILLink.Substitutions.Browser.xml" Condition="'$(TargetsBrowser)' == 'true'" />
<ILLinkLinkAttributesXmls Include="$(ILLinkSharedDirectory)ILLink.LinkAttributes.Shared.xml" />
<ILLinkSuppressionsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Suppressions.LibraryBuild.xml" />
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.xml" />
<ILLinkDescriptorsLibraryBuildXml Include="$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.WASI.xml" Condition="'$(TargetsWasi)' == 'true'" />
</ItemGroup>
<PropertyGroup>
<ILLinkDescriptorsLibraryBuildXml>$(ILLinkSharedDirectory)ILLink.Descriptors.LibraryBuild.xml</ILLinkDescriptorsLibraryBuildXml>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Internal\AssemblyAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Internal\Console.cs" />
Expand Down Expand Up @@ -2804,6 +2803,7 @@
<ItemGroup Condition="'$(TargetsWasi)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiEventLoop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.clocks.v0_2_1.MonotonicClockInterop.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\Wasi\WasiPollWorld.wit.imports.wasi.io.v0_2_1.PollInterop.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,34 @@
using System.Runtime;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.Threading.Tasks;

namespace System.Threading
{
public sealed partial class Thread
{
// these methods are temporarily accessed via UnsafeAccessor from generated code until we have it in public API, probably in WASI preview3 and promises
#if TARGET_WASI
internal static System.Threading.Tasks.Task RegisterWasiPollable(int handle)
internal static System.Threading.Tasks.Task RegisterWasiPollableHandle(int handle)
{
return WasiEventLoop.RegisterWasiPollable(handle);
return WasiEventLoop.RegisterWasiPollableHandle(handle);
}

internal static void DispatchWasiEventLoop()
internal static int PollWasiEventLoopUntilResolved(Task<int> mainTask)
{
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.

{
WasiEventLoop.DispatchWasiEventLoop();
}
var exception = mainTask.Exception;
if (exception is not null)
{
throw exception;
}

return mainTask.Result;
}

#endif

// the closest analog to Sleep(0) on Unix is sched_yield
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,64 @@ namespace System.Threading
{
internal static class WasiEventLoop
{
private static List<(IPoll.Pollable, TaskCompletionSource)> pollables = new();
private static List<WeakReference<TaskCompletionSource>> s_pollables = new();

internal static Task RegisterWasiPollable(int handle)
internal static Task RegisterWasiPollableHandle(int handle)
{
var source = new TaskCompletionSource(TaskCreationOptions.AttachedToParent);
pollables.Add((new IPoll.Pollable(new IPoll.Pollable.THandle(handle)), source));
return source.Task;
// note that this is duplicate of the original Pollable
// the original should be neutralized without disposing the handle
var pollableCpy = new IPoll.Pollable(new IPoll.Pollable.THandle(handle));
return RegisterWasiPollable(pollableCpy);
}

internal static Task RegisterWasiPollable(IPoll.Pollable pollable)
{
var tcs = new TaskCompletionSource(pollable);
var weakRef = new WeakReference<TaskCompletionSource>(tcs);
s_pollables.Add(weakRef);
return tcs.Task;
}

internal static void DispatchWasiEventLoop()
{
ThreadPoolWorkQueue.Dispatch();

if (WasiEventLoop.pollables.Count > 0)
if (s_pollables.Count > 0)
{
var pollables = WasiEventLoop.pollables;
WasiEventLoop.pollables = new();
var arguments = new List<IPoll.Pollable>();
var sources = new List<TaskCompletionSource>();
foreach ((var pollable, var source) in pollables)
var pollables = s_pollables;
s_pollables = new List<WeakReference<TaskCompletionSource>>(pollables.Count);
var arguments = new List<IPoll.Pollable>(pollables.Count);
var indexes = new List<int>(pollables.Count);
for (var i = 0; i < pollables.Count; i++)
{
arguments.Add(pollable);
sources.Add(source);
var weakRef = pollables[i];
if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
{
var pollable = (IPoll.Pollable)tcs!.Task.AsyncState!;
arguments.Add(pollable);
indexes.Add(i);
}
}
var results = PollInterop.Poll(arguments);

// this is blocking until at least one pollable resolves
var readyIndexes = PollInterop.Poll(arguments);

var ready = new bool[arguments.Count];
foreach (var result in results)
foreach (int readyIndex in readyIndexes)
{
ready[result] = true;
arguments[(int)result].Dispose();
sources[(int)result].SetResult();
ready[readyIndex] = true;
arguments[readyIndex].Dispose();
var weakRef = pollables[indexes[readyIndex]];
if (weakRef.TryGetTarget(out TaskCompletionSource? tcs))
{
tcs!.SetResult();
}
}
for (var i = 0; i < arguments.Count; ++i)
{
if (!ready[i])
{
WasiEventLoop.pollables.Add((arguments[i], sources[i]));
s_pollables.Add(pollables[indexes[i]]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Generated by `wit-bindgen` 0.29.0. DO NOT EDIT!
// <auto-generated />
#nullable enable

using System;
using System.Runtime.CompilerServices;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace WasiPollWorld.wit.imports.wasi.clocks.v0_2_1
{
internal static class MonotonicClockInterop {

internal static class NowWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "now"), WasmImportLinkage]
internal static extern long wasmImportNow();

}

internal static unsafe ulong Now()
{
var result = NowWasmInterop.wasmImportNow();
return unchecked((ulong)(result));

//TODO: free alloc handle (interopString) if exists
}

internal static class ResolutionWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "resolution"), WasmImportLinkage]
internal static extern long wasmImportResolution();

}

internal static unsafe ulong Resolution()
{
var result = ResolutionWasmInterop.wasmImportResolution();
return unchecked((ulong)(result));

//TODO: free alloc handle (interopString) if exists
}

internal static class SubscribeInstantWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-instant"), WasmImportLinkage]
internal static extern int wasmImportSubscribeInstant(long p0);

}

internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeInstant(ulong when)
{
var result = SubscribeInstantWasmInterop.wasmImportSubscribeInstant(unchecked((long)(when)));
var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
return resource;

//TODO: free alloc handle (interopString) if exists
}

internal static class SubscribeDurationWasmInterop
{
[DllImport("wasi:clocks/monotonic-clock@0.2.1", EntryPoint = "subscribe-duration"), WasmImportLinkage]
internal static extern int wasmImportSubscribeDuration(long p0);

}

internal static unsafe global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable SubscribeDuration(ulong when)
{
var result = SubscribeDurationWasmInterop.wasmImportSubscribeDuration(unchecked((long)(when)));
var resource = new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable(new global::WasiPollWorld.wit.imports.wasi.io.v0_2_1.IPoll.Pollable.THandle(result));
return resource;

//TODO: free alloc handle (interopString) if exists
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ tar xzf v0.2.1.tar.gz
cat >wasi-http-0.2.1/wit/world.wit <<EOF
world wasi-poll {
import wasi:io/poll@0.2.1;
import wasi:clocks/monotonic-clock@0.2.1;
}
EOF
wit-bindgen c-sharp -w wasi-poll -r native-aot --internal --skip-support-files wasi-http-0.2.1/wit
Expand Down
1 change: 1 addition & 0 deletions src/libraries/tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Buffers.Tests\System.Buffers.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Tests\Invariant\Invariant.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Globalization.Tests\System.Globalization.Tests.csproj" />
<SmokeTestProject Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Threading.Timer.Tests\System.Threading.Timer.Tests.csproj" />
</ItemGroup>

<!-- wasi/aot smoke tests -->
Expand Down
Loading
Loading