4

I am upgrading a design where data was lightly coupled with the UI:

class Object {
    UI * ui;
};


class UI {
    Object * object;
};

It was fairly straightforward to push update notifications to the ui through the UI pointer, but new requirements for data to be entirely separated from UI and also for different objects to have multiple different UI representations, so a single UI pointer no longer does it nor is allowed to be part of the data layer whatsoever.

It is not possible to use something like QObject and signals due to its overhead because of the high object count (in the range of hundreds of millions) and QObject is several times larger than the biggest object in the hierarchy. For the UI part it doesn't matter that much, because only a portion of the objects are visible at a time.

I implemented a UI registry, which uses a multihash to store all UIs using the Object * as a key in order to be able to get the UI(s) for a given object and send notifications, but the lookup and the registration and deregistration of UIs presents a significant overhead given the high object count.

So I was wondering if there is some design pattern to send notifications between decoupled layers with less overhead?

A clarification: most changes are done on the UI side, the UI elements keep a pointer to the related object, so that's not an issue. But some changes made to some objects from the UI side results in changes which occur in related objects in the data layer which can't be predicted in order to request update of the affected object's UIs. In fact a single change on the UI made to one object can result in a cascade of changes to other objects, so I need to be able to notify their eventual UI representations to update to reflect those changes.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • @FabioCeconello - no, that's the idea, the `Object` should not be concerned with UI at all. The creation of the UIs happens on the UI side alone, by default only a UI for the root object is created, and from there the user can expand and collapse different branches. – dtech Mar 06 '15 at 00:41
  • It sounds like the actual problem is with the efficiency of UI registration/de-registration *performance*, not with the actual design. You could certainly speed things up by storing an array or a collection of UI elements that correspond to a model object (the *Composite Pattern* might be applicable) but it may not fully address the requirement that objects must not know about their UI. – Sergey Kalinichenko Mar 06 '15 at 01:28
  • @dasblinkenlight - that's my dilemma. Trying to figure a way to both have the cake and eat it too. Having an array of UIs for each object sounds like a straightforward solution, but it pollutes the object model and introduces even more memory overhead - an array has not only a pointer but also size and capacity. 12 extra bytes per object without any UI is 1+ gigabyte of memory wasted in a typical 100 mil object scenario on a 32bit platform, already 1/3 of addressable memory. I'd much rather store UI data only for objects that actually have it. – dtech Mar 06 '15 at 01:36
  • You can roll the UI association as you go. Assuming that UI structure is relatively static, you could set up associations (and the arrays) when an object becomes "visible" through the UI, and "recycle" the memory used for the association when the object "scrolls off" the screen. – Sergey Kalinichenko Mar 06 '15 at 01:39
  • @dasblinkenlight - that stuff is handled by Qt at least rendering wise. Point is even though the total object count is tremendous, typically there would be no more than at most a few thousand "visible" (having UI, not being on screen) objects at a time. Which is the reason I'd rather not have an array for each of the 100 million objects when I am going to have a few thousand with UI at a time. I established the extra CPU time is a good tradeoff for the savings in memory and the extra flexibility and cleanness of the design, I was merely wondering if I could do better than that. – dtech Mar 06 '15 at 01:43
  • Note that I've already employed memory pools to keep the overhead from registering and deregistering UIs low, as well as keeping the registry compact in memory and thus hopefully more "cache friendly", that already provided decent boost vs a stock multihash using dynamic allocation. – dtech Mar 06 '15 at 01:54
  • Well, there's no magic in software design, especially when it comes to designing straightforward object associations: it's either a lot of memory and little CPU, or a little memory and a lot of CPU, unless you can come up with some very cheap, CPU-wise, way of "rolling forward" the association of objects to UI elements when old objects go out of the view, and new objects become visible. – Sergey Kalinichenko Mar 06 '15 at 01:56
  • I guess I am just insecure of what I've been able to come up so far and wonder if there is a better way, I am self taught and have spent like 3 years casually programming, so understandably I am not overconfident in what I can come up with using only my limited knowledge and intuition. As for keeping only "on screen" object associations alive, I think it would be even more of an overhead in doing all this extra work plus rigging it to work with the particular UI library which may be problematic given its design limitations. Lookup itself is rather constant for the typical amount of keys. – dtech Mar 06 '15 at 02:03
  • A fine question, but you may wish to ask this on http://programmers.stackexchange.com instead; you may get better input there, and it is a bit too broad for SO (SO tends to focus on the concrete). – Jason C Mar 07 '15 at 13:43

2 Answers2

1

One generic mechanism for decoupled communication is the publish-subscribe pattern. In this situation, the updated objects would post a notification to a message queue, and then the message queue is responsible for informing the UI components who have registered with the queue an interest in accepting that particular class of notification.

This is similar, in principle, to the UI registry that you have already tried. The main difference is that UI components to update are identified not purely by their referenced Objects, but rather by the notification type.

This allows a trade off between specificity and state keeping: if the model is set up such that every UI component associated with an Object obj gets notified by every update of obj, then it's equivalent to the UI registry. On the other hand, the model could be arranged such that some UI components are notified whenever a certain sub-category of Object posts an update, and then each component can check for itself if it needs to modify its state based on the content of the notification. Carried to the extreme, every UI object could be notified by any message posted by any Object, which would be equivalent to a global 'update-UI-state' approach.

The publish-subscribe model encompasses both these extremes, but also the range in between, where you may be able to find a suitable compromise.

halfflat
  • 1,584
  • 8
  • 11
0

I managed to come up with a immensely more efficient solution.

Instead of tracking all UIs using a "UI registry" I created a Proxy object and replaced the UI registry with a Proxy registry.

The Proxy object is created for each object that has any visual representation. It itself extends QObject and implements an interface to access the properties of the underlying Object, wrapping them in Qt style properties.

Then the Proxy object is used as a property for each UI to read and write the underlying Object properties, so it works "automatically" for every UI that might be referencing the particular proxy.

Which means there is no need to track every particular UI for every Object, instead the lifetime of the Proxy is managed simply by counting the number of UIs which reference it.

I also managed to eliminate all the look-ups which would not yield a result by adding a single bit hasProxy flag (had a few free bits left from the other flags) which is toggled for every object when a proxy is created or destroyed. This way in the actual Object's members I can quickly check if the object has a proxy without a look-up in the registry, if not use the "blind" data routines, if so look-up the proxy and manipulate the object through it. This limits registry look-ups to only the few which will actually get a result and eliminates a tremendous amount of those which would be pretty much in vain, just to realize the object has no visual representation at all.

In short, to summarize the improvements over the previous design:

  • the registry is now much smaller, from having to store a pointer for the object itself and a vector of all associated UIs I am now down to 8 bytes for the Proxy - the pointer to the object and a counter for any number of associated UIs

  • notifications are automated, only the proxy needs to be notified, it automatically notifies all UIs which reference it

  • the functionality previously bestowed to the UIs is now moved to the proxy and shared between all UIs, so the UIs themselves are lighter and easier to implement, in fact I've gone from having to specialize a unique QQuickItem for each object type to being able to use a generic QML Item without having to implement and compile any native classes for the UI

  • stuff I previously had to manage manually, both the actual notifications and the objects, responsible for them are now managed automatically

  • the overhead in both memory usage and CPU cycles has been reduced tremendously. The previous solution sacrificed CPU time for less memory usage relative to the original design, but the new design eliminates most of the CPU overhead and decreases memory usage further, plus makes the implementation much easier and faster.

It's like having a cake and eating it too :)

dtech
  • 47,916
  • 17
  • 112
  • 190