Как я могу использовать буферизацию 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
}
Есть ли способ предотвратить это? Или обход, чтобы обнаружить, что он собирается выводить гигантское количество информации для определенной переменной? Я действительно не контролирую, какие переменные передаются в эту функцию. Это может быть любой тип.
Ответы
Ответ 1
Ну, если физическая память ограничена (вы видите фатальную ошибку:)
Неустранимая ошибка: допустимый размер памяти 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";
Вывод:
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"
Ответ 2
Как все остальные упоминают то, что вы просите, невозможно. Единственное, что вы можете сделать, это попытаться справиться с этим как можно лучше.
То, что вы можете попробовать, состоит в том, чтобы разбить его на более мелкие части, а затем объединить. Я создал небольшой тест, чтобы попытаться получить ошибку памяти. Очевидно, что пример реального мира может вести себя по-другому, но это, похоже, делает трюк.
<?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;
}
?>
UPDATE
В приведенной выше версии все еще есть некоторая ошибка. Я воссоздал его для использования класса и некоторых других функций
- Проверить рекурсию
- Исправление для одного большого атрибута
- 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;
}
}
?>
Ответ 3
Когда вы отказываетесь от xdebug, вы можете ограничить, насколько глубокий var_dump следует за объектами. В некоторых программных продуктах вы можете столкнуться с какой-то рекурсией, которая раздувает выход var_dump.
Кроме этого, вы можете увеличить лимит памяти.
См. http://www.xdebug.org/docs/display
Ответ 4
Извините, но я думаю, что для вашей проблемы нет решения. Вы запрашиваете определение размера, чтобы предотвратить выделение памяти для этого размера. PHP не может дать вам ответ о том, "сколько памяти он будет потреблять", поскольку структуры ZVAL создаются во время использования в PHP. См. Программирование PHP - 14.5. Управление памятью для обзора внутренних функций распределения памяти PHP.
Вы дали правильный намек: "в нем может быть что-то", и это проблема с моей точки зрения. Существует архитектурная проблема, которая приводит к случаю, который вы описываете. И я думаю, вы пытаетесь решить это на другом конце.
Например: вы можете начать с коммутатора для каждого типа в php и попытаться установить лимиты для каждого размера. Это длится до тех пор, пока никто не приходит к идее изменения предела памяти в процессе.
Xdebug - хорошее решение, так как оно препятствует вашему приложению взломать из-за (даже не зависящей от бизнеса) функции журнала, и это плохое решение, так как вы не должны активировать xdebug в процессе производства.
Я думаю, что исключение памяти - это правильное поведение, и вы не должны пытаться его обойти.
[rant] Если тот, кто сбрасывает 50 мегабайт или более строк, не заботится о поведении своего приложения, он/она заслуживает этого;) [/rant]
Ответ 5
Я не верю, что есть какой-то способ определить, сколько памяти в конечном итоге займет определенная функция. Единственное, что вы можете сделать, это использовать memory_get_usage(), чтобы проверить, сколько памяти занимает script до установки $largeVar
, затем сравните его с суммой после. Это даст вам представление о размере $largeVar
, и вы можете запускать пробные версии, чтобы определить, какой максимальный допустимый предел будет до того, как вы выйдете изящно.
Вы также можете повторно реализовать функцию var_dump(). Попросите функцию пройти через структуру и эхо результирующее содержимое по мере ее создания или сохранить в временном файле, вместо того чтобы хранить гигантскую строку в памяти. Это позволит вам получить тот же желаемый результат, но без проблем с памятью, с которыми вы сталкиваетесь.