Проблема/непонимание работы сборщика мусора или утечка памяти
Столкнулся с проблемой возростающего потребления памяти и невозможности освободить ее. По сути:
Есть некий скрипт, который производит большое кол-во операций. В частности загружает с другого сервера XML файл, парсит и заносит в базу. И все это происходит в цикле.
Класс ImportClass загружает данные с другого сайта, импортирует и возвращает импортированные данные. В переменные ничего не сохраняет, @OPTIONS = locals.
Проблема: Если замерять кол-во потребленной памяти до и после в @main, то оно не освобождается, а только растет.
Попытался подготовить данные по маленькому циклу для демонстрации проблемы:
Правильно ли я понимаю, что "по идеи" на начало следующего цикла used памяти на начало каждого цикла в @main должно быть примерно около начального значения? ([BEFORE 1] "used" => "4524")
И скорее всего мне нужно покопать в сторону утечки памяти в классе ImportClass? Возможно есть какой-то известный баг, или особенность, которую не учитываю, т.к. свои классы я прочесал уже несколько раз.
У меня есть большое подозрение, что память перестает собираться если что-то загружается $file[^file::load[http://...]] но я не проверял еще. Не замечали совпадений?
С кодом проблема, его там очень много, там с десяток классов на импорт завязанно. Пока попробую еще покопать...
[AFTER load] "used" => "14648" "ever_allocated_since_compact" => "57827.6" - note: странно, что это число может быть насколько больше used "ever_allocated_since_start" => "306299" - а это, между прочим, 300Mb.
[AFTER import] "used" => "24584" "ever_allocated_since_compact" => "14020.2" "ever_allocated_since_start" => "658280" - а это 650Mb.
На фоне 650Mb за одну итерацию загрузки потеря 10Mb (дельта между итерациями), то есть меньше 2% от аллоцированной памяти выглядит естественной "усушкой/утряской" - байты в памяти бывают похожи на адреса, поэтому GC может освобождать не все.
Другое дело, что 650Mb за цикл - это как-то много. Какой объем данных (в килобайтах/мегабайтах) обрабатывается за итерацию?
P.S. Еще один возможный источник утечек - какие-нибудь глобальные переменные в используемых классах (особенно хеши).
процесс при большом кол-ве итераций в main вырастает до 1,4 Гб и более.
Загружаемый файл - около 2-4 КБ. порядка 200 записей.
Процесс преобразования примерно такой text -> xml -> hash of hash -> hash of objects (в объектах хранится хеш) -> save to db (и множество операций по преобразованию)
Глобальные переменные есть, но их не так много. Везде пытаемся работать с хешами, даже в классах храним данные в хешах. С этим есть определенная сложность?
кажется не на то обратили внимание: Костя подсказывает, что между before load и after load съелось ~300 МБ. как такое может быть, если там всего-то загрузился 4-КБ файл? (или 200 файлов? да не суть, всё равно получается 800 КБ, а не 300 МБ)
BEFORE load - на начало цикла BEFORE _request API - до вызова загрузки с сайта AFTER _request API - после загрузки
итого на загрузку ушло 11200 - 4508 = 6684 (под загрузкой здесь подразумевается: подготовка запроса (он хитрый), получение данных с сервера + преобразование в xml) Получение происходит из тветера () Много уходит на преобразование в xml такого маленького документа?
AFTER xml2hash - преоразование xml документа в хеш. Здесь и возрастает ever_allocated_since_start до бешенных размеров (функцию привожу ниже). Не вижу в ней откуда такой рост расхода памяти :(
AFTER import - из xml генерируются объекты и сохраняются в БД. Здесь тоже большой расход. Но я тоже не понимаю отчего такое. Может ли быть проблема в 64 битной версии парсера? Или в работе с хешами, т.к. по факту каждый объект, который создается - хранит только хеш данных и имеет с десяток методов (глубина наследования - 3 класса: Message -> ActiveMode -> Model).
Данные по расходу памяти (вывожу только $status:memory.ever_allocated_since_compact):
1. парсер пришёл в начало @main[] -- mu: 164.9922. загрузил xml с диска в память (как объект) -- mu: 836.5083. для каждого узла /statuses/status (28 итераций) выполнил @_xml2hash -- mu: 6730.11
^memory:compact[] в тесте не использовался ни разу.
я сам никогда не использую xpath в сложных ситуциях. обойти всё DOM методами (firstChild, nextSibling) будет существенно менее ресурсоёмко.
как выяснилось, :)
P.S. в вижу: Fix memory leak in xmlXPathEvalExpression(). можете попробовать собрать парсер с этой последней версией либы. P.P.S. я попробовал с последней libxml -- расход памяти без кардинальных изменений.
если использовать (убрал вызов compact и определил локальные переменые), то результаты такие (Time, BL и KB -- это то, что возвращаем Erusage): Time: 1031.25 ms BL: 12668 KB: 13152 ever_allocated_since_compact: 161322
то-же, но с явным определением result: Time: 968.75 ms BL: 9560 KB: 10128 ever_allocated_since_compact: 158294
если переписать метод на использование DOM (см. ниже. я использовал Node:foreachChild, который делает кучей ненужной для данного случая работы), то результаты такие: Time: 515.625 ms BL: 7824 KB: 8436 ever_allocated_since_compact: 22595.8
почувствуйте, как говорится, разницу. особенно в ever_allocated_since_compact...
кстати результаты работы методов немного не совпадают, и мне кажется, что ошибка в оригинальном xml2hash. если расскоментировать dstop и смотреть на получившийся хеш, то видим.
xml2hash:
$.0[$.created_at[Wed Jun 16 15:04:36 +0000 2010]$.id[16312661011]
...
$.in_reply_to_screen_name[void]$.contributors_enabled[$.id[12]$.name[Jack Dorsey]
xml2hash2:
$.0[$.created_at[Wed Jun 16 15:04:36 +0000 2010]$.id[16312661011]
...
$.in_reply_to_screen_name[void]$.user[$.id[12]$.name[Jack Dorsey]
разница в 3 строке снизу (contributors_enabled/user). судя по xml должно быть user, хотя может я не понял исходную задумку.
С foreachChild интересно получается. Надо будет последнюю версию либы собрать, может там xPath действительно хорошо поправили.
Ради интереса я всё-таки реализовал url="http://www.parser.ru/forum/?id=72982"]озвученный вариант с process[/url]:
_xml2hash:
Time: 0.579 s
MU: 187508 (локальный result)
xslt (прямыми руками*) + process + всякие хитрости:
Time: TT: 0.063 s
MU: 22245
* если руки не прямы, то память легко растёт до 90 мб :-)
P.S.: конечно же, process не стоит применять, это как минимум опасно, а если защищаться (tainting и замены через match), то выйдет дорого по времени и памяти на таком объёме данных. А вот xslt оказался очень быстрым (опять же, с прямыми руками:-).
а это для данного метода очень даже неплохо, потому что он выполняется кучу раз.
новая либа проблему не решила, я уже попробовал %-) возможно проблема в неэффективных парсерных обёртках для xpath. надо будет смотреть.
я для подобных задач (xml->hash) постоянно пользуюсь DOM-ом (на рессурсоёмкость xpath наступил давным давно). да и xpath тут по сути не нужен, ведь тут простой обход дерева.
в приведённом ранее методе, использующем DOM операции можно сделать ещё одну небольшую оптимизацию (на обсуждаемом xml-е выигрывается ещё примерно 1 МБ):
С process'ом придётся повозиться, проверил. tab-delimited безопаснее и быстрее.
Насколько понимаю задачу: надо из xml выбрать лишь несколько полей и положить их в БД, или передать в конструктор объекта, а потом уже сохранить его поля в БД. В обоих случаях нужны сериализованные данные и необходимости получить именно хеш-структуру из xml нет.
с tab-delimited у вас тоже будут небольшие проблемы...
табы и ентеры для tab-delimited будет выдавать xsl. он-же будет выдавать содержимое, которое было в исходном xml-е.
но вот отличить разные переводы строки будет невозможно, т.к. у них после transform будет одинаковый taint.
т.е. придётся немножко корячиться, выдавая вместо них какие-нить сигнатуры, а потом натравливая taint с replace-ом.
лично я в сторону tab-delimited смотрел бы только если бы было возможно сделать LOAD DATA INFILE. а иначе использовал бы уже имеющийся код, просто убрав xpath из обхода дерева. в этом случае и compact будет лучше работать, т.к. ему не придётся в сотнях мегабайт ковыряться.
Но в любом случае xslt будет быстрее и по памяти нормально
, что и показал мой тест с process.
taint и replace в разумном подходе не осложнят ситуацию, особенно, если результирующих данных после трансформации будет сильно меньше, чем в исходном xml. А их точно будет меньше хотя бы из-за tab-delimided формата:-)