-
Notifications
You must be signed in to change notification settings - Fork 2
Remaining pyside qobjects use consistent shared updates[CPP-687] #458
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
Remaining pyside qobjects use consistent shared updates[CPP-687] #458
Conversation
c8b273d
to
d22eb5c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logged a few suggestions, but overall looks good
18295bf
to
a46fb27
Compare
} | ||
|
||
|
||
STATUS_BAR: List[Dict[str, Any]] = [status_bar_update()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have a bunch of these single element lists for each tab? Like for example couldn't this just be
STATUS_BAR: Dict[str, Any] = status_bar_update()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, this was initially pitched by Jason so he may be able to explain it better than me guessing. We are calling this function in a separate thread throughout the app. Ex:
@classmethod
def post_data_update(cls, update_data: Dict[str, Any]) -> None:
TRACKING_SKY_PLOT_TAB[0] = update_data
cls._instance._data_updated.emit()
And then accessing the data in pyside/qml qobject land. Ex:
@Slot() # type: ignore
def handle_data_updated(self) -> None:
self._tracking_sky_plot = TRACKING_SKY_PLOT_TAB[0]
I guess this in ensures there is always a valid reference to the parent list which could have some implications with respect to garbage collection. But not sure if this is technically needed?
Probably need @silverjam to explain 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@notoriaga It's using a list as a "pointer" so we can modify a global variable without using a global
statement -- was also thinking that for some of these updates we might need to eventually replace these with a queue.Queue
or use list.pop(0)
and list.push(item)
to insert/remove data updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I see, makes sense!
It's been a little while since I've looked at the console, and to be honest I'm not sure I ever fully understood the data update model, so I've written up my understanding as a precursor to my question just in case my understanding is incorrect! Tab StateEach tab holds state in the way of a global list that contains a dictionary. This represents the latest data that has arrived from the backend. Eg:
Tabs generally have a
To interact with this data from the QML code, there are models registered that allow the QML to control when data is read out from the global state into the class, eg:
After calling the above from QML, the QML code can then access The QML calls this code to transfer data out from the global state on a timer, if the tab is active. Messages
QuestionSo this leads me onto my question - can there be a data race between the QML code calling in and
Which would make I guess this question maybe isn't that relevant to this particular PR as it looks like we've always been running |
This is a great write-up, we probably should've done this kind of write-up for the PR itself, just to explain what was going on.
These global dictionaries are kind of hold-overs from a very early prototype of the console, in many cases we could probably eliminate them by moving data storage in the QObject models themselves (assuming we do it in a way that ensures consistency).
Again, this is where we could probably access the model directly and we could eliminate these As you mentioned, these fill methods are driven by timers in the Qt code. We could probably gain some performance by eliminating the fill methods (and the timers) and just letting Qt poll the data model for changes whenever it wanted to paint. This is what we do for the table model and (as far as I remember) it gained us lot of performance advantage in that tab.
The important new thing here is the
The fact that we're posting a message and updating the internal reference to new copy of an "update dictionary" prevents any inconsistency. So, in theory, as long as the data is consistent and ownership is unique, we should be able to prevent data races without locks.
The global dictionaries could be inconsistent in the old model because the receive thread could be updating different keys of the dictionary and even with Python's GIL this would result in keys in inconsistent states (especially if those keys contained related data, like lists that needed to be the same length). |
Nice, mind adding this to https://github.com/swift-nav/swift-toolbox/blob/main/docs/DESIGN.md? |
Converted all of the remaining tabs to use the consistent shared updates approach pitched in this PR:
#447
Also moved the "data_model" for sending data from frontend to backend to its own file to appease the linter's max 1k lines per file requirement.
There were a few files we had to add more than a single container to follow the paradigm, an example of how this was done throughout all cases can be found in
swiftnav_console/connection.py
.The Observation tab strictly requires the reuse of the table model which would not work for our paradigm of "singleton" usage so two child classes were made Remote/Local to stick to the paradigm.