parsing haskell attoparsec

parsing - Attoparsec asigna una tonelada de memoria en llamadas de "toma" grandes



haskell (1)

chico combinatorrent aquí :)

Si la memoria sirve, el problema con attoparsec es que exige ingresar un poco a la vez, construyendo una cadena de bytes lenta que finalmente se concatena. Mi "solución" fue rodar la función de entrada yo mismo. Es decir, obtengo el flujo de entrada para attoparsec desde un socket de red y sé cuántos bytes esperar en un mensaje. Básicamente, me dividí en dos casos:

  • El mensaje es pequeño: lee hasta 4k desde el socket y cómete ese Bytestring un poco a la vez (las porciones de cadenas de bytes son rápidas y tiramos los 4k después de que se hayan agotado).

  • El mensaje es "grande" (grande aquí significa alrededor de 16 kilobytes en bittorrent): calculamos cuánto puede satisfacer el trozo 4k que tenemos, y luego simplemente solicitamos el socket de red subyacente para completar las cosas. Ahora tenemos dos cadenas de bytes, la parte restante del trozo 4k y el trozo grande. Tienen todos los datos, por lo que concatenarlos y analizarlos es lo que hacemos.

    Puede optimizar el paso de concatenación.

La versión TL; DR: la manejo fuera de attoparsec y manipulo el ciclo para evitar el problema.

La confirmación combinatorrent relevante es fc131fe24, ver

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

para los detalles.

Así que estoy escribiendo una aplicación para olfatear paquetes. Básicamente, quería que husmeara las sesiones de tcp, y luego analizarlas para ver si son http, y si lo son, y si tienen el tipo de contenido correcto, etc., guárdelas como un archivo en mi disco duro.

Entonces, para ese fin, quería que fuera eficiente. Dado que la biblioteca http actual está basada en cadenas, y trataré con archivos grandes, y realmente solo necesitaba analizar las respuestas http, decidí lanzar la mía propia en attoparsec.

Cuando terminé mi programa, encontré que cuando estaba analizando una respuesta http de 9 megas con un archivo wav, cuando lo perfilaba, estaba asignando un registro de memoria cuando estaba tratando de analizar el cuerpo de la respuesta http. . Cuando miro HTTP.prof veo algunas líneas:

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

Como puede ver, en algún lugar dentro de httpbody, tomar se llama 1201 veces, causando más de 500 (+++) concatenaciones de cadenas de bytes, lo que causa una cantidad absurda de asignación de memoria.

Aquí está el código. N es solo la longitud del contenido de la respuesta http, si hay una. Si no hay uno, simplemente trata de tomar todo.

Quería que devolviera una cadena de bytes floja de 1000 o más cadenas de caracteres, pero incluso si lo cambio para simplemente tomar n y devolver una cadena de bytes estricta, todavía tiene esas asignaciones (y usa 14 gigas de memoria).

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)

Estaba leyendo un blog por el tipo que combinatorrent y él estaba teniendo el mismo problema, pero nunca escuché de una resolución. ¿Alguna vez alguien ha encontrado este problema o ha encontrado una solución?

Edit: Bien, bueno, dejé esto todo el día y no obtuve nada. Después de investigar el problema, no creo que haya una manera de hacerlo sin agregar un acceso desactualizado de bytesteado a attoparsec. También miré todas las otras bibliotecas y carecían de cadenas de bytes u otras cosas.

Entonces encontré una solución. Si piensa en una solicitud http, va a los encabezados, nueva línea, nueva línea, cuerpo. Como el cuerpo es el último, y el análisis regresa una tupla con lo que analizó y lo que queda de la cadena de bytes, puedo omitir el análisis del cuerpo dentro de attoparsec y en su lugar extraer el cuerpo directamente de la cadena de bytes que queda.

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 }

Es un poco desordenado, pero al final funciona rápido y no asigna nada más de lo que quería. Así que básicamente doblas sobre la cadena de bytes recogiendo estructuras de datos http, luego entre colecciones, compruebo la longitud del contenido de la estructura que acabo de obtener, tomo una cantidad apropiada de la cadena de bytes restante, y luego continúo si queda alguna cadena de bytes.

Editar: de hecho terminé este proyecto. Funciona de maravilla. No estoy bien organizado, pero si alguien quiere ver la fuente completa, puede encontrarla en https://github.com/onmach/Audio-Sniffer .