проводник: создание утечки памяти

Работа над некоторыми наблюдениями по предыдущему вопросу (haskell-data-hashset-from-unordered-container-performance-for-large-sets) Наткнулся на странную утечку памяти

module Main where

import System.Environment (getArgs)
import Control.Monad.Trans.Resource (runResourceT)
import Data.Attoparsec.ByteString (sepBy, Parser)
import Data.Attoparsec.ByteString.Char8 (decimal, char)
import Data.Conduit
import qualified Data.Conduit.Attoparsec as CA
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL

main :: IO ()
main = do (args:_) <- getArgs
          writeFile "input.txt" $ unlines $ map show [1..4 :: Int]
          case args of "list" -> m1
                       "fail" -> m2
                       "listlist" -> m3
                       "memoryleak" -> m4
                       --UPDATE
                       "bs-lines":_ -> m5
                       "bs":_ -> m6
                       _ -> putStr $ unlines ["Usage: conduit list"
                                             ,"               fail"
                                             ,"               listlist"
                                             ,"               memoryleak"
                                             --UPDATE
                                             ,"               bs-lines"
                                             ,"               bs"
                                             ]
m1,m2,m3,m4 :: IO ()
m1 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CB.lines
           =$= CA.conduitParser (decimal :: Parser Int)
           =$= CL.map snd
           =$= CL.consume
        print hs
m2 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CA.conduitParser (decimal :: Parser Int)
           =$= CL.map snd
           =$= CL.consume
        print hs
m3 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CB.lines
           =$= CA.conduitParser (decimal `sepBy` (char '\n') :: Parser [Int])
           =$= CL.map snd
           =$= CL.consume
        print hs
m4 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CA.conduitParser (decimal `sepBy` (char '\n') :: Parser [Int])
           =$= CL.map snd
           =$= CL.consume
        print hs
-- UPDATE
m5 = do inpt <- BS.lines <$> BS.readFile "input.txt"
        let Right hs =  mapM (parseOnly (decimal :: Parser Int)) inpt
        print hs
m6 = do inpt <- BS.readFile "input.txt"
        let Right hs =  (parseOnly (decimal `sepBy` (char '\n') :: Parser [Int])) inpt
        print hs

Вот пример вывода:

$ > stack exec -- example list
[1234]
$ > stack exec -- example listlist
[[1234]]
$ > stack exec -- conduit fail
conduit: ParseError {errorContexts = [], errorMessage = "Failed reading: takeWhile1", errorPosition = 1:2}
$ > stack exec -- example memoryleak
(Ctrl+C)

-- UPDATE
$ > stack exec -- example bs-lines
[1,2,3,4]
$ > stack exec -- example bs
[1,2,3,4]

Теперь вопросы у меня такие:

  • Почему m1 не производит [1,2,3,4]?
  • Почему m2 не работает?
  • Почему m4 ведет себя совершенно иначе по сравнению со всеми другими версиями и вызывает утечку пространства?

person epsilonhalbe    schedule 28.03.2016    source источник
comment
Вы проверяли свой код, используя только ленивый ввод-вывод и сам attoparsec? Я не удивлюсь, если откат в attoparsec удерживает данные дольше, чем ожидалось.   -  person Michael Snoyman    schedule 28.03.2016
comment
@MichaelSnoyman Я обновил свой вопрос версией bytestring/attoparsec, но я не уверен, эквивалентна ли она версии канала (поскольку я не слишком знаком с каналом). По крайней мере, он производит ожидаемый результат.   -  person epsilonhalbe    schedule 28.03.2016


Ответы (2)


Почему m2 не работает?

Входной файл как поток символов:

1\n2\n3\n4\n

Поскольку синтаксический анализатор decimal не ожидает символа новой строки, после использования первого числа оставшийся поток будет таким:

\n2\n3\n4\n

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

Почему m4 ведет себя совершенно иначе по сравнению со всеми другими версиями и создает утечку пространства?

decimal `sepBy` (char '\n') будет потреблять только \n между двумя целыми числами, после успешного анализа четырех чисел входной поток содержит только один символ:

\n

и decimal `sepBy` (char '\n') не может его потреблять, даже хуже, он не выйдет из строя: sepBy не может ничего потреблять и возвращать пустой список. Поэтому он ничего не анализирует бесконечно и никогда не завершается.

Почему m1 не производит [1,2,3,4]?

Я тоже хочу это знать! Я предполагаю, что это как-то связано с фузингом, возможно, вам следует связаться с автором пакета conduit, который только что прокомментировал ваш вопрос.

person zakyggaps    schedule 28.03.2016
comment
Поскольку объяснение слишком длинное для комментария, я добавил его как еще один ответ. - person Michael Snoyman; 29.03.2016

Чтобы ответить на вопрос о m1: когда вы используете CB.lines, вы включаете ввод, который выглядит так:

["1\n2\n3\n4\n"]

в:

["1", "2", "3", "4"]

Затем attoparsec анализирует «1», ожидает ввода, видит «2» и так далее.

person Michael Snoyman    schedule 29.03.2016
comment
тогда неправильно использовать conduitParser, если я предпочитаю использовать vanilla parseOnly и CL.map, если я правильно понимаю, conduitParser анализирует поток постепенно и не является правильным выбором для анализа [1,2,3,4]. - person epsilonhalbe; 30.03.2016
comment
Это не то, что я говорю. Объединение CB.lines и conduitParser проблематично, потому что первое удаляет символы новой строки, необходимые второму для правильного разбора. - person Michael Snoyman; 30.03.2016