Closed vshapenko closed 2 years ago
Interesting idea. We can indeed limit the number of view update to better handle cases where we receive a lot of updates, since at all time, only the last view is really relevant. So this could be a good way to improve performance.
Though I think it would be better not to depend on a specific time rate. Because it doesn't really make sense in the context of a mobile application.
I would see it a bit differently:
processMsg
like today, except instead of directly calling updateView
, we would post a message to a mailbox with the last generated model (from let (updatedModel,newCommands) = program.update msg lastModel
, not from let mutable lastModel = initialModel
because this one needs to be the same as the displayed UI).view
would eagerly consume messages to keep only the last one at a specific momentupdateView
which can take up to a few hundred milliseconds to execute.This way, if the application has a quick update rate (like a timer) and it's quicker than Fabulous can handle, we would discard all "old" updates and only take the most recent one to apply when Fabulous finished the previous view update.
This is good because it doesn't change anything for most use cases where updates are "slow" (user interactions for example). And it is slightly when the updates are numerous in a very short time because we won't be updating the UI unnecessarily.
This would also let us put the init/update call on a different thread than UI thread. And also let us change ViewElement.UpdateIncremental to Async and support #158
@TimLariviere , current implementation does not set specific time rate. It just sets minimum possible view update interval. I will try to play with eager update
@TimLariviere , and just one last note on eager - i think we should limit the time we consume updates, otherwise there can be scenario redraw never happens (imagine constant updates at high frequency)
and just one last note on eager - i think we should limit the time we consume updates, otherwise there can be scenario redraw never happens (imagine constant updates at high frequency)
To avoid that, we could check the length of the message queue before eagerly consuming messages and only consume those messages (discarding all except last). That way, we aren't stuck in an infinite loop if messages are pushed quicker than we can consume them.
and just one last note on eager - i think we should limit the time we consume updates, otherwise there can be scenario redraw never happens (imagine constant updates at high frequency)
To avoid that, we could check the length of the message queue before eagerly consuming messages and only consume those messages (discarding all except last). That way, we aren't stuck in an infinite loop if messages are pushed quicker than we can consume them.
So, we would have update count as mailbox state. Hmmm. And in case of count =0 we would send a view render command.
Ok, made an "eager" model, but i am in doubts. This would help if we have "heavy" updates, but i am not sure how this is better than limiting minimum view render interval.Imagine we have very frequent and fast updates, but slow redraws - current model will not give us much advantage in terms of performance.
This would help if we have "heavy" updates, but i am not sure how this is better than limiting minimum view render interval.Imagine we have very frequent and fast updates, but slow redraws - current model will not give us much advantage in terms of performance.
The idea of the eager model is to let the Fabulous update the UI as soon as it can. But if there's more updates than Fabulous can handle, it will discard all "old" updates and only updates the UI with the latest model available at the moment Fabulous is free to diff the UI.
Note that during v1, updates post their updated model to the mailbox. But when the mailbox is free (it finally finished executing ViewElement.UpdateIncremental), it will check the queue length and only take the last updated model.
So it should achieve good performance even in high frequency update. Redraws will most likely be slower than updates everytime, so we're bound to the redraw time in any case.
@TimLariviere , do i understand correctly that you propose a kind of "blocking" model on view updates?
do i understand correctly that you propose a kind of "blocking" model on view updates?
Not sure what you mean by blocking model.
The thing with view updates is that they need to be run sequentially on the same UI thread. So while we're diffing the view for Model 1, we can't process other updated models.
Once we're done diffing the view, today, Fabulous will take Model 2 and diff the view, so forth and so on until there's no longer any updated models left.
My proposition is when Fabulous is done diffing the view for Model 1 and Models 2, 3 & 4 have been sent while we were working on the UI, Fabulous will ignore Models 2 and 3, and will directly diff the view for Model 4 (because previous models are no longer relevant). New updated models will continue to accumulate, waiting for Fabulous to be available to diff the view for a new model.
@TimLariviere , take a look to latest commit, i think this is what you need. We "accumulate" changes while rendering happens, after that we ensure that current renderred state is actual or not. If we got more messages while rendering, we just launch render on actual state.
@vshapenko Tested it on the CounterApp, it's working! 👍
I've put a timer of 15ms and added Async.Sleep 250
before updateView
to simulate a slow rendering.
I get the following results
Also tried to simplify the mailbox by not using blocking flags, instead pulling "old" messages with a timeout of 0 (no wait).
let viewInbox = MailboxProcessor.Start (fun inbox ->
let rec loop () = async {
let queueLength = inbox.CurrentQueueLength
if queueLength > 1 then
Console.WriteLine(sprintf "Dropped %i messages" (queueLength - 1))
for i = 1 to queueLength - 1 do
let! _ = inbox.Receive 0
()
let! updatedModel = inbox.Receive()
program.syncAction (fun()->updateView updatedModel) ()
return! loop()
}
loop ()
)
I got the same result.
@TimLariviere , i've never seen such "drop" technique in prod, but maybe it is valid. Anyway, i prefer do not trust queue size and write more strict code. I have pushed a version with mutable flag, should be more consistent.
Anyway, i prefer do not trust queue size and write more strict code. I have pushed a version with mutable flag, should be more consistent.
CurrentQueueLength
is an approximation of the number of messages based on the comments inside the source code, but it should be good enough for our case.
Because there's only one reader, so if the approximation is inferior or equal to the real number of newly updated models, we're good.
Maybe we can be 1-2 messages behind, I think, when under heavy load, but that's not a problem in my opinion (such case is like very improbable, rarely in mobile apps there's such an update rate).
The issue with a more strict version is that there's 2 asynchronous processes that can send messages (of different types) at the same time.
So it's becoming harder to understand and a lot harder to debug. We can have race condition, invalid ordering (could be an oversight, will add a review on the line), etc.
Also it is forced to start an async task to then call program.syncAction
that will marshal back on the UI thread. So if we could avoid starting new thread for that, especially for really quick updates, it will be better for performance.
On the less strict version, it's still very sequential and so a lot easier to understand and debug. The loop starts, checks the queue length one single time, discards old messages if more than 1 in the queue and then synchronously wait for the UI to render. Once done, it starts a new loop that does the exact same thing. If there's no message, it will wait and immediately render the UI when a new message arrives.
Problem with your approach on message dropping is following :
Like we discussed on another channel, I think we should refactor the Runner
class a bit to allow for an easier subclassing/extension to change how the update-view loop behaves.
Because in the vast majority of cases, people won't need a high throughput of updates in their apps so the current implementation is good enough and is reliable due to being fully-sequential.
Then, you'll be able to write your own Runner and start it inside a function in the Program
module just like Program.run
.
https://github.com/fsprojects/Fabulous/blob/fb4f251ce5e7cd3f9755099dcd6f12991bcce794/src/Fabulous/Program.fs#L253-L255
Ok, i will try to find time.
вт, 7 июл. 2020 г., 11:48 Timothé Larivière notifications@github.com:
Like we discussed on another channel, I think we should refactor the Runner class a bit to allow for an easier subclassing/extension to change how the update-view loop behaves.
Because in the vast majority of cases, people won't need a high throughput of updates in their apps so the current implementation is good enough and is reliable due to being fully-sequential.
Then, you'll be able to write your own Runner and start it inside a function in the Program module just like Program.run.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/fsprojects/Fabulous/pull/771#issuecomment-654699101, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEKNDOXDRHETYCI7QDRQBI3R2LOMLANCNFSM4OLCT3VA .
Concept of limited framerate on view updating. Minimum interval between view updated - 15ms. Model updates are processed as usual
@TimLariviere , FYI