Sorry I wasn’t very clear, let me try to clarify.
I have been looking at this code and observed some properties that might help us solve this problem, but we need help from the blink team to understand whether what I have in mind makes any sense at all. This is what I have noticed:
- The document cycles (in an unordely manner jumping back and forth) through the states in DocumentLifecycle.h (ex. InStyleRecalc, InPerformLayout, InCompositingUpdate, etc.)
- The document goes through these states regardless of whether a commit has been requested or not
- My guess is that transitions between these states happen when the asynchronous tasks that you mention complete
- At some point we send a commit request to the impl thread. When the impl thread decides that this is a good moment then we get the BeingMainFrame call on the main thread. My assumption is that the impl thread always send us the BeginMainFrame after we have requested a commit.
- The actual commit and the DocumentLifecycle are synchronized, ie. the layer_tree_host()->CommitComplete() call in BeginMainFrame always happens when the document is in the CompositingClean state.
- BeginMainFrame will request new commits if those are needed. In other words, once BeginMainFrame ends there may or may not be an active commit request in the impl thread.
Now given all of those, and I do understand that there are a lot of assumptions in there that may be incorrect (blink team are 3, 4, 5 correct?), then I can glimpse a possible solution. The basic idea is to request a forced commit whenever we get a flush request. A forced commit is a commit in which any early outs in BeginMainFrame will be ignored, and the commit will complete even if there are no updates (this could result in new frame with the same contents as the previous frame).
Let me go through the possible scenarios and explain how this would work.
A) Flush request received when there is nothing pending to do (no new frames)
We request the forced commit which will complete and deliver the same frame again. The flush response will be delivered with that commit with the mechanism that we already have.
B) Flush request received when there is pending work, but no active commit request
In this case we are just requesting the commit ahead of time, for example say when the document is in state 2 instead of state 4. My assumption is that given that commits only complete when the document is in state CompositingClean then requesting the commit ahead of time has no practical effects.
C) Flush request received when there is pending work and an active commit request
The forced commit that we request when we get the flush request will just convert the active commit into a forced commit without actually needing to notify the impl thread (see SendCommitRequestToImplThreadIfNeeded). Only the main frame needs to distinguish between normal and forced commits.
D) Flush request received but updating the visual state requires more than 1 commit.
This is the tricky case. Essentially we need a mechanism to decide whether we need to wait for a new frame or not. My solution is based on this assumption:
Once BeginMainFrame completes then a new commit will have been requested if there is still pending work.
So at the appropriate point in BeingMainFrame we need to check whether there is an active commit request. If there is none then we know that we need to deliver the flush response with the current commit. If there is one then we know that we need to wait.
This raises the question of termination. We could continue waiting forever if there is always some extra work to do. In that case we can perhaps introduce some heuristic and wait for a maximum of N commits before delivering the response.