diff --git a/src/UniTask.NetCore/UniTask.NetCore.csproj b/src/UniTask.NetCore/UniTask.NetCore.csproj index 6a9b15d..13f90c7 100644 --- a/src/UniTask.NetCore/UniTask.NetCore.csproj +++ b/src/UniTask.NetCore/UniTask.NetCore.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + netcoreapp3.1;netstandard2.1 UniTask 8.0 Cysharp.Threading.Tasks diff --git a/src/UniTask.NetCoreSandbox/AllocationCheck.cs b/src/UniTask.NetCoreSandbox/AllocationCheck.cs new file mode 100644 index 0000000..c594944 --- /dev/null +++ b/src/UniTask.NetCoreSandbox/AllocationCheck.cs @@ -0,0 +1,198 @@ +using BenchmarkDotNet.Attributes; +using System.Linq; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using Cysharp.Threading.Tasks; +using PooledAwait; +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using System.Threading; +using System.Runtime.CompilerServices; +using Cysharp.Threading.Tasks.CompilerServices; +using System.Collections.Concurrent; + +[Config(typeof(BenchmarkConfig))] +public class AllocationCheck +{ + // note: all the benchmarks use Task/Task for the public API, because BenchmarkDotNet + // doesn't work reliably with more exotic task-types (even just ValueTask fails); instead, + // we'll obscure the cost of the outer awaitable by doing a relatively large number of + // iterations, so that we're only really measuring the inner loop + private const int InnerOps = 1000; + + [Benchmark(OperationsPerInvoke = InnerOps)] + public async Task ViaUniTask() + { + for (int i = 0; i < InnerOps; i++) + { + await Core(); + } + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + } + } + + [Benchmark(OperationsPerInvoke = InnerOps)] + public async Task ViaUniTaskT() + { + var sum = 0; + for (int i = 0; i < InnerOps; i++) + { + sum += await Core(); + } + return sum; + + static async UniTask Core() + { + var a = await new TestAwaiter(false, UniTaskStatus.Succeeded, 10); + var b = await new TestAwaiter(false, UniTaskStatus.Succeeded, 10); + var c = await new TestAwaiter(false, UniTaskStatus.Succeeded, 10); + return 10; + } + } + + [Benchmark(OperationsPerInvoke = InnerOps)] + public Task ViaUniTaskVoid() + { + for (int i = 0; i < InnerOps; i++) + { + Core().Forget(); + } + return Task.CompletedTask; + + static async UniTaskVoid Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + } + } +} + +public class TaskTestException : Exception +{ + +} + +public struct TestAwaiter : ICriticalNotifyCompletion +{ + readonly UniTaskStatus status; + readonly bool isCompleted; + + public TestAwaiter(bool isCompleted, UniTaskStatus status) + { + this.isCompleted = isCompleted; + this.status = status; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public void GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + break; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(ThreadPoolWorkItem.Create(continuation), false); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(ThreadPoolWorkItem.Create(continuation), false); + } + + +} + +public struct TestAwaiter : ICriticalNotifyCompletion +{ + readonly UniTaskStatus status; + readonly bool isCompleted; + readonly T value; + + public TestAwaiter(bool isCompleted, UniTaskStatus status, T value) + { + this.isCompleted = isCompleted; + this.status = status; + this.value = value; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public T GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + return value; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(ThreadPoolWorkItem.Create(continuation), false); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(ThreadPoolWorkItem.Create(continuation), false); + } +} + +public sealed class ThreadPoolWorkItem : IThreadPoolWorkItem +{ + static readonly ConcurrentQueue pool = new ConcurrentQueue(); + + Action continuation; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ThreadPoolWorkItem Create(Action continuation) + { + if (!pool.TryDequeue(out var item)) + { + item = new ThreadPoolWorkItem(); + } + + item.continuation = continuation; + return item; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Execute() + { + var call = continuation; + continuation = null; + pool.Enqueue(this); + + call.Invoke(); + } +} \ No newline at end of file diff --git a/src/UniTask.NetCoreSandbox/Benchmark.cs b/src/UniTask.NetCoreSandbox/Benchmark.cs new file mode 100644 index 0000000..b47294e --- /dev/null +++ b/src/UniTask.NetCoreSandbox/Benchmark.cs @@ -0,0 +1,283 @@ +using BenchmarkDotNet.Attributes; +using System.Linq; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using Cysharp.Threading.Tasks; +using PooledAwait; +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using System.Threading; +using System.Runtime.CompilerServices; +using Cysharp.Threading.Tasks.CompilerServices; + +//class Program +//{ +// static void Main(string[] args) +// { +// var switcher = new BenchmarkSwitcher(new[] +// { +// typeof(StandardBenchmark) +// }); + +//#if DEBUG +// var b = new StandardBenchmark(); + +//#else +// switcher.Run(args); +//#endif +// } +//} + +public class BenchmarkConfig : ManualConfig +{ + public BenchmarkConfig() + { + AddDiagnoser(MemoryDiagnoser.Default); + AddJob(Job.ShortRun.WithLaunchCount(1).WithIterationCount(1).WithWarmupCount(1)); + } +} + +// borrowed from PooledAwait + +[Config(typeof(BenchmarkConfig))] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class ComparisonBenchmarks +{ + // note: all the benchmarks use Task/Task for the public API, because BenchmarkDotNet + // doesn't work reliably with more exotic task-types (even just ValueTask fails); instead, + // we'll obscure the cost of the outer awaitable by doing a relatively large number of + // iterations, so that we're only really measuring the inner loop + private const int InnerOps = 1000; + + public bool ConfigureAwait { get; set; } = false; + + [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] + [BenchmarkCategory("Task")] + public async Task ViaTaskT() + { + int sum = 0; + for (int i = 0; i < InnerOps; i++) + sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); + return sum; + + static async Task Inner(int x, int y) + { + int i = x; + await Task.Yield(); + i *= y; + await Task.Yield(); + return 5 * i; + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] + [BenchmarkCategory("Task")] + public async Task ViaTask() + { + for (int i = 0; i < InnerOps; i++) + await Inner().ConfigureAwait(ConfigureAwait); + + static async Task Inner() + { + await Task.Yield(); + await Task.Yield(); + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] + [BenchmarkCategory("ValueTask")] + public async Task ViaValueTaskT() + { + int sum = 0; + for (int i = 0; i < InnerOps; i++) + sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); + return sum; + + static async ValueTask Inner(int x, int y) + { + int i = x; + await Task.Yield(); + i *= y; + await Task.Yield(); + return 5 * i; + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = ".NET")] + [BenchmarkCategory("ValueTask")] + public async Task ViaValueTask() + { + for (int i = 0; i < InnerOps; i++) + await Inner().ConfigureAwait(ConfigureAwait); + + static async ValueTask Inner() + { + await Task.Yield(); + await Task.Yield(); + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] + [BenchmarkCategory("ValueTask")] + public async Task ViaPooledValueTaskT() + { + int sum = 0; + for (int i = 0; i < InnerOps; i++) + sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); + return sum; + + static async PooledValueTask Inner(int x, int y) + { + int i = x; + await Task.Yield(); + i *= y; + await Task.Yield(); + return 5 * i; + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] + [BenchmarkCategory("ValueTask")] + public async Task ViaPooledValueTask() + { + for (int i = 0; i < InnerOps; i++) + await Inner().ConfigureAwait(ConfigureAwait); + + static async PooledValueTask Inner() + { + await Task.Yield(); + await Task.Yield(); + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] + [BenchmarkCategory("Task")] + public async Task ViaPooledTaskT() + { + int sum = 0; + for (int i = 0; i < InnerOps; i++) + sum += await Inner(1, 2).ConfigureAwait(ConfigureAwait); + return sum; + + static async PooledTask Inner(int x, int y) + { + int i = x; + await Task.Yield(); + i *= y; + await Task.Yield(); + return 5 * i; + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "Pooled")] + [BenchmarkCategory("Task")] + public async Task ViaPooledTask() + { + for (int i = 0; i < InnerOps; i++) + await Inner().ConfigureAwait(ConfigureAwait); + + static async PooledTask Inner() + { + await Task.Yield(); + await Task.Yield(); + } + } + + // --- + + //[Benchmark(OperationsPerInvoke = InnerOps, Description = "UniTaskVoid")] + //[BenchmarkCategory("UniTask")] + //public async Task ViaUniTaskVoid() + //{ + // for (int i = 0; i < InnerOps; i++) + // { + // await Inner(); + // } + + // static async UniTaskVoid Inner() + // { + // await UniTask.Yield(); + // await UniTask.Yield(); + // } + //} + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "UniTask")] + [BenchmarkCategory("UniTask")] + public async Task ViaUniTask() + { + for (int i = 0; i < InnerOps; i++) + { + await Inner(); + } + + static async UniTask Inner() + { + await UniTask.Yield(); + await UniTask.Yield(); + } + } + + [Benchmark(OperationsPerInvoke = InnerOps, Description = "UniTaskT")] + [BenchmarkCategory("UniTask")] + public async Task ViaUniTaskT() + { + var sum = 0; + for (int i = 0; i < InnerOps; i++) + { + sum += await Inner(1, 2); + } + return sum; + + static async UniTask Inner(int x, int y) + { + int i = x; + await UniTask.Yield(); + i *= y; + await UniTask.Yield(); + return 5 * i; + } + } +} + +public struct MyAwaiter : ICriticalNotifyCompletion +{ + public MyAwaiter GetAwaiter() => this; + + public bool IsCompleted => false; + + public void GetResult() + { + } + + public void OnCompleted(Action continuation) + { + continuation(); + } + + public void UnsafeOnCompleted(Action continuation) + { + continuation(); + } +} + +public struct MyTestStateMachine : IAsyncStateMachine +{ + public void MoveNext() + { + //throw new NotImplementedException(); + + + + + } + + public void SetStateMachine(IAsyncStateMachine stateMachine) + { + //throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/UniTask.NetCoreSandbox/Program.cs b/src/UniTask.NetCoreSandbox/Program.cs index ee1989e..3961823 100644 --- a/src/UniTask.NetCoreSandbox/Program.cs +++ b/src/UniTask.NetCoreSandbox/Program.cs @@ -38,6 +38,94 @@ namespace NetCoreSandbox } + public class TaskTestException : Exception + { + + } + + + public struct TestAwaiter : ICriticalNotifyCompletion + { + readonly UniTaskStatus status; + readonly bool isCompleted; + + public TestAwaiter(bool isCompleted, UniTaskStatus status) + { + this.isCompleted = isCompleted; + this.status = status; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public void GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + break; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.QueueUserWorkItem(_ => continuation(), null); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), null); + } + } + public struct TestAwaiter : ICriticalNotifyCompletion + { + readonly UniTaskStatus status; + readonly bool isCompleted; + readonly T value; + + public TestAwaiter(bool isCompleted, UniTaskStatus status, T value) + { + this.isCompleted = isCompleted; + this.status = status; + this.value = value; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public T GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + return value; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.QueueUserWorkItem(_ => continuation(), null); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), null); + } + } public static partial class UnityUIComponentExtensions @@ -109,26 +197,45 @@ namespace NetCoreSandbox static async Task Main(string[] args) { - var foo = await new ZeroAllocAsyncAwaitInDotNetCore().NanikaAsync(1, 2); - Console.WriteLine(foo); +#if !DEBUG + BenchmarkDotNet.Running.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - var channel = Channel.CreateSingleConsumerUnbounded(); - - // Observable.Range(1,10).CombineLatest( - - var cts = new CancellationTokenSource(); - - var token = cts.Token; - - await FooAsync(token).ForEachAsync(x => { }, token); + //await new ComparisonBenchmarks().ViaUniTaskT(); + return; +#endif - // Observable.Range(1,10).CombineLatest( + AsyncTest().Forget(); + + //AsyncTest().Forget(); + + // AsyncTest().Forget(); + + + await UniTask.Yield(); + Console.ReadLine(); + } + +#pragma warning disable CS1998 + + + static async UniTaskVoid AsyncTest() + { + // empty + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(true, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + + + Console.WriteLine("foo"); + //return 10; } +#pragma warning restore CS1998 + void Foo() { diff --git a/src/UniTask.NetCoreSandbox/UniTask.NetCoreSandbox.csproj b/src/UniTask.NetCoreSandbox/UniTask.NetCoreSandbox.csproj index c7cf11b..6d531d0 100644 --- a/src/UniTask.NetCoreSandbox/UniTask.NetCoreSandbox.csproj +++ b/src/UniTask.NetCoreSandbox/UniTask.NetCoreSandbox.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/UniTask.NetCoreTests/TaskBuilderCases.cs b/src/UniTask.NetCoreTests/TaskBuilderCases.cs new file mode 100644 index 0000000..fbe056a --- /dev/null +++ b/src/UniTask.NetCoreTests/TaskBuilderCases.cs @@ -0,0 +1,301 @@ +#pragma warning disable CS1998 + +using Cysharp.Threading.Tasks; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using Cysharp.Threading.Tasks.Linq; +using System.Threading.Tasks; +using Xunit; +using System.Runtime.CompilerServices; + +namespace NetCoreTests +{ + public class UniTaskBuilderTest + { + [Fact] + public async Task Empty() + { + await Core(); + + static async UniTask Core() + { + } + } + + [Fact] + public async Task EmptyThrow() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + throw new TaskTestException(); + } + } + + [Fact] + public async Task Task_Done() + { + await Core(); + + static async UniTask Core() + { + await new TestAwaiter(true, UniTaskStatus.Succeeded); + } + } + + [Fact] + public async Task Task_Fail() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(true, UniTaskStatus.Faulted); + } + } + + [Fact] + public async Task Task_Cancel() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(true, UniTaskStatus.Canceled); + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetResult() + { + await Core(); + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Succeeded); + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetException() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Faulted); + throw new InvalidOperationException(); + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetCancelException() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded); + await new TestAwaiter(false, UniTaskStatus.Canceled); + throw new InvalidOperationException(); + } + } + } + + public class UniTask_T_BuilderTest + { + [Fact] + public async Task Empty() + { + (await Core()).Should().Be(10); + + static async UniTask Core() + { + return 10; + } + } + + [Fact] + public async Task EmptyThrow() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + throw new TaskTestException(); + } + } + + [Fact] + public async Task Task_Done() + { + (await Core()).Should().Be(10); + + static async UniTask Core() + { + return await new TestAwaiter(true, UniTaskStatus.Succeeded, 10); + } + } + + [Fact] + public async Task Task_Fail() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + return await new TestAwaiter(true, UniTaskStatus.Faulted, 10); + } + } + + [Fact] + public async Task Task_Cancel() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + return await new TestAwaiter(true, UniTaskStatus.Canceled, 10); + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetResult() + { + (await Core()).Should().Be(6); + + static async UniTask Core() + { + var sum = 0; + sum += await new TestAwaiter(false, UniTaskStatus.Succeeded, 1); + sum += await new TestAwaiter(false, UniTaskStatus.Succeeded, 2); + sum += await new TestAwaiter(false, UniTaskStatus.Succeeded, 3); + return sum; + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetException() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded, 10); + await new TestAwaiter(false, UniTaskStatus.Faulted, 10); + throw new InvalidOperationException(); + } + } + + [Fact] + public async Task AwaitUnsafeOnCompletedCall_Task_SetCancelException() + { + await Assert.ThrowsAsync(async () => await Core()); + + static async UniTask Core() + { + await new TestAwaiter(false, UniTaskStatus.Succeeded, 10); + await new TestAwaiter(false, UniTaskStatus.Canceled, 10); + throw new InvalidOperationException(); + } + } + } + + public class TaskTestException : Exception + { + + } + + public struct TestAwaiter : ICriticalNotifyCompletion + { + readonly UniTaskStatus status; + readonly bool isCompleted; + + public TestAwaiter(bool isCompleted, UniTaskStatus status) + { + this.isCompleted = isCompleted; + this.status = status; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public void GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + break; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.QueueUserWorkItem(_ => continuation(), null); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), null); + } + } + + public struct TestAwaiter : ICriticalNotifyCompletion + { + readonly UniTaskStatus status; + readonly bool isCompleted; + readonly T value; + + public TestAwaiter(bool isCompleted, UniTaskStatus status, T value) + { + this.isCompleted = isCompleted; + this.status = status; + this.value = value; + } + + public TestAwaiter GetAwaiter() => this; + + public bool IsCompleted => isCompleted; + + public T GetResult() + { + switch (status) + { + case UniTaskStatus.Faulted: + throw new TaskTestException(); + case UniTaskStatus.Canceled: + throw new OperationCanceledException(); + case UniTaskStatus.Pending: + case UniTaskStatus.Succeeded: + default: + return value; + } + } + + public void OnCompleted(Action continuation) + { + ThreadPool.QueueUserWorkItem(_ => continuation(), null); + } + + public void UnsafeOnCompleted(Action continuation) + { + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), null); + } + } +}