Почему это вызывает утечку памяти в библиотеке Haskell Conduit?

У меня есть conduit конвейер, обрабатывающий длинный файл. Я хочу распечатать отчет о проделанной работе для каждого 1000 записей, поэтому я написал следующее:

-- | Every n records, perform the IO action.
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = skipN n 1
   where
      skipN c t = do
         mv <- await
         case mv of
            Nothing -> return ()
            Just v ->
               if c <= 1
                  then do
                     liftIO $ act t v
                     yield v
                     skipN n (succ t)
                  else do
                     yield v
                     skipN (pred c) (succ t)

Независимо от того, с каким действием я это называю, он утечки памяти, даже если я просто скажу, чтобы он распечатал полную остановку.

Насколько я вижу, функция является хвостовой рекурсивной, и оба счетчика регулярно принудительно (я попытался поставить "seq c" и "seq t", безрезультатно). Любая подсказка?

Если я добавлю "awaitForever", который печатает отчет для каждой записи, тогда он отлично работает.

Обновление 1: Это происходит только при компиляции с -O2. Профилирование указывает, что утечка памяти выделяется в рекурсивной функции "skipN" и сохраняется "SYSTEM" (что бы это ни значило).

Обновление 2: мне удалось вылечить его, по крайней мере, в контексте моей текущей программы. Я заменил эту функцию выше. Обратите внимание, что "proc" имеет тип "Int → Int → Maybe я → m()": для его использования вы вызываете "ожидание" и передаете ему результат. По какой-то причине переключение на "ожидание" и "выход" решило проблему. Итак, теперь он ждет следующего ввода, прежде чем уступить предыдущему результату.

-- | Every n records, perform the monadic action. 
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = await >>= proc 1 n
   where
      proc c t = seq c $ seq t $ maybe (return ()) $ \v ->
         if c <= 1
            then {-# SCC "progress.then" #-} do
               liftIO $ act t v
               v1 <- await
               yield v
               proc n (succ t) v1
            else {-# SCC "progress.else" #-} do
               v1 <- await
               yield v
               proc (pred c) (succ t) v1

Итак, если у вас есть утечка памяти в кабелепроводе, попробуйте заменить выход и ждать действий.

Ответ 1

Это не ответ, но это полный код, который я взломал для тестирования. Я вообще не знаю канал, так что, возможно, это не лучший код канала. Я вынудил все, что кажется, должно быть принуждено, но это все еще протекает.

{-# LANGUAGE BangPatterns #-}

import Data.Conduit
import Data.Conduit.List
import Control.Monad.IO.Class

-- | Every n records, perform the IO action.
--   Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = skipN n 1
   where
      skipN !c !t = do
         mv <- await
         case mv of
            Nothing -> return ()
            Just !v ->
               if (c :: Int) <= 1
                  then do
                     liftIO $ act t v
                     yield v
                     skipN n (succ t)
                  else do
                     yield v
                     skipN (pred c) (succ t)

main :: IO ()
main = unfold (\b -> b 'seq' Just (b, b+1)) 1
       $= progress 100000 (\_ b -> print b)
       $$ fold (\_ _ -> ()) ()

С другой стороны,

main = unfold (\b -> b 'seq' Just (b, b+1)) 1 $$ fold (\_ _ -> ()) ()

не протекает, поэтому что-то в progress действительно кажется проблемой. Я не вижу что.

ОБНОВЛЕНИЕ: утечка происходит только с ghci! Если я компилирую бинарный файл и запускаю его, утечки нет (я должен был проверить это раньше...)

Ответ 2

Я думаю, что ответ тома правильный, я начинаю его как отдельный ответ, так как он, вероятно, введет какое-то новое обсуждение (и потому что это слишком долго для простого комментария). В моем тестировании замена print b в примере Tom на return () избавляет от утечки памяти. Это заставило меня думать, что проблема на самом деле с print, а не с conduit. Чтобы проверить эту теорию, я написал простую вспомогательную функцию в C (помещенную в helper.c):

#include <stdio.h>

void helper(int c)
{
    printf("%d\n", c);
}

Затем я импортировал эту функцию в код на Haskell:

foreign import ccall "helper" helper :: Int -> IO ()

и я заменил звонок на print звонком на helper. Вывод из программы идентичен, но я показываю отсутствие утечек и максимальную резидентность 32 КБ против 62 КБ (я также изменил код, чтобы он останавливался на 10 м записи для лучшего сравнения).

Я вижу подобное поведение, когда полностью отключаю канал, например:

main :: IO ()
main = forM_ [1..10000000] $ \i ->
    when (i 'mod' 100000 == 0) (helper i)

Однако я не уверен, что это действительно ошибка в print или Handle. Мое тестирование никогда не показывало утечку, достигающую сколько-нибудь существенного использования памяти, поэтому могло случиться так, что буфер увеличивается до предела. Мне нужно было провести дополнительные исследования, чтобы лучше понять это, но я сначала хотел посмотреть, соответствует ли этот анализ тому, что видят другие.

Ответ 3

Я знаю это два года спустя, но я подозреваю, что происходит то, что полная лень поднимает часть тела, ожидаемого до самого ожидания, и это вызывает утечку пространства. Он похож на случай в разделе "Увеличение общего доступа" в m y блоге на эту тему.