Как я могу использовать буферизацию var_dump + output без ошибок памяти?

Я использую средство отладки в приложении, которое использует var_dump() с буферизацией вывода для захвата переменных и отображения их. Тем не менее, я столкнулся с проблемой с большими объектами, которые в конечном итоге используют слишком много памяти в буфере.

 function getFormattedOutput(mixed $var) { if (isTooLarge($var)) { return 'Too large! Abort!'; // What a solution *might* look like } ob_start(); var_dump($var); // Fatal error: Allowed memory size of 536870912 bytes exhausted $data = ob_get_clean(); // Return the nicely-formated data to use later return $data } 

Есть ли способ предотвратить это? Или обход, чтобы обнаружить, что он собирается вывести огромное количество информации для конкретной переменной? Я действительно не контролирую, какие переменные передаются в эту функцию. Это может быть любой тип.

Ну, если физическая память ограничена (вы видите фатальную ошибку 🙂

Неустранимая ошибка: допустимый размер памяти 536870912 байт исчерпан

Я бы предложил сделать буферизацию вывода на диске (см. Параметр обратного вызова на ob_start ). Операция буферизации вывода работает, а значит, если памяти недостаточно, чтобы сохранить один блок в памяти, вы можете сохранить его во временный файл.

 // handle output buffering via callback, set chunksize to one kilobyte ob_start($output_callback, $chunk_size = 1024); 

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

Однако вы можете использовать этот файл, чтобы получить размер в байтах. Накладные расходы для строк PHP не так много IIRC, поэтому, если по-прежнему достаточно свободного места для файлов, это должно работать хорошо. Вы можете компенсировать смещение, чтобы иметь небольшую комнату и играть в безопасности. Просто попробуйте и сделайте ошибку, что она делает.

Некоторые примеры кода (PHP 5.4):

 <?php /** * @link http://stackoverflow.com/questions/5446647/how-can-i-use-var-dump-output-buffering-without-memory-errors/ */ class OutputBuffer { /** * @var int */ private $chunkSize; /** * @var bool */ private $started; /** * @var SplFileObject */ private $store; /** * @var bool Set Verbosity to true to output analysis data to stderr */ private $verbose = true; public function __construct($chunkSize = 1024) { $this->chunkSize = $chunkSize; $this->store = new SplTempFileObject(); } public function start() { if ($this->started) { throw new BadMethodCallException('Buffering already started, can not start again.'); } $this->started = true; $result = ob_start(array($this, 'bufferCallback'), $this->chunkSize); $this->verbose && file_put_contents('php://stderr', sprintf("Starting Buffering: %d; Level %d\n", $result, ob_get_level())); return $result; } public function flush() { $this->started && ob_flush(); } public function stop() { if ($this->started) { ob_flush(); $result = ob_end_flush(); $this->started = false; $this->verbose && file_put_contents('php://stderr', sprintf("Buffering stopped: %d; Level %d\n", $result, ob_get_level())); } } private function bufferCallback($chunk, $flags) { $chunkSize = strlen($chunk); if ($this->verbose) { $level = ob_get_level(); $constants = ['PHP_OUTPUT_HANDLER_START', 'PHP_OUTPUT_HANDLER_WRITE', 'PHP_OUTPUT_HANDLER_FLUSH', 'PHP_OUTPUT_HANDLER_CLEAN', 'PHP_OUTPUT_HANDLER_FINAL']; $flagsText = ''; foreach ($constants as $i => $constant) { if ($flags & ($value = constant($constant)) || $value == $flags) { $flagsText .= (strlen($flagsText) ? ' | ' : '') . $constant . "[$value]"; } } file_put_contents('php://stderr', "Buffer Callback: Chunk Size $chunkSize; Flags $flags ($flagsText); Level $level\n"); } if ($flags & PHP_OUTPUT_HANDLER_FINAL) { return TRUE; } if ($flags & PHP_OUTPUT_HANDLER_START) { $this->store->fseek(0, SEEK_END); } $chunkSize && $this->store->fwrite($chunk); if ($flags & PHP_OUTPUT_HANDLER_FLUSH) { // there is nothing to d } if ($flags & PHP_OUTPUT_HANDLER_CLEAN) { $this->store->ftruncate(0); } return ""; } public function getSize() { $this->store->fseek(0, SEEK_END); return $this->store->ftell(); } public function getBufferFile() { return $this->store; } public function getBuffer() { $array = iterator_to_array($this->store); return implode('', $array); } public function __toString() { return $this->getBuffer(); } public function endClean() { return ob_end_clean(); } } $buffer = new OutputBuffer(); echo "Starting Buffering now.\n=======================\n"; $buffer->start(); foreach (range(1, 10) as $iteration) { $string = "fill{$iteration}"; echo str_repeat($string, 100), "\n"; } $buffer->stop(); echo "Buffering Results:\n==================\n"; $size = $buffer->getSize(); echo "Buffer Size: $size (string length: ", strlen($buffer), ").\n"; echo "Peeking into buffer: ", var_dump(substr($buffer, 0, 10)), ' ...', var_dump(substr($buffer, -10)), "\n"; - <?php /** * @link http://stackoverflow.com/questions/5446647/how-can-i-use-var-dump-output-buffering-without-memory-errors/ */ class OutputBuffer { /** * @var int */ private $chunkSize; /** * @var bool */ private $started; /** * @var SplFileObject */ private $store; /** * @var bool Set Verbosity to true to output analysis data to stderr */ private $verbose = true; public function __construct($chunkSize = 1024) { $this->chunkSize = $chunkSize; $this->store = new SplTempFileObject(); } public function start() { if ($this->started) { throw new BadMethodCallException('Buffering already started, can not start again.'); } $this->started = true; $result = ob_start(array($this, 'bufferCallback'), $this->chunkSize); $this->verbose && file_put_contents('php://stderr', sprintf("Starting Buffering: %d; Level %d\n", $result, ob_get_level())); return $result; } public function flush() { $this->started && ob_flush(); } public function stop() { if ($this->started) { ob_flush(); $result = ob_end_flush(); $this->started = false; $this->verbose && file_put_contents('php://stderr', sprintf("Buffering stopped: %d; Level %d\n", $result, ob_get_level())); } } private function bufferCallback($chunk, $flags) { $chunkSize = strlen($chunk); if ($this->verbose) { $level = ob_get_level(); $constants = ['PHP_OUTPUT_HANDLER_START', 'PHP_OUTPUT_HANDLER_WRITE', 'PHP_OUTPUT_HANDLER_FLUSH', 'PHP_OUTPUT_HANDLER_CLEAN', 'PHP_OUTPUT_HANDLER_FINAL']; $flagsText = ''; foreach ($constants as $i => $constant) { if ($flags & ($value = constant($constant)) || $value == $flags) { $flagsText .= (strlen($flagsText) ? ' | ' : '') . $constant . "[$value]"; } } file_put_contents('php://stderr', "Buffer Callback: Chunk Size $chunkSize; Flags $flags ($flagsText); Level $level\n"); } if ($flags & PHP_OUTPUT_HANDLER_FINAL) { return TRUE; } if ($flags & PHP_OUTPUT_HANDLER_START) { $this->store->fseek(0, SEEK_END); } $chunkSize && $this->store->fwrite($chunk); if ($flags & PHP_OUTPUT_HANDLER_FLUSH) { // there is nothing to d } if ($flags & PHP_OUTPUT_HANDLER_CLEAN) { $this->store->ftruncate(0); } return ""; } public function getSize() { $this->store->fseek(0, SEEK_END); return $this->store->ftell(); } public function getBufferFile() { return $this->store; } public function getBuffer() { $array = iterator_to_array($this->store); return implode('', $array); } public function __toString() { return $this->getBuffer(); } public function endClean() { return ob_end_clean(); } } $buffer = new OutputBuffer(); echo "Starting Buffering now.\n=======================\n"; $buffer->start(); foreach (range(1, 10) as $iteration) { $string = "fill{$iteration}"; echo str_repeat($string, 100), "\n"; } $buffer->stop(); echo "Buffering Results:\n==================\n"; $size = $buffer->getSize(); echo "Buffer Size: $size (string length: ", strlen($buffer), ").\n"; echo "Peeking into buffer: ", var_dump(substr($buffer, 0, 10)), ' ...', var_dump(substr($buffer, -10)), "\n"; 

Вывод:

 STDERR: Starting Buffering: 1; Level 1 STDERR: Buffer Callback: Chunk Size 1502; Flags 1 (PHP_OUTPUT_HANDLER_START[1]); Level 1 STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 STDERR: Buffer Callback: Chunk Size 602; Flags 4 (PHP_OUTPUT_HANDLER_FLUSH[4]); Level 1 STDERR: Buffer Callback: Chunk Size 0; Flags 8 (PHP_OUTPUT_HANDLER_FINAL[8]); Level 1 STDERR: Buffering stopped: 1; Level 0 Starting Buffering now. ======================= Buffering Results: ================== Buffer Size: 5110 (string length: 5110). Peeking into buffer: string(10) "fill1fill1" ...string(10) "l10fill10\n" 

Как все остальные упоминают то, что вы просите, невозможно. Единственное, что вы можете сделать, это попытаться справиться с этим как можно лучше.

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

 <?php define('mem_limit', return_bytes(ini_get('memory_limit'))); //allowed memory /* SIMPLE TEST CLASS */ class test { } $loop = 260; $t = new Test(); for ($x=0;$x<=$loop;$x++) { $v = 'test'.$x; $t->$v = new Test(); for ($y=0;$y<=$loop;$y++) { $v2 = 'test'.$y; $t->$v->$v2 = str_repeat('something to test! ', 200); } } /* ---------------- */ echo saferVarDumpObject($t); function varDumpToString($v) { ob_start(); var_dump($v); $content = ob_get_contents(); ob_end_clean(); return $content; } function saferVarDumpObject($var) { if (!is_object($var) && !is_array($var)) return varDumpToString($var); $content = ''; foreach($var as $v) { $content .= saferVarDumpObject($v); } //adding these smaller pieces to a single var works fine. //returning the complete larger piece gives memory error $length = strlen($content); $left = mem_limit-memory_get_usage(true); if ($left>$length) return $content; //enough memory left echo "WARNING! NOT ENOUGH MEMORY<hr>"; if ($left>100) { return substr($content, 0, $left-100); //100 is a margin I choose, return everything you have that fits in the memory } else { return ""; //return nothing. } } function return_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); switch($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } ?> 

ОБНОВЛЕНИЕ В приведенной выше версии все еще есть некоторая ошибка. Я воссоздал его для использования класса и некоторых других функций

  • Проверить рекурсию
  • Исправить для одного большого атрибута
  • Mimic var_dump output
  • trigger_error при предупреждении, чтобы уловить / скрыть

Как показано в комментариях, идентификатор ресурса для класса отличается от вывода var_dump. Насколько я могу судить, другие вещи равны.

 <?php /* RECURSION TEST */ class sibling { public $brother; public $sister; } $brother = new sibling(); $sister = new sibling(); $brother->sister = $sister; $sister->sister = $brother; Dump::Safer($brother); //simple class class test { } /* LARGE TEST CLASS - Many items */ $loop = 260; $t = new Test(); for ($x=0;$x<=$loop;$x++) { $v = 'test'.$x; $t->$v = new Test(); for ($y=0;$y<=$loop;$y++) { $v2 = 'test'.$y; $t->$v->$v2 = str_repeat('something to test! ', 200); } } //Dump::Safer($t); /* ---------------- */ /* LARGE TEST CLASS - Large attribute */ $a = new Test(); $a->t2 = new Test(); $a->t2->testlargeattribute = str_repeat('1', 268435456 - memory_get_usage(true) - 1000000); $a->smallattr1 = 'test small1'; $a->smallattr2 = 'test small2'; //Dump::Safer($a); /* ---------------- */ class Dump { private static $recursionhash; private static $memorylimit; private static $spacing; private static $mimicoutput = true; final public static function MimicOutput($v) { //show results similar to var_dump or without array/object information //defaults to similar as var_dump and cancels this on out of memory warning self::$mimicoutput = $v===false ? false : true; } final public static function Safer($var) { //set defaults self::$recursionhash = array(); self::$memorylimit = self::return_bytes(ini_get('memory_limit')); self::$spacing = 0; //echo output echo self::saferVarDumpObject($var); } final private static function saferVarDumpObject($var) { if (!is_object($var) && !is_array($var)) return self::Spacing().self::varDumpToString($var); //recursion check $hash = spl_object_hash($var); if (!empty(self::$recursionhash[$hash])) { return self::Spacing().'*RECURSION*'.self::Eol(); } self::$recursionhash[$hash] = true; //create a similar output as var dump to identify the instance $content = self::Spacing() . self::Header($var); //add some spacing to mimic vardump output //Perhaps not the best idea because the idea is to use as little memory as possible. self::$spacing++; //Loop trough everything to output the result foreach($var as $k=>$v) { $content .= self::Spacing().self::Key($k).self::Eol().self::saferVarDumpObject($v); } self::$spacing--; //decrease spacing and end the object/array $content .= self::Spacing().self::Footer().self::Eol(); //adding these smaller pieces to a single var works fine. //returning the complete larger piece gives memory error //length of string and the remaining memory $length = strlen($content); $left = self::$memorylimit-memory_get_usage(true); //enough memory left? if ($left>$length) return $content; //show warning trigger_error('Not enough memory to dump "'.get_class($var).'" memory left:'.$left, E_USER_WARNING); //stop mimic output to prevent fatal memory error self::MimicOutput(false); if ($left>100) { return substr($content, 0, $left-100); //100 is a margin I chose, return everything you have that fits in the memory } else { return ""; //return nothing. } } final private static function Spacing() { return self::$mimicoutput ? str_repeat(' ', self::$spacing*2) : ''; } final private static function Eol() { return self::$mimicoutput ? PHP_EOL : ''; } final private static function Header($var) { //the resource identifier for an object is WRONG! Its always 1 because you are passing around parts and not the actual object. Havent foundnd a fix yet return self::$mimicoutput ? (is_array($var) ? 'array('.count($var).')' : 'object('.get_class($var).')#'.intval($var).' ('.count((array)$var).')') . ' {'.PHP_EOL : ''; } final private static function Footer() { return self::$mimicoutput ? '}' : ''; } final private static function Key($k) { return self::$mimicoutput ? '['.(gettype($k)=='string' ? '"'.$k.'"' : $k ).']=>' : ''; } final private static function varDumpToString($v) { ob_start(); var_dump($v); $length = strlen($v); $left = self::$memorylimit-memory_get_usage(true); //enough memory left with some margin? if ($left-100>$length) { $content = ob_get_contents(); ob_end_clean(); return $content; } ob_end_clean(); //show warning trigger_error('Not enough memory to dump "'.gettype($v).'" memory left:'.$left, E_USER_WARNING); if ($left>100) { $header = gettype($v).'('.strlen($v).')'; return $header . substr($v, $left - strlen($header)); } else { return ""; //return nothing. } } final private static function return_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); switch($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } } ?> 

Когда вы отказываетесь от xdebug, вы можете ограничить, как глубокий var_dump следует за объектами. В некоторых программных продуктах вы можете столкнуться с какой-то рекурсией, которая раздувает выход var_dump. Кроме этого, вы можете увеличить лимит памяти.

См. http://www.xdebug.org/docs/display.

Извините, но я думаю, что для вашей проблемы нет решения. Вы запрашиваете определение размера, чтобы предотвратить выделение памяти для этого размера. PHP не может дать вам ответ о том, «сколько памяти он будет потреблять», поскольку структуры ZVAL создаются во время использования в PHP. См. Программирование PHP – 14.5. Управление памятью для обзора внутренних функций распределения памяти PHP.

Вы дали правильный намек: «в этом может быть что-то», и это проблема с моей точки зрения. Существует архитектурная проблема, которая приводит к случаю, который вы описываете. И я думаю, вы пытаетесь решить это на неправильном конце.

Например: вы можете начать с коммутатора для каждого типа в php и попытаться установить лимиты для каждого размера. Это длится до тех пор, пока никто не приходит к идее изменения предела памяти в процессе.

Xdebug – хорошее решение, так как оно удерживает приложение от взлома из-за (даже не зависящей от бизнеса) функции журнала, и это плохое решение, так как вы не должны активировать xdebug в процессе производства.

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

[rant] Если тот, кто сбрасывает 50 мегабайт или более строк, не заботится о поведении своего приложения, он / она заслуживает того, чтобы страдать от этого;) [/ rant]

Я не верю, что есть какой-то способ определить, сколько памяти определит определенная функция. Одна вещь, которую вы можете сделать, это использовать memory_get_usage (), чтобы проверить, сколько памяти занимает $largeVar скрипт, прежде чем $largeVar установлен, а затем сравните его с суммой после. Это даст вам представление о размере $largeVar , и вы можете запустить пробные версии, чтобы определить, какой максимальный допустимый предел будет до того, как вы выйдете изящно.

Вы также можете переопределить функцию var_dump () самостоятельно. Попросите функцию пройти через структуру и эхо результирующее содержимое по мере ее создания или сохранить в временном файле, вместо того чтобы хранить гигантскую строку в памяти. Это позволит вам получить тот же желаемый результат, но без проблем с памятью, с которыми вы сталкиваетесь.