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

Create task queue to synchronize function calls to media pool elements #13259

Merged
merged 47 commits into from
Feb 9, 2018

Conversation

newmuis
Copy link
Contributor

@newmuis newmuis commented Feb 4, 2018

Fixes #13232. Also includes changes from #13245.

This PR creates a task queue on each media element in the MediaPool, implemented as an array of strings. The array stores the next functions to be invoked for that element (e.g. `['bless', 'load', 'play', 'unmute']), and ensures that they are each only called after the previous one has finished executing.

This is necessary due to the async nature of the amp-story code, as well as the async nature of the media element's play() function. In some cases, play() is invoked but then other code is executed before any callbacks on play()'s promise chain are invoked. In our case, the "other code" that gets executed is sometimes also media-related code, calling load() or play() again, both of which can cause the media in question to cease playback.

See #13232 for more details on the issue.

@newmuis newmuis changed the title Create task queue to serialize function calls to media pool elements Create task queue to serialize function calls to media pool elements (WIP) Feb 4, 2018
@newmuis
Copy link
Contributor Author

newmuis commented Feb 5, 2018

The play() promise (at least the one used for the bless task) seems to hang indefinitely in a pending state sometimes, on iOS. I'm not sure what is causing this, as it meets all of the criteria stated in the WebKit .

I will continue investigating the cause there. Additionally, this seems to make the overall navigation slower, as these operations are all blocking on one another. I can't think of a good way to feature detect whether the browser can handle allowing these to be completely async as it was before, but I will also at least try using vsync in the cases where we swap elements in/out of the DOM to ensure that the swap is done within a single frame.

Copy link
Contributor

@gmajoulet gmajoulet left a comment

Choose a reason for hiding this comment

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

Great! Do you think we could improve this queue, later, to bless several elements in parallel? From what I understand they're blessed one by one here

blessPromise.catch(reason => {
dev().expectedError('AMP-STORY', 'Blessing media element failed:',
reason, mediaEl);
}).then(() => dispatch(mediaEl, 'bless', false));
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth adding a comment to mention this line works as a finally and is triggered on both success or failure of the promise

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored this

/**
* @const {!ElementTaskDef}
*/
const NOOP_ELEMENT_TASK = () => 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.

Should this method store the expected taskName and log an error message saying this task does not exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored this


task(mediaEl)
.catch(reason => dev().error('AMP-STORY', reason))
.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool pattern, but unless it's widely used, I think it's worth adding a // Finally comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

dev().expectedError('AMP-STORY', 'Blessing media element failed:',
reason);
});
* @param {!HTMLMediaElement} poolMediaEl The media element to bless.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: s/poolMediaEl/mediaEl

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done (moved to media-tasks.js)

const queue = mediaEl[ELEMENT_TASK_QUEUE_PROPERTY_NAME];
queue.push(taskName);

if (queue.length === 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: might be worth implementing a isQueueRunning() or shouldExecuteQueue or something helper, that would do just that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

made a constant

@newmuis
Copy link
Contributor Author

newmuis commented Feb 7, 2018

The queue is per-element, so there's nothing preventing multiple elements from being blessed in parallel. But, for a given element, only a single task is executed at a time.

@newmuis
Copy link
Contributor Author

newmuis commented Feb 7, 2018

The performance seems a bit better after the promise changes to amp-story-page, but note that this PR still has a problem where when the 8th page gets preloaded, it throws a JS error which causes the page to not load its video:

TypeError: _this10.newSources_.applyToElement is not a function
    at media-tasks.js:406
    at callTaskNoInline (vsync-impl.js:455)
    at Vsync.runScheduledTasks_ (vsync-impl.js:412)

Copy link
Contributor

@gmajoulet gmajoulet left a comment

Choose a reason for hiding this comment

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

Really great work, just a few nits/questions

* to replace the specified element.
*/
loadInternal_(domMediaEl) {
if (!isConnectedNode(domMediaEl)) {
// Don't handle nodes that aren't even in the document.
return null;
return 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.

Should we reject the promise instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure this is really a rejection; we've done all the loading we want to do for that element, it just happens that we don't want to do any :)

*/
register(domMediaEl) {
const mediaType = this.getMediaType_(domMediaEl);
if (this.isAllocatedMediaElement_(mediaType, domMediaEl)) {
// This media element originated from the media pool.
return;
return Promise.resolve();
}

const id = domMediaEl.id || this.createDomMediaElementId_();
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like I'm missing something here, but should the result from this.createDomMediaElementId_() be assigned to domMediaEl.id?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Below, we set domMediaEl.id = id, which handles this.

}


/**
* Preloads the content of the specified media element in the DOM.
* @param {!HTMLMediaElement} domMediaEl The media element, found in the DOM,
* whose content should be loaded.
* @return {!Promise<!HTMLMediaElement>} A promise that is resolved when the
* specified media element has been successfully loaded.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/has been successfully loaded/has successfully started preloading?
This comment makes me think all the media content is loaded and ready to play until the end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, copy/paste error. Changed.

return this.enqueueMediaElementTask_(poolMediaEl, new PauseTask())
.then(() => {
if (opt_rewindToBeginning) {
this.enqueueMediaElementTask_(poolMediaEl, new RewindTask());
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe: this.rewindToBeginning(domMediaEl)

Also, do we actually need to wait for the PauseTask to be completed before we rewind the video? Looks like it was working synchronously before

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, this was just to be very cautious. We can experiment later, since none of this is well-documented :)

Didn't use rewindToBeginning to prevent getting the matching element from the pool twice and wasting cycles.

@@ -0,0 +1,459 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

I think eventually we'll want to have all these in a media-tasks folder.

Copy link
Contributor

Choose a reason for hiding this comment

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

in separate files*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1

mutatePromise: () => {},
};
sandbox.stub(vsyncApi, 'mutatePromise')
.callsFake(callback => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, does this work?

sandbox.stub(vsyncApi, 'mutatePromise').resolves(callback => {callback()});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep!

@newmuis newmuis changed the title Create task queue to serialize function calls to media pool elements (WIP) Create task queue to synchronize function calls to media pool elements Feb 8, 2018
@newmuis newmuis merged commit 619bcfb into ampproject:master Feb 9, 2018
protonate pushed a commit to protonate/amphtml that referenced this pull request Feb 26, 2018
ampproject#13259)

* Return play promise from playFn to ensure media playback is not interrupted.

* Refactor playFn to always return a promise.  Improve documentation.

* Add debug messages

* Remove muted property change, since this is in a separate PR

* Fix merge

* Implement a task queue for media element functions

* Only dispatch event for blessing media, to avoid conflicts with existing event names.

* Add debugging

* Add element to debug logging

* Move debug message

* Attempt fix bless promises

* Simplify debug logs

* Add logging and fix event setup

* Add queue contents debugging

* Capture event

* Logging changes

* logging updates

* More logging

* More logging

* Short circuit bless before enqueue

* Logging

* More logging

* Make bless promise

* Inline bless play

* More logging

* Add bless play logging

* Add vsync

* Revert "Add vsync"

This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10.

* Reset media source when deallocated.

* Serialize dom el for testing

* Update video source as a task

* Migrate tasks to classes; schedule microtasks where possible.

* Use vsync, make all methods promise, block page methods on promise resolution, split into separate files

* Add missing added files

* Add unit tests for media tasks

* Fix wrong class name

* Sort imports

* Remove logging

* Address review comments

* Sort imports in utils

* Log queue contents

* Remove vsync, add logging

* Fix getDefaultSources and isQueueEmpty.

* Fix initializing to default source.  Don't play for bless when already playing.

* Remove allocated elements from unallocated array.

* Fix types and address review comments.

* Update MediaPool tests
RanAbram pushed a commit to RanAbram/amphtml that referenced this pull request Mar 12, 2018
ampproject#13259)

* Return play promise from playFn to ensure media playback is not interrupted.

* Refactor playFn to always return a promise.  Improve documentation.

* Add debug messages

* Remove muted property change, since this is in a separate PR

* Fix merge

* Implement a task queue for media element functions

* Only dispatch event for blessing media, to avoid conflicts with existing event names.

* Add debugging

* Add element to debug logging

* Move debug message

* Attempt fix bless promises

* Simplify debug logs

* Add logging and fix event setup

* Add queue contents debugging

* Capture event

* Logging changes

* logging updates

* More logging

* More logging

* Short circuit bless before enqueue

* Logging

* More logging

* Make bless promise

* Inline bless play

* More logging

* Add bless play logging

* Add vsync

* Revert "Add vsync"

This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10.

* Reset media source when deallocated.

* Serialize dom el for testing

* Update video source as a task

* Migrate tasks to classes; schedule microtasks where possible.

* Use vsync, make all methods promise, block page methods on promise resolution, split into separate files

* Add missing added files

* Add unit tests for media tasks

* Fix wrong class name

* Sort imports

* Remove logging

* Address review comments

* Sort imports in utils

* Log queue contents

* Remove vsync, add logging

* Fix getDefaultSources and isQueueEmpty.

* Fix initializing to default source.  Don't play for bless when already playing.

* Remove allocated elements from unallocated array.

* Fix types and address review comments.

* Update MediaPool tests
protonate pushed a commit to protonate/amphtml that referenced this pull request Mar 15, 2018
ampproject#13259)

* Return play promise from playFn to ensure media playback is not interrupted.

* Refactor playFn to always return a promise.  Improve documentation.

* Add debug messages

* Remove muted property change, since this is in a separate PR

* Fix merge

* Implement a task queue for media element functions

* Only dispatch event for blessing media, to avoid conflicts with existing event names.

* Add debugging

* Add element to debug logging

* Move debug message

* Attempt fix bless promises

* Simplify debug logs

* Add logging and fix event setup

* Add queue contents debugging

* Capture event

* Logging changes

* logging updates

* More logging

* More logging

* Short circuit bless before enqueue

* Logging

* More logging

* Make bless promise

* Inline bless play

* More logging

* Add bless play logging

* Add vsync

* Revert "Add vsync"

This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10.

* Reset media source when deallocated.

* Serialize dom el for testing

* Update video source as a task

* Migrate tasks to classes; schedule microtasks where possible.

* Use vsync, make all methods promise, block page methods on promise resolution, split into separate files

* Add missing added files

* Add unit tests for media tasks

* Fix wrong class name

* Sort imports

* Remove logging

* Address review comments

* Sort imports in utils

* Log queue contents

* Remove vsync, add logging

* Fix getDefaultSources and isQueueEmpty.

* Fix initializing to default source.  Don't play for bless when already playing.

* Remove allocated elements from unallocated array.

* Fix types and address review comments.

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

Successfully merging this pull request may close these issues.

None yet

4 participants