You-Dont-Know-JS icon indicating copy to clipboard operation
You-Dont-Know-JS copied to clipboard

Async & Performance: optimize gen-runner in chapter 4

Open getify opened this issue 7 years ago • 13 comments

In the promise-aware gen runner, to prevent the unnecessary extra tick at the beginning, change:

return Promise.resolve().then(
   function handleNext(value){ .. }
);

...to:

return Promise.resolve(
   (function handleNext(value){ .. })()
);

getify avatar Jan 07 '18 16:01 getify

Code also shown in chapter 4 of "ES6 & Beyond"

getify avatar Jan 09 '18 17:01 getify

When I wrote that suggestion the deferring of a tick was intentional for clarity of code - but I don't have strong feelings about it.

benjamingr avatar Jan 16 '18 09:01 benjamingr

The reason I don't like the extra tick is because an equivalent async function doesn't wait a tick to start, it runs its first part immediately.

getify avatar Jan 16 '18 13:01 getify

@getify you're right :) I didn't know that would be the case when I first wrote that pump code.

Another part of the fact it's wrapped in a then provides throw safety which would need to be a try/catch - although I trust you know better than me what people might find easier to understand in the book.

benjamingr avatar Jan 16 '18 13:01 benjamingr

the fact it's wrapped in a then provides throw safety which would need to be a try/catch

Ahh, you're absolutely right, totally forgot that was one of the motivations. That should have been commented. ;-)

OK, so we need this, huh?:

return Promise.resolve(
   (function handleNext(value){
      try {
         var next = it.next(value);
         // ..
      }
      catch (err) {
         return Promise.reject(err);
      }
   })()
);

getify avatar Jan 16 '18 14:01 getify

Pretty much, yeah, although then you can probably add a Promise.resolve on the value if the iterator is done and drop the Promise.resolve around everything.

benjamingr avatar Jan 16 '18 14:01 benjamingr

the other (minor) downside is that this new try..catch is run (unnecessarily) on every subsequent step, not just the first step...it's unnecessary becasue the subsequent steps already have their own try..catch via the then(..). :/

getify avatar Jan 16 '18 15:01 getify

FYI: I've previously adapted this gen-runner to use in another of my projects, CAF.

So just tweaked it based on our discussions here: https://github.com/getify/CAF/blob/94c60e2cdba714680f770ca04bd3fe2e5201b830/src/caf.src.js#L58-L98

Thoughts?

In particular, I'm curious/pondering if it's better (or different/broken) to move that outer catch(..) { .. } clause from line 94 up to line 65, and then just leave the return .. on line 66 outside of the try..catch? What do you think?

getify avatar Jan 16 '18 15:01 getify

(edit): I thought moving the catch broke my tests, but it didn't. So I've made that change now: https://github.com/getify/CAF/blob/c084ebfeea8eabe28676f105700eb8575776024b/src/caf.src.js#L58-L98

Thoughts?

getify avatar Jan 16 '18 16:01 getify

CAF looks really interesting, it's a shame we don't have proper cancel token support from the platform. I see you .return on the generator on cancellation which is what we did at bluebird and the reasonable thing to do IMO.

I would probably write it with an async function which would be really cute usage IMO:

function _runner(gen, ...args) {
  const it = gen.apply(this, args);
  async function result() {
    let done = false, value = undefined;
    while(!done) {
      try {
        ({done, value}) = it.next(await value);
      } catch (e) {
        it.throw(e);
      }
    }
    return value;
  }
  return {it, result() }
}

Although I'm not sure of its performance characteristics. What about the perhaps simpler:

function _runner(gen, ...args) {
  const it = gen.apply(this, args);
  function result(oldValue) {
    let {done, value} = it.next(oldValue);
    return Promise.resolve(value).then(result, it.throw.bind(it));
  }
  return {it, (async () => result())() } // the `async` lambda is to handle `throw`s
}

benjamingr avatar Jan 16 '18 19:01 benjamingr

My main reason for not making it an async function is that my baseline target is ES6 support rather than ES2017 support. I don't like people to have to transpile my libraries if it's avoidable.

getify avatar Jan 16 '18 21:01 getify

it's a shame we don't have proper cancel token support from the platform.

We do have the web platform's standard of AbortController, which CAF adheres to.

getify avatar Jan 16 '18 22:01 getify

@getify What about that?

function run(gen, ...args) {

    let it = gen(...args);
    let next;

    try {
        next = it.next();
        return handleValue(next);
    } catch (err) {
        return Promise.reject(err);
    }


    function handleErr(err) {
        return Promise.resolve(it.throw(err)).then(handleValue);
    }


    function handleValue(next) {
        if (!next.done) {
            return Promise.resolve(next.value).then(nextStep, handleErr);
        } else {
            return Promise.resolve(next.value);
        }
    }

    function nextStep(value) {
        next = it.next(value);
        return handleValue(next);
    }
}

jfet97 avatar Aug 30 '18 14:08 jfet97