Readers' Questions: "Why does Ember still use RSVP?"

Hello once again to Readers’ Questions, presented by the Ember.js Times. Today I’m answering this question:

Why does Ember ship with RSVP instead of a Promise polyfill?

Well, RSVP.Promise is a spec-compliant Promise polyfill, and Ember folks even helped write that spec based on early adoption experience in Ember. But you are probably asking why it is more than just a Promise polyfill, and why we don’t always use native Promise where it’s available, and only fall back to RSVP where it’s not.

Ember (and every other modern Javascript rendering framework) batches up your changes before rendering them out to the DOM. If we didn’t do this, every time you called set to change something, we would instantly rerender the DOM, and when you are calling set many times in a loop that would get expensive quickly.

So instead, whenever you change some data we schedule rendering for “after all your current work is done”. And there are (for purposes of this discussion) two different kinds of “after” in Javascript: tasks and microtasks.

In this example:

setTimeout(() => console.log("setTimeout fired"), 0);
Promise.resolve().then(() => console.log("promise resolved"));

a correct Javascript implementation should always print “promise resolved” before “setTimeout fired”, because resolved promises are scheduled on the microtask queue and setTimeout is scheduled on the task queue. The browser is not allowed to move on to the next task until all the microtasks are done. That is the purpose of microtasks – they let you continue your current work later-ish, in the sense of being asynchronous, but still guarantee that it will get grouped together before any other task begins.

When you have microtasks, the problem of scheduling render for “after I finish all my current work” is relatively straightforward: you do all your “current work” using microtasks, and you schedule the rerender with a task. Thus, you’re guaranteed that the render won’t happen until all the currently running work finishes.

But up until Ember 3.0 (February 14, 2018) we supported browsers that didn’t have reliable, correct microtask scheduling. So Ember has long had it’s own built-in implementation of this same pattern: the runloop. The runloop achieves the same thing you would with the native microtask queue: it groups together work that’s supposed to go together, and lets you schedule what should happen “after” all that work has finished.

In order to know when “after” is, the runloop needs to be aware of all the asynchronous work that’s going on. So in order for the runloop and promises to interoperate, Ember configures RSVP.Promise to not use the native microtask queue, but instead to delegate to the runloop. Therefore in Ember apps, RSVP.Promise doesn’t behave exactly the same as native Promise, and that’s why we continue to use it.

The good news is, now we are post-ember-3.0 and we can trust native microtask queues in all supported browsers. Switching over to the native microtask queue is not a large amount of work, though it needs to be done carefully. Two attempts have already been made to merge this fix into Ember, and both times we found serious bugs and reverted to try again, rather than break people’s apps. I think third time’s a charm.

Embracing the autorun

When most Ember developers hear “run loop”, they probably think of the dreaded autorun assertion:

You have turned on testing mode, which disabled the run-loop’s autorun. You will need to wrap any code with asynchronous side-effects in a run.

which sometimes forces them to manually call run to establish the start and end of the run loop, which is super annoying.

The run loop has always been capable of automatically starting itself (this is an “autorun”), but starting a new run loop automatically can cause subtle timing changes and make your tests fail in very confusing ways. The problem with our classic autorun is that it can only detect “when things are done” by scheduling a new task (a setTimeout), and there’s no way to stop other tasks from intervening between when you schedule your work and when the work is done. This is in contrast with explicit run loops (which Ember uses internally almost exclusively, and which users sometimes add to their code via Ember.run), which have an explicit beginning and end, so that they can guarantee that they finish their work before any other tasks (including things like click events or xhr handlers) can intervene.

But now that we can leverage the native microtask queue, we can make autoruns as robust as explicit run loops. The question of when to start and end the loop goes away – the native microtask queue is just always present. It’s always safe to schedule more work onto it, and rely on a native-task-based flush that will take care of all the “after” work. So the plan is to remove the assertion and just embrace autoruns, because they become no longer a problem.

No more run loop?

Does this mean the run loop is going away? Not really – it’s still a thing that you want to exist behind the scenes, reliably making sure there’s always a render scheduled to happen at the right time to keep your work optimally batched. But the runloop as something that app developers will need to learn about just to get their app working & tested can definitely go away, and that will make me very happy.

That is also the point where RSVP.Promise and native Promise will be on equal footing, and there will be no reason not to just use native promises everywhere.

What about async functions?

The async and await keywords are “the best thing to happen to modern javascript”, says @rwjblue. And I agree.

There are a lot of places where you can already take advantage of them in Ember, particularly in our modern-style tests. But making them work everywhere is the same as making native Promises work everywhere, because they are just syntactic sugar for native promises. So everything above about Ember soon working with native promises also applies to async / await.

Further reading

For a good introduction to microtasks and tasks (along with some detail on how bad browsers were at consistently supporting them until recently) check out Tasks, microtasks, queues and schedules by Jake Archibald

I have often referred back to Chris Thoburn’s (@runspired) thorough presentation on scheduling in Ember, “Beyond 60 FPS” (video, slides). It includes illustrated examples of how the task and microtask queues work.

Here is the bug that caused us to twice revert switching Ember to native microtask queue.

65 Likes

Thank you for this in-depth write-up @ef4! :slight_smile:

1 Like

A simply amazing answer, thank you! I didn’t know half of that (and will need some time and experience to digest it) although I kind of hope I don’t have to, that it’ll be made irrelevant.

2 Likes

Thank you for the answer! It thought me a lot.

Thanks @ef4, good answer!

I just wanted to add, as the current maintainer of RSVP I hold no attachment in us using it long term. As developers we tend to become code hoarders, it’s important to be aware of that and fight the urge.

One of my goals from the start, has been to ensure that transition off, once the web platform is sufficiently mature, is smooth. Hence the active involvement(and adhearance) in the language spec related to promises. A interesting side affect for me, was a wealth of transferable learnings in performance, async programming, spec reading and writing, contributing to chrome/v8/JSC/spidermonky, TC39 collaboration (and lots more) I did not expect, but am very happy for.

Also, a side-bit, in modern versions of node even the maintainer of RSVP (me) uses native promises.

One area we likely will need to provide utils for post RSVP retirement is: hash, hashSettled and potentially also allSettled(a early spec proposal exists for this one).

Anyways, RSVP has served us well, but hopefully soon it can be retired as it just can’t compete when it comes to developer ergonomics (debugging, stack traces, etc)

8 Likes

@stefan is this really required though? All of those methods can be replaced really easily using the bluebird promise library, which is more robust and quite stable. Of course we can keep these around if you all think their removal is detrimental, but I think its more efficient to just drop them so that Ember contributors are freed up to spend their valuable time solving problems that haven’t already been solved by other open-source software :stuck_out_tongue:

@mkay581 We do need to keep those methods working until the next major version of Ember for semver reasons, plus we don’t want to break people’s apps. While I like bluebird, what we want to do is go to native promises, not a different library.

3 Likes

@mkay581 We do need to keep those methods working until the next major version of Ember for semver reasons, plus we don’t want to break people’s apps. While I like bluebird, what we want to do is go to native promises, not a different library.

Yup, basically this.

User-land promises can’t compete with Native when it comes dev ergonomics, specifically debugger and stack explorer stuff, and previous downsides where re: performance, and those have reach a point where all but the extreme use-cases are well satisfied, and relevant perf work continues.


Some historic context as to why ember didn’t go with Bluebird (or similar) from the start. Because I absolute agree, re-inventing the wheel is a waste.

Warning: this is from memory, so I may get some points wrong

TL;DR

RSVP existed earlier, in the more wild wild west time of promises, it helped shape promises we have today, alongside bluebird came and taught us promises efficiently. This all lead to healthy ecosystem of promise libraries, and healthy ecosystem of promise library maintainers working together to improve the status quo for everyone. Now we are swole with options, but most importantly really great platform provided infrastructure. This lets us retire libraries such as RSVP, which is the best possible outcome IMHO.


Longer:

When RSVP was created bluebird was not around, instead at the time Q was the defacto promise library. Although Q was powerful, and exploring new ground in JS (inspired by learnings from E (programming language) - Wikipedia and Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control), it was more of a full distributed programming paradigm then a basic async primitive. This suggested to us it had a scope broader then we felt comfortable supporting. (relevant HN comment from the RSVP.js release announcement by @wycats from 2012 Yet another promises library (: It's great that people are writing and sharing c... | Hacker News)

Also, around this time. Promises were conceptually similar, their functionality and API does not resemble what we have today.

In-fact, our hap-hazard foray into promises frustrated @domenic (rightfully), @lukemelia recommend him provide more actionable feedback, which more then likely contributed to him writing You're Missing the Point of Promises · GitHub which eventually worked to bring the land of JS the promise/a+ Spec https://promisesaplus.com, which was a served as foundation for the final spec we have in JS today.

Around this time, @teddyzeenny and myself worked to get RSVP to be spec compliant, and @wycats / @domenic / @dherman / et al continued to evolve the specs, and to some small degree our feedback in implementation helped. I was even fortunate enough to get @Domenic into hanging out over beer to talk promises a few times. This period, lead to promises rapidly turning into what we see today.

And also around this time, Petka a developer obsessed with JavaScript performance began work on bluebird, after reading You're Missing the Point of Promises · GitHub knowing his expertise would allow him to create something much faster and he was right.

Bluebird played an important role, by setting an impressive baseline for performance. Which got a-few of us to get our shit together, narrowing the performance gap and in some cases surpassing.

Alongside all this an irc channel was created and basically all us promise library maintainers (q,when,rsvp,bluebird etc) worked together to explore features/problems/specs/benchmarks/vendor bugs/ etc, and inspire and help each others projects.


Now as to the cost of RSVP cannabolizing maintainer time, in recent years it has largely just been in maintenance mode, with close to 0 costs. In the past (the story above) was a decent amount of work, but I wouldn’t trade that investment as the learnings we have made, have paid good dividends.

5 Likes

Bluebird is much more than we would actually need, in the era of native Promise. All the remaining non-spec features in RSVP (hash, allSettled, etc) can just be utility functions.

2 Likes

It seems like the bug mentioned has been fixed now and the backburner version used by ember uses microtasks since 3.2: Microtask update introduced event dropping · Issue #332 · BackburnerJS/backburner.js · GitHub

Does this mean we could start using async / await in app code or is there still more left?

2 Likes
3 Likes

Has anyone started work on writing a version of RSVP that uses native promises?

I don’t really see a point. RSVP is a polyfill for native promises, once you have those, you drop RSVP.

would this mean that if all of your build targets supports native promises, you could bundle without RSVP?

Yes. AFAIK there’s no reason not to disentangle it from ember, the same way we’ve done with jquery.

Except for hash etc no?

Yes, it would be fine to have a tiny library for the functions that aren’t built-in to Promise.

1 Like

I use RSVP for hash. I use it when I need to send multiple models to the template

1 Like

Couldn’t you just use Promise.all and then build the hash yourself in the response? What benefit does hash have (besides eliminating the boilerplate) over doing something like:

return Promise.all([
  this.store.findAll('stuff'),
  this.store.findAll('otherStuff')
]).then(data => {
  return {
    stuff: data[0],
    otherStuff: data[1]
  };
});

You can, it’s just nice to have utility functions to make that shorter and clearer. My point was just that a function like hash doesn’t necessarily have anything to do with the internals of a Promise implementation. It can be in a separate utility library.