Skip to content

Unoptimized implementation of pMapIterable preserveOrder option #79

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

tgfisher4
Copy link

Fixes #72.

Spun off from #74 in light of

Summary

Adds a preserveOrder option to pMapIterable, which indicates

Whether the output iterable should produce the results of the mapper on elements of the input iterable in the same order as the elements were produced.
If false, mapper results will be produced in the order they are available, which may not match the order the mapper inputs were produced by the input iterable, but may improve throughput.
Type: boolean
Default: true

Implementation considerations

  • promise must return reference to itself so popRandomPromise can find indexOf.
  • When preserveOrder: false, racing the promises pool is not enough, since trySpawn may be add additional promises to the buffer after the race has begun: so we race an event listener alongside (promises pool is still needed to buffer multiple promises finishing simultaneously.
  • When returnValue === pMapSkip && preserveOrder: false, unlike when preserveOrder: false we have no way to know if returning will cause our promise to win an ongoing race, and so have no way of knowing whether to skip popping ourselves (to allow the main loop to pop us instead). So, when returnValue === pMapSkip, we instead popRandomPromise ourselves unconditionally and instead skip popNextPromise in main while loop.
  • move promise inner try-catch logic to promise.catch (which now also catches when iterator.next() throws)
  • when preserveOrder: false && result.done, some promises in the pool may still be pending, so continue (as a matter of fact, we could probably continue in all cases since the current implementation does not run concurrent iterator.next calls, so if preserveOrder: true && result.done there should be no remaining promises in the queue - but hopefully that will change soon as we think about concurrent iterator.next calls!

…fer and `promiseEmitter`

- move `promise` inner `try-catch` logic to `promise.catch` (now also catches when `iterator.next()` throws)
- when `returnValue === pMapSkip`, `popRandomPromise` unconditionally (skip `popNextPromise` in main `while` loop instead)
- add tests for `preserveOrder: false`, sync throwing mappers/iterables, more intricate `pMapSkip` scenarios
index.js Outdated
}

trySpawn();

while (promises.length > 0) {
const {error, done, value} = await promises[0]; // eslint-disable-line no-await-in-loop
const {promise, result: {error, done, value}} = await nextPromise(); // eslint-disable-line no-await-in-loop
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline the function here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I had not done so to avoid checking preserveOrder on every yield loop iteration, but now that you mention it, I imagine the overhead of a function call is much more than that of a ternary.

index.js Outdated

promises.push(promise);
promise.then(p => {
Copy link
Contributor

@Richienb Richienb Sep 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use try/catch/async/await here. Also, would be better to use a deferred promise and we call it after anything resolves, instead of racing? https://github.com/sindresorhus/p-defer

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring the promise is a neat technique! I had not encountered that before. It can definitely replace my use of EventTarget. However, I notice this library currently has 0 runtime dependencies - would it be acceptable to introduce p-defer as a runtime dependency or should I just copy the code over? I've done the latter for now.

However, I don't think it can replace Promise.race altogether, with the reason being that multiple promises can resolve between yield loop runs, before we have a chance to reset the somePromiseHasSettled deferred promise (since a promise can only hold one resolved value, the value of the second resolved promise would be lost). So, promises is needed as a buffer for resolved promises we have not gotten a chance to yield yet, and Promise.race is needed to check if any of those have resolved yet.

I've also used a deferred promise in conjunction with a new helper function to eliminate use of .then/.catch.

index.js Outdated
// This occurs when `concurrency > 1`: the first `promise` will `trySpawn` and `promises.push` another promise before `await`ing the mapper,
// but the ongoing `Promise.race(promises)` call from `nextPromise` is oblivious to this new promise as it was not present in `promises`
// when the race began.
new Promise(resolve => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good thinking!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less "thinking" and more "debugging after things didn't work as expected" 😅

@artsiommiksiuk
Copy link

So, will we get this published?

…cificPromise, done condition continue vs return explanation, comment on non-use of spread
@tgfisher4
Copy link
Author

@artsiommiksiuk thanks for the ping! Last time I worked on this, I had been waiting to see if #78 or #76 would drastically change the implementation here before I continued with my own changes. Since no such changed have materialized, I'll pick this back up.

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

Successfully merging this pull request may close these issues.

Optionally allow pMapIterable to yield results out of order
3 participants