如何防止 scotty 为大文本输出耗尽内存?

How do I prevent scotty from eating up memory for large text output?

我有一个 Scotty/WAI 应用程序,其中一个端点发送一个大的 Text 输出,该输出是根据元素列表构建的。这是相关代码:

  import Data.Text.Lazy as L
  import Data.Text.Lazy.Encoding as E

  class (Show csv) => ToCSV csv where
    toCSV :: csv -> L.Text
    toCSV = pack . show

  instance (ToCSV c) => ToCSV [c] where
    toCSV []     = empty
    toCSV (c:cs) = toCSV c <> "\n" <> toCSV cs


  get "/api/transactions" $ accept "text/csv" $ do
    purp <- selectPurpose
    txs <- allEntries <$> inWeb (listTransactions purp)
    setHeader "Content-Type" "text/csv"
    raw $ E.encodeUtf8 $ toCSV txs

据我所知,Scotty's documentation 输出应该延迟构建并通过网络发送,而不需要在内存中构建整个 text/bytestring。然而,这不是我观察到的行为:当我调用此端点时,服务器开始耗尽内存,我推断它正在构建整个字符串,然后一次性发送。

我是不是漏掉了什么?

编辑 1:

我写了一个 doStream 函数,它应该一个一个地发送结果 BS 块:

doStream :: Text -> W.StreamingBody   
doStream t build flush = do
  let bs = E.encodeUtf8 t
  mapM_ (\ chunk -> build (B.fromByteString chunk)) (BS.toChunks bs)
  flush

但实际上它仍然在内存中构建整个输出...

编辑 2:

实际上,以这种方式流式传输效果很好。服务器进程仍然会占用大量内存,在发送每个块时实际上可能是垃圾回收器。我将尝试更深入地分析内存使用情况,以了解这种消耗来自何处。

编辑 3:

我试图将堆限制为 2GB,但这会使进程崩溃。整个转换过程中保留了一些记忆...

看看 Web.Scotty.Trans 中的 "stream" 函数。它的目的是对在刷新到套接字之前生成的数据的大小进行更细粒度的控制。

你用一个 StreamingBody 参数调用它,它实际上是一个类型为 (Builder -> IO ()) -> IO () -> IO () 的函数。

所以你写了一个函数:

doMyStreaming send flush =
...

其中您分段发送和刷新数据,然后使用 doMyStreaming 作为参数调用流函数而不是调用 "raw"。