fjvallarino/monomer

Question regarding composite widgets and UI split by only using functions

Closed this issue · 2 comments

Within Main I have a UI that renders the HomeView, which is the default view that you see once you open the app and have an account. Its function is defined as follows:
homeUI :: AppWenv -> AppModel -> WidgetNode AppModel AppEvent
Within this homeUI I will render a bunch of posts, this again is not a composite, but a normal function, defined as follows:

viewPosts
  :: (WidgetModel sp, WidgetEvent ep)
  => (ReceivedEvent -> Bool)
  -> (ReceivedEvent -> ep)
  -> (XOnlyPubKey -> ep)
  -> WidgetEnv sp ep
  -> FutrModel
  -> WidgetNode sp ep

On initial startup, the app will load a few hundred events and this makes the UI completely frozen, as it tries to re-render all the time for a couple of seconds. (Note that all events are coming in within 1-2 secs only, but the UI remains frozen much longer.)
Also on the Main I have the following producer:

timerLoop :: (AppEvent -> IO ()) -> IO ()
timerLoop sendMsg = void $ forever $ do
  now <- getCurrentTime
  sendMsg $ TimerTick now
  threadDelay 250000

this updates the FutrModel 4 times a second with the current time.
Now when I change the UI and switch from homeUI wenv model to box_ [ mergeRequired (\_ old new -> old ^. futr . time /= new ^. futr . time) ] $ homeUI wenv model, the UI gets renderered smoothly, doesn't use up all CPU and the app runs much much better. Hoewever what I don't like about this approach, is that all messages are coming in slightly delayed (also I have to re-render when actually not needed).

Additionally I have a ViewProfile-widget, which is a real composite with it's own model, but also receives the FutrModel as argument. Its definition is the following:

viewProfileWidget
  :: (WidgetModel sp, WidgetEvent ep)
  => TChan Request
  -> FutrModel
  -> (ep)
  -> (ReceivedEvent -> ep)
  -> (XOnlyPubKey -> ep)
  -> (XOnlyPubKey -> ep)
  -> (XOnlyPubKey -> ep)
  -> ALens' sp ViewProfileModel
  -> WidgetNode sp ep

This also calls ViewPosts.viewPosts, but with a different filter function, so only events matching this specific profile are shown. Here no updates are happening at all, even if I put the mergeRequired around this widget in the main UI.

I also attached a short video demonstrating the issue I have. I spent already a couple of hours working on this and can't find a good solution. I hope you can help out somehow or have some advice.

pl-next-2022-06-25_14.06.13.mp4

Hi @prolic!

Something similar happens in the Ticker example, where multiple ticker updates can come within milliseconds between each other. Doing a model update for each of them means merging the complete widget tree every time, which can become expensive quickly.

The solution applied in that example is grouping messages in fixed chunks of time and is discussed here.

I agree that instant updates are nicer, but maybe you can find a middle ground (only updating 10-20 per second, perhaps?). At the same time, Monomer caps frames-per-second to 60 by default (you can change it with appMaxFps). If you receive hundreds of messages per second, they will not be rendered individually because of that cap (several updates will be processed in each frame).


The reason for the frames-per-second cap is that the library needs to check for tasks, producers, etc. I still have pending an item in my list of features to notify the main loop about new messages using an SDL event; this would allow for just waiting in the main loop without having to check for messages every few milliseconds. There are quite a few other things I want to do before this, but I will get to it eventually.

Handling incoming data in batches indeed solves my issues. Thanks for the help.