Heap profiling in Haskell. Am I having space leaks?

Viewed 113

I am writing a small snake game in Haskell as sort of a guided tutorial for beginners. The "rendering" just takes a Board and produces a Data.ByteString.Builder which is printed in the terminal. (the html profiles are pushed to the repo, you can inspect them without compiling the programm)

The problem

The problem I have is that the heap profiling looks weird: There are many spikes, and suddenly Builder, PAP and BuildStep take as same memory as the rest of the program. Considering that rendering is happenning 10 times in a second (i.e. every second we produce 10 builders), it seems inconsistent that every once in a while the builder just takes that much memory. I don't know if this is considered an space leak, since there is no thunks in the profile, but the PAP doesn't look right (I don't know...)

enter image description here

Implementation

The board is represented as an inmutable array of builders indexed by coordinaates (tuples) type Board = Array (Int, Int) Builder (essentialy, what should be printed in each coordinate). The function which converts the board into a builder is the expected strict fold which handle new lines using height and width of the board.

toBuilder :: RenderState -> Builder
--                     |- The Array (Int, Int) Builder
toBuilder (RenderState b binf@(BoardInfo h w) gOver s) =
--                                       ^^^ height and width
  if gOver
    then ppScore s <> fst (boardToString $ emptyGrid binf) -- Not interesting. Case of game over print build an empty grid
    else ppScore s <> fst (boardToString b) -- print the current board
  where
    boardToString =  foldl' fprint (mempty, 0) -- concatenate builders and count the number, such that when #width builders have been concatenated, add a new line.
    fprint (!s, !i) cell =
      if ((i + 1) `mod` w) == 0
        then (s <> cell <> B.charUtf8 '\n', i + 1 )
        else (s <> cell , i + 1)

Up to the .prof file this function take most of the time and space (92%, which is expected). Moreover, this is the only part of the code that produces a big builder, so the problem should be here.

The buffering mode

The above profile happens when BufferMode is set to LineBuffering (default), but interestingly if I change it to NoBuffering then the profile looks the same but a thunk appears and the builder disappear...

enter image description here

The questions

I have reached a point which I don't know whats going on, hence my questions are a little bit vague:

  • Is my code with line buffering (first profile) actually leaking? No thunk appears but the PAP eating so much memory looks like a warning
  • The second profile clearly(?) leaks, is there an standard way to inspect which part of the code is producing the thunk?
  • Am I completely missing something, and actually the profile looks fine?
1 Answers

In case anyone is interested, I think I've found the problem. It is the terminal speed... If I run an smaller board size or a slower rendering time (the picture is for a 50x70 board with 10 renders a second), then the memory usage is completely normal.

What I think is happening, is that the board is printed into the console using B.hPutBuilder stdout, this action takes shorter than the console to actually print it, so the haskell thread continues and creates another board which should wait to be printed because the console is busy. I guess this leads to some how, two boards living in memory for a short time.

Other guesses are welcome!

Related