php - variable - ¿Cómo puedo usar var_dump+output buffering sin errores de memoria?
var_export en php (5)
Bueno, si la memoria física es limitada (verá el error fatal :)
Error fatal: tamaño de memoria permitido de 536870912 bytes agotados
Sugeriría hacer el buffering de salida en el disco (ver el parámetro callback en ob_start
). El almacenamiento en búfer de salida funciona fragmentado, es decir, si todavía hay suficiente memoria para mantener el único fragmento en la memoria, puede almacenarlo en un archivo temporal.
// handle output buffering via callback, set chunksize to one kilobyte
ob_start($output_callback, $chunk_size = 1024);
Sin embargo, debe tener en cuenta que esto solo evitará el error fatal durante el almacenamiento en búfer. Si ahora desea devolver el búfer, aún necesita tener suficiente memoria o devolver el archivo-handle o file-path para que también pueda transmitir el resultado.
Sin embargo, puede usar ese archivo para obtener el tamaño en bytes necesario. La sobrecarga de las cadenas de PHP no es mucho IIRC, por lo que si todavía hay suficiente memoria libre para el tamaño del archivo, esto debería funcionar bien. Puede restar compensación para tener un poco de espacio y jugar seguro. Solo prueba y comete un error un poco lo que hace.
Algunos ejemplos de código (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";
Salida:
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"
Estoy utilizando una ayuda de depuración en una aplicación que usa var_dump()
con buffer de salida para capturar variables y mostrarlas. Sin embargo, me encuentro con un problema con objetos grandes que terminan usando demasiada memoria en el búfer.
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
}
¿Hay alguna manera de prevenir esto? ¿O una solución alternativa para detectar que está a punto de producir una gran cantidad de información para una variable en particular? Realmente no tengo control sobre qué variables pasan a esta función. Podría ser de cualquier tipo.
Como todos los demás mencionan lo que preguntas es imposible. Lo único que puede hacer es tratar de manejarlo lo mejor posible.
Lo que puedes intentar es dividirlo en pedazos más pequeños y luego combinarlo. He creado una pequeña prueba para intentar obtener el error de memoria. Obviamente, un ejemplo del mundo real podría comportarse de manera diferente, pero esto parece ser el truco.
<?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;
}
?>
ACTUALIZAR La versión anterior todavía tiene algún error. Lo recreé para usar una clase y algunas otras funciones
- Verifique la recursión
- Solución para un solo atributo grande
- Salida Mimic var_dump
- trigger_error on warning para poder atraparlo / esconderlo
Como se muestra en los comentarios, el identificador de recursos para una clase es diferente de la salida de var_dump. Por lo que puedo decir, las otras cosas son iguales.
<?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;
}
}
?>
Cuando ingresas xdebug puedes limitar la profundidad con la que var_dump sigue a los objetos. En algunos productos de software, es posible que encuentre un tipo de recursión, que aumenta el rendimiento de var_dump. Aparte de eso, podrías aumentar el límite de memoria.
Lo siento, pero creo que no hay solución para tu problema. Está solicitando la determinación de un tamaño para evitar la asignación de memoria para ese tamaño. PHP no puede darle una respuesta sobre "cuánta memoria consumirá", ya que las estructuras ZVAL se crean en el momento del uso en PHP. Por favor refiérase a Programación PHP - 14.5. Administración de memoria para obtener una descripción general de las funciones internas de asignación de memoria de PHP.
Usted dio la pista correcta de que "puede haber algo en ella" y este es el problema desde mi punto de vista. Hay un problema arquitectónico que lleva al caso que describes. Y creo que tratas de resolverlo en el lado equivocado.
Por ejemplo: puede comenzar con un interruptor para cada tipo en php y tratar de establecer límites para cada tamaño. Esto dura mientras nadie tenga la idea de cambiar el límite de memoria dentro del proceso.
Xdebug es una buena solución ya que evita que la aplicación explote debido a una función de registro (incluso no crítica para el negocio) y es una mala solución, ya que no debería activar xdebug en la producción.
Creo que una excepción de memoria es el comportamiento correcto y no debes tratar de eludirlo.
[rant] Si al que tira una cadena de 50 megabytes o más no le importa el comportamiento de su aplicación, merece sufrirla;) [/ rant]
No creo que haya ninguna forma de determinar cuánta memoria ocupará una función específica. Una cosa que puede hacer es usar memory_get_usage() para verificar cuánta memoria está tomando actualmente el script justo antes $largeVar
se establezca $largeVar
, y luego compararlo con la cantidad posterior. Esto le dará una buena idea del tamaño de $largeVar
, y puede ejecutar pruebas para determinar cuál sería el límite de tamaño máximo aceptable antes de salir con gracia.
También puede volver a implementar la función var_dump () usted mismo. Haga que la función recorra la estructura y haga eco del contenido resultante a medida que se genera, o almacénelo en un archivo temporal, en lugar de almacenar una cadena gigantesca en la memoria. Esto le permitirá obtener el mismo resultado deseado, pero sin los problemas de memoria que está enfrentando.