fjvallarino / monomer

An easy to use, cross platform, GUI library for writing Haskell applications.
BSD 3-Clause "New" or "Revised" License
588 stars 44 forks source link

Add noDispose config option for image to prevent space leaks #316

Open Deltaspace0 opened 8 months ago

Deltaspace0 commented 8 months ago

I noticed that image widgets in some cases can cause huge space leaks when they are requested to be rendered every frame (e.g. during an animation). I made an example where image widgets switch places and immediately an animation starts which leads to big increase of RAM usage:

https://github.com/fjvallarino/monomer/assets/26405676/61850e2e-fd0c-4038-8635-3ea3b602bca7

I believe it happens here because when they switch places, during the subsequent merge each image disposes of the image data, sending the requests to remove the image from the renderer, and then immediately the other image widget resends the image data to the renderer causing some space leak. Although, I don't know why it uses SO much memory (2.4 GB after several animation iterations), when the images of these red buttons are about 100 KB (probably they get bigger when converted to ByteString?).

So, I added the config option to the image called "noDispose", so that image widget won't send dispose requests during the merge and it helped:

https://github.com/fjvallarino/monomer/assets/26405676/c02e1f44-2fdf-4009-92c6-bcb54c9a8a3f

The code of the dev-test-app illustrating the described behavior:

https://github.com/Deltaspace0/monomer/blob/memleak-example/dev-test-app/Main.hs

buildUI
  :: WidgetEnv AppModel AppEvent
  -> AppModel
  -> WidgetNode AppModel AppEvent
buildUI wenv model = widgetTree where
  widgetTree = vstack [
      hstack [
          button "Start anim1" AppStartFirst,
          button "Start anim2" AppStartSecond
        ],
      spacer,
      imageGrid
    ] `styleBasic` [padding 10]
  imageGrid = vgrid $ zipWith makeAnimationNode nodeKeys images
  makeAnimationNode key imageNode = animSlideIn imageNode `nodeKey` key
  nodeKeys = ["anim1", "anim2"]
  images = if model ^. imageOrder
    then [image1, image2]
    else [image2, image1]
  image1 = image_ "assets/images/red-button.png" [fitEither, noDispose]
  image2 = image_ "assets/images/red-button-hover.png" [fitEither, noDispose]

handleEvent
  :: WidgetEnv AppModel AppEvent
  -> WidgetNode AppModel AppEvent
  -> AppModel
  -> AppEvent
  -> [AppEventResponse AppModel AppEvent]
handleEvent wenv node model evt = case evt of
  AppInit -> []
  AppStartFirst -> [
      Model $ model & imageOrder %~ not,
      Message "anim1" AnimationStart
    ]
  AppStartSecond -> [
      Model $ model & imageOrder %~ not,
      Message "anim2" AnimationStart
    ]

If you remove the "noDispose" config, the app will consume a lot of memory during animations.