Подтвердить что ты не робот

Attoparsec выделяет тонну памяти при большом вызове "take"

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

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

Когда я закончил свою программу, я обнаружил, что когда я разбирал 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+ (+++) конкатенации bytestrings, что вызывает абсурдный объем выделения памяти.

Вот код. 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)

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

Изменить: Хорошо, я оставил это весь день и ничего не получил. После изучения проблемы я не думаю, что есть способ сделать это, не добавляя ленивый totestring accessor к 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.

4b9b3361

Ответ 1

combinatorrent guy здесь:)

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

  • Сообщение невелик: прочитайте до 4k из сокета и купите, что Bytestring немного за раз (кусочки bytestrings бывают быстрыми, и мы выбрасываем 4k после его исчерпания).

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

    Вы можете оптимизировать шаг конкатенации.

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

Соответствующий combinatorrent commit - fc131fe24, см.

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

для деталей.