timcassell / protopromise Goto Github PK
View Code? Open in Web Editor NEWRobust and efficient library for management of asynchronous operations in C#/.Net.
License: MIT License
Robust and efficient library for management of asynchronous operations in C#/.Net.
License: MIT License
This was mentioned at the bottom of #12. Promises currently can only be awaited asynchronously to prevent deadlocks. However, there may be times where parallel workloads needs to be waited on to complete in a synchronous method. A PromiseTask
class can be introduced to support this type of work.
public int Func()
{
PromiseTask<int> task = PromiseTask.Run(() =>
{
// Do work on background thread.
return 42;
});
// Do work on current thread.
int fgResult = 21;
int bgResult = task.GetResult(); // Wait for background work to complete and get its result synchronously.
return fgResult + bgResult;
}
Built-in Task
s already provide this functionality via Task.Wait()
and Task<T>.Result
, but it's easy to accidentally cause deadlocks with that. PromiseTask
represents a work item that is running on a background thread (never foreground), thus there is no concern for deadlocks. It can be waited on synchronously or asynchronously, and will have implicit casts to Promise(<T>)
.
PromiseTask.Run
runs a single action on a background thread.
public static PromiseTask Run(Action action)
runs an action returning void, and returns PromiseTask
.
public static PromiseTask<T> Run<T>(Func<T> function)
runs a function returning a T value, and returns PromiseTask<T>
.
Unlike Promise.Then
callbacks, no special considerations should be made to adopt the state of a returned PromiseTask
or Promise
(as that would mean they could be waiting for an action to complete on the current or foreground thread and cause a deadlock)
Besides that, PromisTask
can have more APIs to do more complex things, like PromiseTask.Merge
, PromiseTask.All
, PromiseTask.Race
, PromiseTask.First
to complement the static Promise.All
etc merge functions.
It can also have APIs like PromiseTask.ForEach
to iterate over an enumerable and do an action on each item in parallel, and a PromiseTask.For
. These would work very similar to Parallel.For
and Parallel.ForEach
except that they are asynchronous work items, meaning you can still do more work on the current thread while they are executing, then wait for their results later.
void PromiseTask.Wait()
is an instance method to wait for it to complete synchronously, and will throw if the background action(s) did not complete successfully.
T PromiseTask<T>.GetResult()
is an instance method that behaves the same as Wait()
, except it returns the T
value that the PromiseTask<T>
produced.
To avoid having to catch exceptions, a PromiseTask<T>.GetResultContainer()
may be added to get a result container that wraps the final state (Resolved
, Rejected
, or Canceled
) and the result or exception.
It can also have a PromiseTask.Forget()
call the same as Promise.Forget()
if the user just wants to fire-and-forget.
Not all of these APIs need to be implemented at once. We can start with PromiseTask.Run
and PromiseTask.Wait
and PromiseTask<T>.GetResult
and awaitable and implicit cast to Promise(<T>)
.
Promise Promise.ParallelForEach<T>(IEnumerable<T> source, Action<T> body)
iterates over an enumerable and invokes an action on each item in parallel (posted to the background context).
Promise Promise.ParallelFor(int fromInclusive, int toExclusive, Action<int> body)
executes a for loop, where the index of each iteration is passed to an action in parallel.
They both return a Promise
that will be resolved when all actions are completed successfully, or rejected with an AggregateException
containing all exceptions if any action failed.
And of course overloads to pass capture values to the actions.
These work very similar to Parallel.For
and Parallel.ForEach
except that they are asynchronous work items, meaning you can still do more work on the current thread while they are executing, then wait for their results later.
These are starting simple APIs, we can expand to more advanced APIs in the future (like MaxDegreeOfParallelism
, ParallelLoopState
, etc).
Split from #24.
Context switching in this case refers to switching execution between the main/UI thread and background threads. Executing code on a background thread frees up the UI thread to draw the application at a higher frame-rate and not freeze the application when executing an expensive computation. Multiple work items can also be executed in parallel, speeding up some calculations.
Built-in Task
s allow you to explicitly switch to background threads, then from the background thread switch back to the UI thread by utilizing the SynchronizationContext
captured before switching to the background thread. In an async Task
function, this is done with the simple Task.ConfigureAwait(bool)
method. The problem with this method is, once you have switched to the background context, you cannot return to the UI context later in the function unless you manually capture the SynchronizationContext
and use a much more complex API. You might also have no idea whether the context you're capturing is the UI context or the background context, in which case it is impossible to marshal any work to the UI thread.
For ProtoPromise, we can simplify this functionality and solve the impossible problem. Since we already have Promise.Manager.HandleCompletes()
which is called at regular intervals (every frame) on the main/UI thread, we can ignore SynchronizationContext
and use a simple enum:
public enum ExecutionOption
{
Synchronous,
UI,
Background
}
With these options, the context is no longer captured, but rather stated explicitly. Synchronous
will execute synchronously on whatever thread happens to complete the promise. UI
will marshal the execution back to the UI thread via Promise.Manager.HandleCompletes()
. Background
will marshal the execution onto a background thread.
It can be used like so:
public async Promise Func()
{
// Who knows what thread we're on?
var text = await DownloadText1().ConfigureAwait(ExecutionOption.Background);
// Do work on a background thread.
int value = Parse(text);
text = await DownloadText2().ConfigureAwait(ExecutionOption.UI);
// Do work on the UI thread.
int result = value + Parse(text);
}
If a Promise
is awaited without being configured, the default option is Synchronous
.
For async Promise
functions, it would be useful to simply switch contexts without awaiting a Promise
:
public async Promise Func()
{
// Who knows what thread we're on?
await Promise.SwitchToBackground();
// Do work on a background thread.
await Promise.SwitchToUI();
// Do work on the UI thread.
}
ExecutionOption
can also be added as an additional optional parameter to Then
, ContinueWith
, etc. methods, allowing the callback to be ran on the chosen context.
Task
s can start an operation in the background by calling Task.Run(callback)
. ProtoPromise should add similar Promise.RunInBackground(callback)
functions that can be used to marshal a work item to the background, and Promise.RunInUI(callback)
to marshal a work item to the UI thread.
Task.Run
returns a Task
which can be waited on synchronously to complete via Task.Wait()
or Task.Result
, blocking the thread until it's complete. This is inherently dangerous, as any task can be waited in that fashion, easily causing a deadlock. To prevent that from happening while still serving that functionality, Promise.RunInBackground
should return a new type that can block until complete, while normal Promise(<T>)
instances can only be awaited asynchronously. This new type could be called PromiseTask(<T>)
or something similar, and it should have an implicit cast to Promise(<T>)
.
Promise.RunInUI
should return a Promise(<T>)
, as there is no reason to ever block a thread waiting on the UI thread (and we definitely do not want the UI thread to deadlock).
This proposal relies on #8.
It is not a 'critical' issue - but rather a bit Console Polluting.
When the package is added, there are 2 warnings popping up (spawning multiple times after code changes) about some scripts (Demo/DownloadHelperExample.cs, Demo/ProtoPromiseExample.cs) that exist outside the assets folder.
Would it be possible to fix these minor issues?
(Maybe by adding a asmdef for the Demo folder)
I'm currently using Unity2021.3.1f1
Thanks in advance!
void Promise.Wait()
is an instance method to wait for it to complete synchronously, and will throw if the Promise did not complete successfully (the same way await
throws).
T Promise<T>.GetResult()
is an instance method that behaves the same as Wait()
, except it returns the T
value that the Promise<T>
produced.
These APIs are advanced and should not be used unless the user knows what they are doing, as they can easily cause deadlocks. Usually they are meant to be combined with Promise.Run(...)
to wait for the result from another thread synchronously.
Split from #24.
Currently, AwaitWithProgress
takes 2 arguments, float minProgress
and float maxProgress
. When the promise is awaited, the async Promise
's progress is set to minProgress
.
async Promise Func()
{
await DownloadThing("thing1").AwaitWithProgress(0f, 0.25f);
await DownloadThing("thing2").AwaitWithProgress(0.25f, 0.5f);
await DownloadThing("thing3").AwaitWithProgress(0.5f, 0.75f);
await DownloadThing("thing4").AwaitWithProgress(0.75f, 1f);
}
We can add a new overload that takes only 1 float maxProgress
, which will preserve the async Promise
's current progress when it is awaited, effectively setting minProgress
to that current progress. That will simplify the code to:
async Promise Func()
{
await DownloadThing("thing1").AwaitWithProgress(0.25f);
await DownloadThing("thing2").AwaitWithProgress(0.5f);
await DownloadThing("thing3").AwaitWithProgress(0.75f);
await DownloadThing("thing4").AwaitWithProgress(1f);
}
Which results in less typing and clearer code for the most part. This would also make it possible to have progress never go backwards in the event of retries, if the design calls for it:
async Promise Func()
{
Retry:
try
{
await DownloadThing("thing1").AwaitWithProgress(1f);
}
catch
{
goto Retry;
}
}
In this case, if the progress incremented to 0.5, then rejected for some reason, the retry would then lerp the progress from 0.5 to 1, instead of 0 to 1.
.Net Core 2.1 and .Net Standard 2.1 added IValueTaskSource<TResult>
for custom types to back ValueTask<T>
(and same for non-generic).
We can make our internal PromiseRef
classes implement IValueTaskSource<TResult>
to be able to convert a Promise
to a ValueTask
without allocation.
We can add Promise.AsValueTask()
as well as an implicit cast. And we can add a ValueTask.ToPromise()
extension method the same way we have a Task.ToPromise()
extension method.
Something to investigate before releasing v2.0 is throwing the original exception when awaiting promises, instead of throwing a wrapper exception. This is how Tasks behave, and it would make it easier to filter exception types with try/catch clauses in async functions. We can use ExceptionDispatchInfo.Capture(Exception)
and ExceptionDispatchInfo.Throw()
to preserve the causality trace information that the wrapper exception is currently doing.
For non-exception rejections, the wrapper exception can still be thrown (because C# doesn't support throwing and catching non-exception objects).
ProtoPromise v1 only supports usage on the main thread, and even has an extra check in there to make sure it is only accessed from a single thread in DEBUG
mode (it sends a warning to the warning handler if it exists, otherwise it throws). This is undocumented.
The way Promises work are when a promise is resolved, rejected, or canceled, the callbacks that were subscribed to it get added to the event queue instead of executed synchronously. The next time Promise.Manager.HandleCompletes(AndProgress) is called (which happens automatically with the Unity package) is when the callbacks are dequeued and executed.
This behavior follows the Promises/A+ specification 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
It also means that callbacks are executed iteratively instead of recursively so that the library will never cause a stack overflow (this is why the benchmarks always work with high values of N while other libraries fail). This also helps with usability, allowing programmers to add as many callbacks as they want to a single Promise object within a single method. Uncaught rejections and object pooling kicks in later in the execution pipeline when the event queue is handled.
Unfortunately (or rather, fortunately), unlike javascript, C# is not relegated to a single thread (depending on the target application). Advanced C# programmers make great use of threads to speed up their applications. Simply adding "thread safety" to an individual Promise object introduces problems when you want to add multiple callbacks to it. While you're adding callbacks in a background thread, the main thread could be executing the event queue and trying to put that Promise object back in the object pool, thus invalidating any further actions to that Promise object, or throwing unhandled rejections before the background thread has a chance to add a rejection handler.
Of course, if we just get rid of object pooling entirely and remove the event queue to just execute callbacks synchronously, this would be a non-issue (like built-in Tasks), but one of the goals of this library was to reduce GC pressure, so I don't think that's the way to go (otherwise, why not just use built-in Tasks?), plus you can see from the benchmarks that object pooling greatly increases the library's speed (as minuscule of a difference as that makes in a real application, every bit matters). Also, uncaught rejections would need to be caught in the Promise's finalizer rather than during the event handling, further increasing GC pressure.
RSG Promises handle the uncaught rejections by forcing the programmer to call Done()
on the last promise in the chain. UniTask has a similar Forget()
that you can call to try to have the underlying object added back to the object pool. ProtoPromise currently does it opposite, where it automatically handles uncaught rejections and repooling in the event handler, but you can also prevent that from happening by calling Retain()
if you need to use the Promise object for a longer lifetime (not recommended because it can be difficult to keep track of retained objects to release them later).
If we have a Promise.Run(Action)
that executes a delegate on a background thread (to replace Task.Run
), we could set up a thread-local event queue, but that doesn't solve the issue of 2 separate threads using a single Promise object, and actually increases the contention. Also, a lot of C# applications are already using threads by other means (new Thread()
), and those threads should be able to interact with Promises without issues.
UniTask by default only lets you await a task once. You can call Preserve()
to allow you to await it as much as you want, along with Forget()
to allow it to repool again. What if we copy that behavior?
Then
, Catch
, ContinueWith
, await
)Now, to be able to await multiple times/add as many callbacks as we want, we need a similar Preserve()
method that will need to be called before we add any callbacks. We already have Retain()
so let's just keep that. We also need a Forget()
like UniTask to handle uncaught rejections and repooling. We already have Release()
, but if we always use Release()
as the promise chain terminator, that makes it impossible for multiple threads to retain a single Promise object. So let's use Release()
as the counter to Retain()
, and add a new Forget()
as the chain terminator (RSG uses Done
, but I think Forget
is more descriptive).
promise
.Then(() => {})
.Then(() => {})
.Forget();
What if we want to add a callback to a promise, and then return that promise?
promise.Retain();
promise.Then(() => {}).Forget();
promise.Release();
return promise;
Retain
allows as many awaits as you want until Release
is called. After that, it reverts to its initial state of only allowing a single await, or Forget
.
What if you call Forget()
on a retained Promise object? Should it still allow callbacks to be added until Release()
is called? Or should it throw an exception saying that Release
must be called before Forget
? I'm leaning towards the former to allow users to cache Promise objects (even though it's not recommended) and to allow a thread to keep using a promise after another thread forgot it.
If we allow callbacks to still be added after it's forgotten and until it's released, Forget
should also be allowed to be called any number of times. If a promise is already forgotten while retained, when it is fully released, the last Release()
call acts the same as a normal Forget()
call (it handles uncaught rejections and repools the object).
With callbacks being executed synchronously upon Promise resolution, deferreds must be changed to invalidate themselves when they are resolved/rejected/canceled. This makes the State
property completely useless. The only thing that will matter then is IsPending
which is equivalent to IsValid
.
With these hefty changes, Promises might also benefit from being changed to struct
instead of class
, similar to how CancelationToken
s work (and even Deferred
s themselves). This would allow better validity checks and let us enable object pooling in DEBUG
mode (currently Promises are not pooled in DEBUG
mode to allow for validity checks, which are completely absent from RELEASE
builds). Pooling could also be simplified to on or off, completely removing the internal option (all pooled objects would be internal with this change). Furthermore, changing Promises to structs would increase the efficiency of already-completed Promises since it would only need to live on the stack and not have to go to the heap to find/create a Promise object.
[Edit] The downside of changing promises to structs is we lose the Promise<T> : Promise
inheritance, and all the benefits that brings with it.
Such big changes to support threads surely require a major version change. It might even need a separate fork of its own, what do you think?
When racing promises, it can sometimes be useful to know which promise won. Currently, Promise.Race
and Promise.First
return a promise of the same type as the inputs, and there is no easy way to know which promise won.
I propose adding Promise.RaceWithIndex
and Promise.FirstWithIndex
that will return Promise<int>
for void promises, or Promise<(int winIndex, T result)>
for result promises. The resulting index will be that of the promise that won the race.
var (winIndex, result) = await Promise<T>.RaceWithIndex(promise1, promise2);
Adding an AsyncLock
type akin to AsyncEx would be useful for locking in async code, and we can do it more efficiently (AsyncEx
wraps Tasks which are heavier than our Promises).
https://github.com/timcassell/ProtoPromise/actions/runs/3117637137/jobs/5056811984
NewAndRunTests.PromiseRunIsInvokedAndCompletedAndProgressReportedProperly_adopt_capture_T(Background,Foreground,Resolve,False,False)
at ProtoPromiseTests.ProgressHelper.GetCurrentProgressEqualsExpected (System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x0001a] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:121
at ProtoPromiseTests.ProgressHelper.ReportProgressAndAssertResult (Proto.Promises.Promise+DeferredBase deferred, System.Single reportValue, System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00024] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:163
at ProtoPromiseTests.APIs.NewAndRunTests.PromiseRunIsInvokedAndCompletedAndProgressReportedProperly_adopt_capture_T (ProtoPromiseTests.SynchronizationType synchronizationType, ProtoPromiseTests.SynchronizationType invokeContext, ProtoPromiseTests.CompleteType completeType, System.Boolean throwInAction, System.Boolean isAlreadyComplete) [0x0016d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/APIs/NewAndRunTests.cs:1180
at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[],System.Exception&)
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00032] in <c8d0d7b9135640958bff528a1e374758>:0
--TimeoutException
at ProtoPromiseTests.ProgressHelper.MaybeWaitForInvoke (System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan waitTimeout) [0x00085] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:87
at ProtoPromiseTests.ProgressHelper.GetCurrentProgress (System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00014] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:151
at ProtoPromiseTests.ProgressHelper.GetCurrentProgressEqualsExpected (System.Single expectedProgress, System.Boolean waitForInvoke, System.Boolean executeForeground, System.TimeSpan timeout) [0x00002] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ProgressHelper.cs:116
--TearDown
at Proto.Promises.Internal.AssertAllObjectsReleased () [0x000d5] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/InternalShared/PoolInternal.cs:213
at ProtoPromiseTests.TestHelper.Cleanup () [0x0004b] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:146
at ProtoPromiseTests.APIs.NewAndRunTests.Teardown () [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/APIs/NewAndRunTests.cs:26
at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[],System.Exception&)
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00032] in <c8d0d7b9135640958bff528a1e374758>:0
I know the title sounds weird. But here is the API:
public System.Threading.CancellationToken ToCancellationToken()
The motivation for this is that the built-in .Net APIs are increasingly relying on CancellationToken
, which are a separate type from this library's CancelationToken
. Other third-party libraries also rely on it. In order to interop with those APIs while still using this library, there needs to be a simple way to convert a Proto.Promises.CancelationToken
to a System.Threading.CancellationToken
.
You might ask, "why not just ditch Proto.Promises.CancelationToken
and switch over fully to System.Threading.CancellationToken
?".
The answer to this is that System.Threading.CancellationTokenSource
s cannot be re-used. Proto.Promises.CancelationSource
can be re-used to reduce GC pressure.
.Net 6.0 is also adding System.Threading.CancellationTokenSource.TryReset()
as a new API that can be used to attempt to reduce allocations, and that can be used internally by the Proto.Promises.CancelationToken
. Proto.Promises.CancelationToken.ToCancellationToken()
will not be able to guarantee allocation-free the way the rest of the library does with pooling enabled, but cancelations are the exception, not the rule, so it will usually not allocate.
Unfortunately, a ToCancelationToken()
extension cannot be added to System.Threading.CancellationToken
, because there is no way to hook into the internal Dispose
of the source in order to dispose of our source. Because of that, I may look into adding System.Threading.CancellationToken
consumption in the public APIs as overloads. But that would be a large undertaking to support both kinds of tokens.
[Edit] It would be possible if an API is exposed to be able to hook into that (like this discussion), but I don't see that getting off the ground.
[Edit2] Actually, it looks like it is possible to implement a proper System.Threading.CancellationToken.ToCancelationToken()
extension: dotnet/runtime#60843 (comment), though it will be more expensive than converting the other way (more allocations that cannot be amortized away).
Create Nuget package for v2.0 release.
See https://stackoverflow.com/questions/37673692/how-to-create-a-nuget-package-with-both-release-and-debug-dlls-using-nuget-pack for potentially supporting both release and debug in the same package.
https://github.com/timcassell/ProtoPromise/runs/7703914830?check_suite_focus=true
ProtoPromiseTests.Threading.PromiseConcurrencyTests.PromiseProgressMayBeSubscribedWhilePromiseIsCompletedAndProgressIsReportedConcurrently_Pending_T(Parallel,Parallel,Parallel,CancelFromToken,Interface,Foreground)
--IndexOutOfRangeException
at Proto.Promises.Internal.ValueList`1.get_Item(Int32 index) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\InternalShared\ValueCollectionsInternal.cs:line 438
at Proto.Promises.Internal.PromiseRefBase.PromiseMultiAwait`1.InvokeProgressFromContext() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 814
at Proto.Promises.Internal.StackUnwindHelper.InvokeProgressors() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 99
at Proto.Promises.Internal.PromiseRefBase.DeferredPromiseBase`1.ReportProgressAlreadyIncremented(Fixed32 progress, HandleablePromiseBase progressListener) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 882
at Proto.Promises.Internal.PromiseRefBase.DeferredPromiseBase`1.TryReportProgress(Int32 deferredId, Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\ProgressInternal.cs:line 859
at Proto.Promises.Internal.DeferredPromiseHelper.TryReportProgress(IDeferredPromise _this, Int32 deferredId, Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Internal\DeferredInternal.cs:line 48
at Proto.Promises.Promise`1.Deferred.TryReportProgress(Single progress) in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromise\Core\Promises\Deferred.cs:line 821
at ProtoPromiseTests.Threading.PromiseConcurrencyTests.<>c__DisplayClass14_0.<PromiseProgressMayBeSubscribedWhilePromiseIsCompletedAndProgressIsReportedConcurrently_Pending_T>b__3() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromiseTests\Tests\Threading\PromiseConcurrencyTests.cs:line 485
at ProtoPromiseTests.Threading.ThreadHelper.ThreadRunner.Execute() in D:\a\ProtoPromise\ProtoPromise\ProtoPromise_Unity\Assets\Plugins\ProtoPromiseTests\Helpers\ThreadHelper.cs:line 71
Benchmarks are showing an allocation that should not happen:
| Type | Method | N | BaseIsPending | Mean | Code Size | Allocated | Survived |
|---------------------- |---------------- |---- |-------------- |----------:|----------:|----------:|---------:|
| AwaitPending | ProtoPromise_V2 | 100 | ? | 382.57 ฮผs | 13,141 B | - | 416 B |
| AsyncPending | ProtoPromise_V2 | 100 | ? | 86.27 ฮผs | 7,240 B | - | 728 B |
| AsyncResolved | ProtoPromise_V2 | 100 | ? | 16.82 ฮผs | 2,172 B | - | - |
| AwaitResolved | ProtoPromise_V2 | 100 | ? | 11.03 ฮผs | 1,906 B | 72 B | - |
| ContinueWithPending | ProtoPromise_V2 | 100 | False | 412.14 ฮผs | 17,455 B | - | 17,264 B |
| ContinueWithFromValue | ProtoPromise_V2 | 100 | False | 25.16 ฮผs | 8,005 B | - | - |
| ContinueWithResolved | ProtoPromise_V2 | 100 | False | 24.67 ฮผs | 8,195 B | - | - |
| ContinueWithPending | ProtoPromise_V2 | 100 | True | 403.14 ฮผs | 14,912 B | - | 17,216 B |
| ContinueWithFromValue | ProtoPromise_V2 | 100 | True | 112.18 ฮผs | 9,504 B | - | 17,144 B |
| ContinueWithResolved | ProtoPromise_V2 | 100 | True | 126.77 ฮผs | 9,521 B | - | 17,144 B |
Originally posted by @timcassell in #26 (comment)
Got a null ref in a CI run. https://github.com/timcassell/ProtoPromise/actions/runs/3458362187/jobs/5772714156
The stacktrace is useless since it was ran in Release, but putting the log here since it will be lost when Github wipes the logs.
Begin time: 00:30:45.5324112, test: ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(Parallel_WithProgress,CancelFromToken,Resolve,Resolve)
Begin time: 00:30:46.3836158, test: ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(InSetup_ProgressParallel,Resolve,Resolve,Resolve)
Failed DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(Parallel_WithProgress,CancelFromToken,Resolve,Resolve) [851 ms]
Error Message:
System.Exception : No message provided
----> System.NullReferenceException : Object reference not set to an instance of an object.
TearDown : NUnit.Framework.AssertionException : Expected: "Fail"
But was: <Proto.Promises.UnreleasedObjectException: CancelationSource's resources were garbage collected without being disposed.>
Stack Trace:
at ProtoPromiseTests.Threading.ThreadHelper.ExecutePendingParallelActions(TimeSpan timeoutPerAction) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 196
at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsets(Boolean expandToProcessorCount, Action setup, Action teardown, Action[] actions) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 283
at ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(CombineType combineType, CompleteType completeType0, CompleteType completeType1, CompleteType completeTypeVoid) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 180
--NullReferenceException
at ProtoPromiseTests.Threading.ParallelCombineTestHelper.ParallelCombineTestHelperT`1.<MaybeAddParallelAction>b__3_1() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadTestHelper.cs:line 138
at ProtoPromiseTests.Threading.ThreadHelper.ThreadRunner.Execute() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 89
--TearDown
at ProtoPromiseTests.TestHelper.Cleanup() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 166
at ProtoPromiseTests.Threading.MergeConcurrencyTests.Teardown() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 36
Failed DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(InSetup_ProgressParallel,Resolve,Resolve,Resolve) [8 s]
Error Message:
System.TimeoutException : 7 Action(s) timed out after 00:00:07, there may be a deadlock.
TearDown : System.TimeoutException : WaitForAllThreadsToComplete timed out after 00:00:02, _runningActionCount: 2
Stack Trace:
at ProtoPromiseTests.Threading.ThreadHelper.ExecutePendingParallelActions(TimeSpan timeoutPerAction) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 196
at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsets(Boolean expandToProcessorCount, Action setup, Action teardown, Action[] actions) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:line 283
at ProtoPromiseTests.Threading.MergeConcurrencyTests.DeferredsMayBeCompletedWhileTheirPromisesArePassedToMergeConcurrently_T2void(CombineType combineType, CompleteType completeType0, CompleteType completeType1, CompleteType completeTypeVoid) in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 180
--TearDown
at ProtoPromiseTests.BackgroundSynchronizationContext.WaitForAllThreadsToComplete() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/BackgroundSynchronizationContext.cs:line 94
at ProtoPromiseTests.TestHelper.WaitForAllThreadsToCompleteAndGcCollect() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 173
at ProtoPromiseTests.TestHelper.Cleanup() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/TestHelper.cs:line 166
at ProtoPromiseTests.Threading.MergeConcurrencyTests.Teardown() in /home/runner/work/ProtoPromise/ProtoPromise/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/MergeConcurrencyTests.cs:line 36
Currently, progress is subscribed in O(1) time and space, optimized in #63. The optimizations there also improved the baseline performance of promises when progress isn't used, but they added the negative effect of increasing the time of resolving a .Then
chain of promises to O(n^2) time, even if progress is never subscribed to (because progress reports iterate over the entire promise tree). Ideally, resolving a promise chain should complete in O(n) time.
I believe the time can be improved by going back to the previous way of subscribing progress, where it iterates backwards up the promise chain and subscribes the listener to each promise, and progress reports only report directly to the listener instead of iterating over the entire promise tree. This increases the subscribe time back to O(n), but it also decreases progress report time from O(n) to O(1), well worth the trade-off for a 1-time cost vs potentially multiple progress reports, and more importantly, .Then
promise chains without subscribed progress will be more efficient.
The O(1) space can be preserved by just using the same object to subscribe to each promise in the chain (and incrementing a retain counter for each). Calling promise.Progress
twice in a row won't iterate the entire chain, it will just stop at the previous progress object, so multiple objects won't need to be added to each promise object.
Crucially, to prevent increasing the memory footprint and execution time of promises ignoring progress, some tricks must be employed to re-use the existing fields.
PromiseRefBase _previous
field, but this is currently only used in DEBUG mode. Since we don't want to add a new field in RELEASE mode, we can combine it with the _rejectContainer
field, changing its type to object
and using it the same way we used to (if the previous has not yet completed, it will contain the previous, otherwise it will contain the reject container or null).HandleablePromiseBase
type as all the other promise objects (done in #8), so instead of adding a new field, we can re-use the HandleablePromiseBase _next
and just swap the existing waiter with the progress object, and store that waiter in the progress object so that the progress object will act as a pass-through. This should make it so the promise resolution logic remains the same and efficient (no separate HandleProgressListener()
call like we used to do). _next
fields, so it can store them in the form of a linked-stack (only using a single reference field)._next
field with the progress object, we can't link all waiters with the same field. So I will need to either store them in a separate collection, or use a different field to link them (possibly re-use _previousOrRejectContainer
?).The wrinkles are with promise.Preserve
and Promise.Merge/All/Race/First
, since they introduce divergence and convergence in the promise tree. In both cases we should be able to treat the PromiseMultiAwait
and MultiHandleablePromiseBase
/ PromisePassThrough
objects as special variants of PromiseProgress
, but I'm not 100% sure of this until I start the implementation.
These changes will bring back the complexity that was removed in #63, but it should be worth it to get those performance gains, especially considering listening to progress is the exception rather than the norm. These changes should ideally not adversely affect the execution time or memory consumption of promises without progress, but special care will need to be taken for thread-safety (subscribing progress was the most difficult thread-safety issue for v2.0.0, and it will be again with these changes). I need to make sure the thread-safety won't slow anything down (I can't be sure until I actually implement the changes and benchmark it, and I'm unsure of how complex the thread-safety will be until I start digging into it; hopefully less than it was before).
Regarding listening to progress on async Promise
promises, currently they don't have the same performance pitfall as .Then
chains if .AwaitWithProgress
is not used. But if it is used, then the same performance pitfall exists even if .Progress
is never called. A similar strategy should be able to be used for that as whatever implementation I decide to do for promise.Then(() => otherPromise)
, but I will need to investigate this more after I get started on the implementation.
RestClient for Unity is interested in adopting ProtoPromise proyecto26/RestClient#216. They have a Nuget package with UnityEngine dependencies. The ProtoPromise Nuget package does not have any UnityEngine dependencies so that it can work on any .Net platform.
We do have some UnityEngine depencies in the UnityPackage for helpers like converting coroutines to Promises and executing the PromiseSynchronizationContext for foreground scheduling. Obviously, I would recommend just using the UnityPackage for source code, but we should create a separate Nuget package that contains those Unity helpers with the UnityEngine.dll dependency so that RestClient and other Unity-based libraries who wish to use ProtoPromise and host their package on Nuget can add a dependency to that helper package.
.Net 6 added StackTraceHiddenAttribute which we can use to more easily omit internal methods from causality traces. It will also help to eliminate some methods that are impossible to remove from exception stacktraces prior to .Net 6 (delegate wrappers for Promise.Then
).
In #189, I was made aware of a readability issue in the readme where a new user missed some important information because it was placed after the Advanced section. Readability can be improved by placing the Advanced section last in the readme.
Hi,
When I enable Settings > Editor > Enter Play Mode, I get this msg:
There can only be one instance of PromiseBehaviour. Destroying new instance
I suspect this is expected behavior given the nature of the setting but worth raising for those who are OCD about warnings.
This issue which forced us to use a less efficient async method builder in IL2CPP builds was fixed in Unity 2020.3.20f1 and 2021.1.24f1. We can #if UNITY_2021_2_OR_NEWER
to enable the optimized builder for IL2CPP builds that other runtimes enjoy (it would take too many ifdefs to also support 2020, because unity doesn't include OR_NEWER symbols for patch releases).
Originally posted by @timcassell in #26 (comment)
Currently, for optimization purposes, if it is currently executing on the same context that was provided to WaitAsync
or Run
or Progress
, the continuation will be invoked synchronously. This may not be desired in some instances if the user wants to force an action to run on a separate thread for increased parallelization. For that we can add an optional forceAsync
flag to WaitAsync
, Run
, and Progress
functions that, if true, will always schedule the continuation on the desired context. The default will be false.
https://github.com/timcassell/ProtoPromise/runs/6846022233?check_suite_focus=true
AwaitConcurrencyTests.PromiseMayBeCompletedAndAwaitedAndProgressReportedConcurrently_void0(Resolve,Foreground)
Easily reproducible by repeating the test 100 times in a loop.
.Net 6 introduced CancellationTokenSource.TryReset()
which unregisters all callbacks if it hasn't already been canceled. CancellationToken.ToCancelationToken()
caches an internal source so that it doesn't have to allocate a new object if ToCancelationToken()
is called on the same token more than once, but since the System.Threading.CancellationToken
API does not support a way to detect unregistration, the cached object can't re-register itself, making the returned token unable to be canceled when the original token is canceled.
Hi,
What is the proper Promises
invocation to wrap Unity's AssetBundle.LoadAssetAsync()
?
When using UnityAsync
, I was just able to call:
T obj = await _assetBundle.LoadAssetAsync<T>(nameID);
But using Promises
I'm unclear if there's even a convenience wrapper or what it looks like under Promises in this case.
Functions like AsyncMonitor.Wait
and Promise.Wait
return bool, while common practice is for boolean-returning methods to be prefixed with Try
. This was done to closely mimic the existing BCL methods, but I think it makes more sense to update to more sensible method names.
The existing methods don't need to be removed, they can just be deprecated, with suggestions to use the new Try
methods.
Some progress tests fail rarely when background synchronization is involved.
https://github.com/timcassell/ProtoPromise/runs/4752150654?check_suite_focus=true
Currently, it is impossible to report progress from an async Promise
function. The conventional way of doing so is to pass an IProgress<T>
argument into the function.
My proposal is to allow reporting progress to the async Promise
via a Promise.ConfigureAwait
method.
public async Promise Func()
{
await DownloadLargeThing().ConfigureAwait(0f, 0.5f);
await DownloadSmallThing1().ConfigureAwait(0.5f, 0.75f);
await DownloadSmallThing2().ConfigureAwait(0.75f, 1f);
}
This allows the programmer to manually normalize the progress that is reported. The ConfigureAwait
takes 2 arguments: minProgress
and maxProgress
. When the configured awaitable is awaited, the progress is immediately set to minProgress
and goes up to maxProgress
as the promise reports its own progress from 0 to 1. This means the progress can go backwards if the programmer so chooses (this can actually be useful for repeating async actions like error retries).
Currently in master, benchmark results are showing speedups when dealing with already resolved promises and slowdowns when dealing with pending promises.
[Edit] Updated table from #27 and adjusting the AsyncPending
benchmark to use a simpler reusable awaiter.
Type | Method | N | BaseIsPending | Mean | Code Size |
---|---|---|---|---|---|
AsyncPending | ProtoPromise_V1 | 100 | ? | 73.930 ฮผs | 4,702 B |
AsyncPending | ProtoPromise_V2 | 100 | ? | 107.352 ฮผs | 7,553 B |
AsyncResolved | ProtoPromise_V1 | 100 | ? | 39.307 ฮผs | 2,982 B |
AsyncResolved | ProtoPromise_V2 | 100 | ? | 18.702 ฮผs | 2,215 B |
AwaitPending | ProtoPromise_V1 | 100 | ? | 278.061 ฮผs | 9,178 B |
AwaitPending | ProtoPromise_V2 | 100 | ? | 424.685 ฮผs | 16,647 B |
AwaitResolved | ProtoPromise_V1 | 100 | ? | 3.931 ฮผs | 2,025 B |
AwaitResolved | ProtoPromise_V2 | 100 | ? | 13.674 ฮผs | 2,845 B |
ContinueWithPending | ProtoPromise_V1 | 100 | False | 328.419 ฮผs | 9,248 B |
ContinueWithPending | ProtoPromise_V2 | 100 | False | 487.998 ฮผs | 18,336 B |
ContinueWithPending | ProtoPromise_V1 | 100 | True | 336.582 ฮผs | 9,248 B |
ContinueWithPending | ProtoPromise_V2 | 100 | True | 484.569 ฮผs | 14,874 B |
ContinueWithFromValue | ProtoPromise_V1 | 100 | False | 77.698 ฮผs | 4,059 B |
ContinueWithFromValue | ProtoPromise_V2 | 100 | False | 24.998 ฮผs | 7,238 B |
ContinueWithFromValue | ProtoPromise_V1 | 100 | True | 73.843 ฮผs | 4,391 B |
ContinueWithFromValue | ProtoPromise_V2 | 100 | True | 128.037 ฮผs | 9,317 B |
ContinueWithResolved | ProtoPromise_V1 | 100 | False | 106.440 ฮผs | 4,017 B |
ContinueWithResolved | ProtoPromise_V2 | 100 | False | 32.014 ฮผs | 9,193 B |
ContinueWithResolved | ProtoPromise_V1 | 100 | True | 100.274 ฮผs | 4,522 B |
ContinueWithResolved | ProtoPromise_V2 | 100 | True | 137.503 ฮผs | 8,947 B |
This is expected due to changing promises to structs and adding thread safety. But there are areas where we can improve the performance for pending promises.
ITreeHandleable
, IValueContainer
, and IMultiTreeHandleable
.Promise.Then
), MarkAwaited
is a virtual call, then the new PromiseRef
is created, then HookupNewPromise
is a second virtual call. This was done for validation purposes, but it can be reduced to a single virtual call, with careful consideration for the PromiseRef
created when the call is invalid. Also note that we cannot use a generic creator passed into the virtual function, because AOT compilers have trouble with generic virtual functions and structs (and using a non-struct would still be a second virtual call and consume heap space).ITreeHandleable.Handle
is invoked, PromiseRef
calls a separate virtual Execute
function. This is done so that all calls to Execute
are wrapped in the try/catch, but that logic can be shifted to pass the IDelegate
structs to a generic function with the try/catch in it instead, similar to the CallbackHelper
for synchronous invokes.ValueContainer
s are created with 0 retains and retain is called when it is passed to ResolveInternal
or RejectOrCancelInternal
. This can be changed to always create with 1 retain and don't call retain in those methods.PromiseRef.MaybeDispose()
will always call Dispose()
virtually. Most of the time MaybeDispose()
is called from the most derived class, so that can be changed to a direct call instead of virtual. [Edit] No longer true since #55AsyncPromiseRef
directly to the awaited PromiseRef
instead of creating an AwaiterRef
, by doing a special type check on the awaiter for PromiseAwaiter
in the PromiseMethodBuilder
. This optimization is trickier than the others and will need to make sure it doesn't box (very similar to #10).ref local
and ref return
features. This can be done while still supporting older language versions, but it will get ugly with the #ifdefs.IPromiseWaiter.AwaitOnCompletedInternal
directly in .Net Core instead of going through AwaitOverrider
, since the JIT optimizes away the box instructions (but not in Unity).Unsafe.As<T>(object)
in .Net 5+Unfortunately, I don't think there is much that can be done for awaiting a resolved promise, as v1 was branchless (the promise was its own awaiter and didn't have to check itself for null). V2 is more complex with the struct wrapping a reference and using a separate PromiseAwaiter
struct for safety. [Edit] Await promise was improved in #39.
This test fails. It appears to be due to the optimization I added that continues to use the already-running coroutine rather than starting a new coroutine. I'm not sure why it happens, but the first call waits 1 frame, and successive (recursive) calls wait 2 frames.
[UnityTest]
public IEnumerator PromiseYielderWaitOneFrame_WaitsOneFrameMultiple(
[Values] RunnerType runnerType)
{
int currentFrame = Time.frameCount;
var promise = PromiseYielder.WaitOneFrame(GetRunner(runnerType))
.Then(() =>
{
Assert.AreEqual(currentFrame + 1, Time.frameCount);
return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
})
.Then(() =>
{
Assert.AreEqual(currentFrame + 2, Time.frameCount);
return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
})
.Then(() =>
{
Assert.AreEqual(currentFrame + 3, Time.frameCount);
return PromiseYielder.WaitOneFrame(GetRunner(runnerType));
})
.Then(() =>
{
Assert.AreEqual(currentFrame + 4, Time.frameCount);
});
using (var yieldInstruction = promise.ToYieldInstruction())
{
yield return yieldInstruction;
if (yieldInstruction.State == Promise.State.Rejected)
yieldInstruction.GetResult();
}
}
Unfortunately, it seems the only way to fix it is to remove the optimization and always start a new coroutine. At least I am working on more optimized Unity awaits (#196) to offset the performance regression.
C# 10 added async method builder overrides. We can add a new method builder to be able to override async void
functions to use the Promise
system instead of the Task
system.
[AsyncMethodBuilder(typeof(PromiseVoidMethodBuilder))]
public async void FuncAsync()
{
await ...
}
If the token is already canceled, the lock should be released, then re-enter the lock queue, in case another part of code is already waiting to acquire the lock, in which case the wait should receive the signal and return true. The behavior should match Monitor.Wait(obj, 0)
; This test fails:
[Test]
public void AsyncConditionVariable_TryWaitAsync_AlreadyCanceled_AnotherLockWaiting_ReturnsTrue()
{
var mutex = new AsyncLock();
Promise.Run(async () =>
{
Promise notifyPromise;
using (var key = await mutex.LockAsync())
{
notifyPromise = Promise.Run(async () =>
{
using (var key2 = await mutex.LockAsync())
{
AsyncMonitor.Pulse(key2);
}
}, SynchronizationOption.Synchronous);
var success = await AsyncMonitor.TryWaitAsync(key, CancelationToken.Canceled());
Assert.True(success);
}
return notifyPromise;
}, SynchronizationOption.Synchronous)
.WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1));
}
.Net Core hugely optimized ExecutionContext
so capturing and executing a callback through it are now allocation-free and have almost no overhead if it is not written to. We can take advantage of this optimization to add AsyncLocal<T>
support without hampering performance when it is not used.
This test fails (.Net 6+):
[Test]
public void LinkedCancelationSourceFromToCancelationToken_RegisterCallbackIsInvoked_WhenSourceIsResetThenCanceled()
{
var cancelationSource = new CancellationTokenSource();
var linkedCancelationSource = CancelationSource.New(cancelationSource.Token.ToCancelationToken());
var token = linkedCancelationSource .Token;
int canceledCount = 0;
token.Register(() => ++canceledCount);
Assert.AreEqual(0, canceledCount);
cancelationSource.TryReset();
token.Register(() => ++canceledCount);
Assert.AreEqual(0, canceledCount);
cancelationSource.Cancel();
Assert.AreEqual(1, canceledCount);
token.Register(() => ++canceledCount);
Assert.AreEqual(2, canceledCount);
cancelationSource.Dispose();
linkedCancelationSource .Dispose();
}
Currently, CancelationToken.TryRetain()
and CancelationToken.Release()
combination is kind of clunky. Example:
public IEnumerator FuncEnumerator(CancelationToken token)
{
bool retained = token.TryRetain();
try
{
while (!token.IsCancelationRequested)
{
Console.Log("Doing something");
if (DoSomething())
{
yield break;
}
yield return null;
}
Console.Log("token was canceled");
}
finally
{
if (retained)
{
token.Release();
}
}
}
If we add a GetRetention()
API, it could simplify a lot of that boilerplate code to a using
statement:
public IEnumerator FuncEnumerator(CancelationToken token)
{
using (token.GetRetention())
{
while (!token.IsCancelationRequested)
{
Console.Log("Doing something");
if (DoSomething())
{
yield break;
}
yield return null;
}
Console.Log("token was canceled");
}
}
GetRetention()
would return a simple struct that implements IDisposable
to wrap the exact same behavior.
C# 8 added IAsyncEnumerable
to await
and yield
in the same function.
static async IAsyncEnumerable<int> RangeAsync(int start, int count)
{
for (int i = 0; i < count; i++)
{
await Task.Delay(i);
yield return start + i;
}
}
And then it's consumed like this in an async function:
await foreach (int i in RangeAsync(0, 100))
{
// ...
}
There is a downside to this, though: it causes an allocation every time the function is called, even if it completes synchronously, and another allocation when it is iterated. We can solve that issue by implementing our own custom PromiseEnumerable
type that implements IAsyncEnumerable
. It will be able to be consumed by the same await foreach
syntax.
Unfortunately, C# has not added a custom AsyncEnumerableMethodBuilder
like it did with AsyncMethodBuilder
, so we can't do async PromiseEnumerable<int>
like we can do async Promise<int>
(dotnet/csharplang#3629). So, instead, we'll have to create a static factory API to run the async function and yield:
static PromiseEnumerable<int> RangeAsync(int start, int count)
{
return PromiseEnumerable<int>.Create(async (writer, cancelationToken) =>
{
for (int i = 0; i < count && !cancelationToken.IsCancelationRequested; i++)
{
await Task.Yield();
await writer.YieldAsync(start + 1); // instead of yield return start + i;
}
});
}
Along with overloads to accept capture values and CancelationToken.
This should be able to be allocation-free if it actually runs to completion synchronously, or we can use object pooling to prevent allocating every time (same as we do with regular promises).
The same code that runs without messages or issues on macOS desktop gets a reproducible exception when running under iOS:
IndexOutOfRangeException: Index was outside the bounds of the array.
at Proto.Promises.InternalHelper+InstructionProcessorGroup+InstructionProcessor`1[TYieldInstruction].WaitFor (TYieldInstruction& instruction) [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted[TAwaiter,TStateMachine] (TAwaiter& awaiter, TStateMachine& stateMachine) [0x00000] in <00000000000000000000000000000000>:0
at Arcspace.AssetService.loadTextureUsingWebRequest[T] (Arcspace.AssetReq`1[T] texReq) [0x00000] in <00000000000000000000000000000000>:0
at System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Start[TStateMachine] (TStateMachine& stateMachine) [0x00000] in <00000000000000000000000000000000>:0
at Arcspace.AssetService.loadTextureUsingWebRequest[T] (Arcspace.AssetReq`1[T] texReq) [0x00000] in <00000000000000000000000000000000>:0
at Arcspace.AssetService.TryResolveAsset[T] (Arcspace.AssetReq`1[T] req) [0x00000] in <00000000000000000000000000000000>:0
at Arcspace.CellScope.TryResolveAsset[T] (Arcspace.AssetReq`1[T] req) [0x00000] in <00000000000000000000000000000000>:0
I'm using v2.5.3 cloned from main
a few days ago and have no reason this issue is new. Willing to test earlier versions for regression on request.
Perhaps there is an exception happening from my side that is propagating to Proto.Promises
?
For reference, here is loadTextureUsingWebRequest()
:
static async void loadTextureUsingWebRequest<T>(AssetReq<T> texReq) where T : UnityEngine.Object {
var url = texReq.AssetRef.URL;
using (var req = UnityWebRequestTexture.GetTexture(url, false)) {
await PromiseYielder.WaitForAsyncOperation(req.SendWebRequest());
if (req.result == UnityWebRequest.Result.Success) {
texReq.Resolve(((DownloadHandlerTexture) req.downloadHandler).texture);
} else {
texReq.Fail(req.error);
}
}
}
Since v2.0 already has a sizable list of breaking changes, it makes sense to add some more in there that improve the library for v2.0, rather than leaving them as-is or changing them later (requiring another major version change).
PromiseYielder's public static Promise<TYieldInstruction> WaitFor<TYieldInstruction>(TYieldInstruction yieldInstruction)
can be changed to public static Promise WaitFor(object yieldInstruction, MonoBehaviour runner = null)
. Because capture values were added a while ago, there is no need to yield the same instruction in the Promises's result, making it more efficient. Async functions also implicitly capture all variables across await
boundaries. runner
is an extra optional argument that the caller can provide to run the coroutine on, otherwise it defaults to the static runner.
Undocumented features (in the README) that can be removed or made internal:
Everything in Proto.Utils
namespace. Most of the namespace's members are unsafe for general use, and none of them thematically belong in this library. Some of the types have already started to change for #8.
Promise(<T>).ResultType
. Since Promise
and Promise<T>
are now structs with no inheritance hierarchy, and Promise
cannot be casted to Promise<T>
, ResultType
no longer has any value.
CatchCancelation
's behavior can be changed to allow continuations (much like the regular Catch
) by returning a value or Promise. ObserveCancelation
can be added to keep the existing CatchCancelation
behavior. This is a more logical and less confusing naming scheme.
Dispose
unregisters the callback if it can, or if it can't, it will wait until the callback has completed invoking, unless it's called from the callback itself (to prevent deadlocks).
DisposeAsync
does the same thing, but will wait asynchronously.
This will bring it to feature parity with System.Threading.CancellationTokenRegistration
and simplify implementing cancelations, worrying less about race conditions, in some cases.
Currently, this is the way to cancel an async Promise
function if the awaited promise does not have a mechanism for being canceled via the provided token (UncancelablePromise()
doesn't accept a cancelation token).
public async Promise Func(CancelationToken token)
{
var val = await UncancelablePromise();
token.ThrowIfCancelationRequested();
// do work
}
This works most of the time, but is unreliable if the CancelationSource
is disposed before the awaited promise completes. That problem can be solved by retaining the token, but that adds a bunch of extra boilerplate code:
public async Promise Func(CancelationToken token)
{
token.Retain();
try
{
var val = await UncancelablePromise();
token.ThrowIfCancelationRequested();
// do work
}
finally
{
token.Release();
}
}
Another issue with this approach is the cancelation will not propagate when the token is canceled, but rather it will wait until the awaited promise completes. This can also be solved via another method, but is unintuitive:
public async Promise Func(CancelationToken token)
{
var val = await UncancelablePromise().Then(v => v, token); // Immediately cancel when the token is canceled.
// do work
}
This approach mixes async/await
style with Then
style, which is undesirable and potentially confusing to anyone looking at the code. It also is less efficient than it could be, because it's adding a new callback simply to act as a pass-through.
My proposal is to add a more intuitive method of canceling the awaited promise via a ConfigureAwait
method on the promise:
public async Promise Func(CancelationToken token)
{
var val = await UncancelablePromise().ConfigureAwait(token); // Immediately cancel when the token is canceled.
// do work
}
README and release notes need to be updated before releasing v2.0.
In .Net 6+, when a System.Threading.CancellationTokenSource
is reset, then cancelled, the converted Proto.Promises.CancelationToken
is not canceled when checking its cancelation status via IsCancelationRequested
. This test fails:
[Test]
public void ToCancelationTokenIsCanceledWhenSourceIsResetThenCanceled()
{
var cancelationSource = new CancellationTokenSource();
var token = cancelationSource.Token.ToCancelationToken();
Assert.IsFalse(token.IsCancelationRequested);
cancelationSource.TryReset();
Assert.IsFalse(token.IsCancelationRequested);
cancelationSource.Cancel();
Assert.IsTrue(token.IsCancelationRequested);
cancelationSource.Dispose();
}
Hi Tim,
I've got an issue when using the WaitForResult when using it with nested promises.
I'm still investigating the issue, but I've got a promise that does multiple Http calls (internally I'm using deferred promises to get the result and state of these calls), for some reason (with the last update of your package) after the first promise gets resolved everything stops working (block the entire process). So the 'then' call of the associated first deferred promise doesn't fire anymore - stalling the process.
Again, I switched to using await for now (skipping the WaitForResult) which seems to resolve the issue. So somehow the 'WaitForResult' is bugged. And it must be related to having multiple promises during that call because I don't have this issue with a single-level promise. I know the explanation is a bit fuzzy, but just wanted to let you know maybe you are aware of it.
Kind Regards
AsyncLock
(#159) is being added in v2.4.0. We can add additional async coordination primitives to sit alongside it, such as AsyncAutoResetEvent
, AsyncManualResetEvent
, AsyncSemaphore
, and AsyncReaderWriterLock
.
AsyncReaderWriterLock
AsyncManualResetEvent
AsyncAutoResetEvent
AsyncSemaphore
AsyncCountdownEvent
AsyncConditionVariable
I ran tests on my local machine with the IL2CPP backend and got some test failures:
PromiseWait_DoesNotReturnUntilOperationIsComplete(False,True) (0.568s)
---
Proto.Promises.InvalidOperationException : PromiseAwaiter.GetResult() is only valid when the promise is completed.
---
at Proto.Promises.Internal+PromiseRefBase.GetExceptionDispatchInfo (Proto.Promises.Promise+State state, System.Int16 promiseId) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Promise.SwitchToForeground (System.Boolean forceAsync) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWait_DoesNotReturnUntilOperationIsComplete (System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0
PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException(Reject,False,False) (0.619s)
---
TearDown : NUnit.Framework.AssertionException : Expected: <Proto.Promises.InvalidOperationException: Test
at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass30_0.<PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+DeferredPromiseHelper.TryReportProgress (Proto.Promises.Internal+IDeferredPromise _this, System.Int32 deferredId, System.Single progress) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException (ProtoPromiseTests.CompleteType throwType, System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0 >
But was: <Proto.Promises.InvalidOperationException: Test
at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass30_0.<PromiseWait_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+DeferredPromiseHelper.TryReportProgress (Proto.Promises.Internal+IDeferredPromise _this, System.Int32 deferredId, System.Single progress) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 >
---
--TearDown
at NUnit.Framework.Assert.That[TActual] (TActual actual, NUnit.Framework.Constraints.IResolveConstraint expression, System.String message, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Assert.AreEqual (System.Object expected, System.Object actual) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.TestHelper+<>c.<Setup>b__6_0 (Proto.Promises.UnhandledException e) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase.Finalize () [0x00000] in <00000000000000000000000000000000>:0
PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndReturnsWithCorrectResult(False,True) (0.566s)
---
Proto.Promises.InvalidOperationException : PromiseAwaiter.GetResult() is only valid when the promise is completed.
---
at Proto.Promises.Internal+PromiseRefBase.GetExceptionDispatchInfo (Proto.Promises.Promise+State state, System.Int16 promiseId) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Promise`1[T].Resolved (T value) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.Wait[T] (Proto.Promises.Promise`1[T] promise, T& result, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndReturnsWithCorrectResult (System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0
PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException(Reject,False,Fa (0.574s)
---
TearDown : NUnit.Framework.AssertionException : Expected: <Proto.Promises.InvalidOperationException: Test
at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass33_0.<PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Promise`1[T].Resolved (T value) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.Wait[T] (Proto.Promises.Promise`1[T] promise, T& result, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.APIs.MiscellaneousTests.PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException (ProtoPromiseTests.CompleteType throwType, System.Boolean alreadyComplete, System.Boolean withTimeout) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0
at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Reflect.InvokeMethod (System.Reflection.MethodInfo method, System.Object fixture, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Internal.Commands.TestActionCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand.Execute (NUnit.Framework.Internal.ITestExecutionContext context) [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.UnityLogCheckDelegatingCommand+<ExecuteEnumerable>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.BeforeAfterTestCommandBase`1+<ExecuteEnumerable>c__Iterator0[T].MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.DefaultTestWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<RunChildren>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestRunner.NUnitExtensions.Runner.CompositeWorkItem+<PerformWork>c__Iterator0.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.TestTools.TestRunner.PlaymodeTestsController+<TestRunnerCorotine>c__Iterator1.MoveNext () [0x00000] in <00000000000000000000000000000000>:0
at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) [0x00000] in <00000000000000000000000000000000>:0 >
But was: <Proto.Promises.InvalidOperationException: Test
at ProtoPromiseTests.APIs.MiscellaneousTests+<>c__DisplayClass33_0.<PromiseWaitForResult_DoesNotReturnUntilOperationIsComplete_AndThrowsCorrectException>b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Func`1[TResult].Invoke () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseResolvePromise`2[TResult,TResolver].Execute (Proto.Promises.Internal+PromiseRefBase handler, System.Boolean& invokingRejected, System.Boolean& handlerDisposedAfterCallback) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase+PromiseConfigured`1[TResult].HandleFromContext () [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal.HandleFromContext (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.BackgroundSynchronizationContext+ThreadRunner.ThreadAction () [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.TestDelegate.Invoke () [0x00000] in <00000000000000000000000000000000>:0 >
---
--TearDown
at NUnit.Framework.Assert.That[TActual] (TActual actual, NUnit.Framework.Constraints.IResolveConstraint expression, System.String message, System.Object[] args) [0x00000] in <00000000000000000000000000000000>:0
at NUnit.Framework.Assert.AreEqual (System.Object expected, System.Object actual) [0x00000] in <00000000000000000000000000000000>:0
at ProtoPromiseTests.TestHelper+<>c.<Setup>b__6_0 (Proto.Promises.UnhandledException e) [0x00000] in <00000000000000000000000000000000>:0
at Proto.Promises.Internal+PromiseRefBase.Finalize () [0x00000] in <00000000000000000000000000000000>:0
Running IL2CPP tests locally (since it still can't be done in CI...) results in the player crashing. I'm looking into it now, but it's a slow process. I'm opening this issue to document and make sure it's fixed before releasing v2.4.0.
https://github.com/timcassell/ProtoPromise/actions/runs/3522270696/jobs/5906008504
Failure in PromiseReturnedInCallbackMayBeCompletedConcurrently_T
.
at ProtoPromiseTests.Threading.PromiseConcurrencyTests+<>c__DisplayClass16_0.<PromiseReturnedInCallbackMayBeCompletedConcurrently_T>b__4 () [0x0000d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/PromiseConcurrencyTests.cs:686
at (wrapper delegate-invoke) <Module>.invoke_void()
at ProtoPromiseTests.Threading.ThreadHelper.ExecuteParallelActionsWithOffsetsAndSetup (System.Action setup, System.Action`1[System.Action][] parallelActionsSetup, System.Action[] parallelActions, System.Action teardown) [0x000da] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Helpers/ThreadHelper.cs:313
at ProtoPromiseTests.Threading.PromiseConcurrencyTests.PromiseReturnedInCallbackMayBeCompletedConcurrently_T (ProtoPromiseTests.CompleteType completeType) [0x00095] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromiseTests/Tests/Threading/PromiseConcurrencyTests.cs:661
--TearDown
--InvalidCastException
at (wrapper castclass) System.Object.__castclass_with_cache(object,intptr,intptr)
at Proto.Promises.Internal.UnsafeAs[T] (System.Object o) [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/InternalShared/HelperFunctionsInternal.cs:227
at Proto.Promises.Promise`1+ResultContainer[T]..ctor (Proto.Promises.Internal+PromiseRefBase source) [0x00000] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/ResultContainers.cs:254
at Proto.Promises.Internal+PromiseRefBase+DelegateContinueArgResult`2[TArg,TResult].Invoke (Proto.Promises.Internal+PromiseRefBase handler, Proto.Promises.Internal+PromiseRefBase owner) [0x00001] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/DelegateWrappersInternal.cs:984
at Proto.Promises.Internal+PromiseRefBase+PromiseContinue`2[TResult,TContinuer].Execute (Proto.Promises.Internal+PromiseRefBase handler, Proto.Promises.Promise+State state, System.Boolean& invokingRejected) [0x0001c] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/PromiseInternal.cs:1436
at Proto.Promises.Internal+PromiseRefBase+PromiseSingleAwait`1[TResult].Handle (Proto.Promises.Internal+PromiseRefBase handler, System.Object rejectContainer, Proto.Promises.Promise+State state) [0x0000d] in /github/workspace/ProtoPromise_Unity/Assets/Plugins/ProtoPromise/Core/Promises/Internal/PromiseInternal.cs:601
This looks like a case of out-of-order execution. The fix should be easy enough. We could add a Thread.MemoryBarrier()
to prevent the re-ordering of instructions, but I think a cleaner approach is to just pass the object rejectContainer, Promise.State state
directly to the new Promise<TArg>.ResultContainer
instead of reading the fields from the handler. And store the rejectContainer as an object
instead of IRejectContainer
and cast it when it's needed instead of in the constructor. Of course, we can also add the memory barrier in just to be safe (best place is probably at the beginning of Dispose()
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.