
Hello list, I am quite new to Haskell and I love the language and community, but I am frustrated by a space leak in a WAI 3.0 Application that for now just echoes the request entity back in the response. Specifically, I am having trouble understanding *why* I have the space leak. I intend to pass some or all of the request entity on to another web service whose response will influence the HTTP status code and headers of my service's response. At the moment, I am preparing the request entity as a lazy bytestring using lazy I/O just like Data.ByteString.Lazy.hGetContents does, at least until I can get around to learning pipes or conduit. When I use this technique to echo the request entity back in the response, it looks like two copies of the entire request entity are being accumulated in memory, presumably the original from the request and a copy for the response. The heap profile says it is all in "PINNED", which I am assuming are the bytestring buffers. However, the efficacy of this technique turns out to be irrelevant as I have been able to distill the problem down to a much simpler example: a WAI Application that responds with 100 MB of zeros read from /dev/zero using Data.ByteString.Lazy.hGetContents. I have prepared two variations that differ only in the composition of the operations. Both applications create identical responses, but version A accumulates the entire 100 MB entity in memory (heap profile shows a huge "PINNED" cost just over 100 MB in size) whereas version B streams the entity in constant space. Source code and a cabal file follow. I am using GHC 7.8.3. It is not necessary to make heap profiles - the symptoms are evident with "+RTS -s". ==== BEGIN Zeros.hs ==== {-# LANGUAGE OverloadedStrings #-} import Blaze.ByteString.Builder import Control.Concurrent import qualified Data.ByteString.Lazy as LBS import Data.Int import Network.HTTP.Types import Network.Wai import Network.Wai.Handler.Warp import System.IO -- | This version binds the large LBS of zeros /outside/ of the -- 'responseStream' body lambda. -- -- This version has the space leak. -- -- @ -- curl -v -o \/dev\/null localhost:3000\/zeros\/a -- @ zerosAppA :: Application zerosAppA _req respond = withZeros 100000000 $ \ largeLBS -> respond $ responseStream status200 [] $ \ write _flush -> write $ fromLazyByteString largeLBS -- | This version binds the large LBS of zeros /inside/ of the -- 'responseStream' body lambda. -- -- This version streams the response entity in constant space. -- -- @ -- curl -v -o \/dev\/null localhost:3000\/zeros\/b -- @ zerosAppB :: Application zerosAppB _req respond = respond $ responseStream status200 [] $ \ write _flush -> withZeros 100000000 $ \ largeLBS -> write $ fromLazyByteString largeLBS -- | Do something with /n/ bytes read lazily from @\/dev\/zero@. -- -- This part is common to both 'zerosAppA' and 'zerosAppB'. withZeros :: Int64 -> (LBS.ByteString -> IO a) -> IO a withZeros n f = withBinaryFile "/dev/zero" ReadMode $ \ h -> do zeros <- LBS.hGetContents h let largeLBS = LBS.take n zeros f largeLBS main :: IO () main = do _ <- forkIO $ run 3000 app putStrLn "Using port 3000. Press ENTER to exit..." _ <- getLine putStrLn "Exit." app :: Application app req respond = case pathInfo req of ["zeros", "a"] -> zerosAppA req respond ["zeros", "b"] -> zerosAppB req respond _ -> respond $ responseLBS status404 [] "Not found." ==== END Zeros.hs ==== ==== BEGIN zeros.cabal ==== name: zeros version: 0.1.0.0 build-type: Simple cabal-version: >=1.10 executable zeros main-is: Zeros.hs build-depends: base >=4.7 && <4.8, blaze-builder ==0.3.3.4, bytestring ==0.10.4.0, http-types ==0.8.5, wai ==3.0.2, warp ==3.0.2.3 default-language: Haskell2010 ghc-options: -Wall -rtsopts ==== END zeros.cabal ==== Why does version A not process the LBS in constant space? What in version A is preventing the GC from collecting the LBS chunks after they have been fed to Warp? What is it about version B that permits the LBS chunks to be collected? Although I believe the issue is not actually specific to WAI or Warp, I am unable to reproduce the space leak without them. But because I am new to Haskell, I suspect I have missed something obvious about lambda bindings, laziness (or strictness) of IO, and GC. Thanks. -- Thomas Koster

On Tue, Nov 18, 2014 at 4:59 PM, Thomas Koster
Why does version A not process the LBS in constant space?
The lazy bytestring is let-lifted out of the function so that subsequent calls reuse the same heap value.
What in version A is preventing the GC from collecting the LBS chunks after they have been fed to Warp?
The value is re-used (and the closure holds a reference) so the GC can't collect it. What is it about version B that permits the LBS chunks to be collected?
The allocation is performed underneath the lambda in version B and so you
get a fresh copy every time.
G
--
Gregory Collins

On Tue, Nov 18, 2014 at 4:59 PM, Thomas Koster
-- | This version has a space leak. zerosAppA :: Application zerosAppA _req respond = withZeros 100000000 $ \ largeLBS -> respond $ responseStream status200 [] $ \ write _flush -> write $ fromLazyByteString largeLBS
-- | This version does not have a space leak. zerosAppB :: Application zerosAppB _req respond = respond $ responseStream status200 [] $ \ write _flush -> withZeros 100000000 $ \ largeLBS -> write $ fromLazyByteString largeLBS
On 20 November 2014 05:30, Gregory Collins
Why does version A not process the LBS in constant space?
The lazy bytestring is let-lifted out of the function so that subsequent calls reuse the same heap value.
What in version A is preventing the GC from collecting the LBS chunks after they have been fed to Warp?
The value is re-used (and the closure holds a reference) so the GC can't collect it.
What is it about version B that permits the LBS chunks to be collected?
The allocation is performed underneath the lambda in version B and so you get a fresh copy every time.
Thanks for your reply. My coding style makes heavy use of let to keep lines succinct so am I going to have to be more careful about the order of lets in case I accidentally introduce unwanted sharing? I was able to re-arrange the leaky version A into version B in my example, but I may not be able to do this in my real application. What if I need the first few bytes of "largeLBS" to determine the response status code or headers? This is only possible in version A. Or will replacing the lazy bytestring with a streaming abstraction like pipes or conduit make the problem go away in both versions? Thanks, -- Thomas Koster

On 2014-11-20 05:40, Thomas Koster wrote:
Thanks for your reply.
My coding style makes heavy use of let to keep lines succinct so am I going to have to be more careful about the order of lets in case I accidentally introduce unwanted sharing?
I was able to re-arrange the leaky version A into version B in my example, but I may not be able to do this in my real application. What if I need the first few bytes of "largeLBS" to determine the response status code or headers? This is only possible in version A.
I would recommend avoiding lazy I/O altogether and using "responseStream" instead. This will let you decide exactly what to write and when.
Or will replacing the lazy bytestring with a streaming abstraction like pipes or conduit make the problem go away in both versions?
Not magically, but both of those can certainly be used to avoid the problem. You'll still have to arrange it so that you don't close over a huge (lazy) byte string. If what you're trying to do is simple, then I'd just recommend writing your responses using "responseStream" and writing your "genereate output" function in terms of the "write" and "flush" callbacks you get access to write "responseStream". It's pretty easy when you get the hang of it. Regards,

On Tue, Nov 18, 2014 at 4:59 PM, Thomas Koster
-- | This version has a space leak. zerosAppA :: Application zerosAppA _req respond = withZeros 100000000 $ \ largeLBS -> respond $ responseStream status200 [] $ \ write _flush -> write $ fromLazyByteString largeLBS
-- | This version does not have a space leak. zerosAppB :: Application zerosAppB _req respond = respond $ responseStream status200 [] $ \ write _flush -> withZeros 100000000 $ \ largeLBS -> write $ fromLazyByteString largeLBS
On 20 November 2014 16:59, Bardur Arantsson
I would recommend avoiding lazy I/O altogether and using "responseStream" instead. This will let you decide exactly what to write and when.
I am already using responseStream. It's just that in my examples and my real application, the 'octets' to be used for the response entity ("largeLBS" from my examples) are provided to the callback as a lazy bytestring built from lazy I/O, until I get my head around pipes and conduit.
Or will replacing the lazy bytestring with a streaming abstraction like pipes or conduit make the problem go away in both versions?
Not magically, but both of those can certainly be used to avoid the problem. You'll still have to arrange it so that you don't close over a huge (lazy) byte string.
OK, but could I use pipes or conduit (no lazy I/O) to remove the space leak in version A without transforming it into version B (which doesn't have the space leak anyway)? That is, can I switch out the lazy bytestring in version A with a pipe or conduit so that I can start streaming the zeros *outside* of responseStream in order to determine the HTTP status code and headers (which is only possible in version A) and continue to stream them *inside* of responseStream for the rest of the response entity, without incurring a space leak? If the answer is "yes" then that's that and I will go and learn pipes and/or conduit right away.
If what you're trying to do is simple, then I'd just recommend writing your responses using "responseStream" and writing your "genereate output" function in terms of the "write" and "flush" callbacks you get access to write "responseStream". It's pretty easy when you get the hang of it.
...except that the HTTP status code and headers are arguments to responseStream and therefore I cannot determine them from within the callback passed as responseStream's third argument. This is why some of my 'long stream' needs to be accessed before invoking responseStream and why my real application looks like the leaky version A. Thanks, -- Thomas Koster

On 2014-11-20 08:29, Thomas Koster wrote:
On Tue, Nov 18, 2014 at 4:59 PM, Thomas Koster
wrote: -- | This version has a space leak. zerosAppA :: Application zerosAppA _req respond = withZeros 100000000 $ \ largeLBS -> respond $ responseStream status200 [] $ \ write _flush -> write $ fromLazyByteString largeLBS
-- | This version does not have a space leak. zerosAppB :: Application zerosAppB _req respond = respond $ responseStream status200 [] $ \ write _flush -> withZeros 100000000 $ \ largeLBS -> write $ fromLazyByteString largeLBS
On 20 November 2014 16:59, Bardur Arantsson
wrote: I would recommend avoiding lazy I/O altogether and using "responseStream" instead. This will let you decide exactly what to write and when.
I am already using responseStream. It's just that in my examples and my real application, the 'octets' to be used for the response entity ("largeLBS" from my examples) are provided to the callback as a lazy bytestring built from lazy I/O, until I get my head around pipes and conduit.
Lol, sorry, I guess I shouldn't be posting when I'm sick. :) I guess I got confused by the fact what you're using a huge LBS when you could just write it in pieces by providing the "write" and "flush" callbacks directly to the code which is actually generating the response (and let it be resposible for avoiding generating huge blobs).
built from lazy I/O,
I would suggest *never* *ever* using lazy I/O. Even though it looks like the easy route, it's much harder to reason about than any of the alternatives. Btw, there are a couple of alternatives which you haven't mentioned (and thus may not be familiar with:) - Strict/direct I/O: Just use write/flush directly, do your own looping "until done" instead of hGetContents, etc. (I realize I've already mentioned it, I just repeat it here for completeness.) - io-streams: A very thin layer of stream abstractions directly on top of I/O. I found this much easier to get a handle on than either conduit or pipes, but then it's also less powerful and gives you very few abstractions independent of IO. Works very well in practice, though, if you just want to "get the job done". See https://hackage.haskell.org/package/io-streams Anyway, sorry about the dud reply before -- hopefully this'll be more helpful! Regards,
participants (3)
-
Bardur Arantsson
-
Gregory Collins
-
Thomas Koster