Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Observable.Delay throws PlatformNotSupportedException on Blazor WASM. #2061

Open
jeremy-morren opened this issue Dec 8, 2023 · 3 comments
Open

Comments

@jeremy-morren
Copy link

jeremy-morren commented Dec 8, 2023

Bug

On Blazor WASM, Observable.Delay is not implemented.

Index.razor

@page "/"
@using System.Reactive.Linq
@using System.Reactive.Threading.Tasks

@code {

    protected override async Task OnInitializedAsync()
    {
        await Observable.Return(1)
            .Delay(TimeSpan.FromSeconds(1))
            .ToTask();
    }

}

Exception:

System.PlatformNotSupportedException: Operation is not supported on this platform.
   at System.Threading.Thread.ThrowIfNoThreadStart(Boolean internalThread)
   at System.Threading.Thread.Start(Object parameter, Boolean captureContext, Boolean internalThread)
   at System.Threading.Thread.Start(Object parameter)
   at System.Reactive.Concurrency.ConcurrencyAbstractionLayerImpl.StartThread(Action`1 action, Object state)
   at System.Reactive.Concurrency.DefaultScheduler.LongRunning.LongScheduledWorkItem`1[[System.Threading.CancellationToken, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]..ctor(CancellationToken state, Action`2 action)
   at System.Reactive.Concurrency.DefaultScheduler.LongRunning.ScheduleLongRunning[CancellationToken](CancellationToken state, Action`2 action)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Base`1.L[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].ScheduleDrain()
   at System.Reactive.Linq.ObservableImpl.Delay`1.Relative.L[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].RunCore(Relative parent)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].Run(Relative parent)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Run(_ sink)
   at System.Reactive.Producer`2.<>c[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].<SubscribeRaw>b__1_0(ValueTuple`2 tuple)
   at System.Reactive.Concurrency.Scheduler.<>c__75`1[[System.ValueTuple`2[[System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].<ScheduleAction>b__75_0(IScheduler _, ValueTuple`2 tuple)
   at System.Reactive.Concurrency.CurrentThreadScheduler.Schedule[ValueTuple`2](ValueTuple`2 state, TimeSpan dueTime, Func`3 action)
   at System.Reactive.Concurrency.LocalScheduler.Schedule[ValueTuple`2](ValueTuple`2 state, Func`3 action)
   at System.Reactive.Concurrency.Scheduler.ScheduleAction[ValueTuple`2](IScheduler scheduler, ValueTuple`2 state, Action`1 action)
   at System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].SubscribeRaw(IObserver`1 observer, Boolean enableSafeguard)
   at System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].Subscribe(IObserver`1 observer)
   at System.Reactive.Threading.Tasks.TaskObservableExtensions.ToTask[Int32](IObservable`1 observable, CancellationToken cancellationToken, Object state)
--- End of stack trace from previous location ---
   at TabletsUI.Pages.Index.OnInitializedAsync() in ..\Pages\Index.razor:line 11
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
@idg10
Copy link
Collaborator

idg10 commented Dec 11, 2023

The issue is slightly deeper than this. Observable.Delay itself isn't the problem—it can be made to work. Here's a modification that works around the issue you're seeing:

IScheduler sch = DefaultScheduler.Instance.DisableOptimizations();
await Observable.Return(1)
    .Delay(TimeSpan.FromSeconds(1), sch)
    .ToTask();

You should then see Delay work correctly. This demonstrates that Delay itself is perfectly capable of working on Blazor WASM.

So the question is why does it fail when used in the default way?

The basic problem here is this: by default all timed operators in Rx default to using SchedulerDefaults.TimeBasedOperations to run work at the necessary time. That property returns an instance of DefaultScheduler, and DefaultScheduler supports an optional scheduler feature: ISchedulerLongRunning which it implements by spinning up a new thread. Delay will use this scheduler feature if available, because the nature of Delay (an inherently very expensive operator, and not one you would use if your only goal was to generate a single notification 1 second from now) is that it can work a lot better if it has a thread to itself.

The problem of course is that you're not allowed to create new threads on WASM. The workaround above basically downgrades the scheduler to one where no optional features (such as long running operation support) are available. This is probably overkill. This may be disabling other optimizations that would actually work.

It might be that the most straightforward way for us to fix this is to have the ConcurrencyAbstractionLayerImpl detect when threads can't be created. It currently has this:

public bool SupportsLongRunning => true;

but if this were to return false on Blazor WASM, you would no longer need the workaround, because the DefaultScheduler checks that property and only tries to create new threads if it returns true.

There was a time where different builds of Rx for different environments would hard-code SupportsLongRunning either to true or false because some of our build targets just didn't support creation of new threads at all.

The problem for WASM is that it's not a distinct build target. Because of the "One .NET" initiative, it just ends up being net6.0.

Presumably there's some way to discover at runtime that thread creation is unavailable—other libraries must have encountered this problem on WASM before. So it might just be a case of finding out what the preferred way is to ask "Are we allowed to create new threads?" and making SupportsLongRunning report that.

However, there might well be other WASM-specific problems. The root cause of why it was possible for this to blow up like this is that we've never executed our test suites on WASM.

So rather than merely fixing up this one symptom, I think the best thing to do would be to work out how to run the full Rx test suite on Blazor WASM. That would reveal any other issues.

@idg10
Copy link
Collaborator

idg10 commented Jan 25, 2024

Note to self: maybe https://devblogs.microsoft.com/dotnet/introducing-ms-test-runner/ could help with this?

@HowardvanRooijen
Copy link
Collaborator

The other suggestion is to use https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop?view=aspnetcore-8.0 as a testing approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants