[go: up one dir, main page]

Skip to content
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

Images held in memory #444

Open
pyuk opened this issue Aug 3, 2024 · 2 comments
Open

Images held in memory #444

pyuk opened this issue Aug 3, 2024 · 2 comments

Comments

@pyuk
Copy link
pyuk commented Aug 3, 2024

I wrote a function to load an image into a Picture that looks like this:

loadImage :: Int32 -> Gtk.Picture -> Text -> IO ()
loadImage width pic imageUrl = void $ forkIO $ do
  bytes <- getUrlBytes imageUrl
  inputStream <- Gio.memoryInputStreamNewFromData bytes Nothing
  let onImageLoaded _ result = do
        Just pixbuf <- Pix.pixbufNewFromStreamFinish result
        texture <- Gdk.textureNewForPixbuf pixbuf
        pic.setPaintable $ Just texture
  Pix.pixbufNewFromStreamAtScaleAsync inputStream width (-1) True
    (Nothing::Maybe Gio.Cancellable) $ Just onImageLoaded

I run this function for a series of images and then load the Pictures into a Flowbox. When i refresh the view, I use the function flowboxRemoveAll to clear it out, and then fill it with new images. It works well, except for the fact that it seems to hold onto the images in memory. So the more I refresh the view, the more memory it uses. What can I do to make sure the images get dropped when I refresh the view?

@pyuk
Copy link
Author
pyuk commented Aug 4, 2024

Here's an example program of what I mean:

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLabels #-}

module Main where

import Data.GI.Base
import qualified GI.Gtk as Gtk
import qualified GI.Gio as Gio
import qualified GI.GdkPixbuf as Pix
import qualified GI.Gdk as Gdk
import qualified GI.Adw as Adw
import Control.Monad
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BL
import Data.Text (Text)
import qualified Data.Text as T
import Network.HTTP.Client
import Network.HTTP.Client.TLS
import Data.Int (Int32)
import Control.Concurrent

main :: IO ()
main = do
  app <- new Gtk.Application [#applicationId := "com.image-memory-test"]
  on app #activate $ activate app
  void $ app.run Nothing

activate :: Gtk.Application -> IO ()
activate app = do
  headerBar <- new Gtk.HeaderBar []
  refresh <- new Gtk.Button [#iconName := "view-refresh-symbolic"]
  headerBar.packStart refresh
  flowbox <- new Gtk.FlowBox []
  scroll <- new Gtk.ScrolledWindow [#child := flowbox]

  on scroll #edgeReached $ \positionType -> do
    when (positionType == Gtk.PositionTypeBottom) $ fillFlowbox flowbox
  on refresh #clicked $ flowbox.removeAll >> fillFlowbox flowbox >>
    scroll.setVadjustment (Nothing::Maybe Gtk.Adjustment)
  
  window <- new Gtk.Window [ #application := app, #defaultWidth := 850
                           , #defaultHeight := 500, #child := scroll
                           , #titlebar := headerBar
                           , #title := "Image Memory Test" ]
  window.present
  fillFlowbox flowbox

fillFlowbox :: Gtk.FlowBox -> IO ()
fillFlowbox flowbox = do
  let imageUrl = "https://s4.anilist.co/file/anilistcdn/\
                 \media/anime/cover/large/bx21-YCDoj1EkAxFn.jpg"
  forM_ [(1::Int)..21] $ \_ -> do
    picture <- new Gtk.Picture [#contentFit := Gtk.ContentFitCover]
    clamp <- new Adw.Clamp [ #child := picture, #maximumSize := 100
                           , #widthRequest := 100, #heightRequest := 150 ]
    loadImage 460 picture imageUrl
    flowbox.append clamp

getUrlBytes :: Text -> IO ByteString
getUrlBytes url = do
  manager <- getGlobalManager
  request <- parseRequest $ T.unpack url
  BL.toStrict . responseBody <$> httpLbs request manager

loadImage :: Int32 -> Gtk.Picture -> Text -> IO ()
loadImage width pic imageUrl = void $ forkIO $ do
  bytes <- getUrlBytes imageUrl
  inputStream <- Gio.memoryInputStreamNewFromData bytes Nothing
  let onImageLoaded _ result = do
        Just pixbuf <- Pix.pixbufNewFromStreamFinish result
        texture <- Gdk.textureNewForPixbuf pixbuf
        pic.setPaintable $ Just texture
  Pix.pixbufNewFromStreamAtScaleAsync inputStream width (-1) True
    (Nothing::Maybe Gio.Cancellable) $ Just onImageLoaded

In this program, scrolling to the bottom of the page loads in more images, and more memory is used which is fine, but after hitting refresh and emptying the flowbox, memory usage never goes back down.

@pyuk pyuk closed this as completed Aug 4, 2024
@pyuk pyuk reopened this Aug 4, 2024
@garetxe
Copy link
Collaborator
garetxe commented Oct 26, 2024

Thanks for the report. This one is a little tricky. I am not sure there's a leak, or at least I cannot see it. I do see the memory increase, but I think it might be a combination of two factors:

  • Haskell's garbage collector can take a little while to kick in: from the point of view of the garbage collector the only memory that needs to be freed is the pointer to each image, so the amount of garbage is never too large.

  • Many malloc implementations don't release memory back to the operating system.

For example when I run the code below (note the manual System.Mem.performMajorGC call) I see no unbounded memory growth:

{- cabal:
build-depends: base, haskell-gi-base, gi-gobject, gi-gtk == 4.0.*, gi-gio, gi-gdkpixbuf, gi-gdk, gi-adwaita, text, bytestring, HTTP, http-client, http-client-tls
-}

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLabels #-}

module Main where

import Data.GI.Base
import qualified GI.Gtk as Gtk
import qualified GI.Gio as Gio
import qualified GI.GdkPixbuf as Pix
import qualified GI.Gdk as Gdk
import qualified GI.Adw as Adw
import Control.Monad
import qualified Data.ByteString as BS
import Data.IORef
import Data.Int (Int32)
import Control.Concurrent
import qualified System.Mem
import Foreign.C.Types (CSize(..), CInt(..))

foreign import ccall malloc_trim :: CSize -> IO CInt

main :: IO ()
main = do
  app <- new Gtk.Application [#applicationId := "com.image-memory-test"]
  _ <- on app #activate $ activate app
  void $ app.run Nothing

activate :: Gtk.Application -> IO ()
activate app = do
  headerBar <- new Gtk.HeaderBar []
  refresh <- new Gtk.Button [#iconName := "view-refresh-symbolic"]
  headerBar.packStart refresh
  flowbox <- new Gtk.FlowBox []
  scroll <- new Gtk.ScrolledWindow [#child := flowbox]

  ref <- newIORef flowbox

  _ <- on scroll #edgeReached $ \positionType ->
    when (positionType == Gtk.PositionTypeBottom) $ do
    refFlowbox <- readIORef ref
    fillFlowbox refFlowbox

  _ <- on refresh #clicked $ do
    newFlowbox <- new Gtk.FlowBox []
    writeIORef ref newFlowbox
    fillFlowbox newFlowbox
    scroll.setVadjustment (Nothing::Maybe Gtk.Adjustment)
    scroll `set` [#child := newFlowbox]
    _ <- malloc_trim 0
    return ()

  window <- new Gtk.Window [ #application := app, #defaultWidth := 850
                           , #defaultHeight := 500, #child := scroll
                           , #titlebar := headerBar
                           , #title := "Image Memory Test" ]
  window.present
  fillFlowbox flowbox

fillFlowbox :: Gtk.FlowBox -> IO ()
fillFlowbox flowbox = do
  forM_ [(1::Int)..21] $ \_ -> do
    picture <- new Gtk.Picture [#contentFit := Gtk.ContentFitCover]
    clamp <- new Adw.Clamp [ #child := picture, #maximumSize := 100
                           , #widthRequest := 100, #heightRequest := 150 ]
    loadImage 460 picture
    flowbox.append clamp

  System.Mem.performMajorGC
  return ()

loadImage :: Int32 -> Gtk.Picture -> IO ()
loadImage width pic  = void $ forkIO $ do
  bytes <- BS.readFile "img.jpg"
  inputStream <- Gio.memoryInputStreamNewFromData bytes Nothing
  let onImageLoaded _ result = do
        Just pixbuf <- Pix.pixbufNewFromStreamFinish result
        texture <- Gdk.textureNewForPixbuf pixbuf
        pic.setPaintable $ Just texture
  Pix.pixbufNewFromStreamAtScaleAsync inputStream width (-1) True
    (Nothing::Maybe Gio.Cancellable) $ Just onImageLoaded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants