-
Notifications
You must be signed in to change notification settings - Fork 3k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
Discussing the future of schedulers #2935
Comments
For a slightly different perspective on testing, I invite you to take a look at cycle/time, a tool that @Widdershin built inspired by the RxJS TestScheduler: https://github.com/cyclejs/cyclejs/tree/master/time#rxjs-why-would-i-want-to-use-cycletime-over-the-testscheduler-from-rxjs The difference there is that the time-related operator, like Globals are tempting, but they are magic and sources of confusion. In Cycle.js we barely have any globals, but the ones we do have, such as the innocently small setAdapt, are constantly a source of discussion/annoyance. I think the story around schedulers isn't entirely about testing, though. Issues like this combineLatest quirk are real gotchas for beginners and still a source of pain when migrating from RxJS v4 to v5+. |
I think the gotcha for beginners in the case outlined is really around not understanding that Another thing to think about is the fact that our In practice, I really do think it's worth taking a step back and asking what the schedulers really provide.
As for this, right now all of our provided scheduler instances are essentially global. They are a single, shared, stateful instance at a top level. They might not live in I think the larger problems are:
|
Simply put, schedulers solve concurrency ambiguities in a declarative style. The real problem of that Although Schedulers were originally introduced to Rx(.NET) and heavily important for thread management, concurrency also exists in "single-threaded" JavaScript. Besides the simple Observable.of examples, concurrency problems with reactive streams occur in less obvious cases. For instance, race conditions that we found in Cycle.js. These conditions emerge easily once you have a multicasted observable and you need multiple observers subscribed and not have them compete in a race. A simple statement of the important problem that schedulers solve: "how can we subscribe to both I understand that testing is clumsy, we can explore our options for that. I understand that we could simplify the codebase by deleting some parts. But let's not reinvent the wheel or throw away an important part of RxJS, which also makes it uniquely interesting over alternatives like Most.js, xstream or Bacon.js. As a secondary note, I much prefer |
There is very little that is declarative about how Rx is using schedulers. It's pretty imperative... A more declarative solution would be specialized operators similar to source$.async()
// or
source$.queue() This would mean your
This is the entire basis of Rx. So I know this. Currently dealing with scheduler defaults in operators is a huge problem when trying to create a test suite. If you're trying to test code that is using Scheduling seems to be something that was designed to accommodate 100% of the use cases 100% of the time, but in practice some of what it accommodates doesn't make sense. "I want immediate scheduling on a$, but animationFrame scheduling on b$ and the delay operation I'd like to be scheduled virtually". I mean, it could happen... but is it worth the additional download size and code complexity? That's the real question I'm asking.
It also makes it uniquely heavier than any of those mentioned. So maybe better questions are:
I suspect that some members of our core team has a very skewed point of view compared to the to outside developers who have started using RxJS in droves over the last year. The MO for most of us, myself included, has been resistance to change and reluctance to explore alternatives. |
If we could simplify the library, make it smaller and faster, and have deterministic tests and still support composition of different scheduling behaviors, is that something worth exploring? |
ALSO: I'm playing devil's advocate this is exploratory, I'm taking the "what if" stance to contrast other opinions and keep people thinking.Just to be clear.. I'm not arguing with @staltz ... just want to get people thinking. |
So one common use case for me and schedulers is for animations like: Observable.interval(0, animationFrame)
// or
Observable.range(0, Number.POSITIVE_INFINITY, animationFrame)
// or
Observable.timer(0, 0, animationFrame) But I wonder if it would be better/more efficient/easier to understand/etc to just have: Observable.animationFrames(); |
subscribeOn / observeOn is not available usecase in here? |
I still maintain my previous arguments, but after having thought about this for some hours I realized it's enough (for the JavaScript Rx) to be able to specify breadth-first ordering or depth-first ordering. I don't see the benefit of multiple Schedulers that accomplish breadth-first, we could have just one breadth-first Scheduler. So there's some opportunity for code deletion somewhere. |
I noticed yet another cool and unexplored benefit of a breadth-first scheduler: avoiding ReplaySubject or BehaviorSubject when subscribing to a multicasted observable. Note const a$ = Rx.Observable.interval(1000).startWith(-1)
.publishReplay(1).refCount();
a$.subscribe(x => console.log(x));
a$.subscribe(x => console.log(x)); versus const a$ = Rx.Observable.interval(1000).startWith(-1)
.observeOn(Rx.Scheduler.asap)
.publish().refCount();
a$.subscribe(x => console.log(x));
a$.subscribe(x => console.log(x)); |
@benlesh I'm still in favor of an option to specify the scheduler at subscription time, which would compose all the way through the operator subscriptions and could be used in place of operator defaults, e.g. |
Great idea, Paul. Are there any obstacles for doing that? And I suppose observeOn would interrupt that bottom-up propagation. Since the recursive scheduler is basically "no scheduler", how would the default case work, given |
@staltz yeah operators would still use the scheduler passed in at Observable creation time, to use when there isn't a Subscriber scheduler. To be clear I haven't thought through all the implications of allowing the Subscriber to control producers this way. For instance some funny things might happen inside operators like Also since the TestScheduler trampolines like the QueueScheduler, we'd need to have a RecursiveTestScheduler for people who want to test the normal recursive behavior. |
With earlier versions of RxPHP we specified the scheduler at subscription time. The main problem we ran into was when you implemented a new operator, you had to remember to pass the scheduler through to the inner subscription. If you didn't, the scheduler would stop propagating upstream, which led to bugs that were very difficult to debug. |
Scheduling seems to be the occult side of RxJS. I was at first drawn toward RxJS Schedulers because the similar workings (queue, context, clock) they shared with the music schedulers presented by IRCAM, where a robust scheduling foundation was extended to many usage cases. Presentation: https://medias.ircam.fr/x6c8804 Looking at the RxJS Scheduler implementation it almost seemed that they could be made to work for music. Just like there is a RAF Scheduler there could also be a WAA Scheduler (for the cross browser Web Audio API). Maybe it would be possible to schedule each note from a parsed score using the currentTime of the WAA context/thread? However, this becomes a bit more involved than just scheduling against a periodic pulse like RAF does. The periodic pulse is a macro level of scheduling where the note-by-note is a micro level of scheduling. Also, this is different than clocking external MIDI signals, where the macro level would suffice. Anytime I've looked into RxJS scheduling the focus tends to be about the Virtual Scheduler and macro level scheduling. At this junction experience tells me that there are other ways to schedule music that involve a combo of RxJS and some quintessential JavaScript. Earlier, I had posted a question about music scheduling to @staltz (a musician) at the RxJS AMA he hosted at HashNode, but that went nowhere after 265 views and not one reply. Below is a link to the post just so it can be logged into the journal of RxJS time. I still hope for a future where all the expressions of time (music, animation, async) can be united in a common RxJS foundation. |
@MVSICA-FICTA ... I've looked at web audio scheduiing for Observables, and I came to the same conclusion I have with the requestAnimationFrame stuff, most of the time what you want is an observable of moments, not really a scheduler. And the former is much easier to engineer. The overall issue we have with Schedulers right now in RxJS, IMO, is that we've over-engineered the solution a bit. Scheduling makes more sense in a multi-threaded environment. The only scheduling distinction I've seen make a lot of sense for us is breadth-first vs depth-first scheduling, but even that is fairly edge-casey, honestly. What do I mean by over-engineered? Well, anything with any sort of delay really just ends up using If we stepped back to use wrapped, native scheduling APIs, we could reduce a lot of code, and simplify and even speed up our operators substantially. Not to mention step away from what's really one of the most imperative parts of RxJS, which is scheduler specification. (Which again, very very few people use or understand) |
Sounds like one of the key concerns here is the additional file size of overloaded operators that accept optional schedulers. Why not split these operators in two? For example have Observable.of, and Observable.ofWithScheduler? This way developers who are not using schedulers don’t have to pay for them. Alternately if we can find a way to dramatically reduce the file size by simplifying the available schedulers, that would be nice as well. |
Scheduling is about concurrency, not threading. Threads are a type of concurrency, but not the only kind. That said with SharedArrayBuffers and workers, JS is getting a type of threading whether we like it or not.
Do you mean JS in general, or in Rx? We explicitly use
Synchronizing events on the animationFrame is an absolute necessity for performance. It's totally a scheduler thing.
First, we do use the native scheduling APIs. Schedulers aren't meant to replace those APIs, they're meant to abstract calling those APIs away from the specific Observable/operator implementations. By abstracting the logic of calling native scheduling APIs, we can then parameterize which APIs Observables and operators ultimately use. This is a critical component of what makes Rx a superset of pure FRP. Second, the schedulers are very lean. The current design was meant to simplify/unify the scheduling interface behind a single // This only allocates a single Subscription for infinite execution
scheduler.schedule(function(i) {
this.schedule(i+1, 100);
}, 0, 100); Considering the apparent difficulty of implementing efficient/safe async scheduling, I'm extremely skeptical of the safety of hard-coding scheduling logic into the Observables and operators themselves. Seems like a heap 'o trouble.
What do you mean? Unless something's changed since I last looked,
I've mentioned before to @benlesh we absolutely can reduce the LOC in the Schedulers. Frankly the only reason they're broken down into so many classes is because I didn't want to confuse anybody in the last big scheduler PR. I'd be happy to submit a PR that reduces the LOC in schedulers by combining logic into a single parameterized Action class. |
I was generalizing. I remember that we're using
It's more that you can provide a scheduler, and there's additional code inside of At this point, I think we should keep scheduling, but isolate it from all of the common use cases. Most things can be handled with |
Agreed with the above, except for direct use of setInterval in delay (and others) operators. Because that would mean we depend on a global, and for testing we mutate the global. Would introduce a new class of bugs when testing. |
Totally agree with making things simpler, but I wouldn't be surprised if we couldn't accomplish the same thing with a helper function that handles the scheduler/no scheduler case generically. The duplication in this area is largely a product of multiple cooks in the kitchen, not a fundamentally unsound abstraction.
👎 from me on this, I frequently use timer and interval with schedulers other than the AsyncScheduler.
observeOn ultimately losslessly buffers events using the scheduler as the buffer queue, which is absolutely not desired in many cases. For example |
I'm personally a fan of what @trxcllnt mentioned above, the idea of passing in schedulers at (or closer to) subscription time make a whole bunch more sense to me than declaring them at construction time. It also gives a much purer taste to the end product. For my own attempts to build a streaming library I looked at doing an interface like:
The ergonomics were still a bit rough and there were some issues around passing schedulers and using defaults, but it does allow for some interesting combinatorial setups, such as applying a scheduler as context. Something along the lines of: const unscheduled =
pipe(
delayTime(1000),
bufferCount(10)
);
// Overrides any incoming scheduler with async
const scheduled = scheduler.async.with(unscheduled);
// Applies a default scheduler which is overridden downstream
scheduled(Observable.fromEvent(window, 'keypress'), scheduler.default)
.subscribe(observer) Again interface is a bit kludgy, but it pulls the scheduling away from the operator so they each exist in their own layer and makes it easier to test things since there isn't a reliance on a global scheduler entity. |
This is a solved problem with Instead of taking schedulers as arguments, we create a scheduler which has time based operators. So instead of: import {Observable, TestScheduler} from 'rxjs';
import * as assert from 'assert';
function main(scheduler = null) { // does this even work or do I need to set the correct default?
return Observable.of('woo').delay(60, scheduler);
}
// in prod
const result = main();
// in test
const testScheduler = new TestScheduler(assert.deepEqual.bind(assert));
const result = main(testScheduler);
const expected = '--x';
const expectedStateMap = {x: 'woo'};
testScheduler.expectObservable(result).toBe(expected, expectedStateMap);
testScheduler.flush(); Using import {Observable} from 'rxjs';
import {timeDriver, mockTimeSource} from '@cycle/time/rxjs';
function main(Time = timeDriver()) {
return Observable.of('woo').let(Time.delay(60));
}
// in prod
const result = main();
// in test
const Time = mockTimeSource();
const result = main(Time);
const expected = Time.diagram('--x', {x: 'woo'});
Time.assertEqual(result, expected);
Time.run(); They have very similar APIs, but Some will dislike how This is also easily achievable with
This is the approach that
Pros:
Cons:
I like the idea of passing the scheduler on subscription, hadn't considered that. Anyway, just another option. It's currently feasible to use |
Hi there, little question, i was trying to sync a video with a custom timer component, and some times i had to pause 1 of them to get them in sync, seems the Rxjs timer is not precise enough and when my update interval is small (say 10 milliseconds) they get off sync very easily. I was reading this article regarding such thing and i'm still a bit lost about how to fix this: Is a custom rxjs scheduler required for accurate music playback? I think it's kind of related to the animation frame discussed above in some comments. |
Great discussion. I am adding a few remarks, as seen from an API user perspective :
|
Sorry for interference, but can you provide use-cases when it is needed? |
Why doesn't the animationFrames Observable -- added in #5021 -- pass the DOMHighResTimeStamp that the requestAnimationFrames callback receives, to its subscribers? According to MDN:
This is critical when running multiple rAF loops to keep them in sync. The current default implementation, using Here's the implementation I use: const animationFrames = new Observable(subscriber => {
let requestId = window.requestAnimationFrame(function step(timestamp) {
subscriber.next(timestamp);
if (!subscriber.closed) {
requestId = window.requestAnimationFrame(step);
}
});
return () => window.cancelAnimationFrame(requestId);
}); And to get the elapsed milliseconds: const elapsedMs = defer(() => {
let start = null;
return animationFrames.pipe(
tap(timestamp => start === null && (start = timestamp)),
map(timestamp => timestamp - start),
);
}); I think this would be a better animationFrames Observable. |
@kirk-l Could you open a new issue for what you've raised in the above comment? I'd suggest opening it as a bug, as the current observable API doesn't allow usage like what's possible with the |
In either case, it's really important there is some notion of being able to change the execution context. For example, I'm planning on exploring a React RxJS scheduler (rx-store/rx-store#13). Eg - if numerous subscriptions are to be delivered a "next" value in any given upcoming animation frame, you'd have the option to ensure all of these subscriptions have their value delivered in some execution context such as a React batched update callback. This is similar to what @kirk-l points out, but this isn't specific to animation frames fyi @cartant . There is a valid use case to being able to change the execution context for subscribing and observing, in general. Painting a bunch of React updates in a singe animation frame, in a single batched update, for example, even when they come from separate subscriptions.
Another thing to consider is if your source of time could be remote, or could come from external user input (like a timeline component to scrub through a powerpoint presentation style app, triggering animations in virtual time controlled by a slider on a timeline UI component)
@staltz suggestion of having a steam of time is not ideal, either. Ideally I do not need a stream that emits every 1ms going on all the time, in the name of "engineering purity". Ideally, my code written in RxJS should not be calling |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Schedulers exist so we can exercise more control over when subscription occurs and when values are observed. They also provide a mechanism we can hook to get more deterministic tests that run very fast.
A few issues with the current Scheduler set up
Observable.of
animationFrame
or the like.Questions
Observable.of(1, 2, 3, async)
orObservable.asyncOf(1, 2, 3)
?Observable.of(1, 2, 3, asap)
orObservable.microtaskOf(1, 2, 3)
(name totally bike-sheddable)setTimeout
orPromise
then
, and patch them temporarily at test time? It seems like that might make it easier to build test suites. It might also reduce the size of the library.Other things
Having schedulers might enable us to centralize error handling in the next version, which could help performance and reduce library size. This was one of the central changes in "T-Rex".
NOTE: This is just a discussion to get us to question what we've been doing with schedulers, not a change suggestion per say
The text was updated successfully, but these errors were encountered: