Attoparsec выделяет тонну памяти при большом вызове 'take' - PullRequest
19 голосов
/ 11 ноября 2010

Итак, я пишу приложение для анализа пакетов. По сути, я хотел, чтобы он прослушивал tcp-сессии, а затем анализировал их, чтобы узнать, являются ли они http, и если они имеют правильный тип содержимого и т. Д., Сохраните их в виде файла на моем жестком диске.

Итак, с этой целью я хотел, чтобы это было эффективно. Поскольку текущая http-библиотека основана на строках, и я буду иметь дело с большими файлами, а мне действительно нужно было только проанализировать http-ответы, я решил свернуть свои собственные в attoparsec.

Когда я закончил свою программу, я обнаружил, что когда я анализировал http-ответ 9 мегабайт с файлом wav в нем, когда я его профилировал, он выделял гигабайт памяти, когда пытался проанализировать тело http ответ. Когда я смотрю на HTTP.prof, я вижу несколько строк:

httpBody              Main                                                 362           1   0.0    0.0    93.8   99.3

 take                 Data.Attoparsec.Internal                             366        1201   0.0    0.0    93.8   99.3
     takeWith            Data.Attoparsec.Internal                             367        3603   0.0    0.0    93.8   99.3
      demandInput        Data.Attoparsec.Internal                             375         293   0.0    0.0    93.8   99.2
       prompt            Data.Attoparsec.Internal                             378         293   0.0    0.0    93.8   99.2
        +++              Data.Attoparsec.Internal                             380         586  93.8   99.2    93.8   99.2

Так что, как вы можете видеть, где-то внутри httpbody, take вызывается 1201 раз, вызывая 500+ (+++) конкатенаций байтовых строк, что вызывает абсурдное выделение памяти.

Вот код. N - это длина содержимого HTTP-ответа, если он есть. Если его нет, он просто пытается взять все.

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


httpBody n = do
  x <- if n > 0
    then AC.take n
    else AC.takeWhile (\_ -> True)
  if B.length x == 0
    then return Nothing
    else return (Just x)

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

Редактировать: Хорошо, я оставил это весь день и ничего не получил. После изучения проблемы я не думаю, что есть способ сделать это без добавления ленивого метода доступа к быстрым цепям в attoparsec. Я также просмотрел все остальные библиотеки, и в них отсутствовали строки или другие вещи.

Итак, я нашел обходной путь. Если вы думаете о HTTP-запросе, то это заголовки, новая строка, новая строка, тело. Так как тело является последним, и синтаксический анализ возвращает кортеж как с тем, что вы проанализировали, так и с тем, что осталось от тестовой строки, я могу пропустить синтаксический анализ тела внутри attoparsec и вместо этого извлечь тело прямо из оставшейся тестовой строки.


parseHTTPs bs = if P.length results == 0
  then Nothing
  else Just results
  where results = foldParse(bs, [])

foldParse (bs,rs) = case ACL.parse httpResponse bs of
  ACL.Done rest r -> addBody (rest,rs) r
  otherwise ->  rs

addBody (rest,rs) http = foldParse (rest', rs')
  where
    contentlength = ((read . BU.toString) (maybe "0" id (hdrContentLength (rspHeaders http))))
    rest' = BL.drop contentlength rest
    rs' = rs ++ [http { rspBody = body' }]
    body'
      | contentlength == 0  = Just rest
      | BL.length rest == 0 = Nothing
      | otherwise           = Just (BL.take contentlength rest)
httpResponse = do
  (code, desc) <- statusLine
  hdrs <- many header
  endOfLine
--  body <- httpBody ((read . BU.toString) (maybe "0" id (hdrContentLength parsedHeaders)))

  return Response { rspCode = code, rspReason = desc, rspHeaders = parseHeaders hdrs,  rspBody = undefined }

Это немного грязно, но в конечном итоге работает быстро и не выделяет ничего больше, чем я хотел. Таким образом, в основном вы сворачиваете всю строку, собирающую структуры данных http, затем между коллекциями я проверяю длину содержимого только что полученной структуры, вытягиваю соответствующее количество из оставшейся строки байтов и затем продолжаю, если остается какая-либо строка байтов.

Редактировать: Я фактически закончил этот проект. Работает как шарм. У меня нет кабализации, но если кто-то захочет просмотреть весь источник, вы можете найти его в https://github.com/onmach/Audio-Sniffer.

1 Ответ

5 голосов
/ 06 декабря 2010

комбинаторный парень тут:)

Если память служит, проблема с attoparsec заключается в том, что требуется ввод немного по одному, создавая ленивую строку байтов, которая в итоге объединяется. Моим «решением» было прокрутить функцию ввода самостоятельно. То есть я получаю входной поток для attoparsec из сетевого сокета и знаю, сколько байтов ожидать в сообщении. По сути, я разделил на два случая:

  • Сообщение маленькое: прочитайте до 4k из сокета и съешьте эту Bytestring чуть-чуть (срезы быстрых строк быстро, и мы выбрасываем 4k после того, как они были исчерпаны).

  • Сообщение «большое» (большое здесь означает около 16 килобайт в битовом потоке): мы рассчитываем, сколько может выдержать 4-килобайтный кусок, и затем просто запрашиваем базовый сетевой сокет для заполнения. Теперь у нас есть две строки: оставшаяся часть блока 4k и большая часть. У них есть все данные, поэтому мы их объединяем и анализируем.

    Возможно, вам удастся оптимизировать этап объединения.

Версия TL; DR: я работаю с ней вне attoparsec и выполняю цикл, чтобы избежать проблемы.

Соответствующим комбинаторным коммитом является fc131fe24, см.

https://github.com/jlouis/combinatorrent/commit/fc131fe24207909dd980c674aae6aaba27b966d4

для деталей.

...