You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Dec 15, 2022. It is now read-only.
The ModelObserver is used as a pseudo-declarative way to watch for models that implement onDidUpdate to change and then take some action (usually fetching new data from the underlying model). This pattern is made more fully declarative via the ObserveModel decorator.
Take the following scenario:
classModel{// ...getSomeData(){returnnewPromise(...)// takes some time to resolve}}constmo=newModelObserver({fetchData: model=>model.getSomeData(),didUpdate: ()=>this.refreshSomeViewOrSomething(),});mo.setActiveModel(newModel());
As written currently, the following scenario could take place:
Something triggers an update in the model, which triggers an onDidUpdate callback in the observer.
The observer calls getSomeData on the model. This takes 3 seconds to complete.
In the meantime, another operation triggers an update in the model. The onDidUpdate callback fires again.
The observer calls getSomeData again. This time it takes 2 seconds to complete.
The second getSomeData callback completes, and since that promise is associated with the latest fetch, it's set as the "current" data.
The firstgetSomeData promise resolves a second later, but since it's associated with an older fetch, the data is considered stale and is thrown away.
Normally, this is perfect — we're able to run multiple fetches concurrently, and only the latest one is actually used, rendering updates as quickly as possible.
However, this triggers a problem when fetching data from the Repository model, since reads are queued (up to a certain point controlled by a parallelism limit). In that case, every read ends up piling on the queue:
In this case, the reads are queued such that the second getSomeData() is forced to wait the rest of the 3 seconds from the initial fetch before it can start it's own 2 seconds worth of fetching, making the time profile look more like this:
So not only does the update feel like it's way slower, because we don't set any data until the latest fetch has all its reads finally executed, but multiple triggers of onDidUpdate() could cause many reads to be placed into the queue, thus causing a huge delay in the actual operations that will ultimately actually be used. This is an actual problem for us, as nsfw triggers updates in batches; in large repos, we've observed as many as 12 discrete events triggering model updates in just over a second, causing the Git operation queue to be saturated for a good 10 or 15 seconds before any UI updates occur.
This PR changes the ModelObserver semantics such that:
If a model fetch is pending when another update is requested, that second update is queued to start after the other is finished.
If a model fetch is pending when another update is requested and we've already queued another update, we discard it.
Any time a fetch finishes, we set it as the current data, even if another update is pending.
After a fetch finishes, run any pending update.
That means we lose concurrent fetches, but we gain the ability to have "partial" updates when a model has only partially updated. This would change the graph to be more like this:
The additional benefit is that no matter how many onDidUpdate callbacks are triggered during the original 3-second fetch, there will only be one additional fetch run after it completes; for us, this results in a Git operation queue that has many fewer operations queued.
We also considered throttling nsfw events at the watcher level, but feel this is ultimately a better experience since partial updates are rendered to the UI as soon as possible (much like unix commands that stream text to a terminal as things update). We've tested this with a large merge conflict in a fairly large repo (github/github) and it feels much better, especially with other changes to the read queue that are still in progress (to better speed up and parallelize the queue).
The tests have been brought up to date for the new behavior.
@kuychaco, I'd like to remove the await from await this.didUpdate(model) in ModelObserver#_refreshModelData. I don't think the timing of the next data fetch should depend on the actions performed in the user-provided didUpdate. It's possible the user will return promises for UI updates or other things from it, and I think we should leave the timing of the side effects up to the user and move on with the next pending fetch (if there is one) immediately.
If you're in favor, I'll make the change; it breaks a lot of other tests (presumably where we're returning etch.update(this) from inside it) but they can likely be fixed without too much trouble.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
None yet
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
ModelObserveris used as a pseudo-declarative way to watch for models that implementonDidUpdateto change and then take some action (usually fetching new data from the underlying model). This pattern is made more fully declarative via theObserveModeldecorator.Take the following scenario:
As written currently, the following scenario could take place:
onDidUpdatecallback in the observer.getSomeDataon the model. This takes 3 seconds to complete.onDidUpdatecallback fires again.getSomeDataagain. This time it takes 2 seconds to complete.getSomeDatacallback completes, and since that promise is associated with the latest fetch, it's set as the "current" data.getSomeDatapromise resolves a second later, but since it's associated with an older fetch, the data is considered stale and is thrown away.Normally, this is perfect — we're able to run multiple fetches concurrently, and only the latest one is actually used, rendering updates as quickly as possible.
However, this triggers a problem when fetching data from the
Repositorymodel, since reads are queued (up to a certain point controlled by a parallelism limit). In that case, every read ends up piling on the queue:In this case, the reads are queued such that the second
getSomeData()is forced to wait the rest of the 3 seconds from the initial fetch before it can start it's own 2 seconds worth of fetching, making the time profile look more like this:So not only does the update feel like it's way slower, because we don't set any data until the latest fetch has all its reads finally executed, but multiple triggers of
onDidUpdate()could cause many reads to be placed into the queue, thus causing a huge delay in the actual operations that will ultimately actually be used. This is an actual problem for us, as nsfw triggers updates in batches; in large repos, we've observed as many as 12 discrete events triggering model updates in just over a second, causing the Git operation queue to be saturated for a good 10 or 15 seconds before any UI updates occur.This PR changes the
ModelObserversemantics such that:That means we lose concurrent fetches, but we gain the ability to have "partial" updates when a model has only partially updated. This would change the graph to be more like this:
The additional benefit is that no matter how many
onDidUpdatecallbacks are triggered during the original 3-second fetch, there will only be one additional fetch run after it completes; for us, this results in a Git operation queue that has many fewer operations queued.We also considered throttling nsfw events at the watcher level, but feel this is ultimately a better experience since partial updates are rendered to the UI as soon as possible (much like unix commands that stream text to a terminal as things update). We've tested this with a large merge conflict in a fairly large repo (
github/github) and it feels much better, especially with other changes to the read queue that are still in progress (to better speed up and parallelize the queue).