2011-03-27 16 views
38

Estoy usando un auxiliar 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.¿Cómo puedo usar var_dump + output buffering sin errores de memoria?

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 evitar 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.

+0

se obtiene el mismo problema con 'print_r', por curiosidad? Si no, ¿ves muchos avisos de recursión? – Charles

+0

@Charles, probablemente no. Yo * podría * usar 'print_r' o' var_export' pero realmente me gusta el hecho de que puedo retener el tipo de variable y la información de longitud proporcionada por 'var_dump'. Además, el formato adicional se beneficia cuando xdebug está disponible. –

+0

Probablemente sea una cantidad infinita de salida debido a la recursión. Intente llamarlo usted mismo en la misma var sin usar el almacenamiento en búfer de salida para ver qué sucede. – Jon

Respuesta

16

Bueno, si la memoria física es limitada (ver que el error fatal :)

Fatal error: Allowed memory size of 536870912 bytes exhausted

que sugeriría para hacer el búfer de salida en el disco (véase el parámetro de devolución de llamada 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 hay que tener en cuenta que esto sólo evitará que el error grave al búfer. Si ahora desea devolver el búfer, aún necesita tener suficiente memoria o devuelve el archivo-handle o file-path para que también pueda transmitir el resultado.

Sin embargo, puede utilizar ese archivo para obtener el tamaño en bytes necesarios. 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.

un código de ejemplo (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" 
+0

Puede simplemente 'fpassthru' el archivo resultante, que hará lectura/escritura/borrado en bloque, y no ocupará una tonelada de memoria. – Leigh

+2

@Leigh: Sí, eso sería posible. Ahora hice una prueba de concepto, no usando un identificador de archivo sino un 'SplTempFileObject'. Técnicamente, un 'tmp: //' sería igualmente posible, probablemente incluso mejor. De todos modos, para la prueba de concepto, funciona. 'SplTempFileObject' y' tmp: // 'permiten incluso la transmisión parcial a la memoria primero y si se usa más memoria lo colocan en el disco. Eso es probablemente lo más buscado. P.ej. manteniendo volcados de hasta 1 MB o similar en la memoria, más grandes en el disco. – hakre

+1

Sufrí esto ahora algunas pruebas más. No espere que funcione con 'var_dump' cuando Xdebug está habilitado. Esto no funciona porque Xdebug hace que toda la salida 'var_dump' se envíe de una vez y no en fragmentos, ya que es común (ver: http://codepad.viper-7.com/PVI5qT - test by @DaveRandom). Algunos códigos y pruebas cambiados están aquí: https://gist.github.com/4341870 - la idea original sigue siendo la misma, simplemente no quiero editar la pregunta todavía. – hakre

13

Cuando ingresa xdebug puede 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, puede aumentar el límite de memoria.

Ver http://www.xdebug.org/docs/display

+3

Agradezco la publicación, pero las respuestas que implican "Hacer que se descarguen menos" realmente solo abordan el síntoma, no el problema. Esta función podría tener una matriz de 10 dimensiones con solo un elemento en cada una, que debería estar bien para volcar. Pero las clases con propiedades de otros objetos grandes pueden causar problemas en el segundo nivel. Me doy cuenta de que lo que estoy pidiendo podría ser imposible –

+1

Propagablemente, solo se trata de la cantidad de memoria física que tiene. =) –

9

Lo siento, pero creo que no hay una solución para su 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. Consulte Programming PHP - 14.5. Memory Management para obtener una descripción general de las funciones internas de asignación de memoria de PHP.

Has dado 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 debe activar xdebug en producción.

Creo que una excepción de memoria es el comportamiento correcto y no debe tratar de evitarlo.

[queja] Si el que vuelca un 50 megabytes o más cuerdas no se preocupa por su/su comportamiento aplicación, él/ella merece sufrir de ella;) [/ diatriba]

+0

Esta debería ser la respuesta correcta. Si no es posible, entonces algo está mal con su enfoque ... –

5

no creo que hay alguna manera 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 de $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.

20

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; 
} 
?> 

ACTUALIZACIÓN La versión anterior todavía tiene algún error. He recreado a utilizar una clase y algunas otras funciones

  • Comprobar si la recursividad
  • Arreglo para el gran atributo único
  • salida var_dump Mimic
  • trigger_error de advertencia a ser capaz de atrapar/ocultarlo

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; 
    } 
} 
?> 
+0

Interesante. Sin embargo, ¿cómo se comportaría si la propiedad de un objeto fuera excesivamente grande? – Charles

+0

Intentó y falló :) Actualizado con una versión más extensa que parece funcionar. –