¿Cómo puedo usar var_dump + almacenamiento en búfer de salida sin errores de memoria?

14 minutos de lectura

avatar de usuario
mike b

Estoy usando una ayuda de depuración en una aplicación que usa var_dump() con almacenamiento en búfer de salida para capturar variables y mostrarlas. Sin embargo, tengo 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 generar una cantidad gigantesca de información para una variable en particular? Realmente no tengo control sobre qué variables se pasan a esta función. Podría ser de cualquier tipo.

  • ¿Tienes el mismo problema con print_r, ¿por curiosidad? Si no, ¿ve muchos avisos de recursividad?

    – Charles

    27 de marzo de 2011 a las 1:42

  • @Charles, probablemente no. yo pudo usar print_r o var_export pero me gusta mucho el hecho de que puedo retener el tipo de variable y la información de longitud proporcionada por var_dump. También los beneficios de formato agregados cuando xdebug está disponible.

    – Mike B.

    27 de marzo de 2011 a las 1:45


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

    – Jon

    27 de marzo de 2011 a las 1:45

  • @Jon: var_dump puede resolver referencias recursivas.

    – zerkms

    27 de marzo de 2011 a las 1:58

  • @Nelson Dado que php usa copiar en escritura, ¿cómo ayudaría eso ya que no estoy cambiando $ var?

    – Mike B.

    12 de diciembre de 2012 a las 20:42

avatar de usuario
Hugo Delsing

Como todos los demás mencionan, lo que preguntas es imposible. Lo único que puedes hacer es tratar de manejarlo lo mejor posible.

Lo que puedes intentar es dividirlo en partes más pequeñas y luego combinarlo. Creé una pequeña prueba para tratar de obtener el error de memoria. Obviamente, un ejemplo del mundo real podría comportarse de manera diferente, pero esto parece funcionar.

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

  • Comprobar la recursividad
  • Arreglo para un solo atributo grande
  • Imitar salida var_dump
  • trigger_error en advertencia para poder atraparlo/ocultarlo

Como se muestra en los comentarios, el identificador de recursos de una clase es diferente del resultado 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;
  }
}
?>

  • Interesante. Sin embargo, ¿cómo se comportaría esto si la propiedad de un objeto fuera abusivamente grande?

    – Charles

    19 de diciembre de 2012 a las 0:49

  • Probado y fallido 🙂 Actualizado con una versión más extensa que parece funcionar.

    – Hugo Delling

    19 de diciembre de 2012 a las 9:37

avatar de usuario
hakré

Bueno, si la memoria física es limitada (verá el error fatal 🙂

Error fatal: tamaño de memoria permitido de 536870912 bytes agotado

Sugeriría hacer el almacenamiento en búfer de salida en el disco (consulte el parámetro de devolución de llamada en ob_start). El almacenamiento en búfer de salida funciona fragmentado, lo que significa que 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 devuelve el identificador de archivo o la ruta del archivo para que también pueda transmitir la salida.

Sin embargo, puede usar ese archivo para obtener el tamaño en bytes necesario. La sobrecarga para las cadenas de PHP no es mucho IIRC, por lo que si todavía hay suficiente memoria libre para el tamaño de archivo, esto debería funcionar bien. Puede restar compensación para tener un poco de espacio y jugar seguro. Solo intente y equivoque un poco lo que hace.

Algún 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";

Producció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"

  • Tu puedes sólo fpassthru el archivo resultante, que hará lectura/escritura/vaciado fragmentado, y no ocupa una tonelada de memoria.

    – Leigh

    19 de diciembre de 2012 a las 10:23

  • @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:// permita incluso transmitir parcialmente a la memoria primero y, si se usa más memoria, la colocan en el disco. Eso es probablemente lo más buscado. Por ejemplo, mantener volcados de hasta 1 MB o similar en la memoria, los más grandes en el disco.

    – hakré

    19 de diciembre de 2012 a las 14:45

  • Me sometí a esto ahora algunas pruebas más. No esperes que funcione con var_dump cuando Xdebug está habilitado. Esto no funciona porque Xdebug provoca todo el var_dump la salida debe enviarse de una vez y no en fragmentos como es común (ver: codepad.viper-7.com/PVI5qT – prueba de @DaveRandom). Algunos códigos modificados y pruebas están aquí: gist.github.com/4341870 – la idea original sigue siendo la misma, simplemente no quiero editar la pregunta todavía.

    – hakré

    20 de diciembre de 2012 a las 0:11


  • @hakre Guau! ¡Eso explica por qué la devolución de llamada no funcionaba cuando publiqué la pregunta originalmente! Me gustaría saber más acerca de por qué var_dump devuelve todo cuando se instala xdebug, pero creo que esa es otra pregunta 🙂

    – Mike B.

    20 de diciembre de 2012 a las 4:20


Cuando instala xdebug, puede limitar la profundidad con la que var_dump sigue a los objetos. En algunos productos de software, es posible que encuentre una especie de recursividad, que infla la salida de var_dump. Aparte de eso, podría aumentar el límite de memoria.

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

  • Agradezco la publicación, pero las respuestas que implican “Hacer que se descargue 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 uno; eso debería estar bien para volcar. Pero las clases con propiedades de otros objetos enormes pueden causar problemas en el segundo nivel. Me doy cuenta de que lo que pido podría ser imposible.

    – Mike B.

    14 de diciembre de 2012 a las 18:49


  • Probablemente es solo una cuestión de cuánta memoria física tiene. =)

    – Ene.

    17 de diciembre de 2012 a las 10:02

avatar de usuario
mario mueller

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. Gestión de la memoria para obtener una descripción general de las funciones internas de asignación de memoria de PHP.

Diste la pista correcta “puede haber cualquier cosa” y este es el problema desde mi punto de vista. Hay un problema arquitectónico que lleva al caso que describes. Y creo que intentas resolverlo por el lado equivocado.

Por ejemplo: puede comenzar con un interruptor para cada tipo en php e intentar establecer límites para cada tamaño. Esto dura mientras a nadie se le ocurra cambiar el límite de memoria dentro del proceso.

Xdebug es una buena solución, ya que evita que su 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 intentar solucionarlo.

[rant]Si al que vuelca una cadena de 50 megabytes o más no le importa el comportamiento de su aplicación, merece sufrirlo ;)[/rant]

No creo que haya ninguna forma de determinar cuánta memoria ocupará finalmente una función específica. Una cosa que puedes hacer es usar memoria_get_usage() para comprobar cuánta memoria ocupa actualmente el script justo antes $largeVar está establecido, luego compárelo con la cantidad posterior. Esto le dará una buena idea del tamaño de $largeVary puede ejecutar pruebas para determinar cuál sería el límite de tamaño máximo aceptable antes de salir correctamente.

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 guárdelo 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á experimentando.

¿Ha sido útil esta solución?